diff --git a/.claude/skills/pr/SKILL.md b/.claude/skills/pr/SKILL.md index 7a332c807d..1e9106ef06 100644 --- a/.claude/skills/pr/SKILL.md +++ b/.claude/skills/pr/SKILL.md @@ -1,12 +1,12 @@ --- name: pr -description: Push the current branch and open a draft PR for Meshtastic-Android the repo way — baseline verified first, body drafted per .github/copilot-pull-request-instructions.md (WHY-first, categorized changes), screenshots embedded via commit-pinned raw URLs. Use whenever work is ready to go up as a PR. +description: Push the current branch and open a draft PR for Meshtastic-Android the repo way — baseline verified first, body drafted per CONTRIBUTING.md (WHY-first, categorized changes, screenshots for UI work). Use whenever work is ready to go up as a PR. disable-model-invocation: true --- # pr -Opens a PR the way this repo expects. The SOP lives in `.github/copilot-pull-request-instructions.md` — **read it now and follow it**; this skill only adds the steps around it. +Opens a PR the way this repo expects. The SOP lives in `CONTRIBUTING.md` → **Pull Requests** — **read it now and follow it**; this skill only adds the steps around it. ## 1. Pre-flight - Target branch is `main` unless told otherwise. @@ -15,9 +15,9 @@ Opens a PR the way this repo expects. The SOP lives in `.github/copilot-pull-req - `git status`: nothing unintended staged. Never commit `.agent_memory/`. If `docs/assets/screenshots/*.png` are dirty from a test run (not an intentional UI change), restore them: `git checkout -- docs/assets/screenshots`. ## 2. Body -Draft per the SOP file (WHY-first summary, then changes under the 🌟 Features / 🛠️ Improvements / 🐛 Bug Fixes / 🧹 Chores categories that apply, **Testing Performed** section when tests were added/changed). +Draft per `CONTRIBUTING.md` → Pull Requests → **Writing the description** (WHY-first summary, then changes under the 🌟 New Features / 🛠️ Refactoring & Architecture / 🐛 Bug Fixes / 🧹 Chores categories that apply, **Testing Performed** section when tests were added/changed). -**Screenshots** (UI changes want them): commit the PNGs on the branch, push, then embed with commit-pinned raw URLs so they render in the PR body immediately: +**Screenshots** (UI changes want them): `CONTRIBUTING.md` → Pull Requests → Screenshots owns the policy, and it allows either a GitHub-hosted upload or a committed image. When committing one, push it first and embed it by commit-pinned raw URL so it renders immediately: ``` https://raw.githubusercontent.com//// ``` diff --git a/.claude/skills/proto-bump/SKILL.md b/.claude/skills/proto-bump/SKILL.md index adc56c4d3c..0bb758dd0c 100644 --- a/.claude/skills/proto-bump/SKILL.md +++ b/.claude/skills/proto-bump/SKILL.md @@ -81,7 +81,7 @@ New proto versions usually add fields/messages the app doesn't consume yet. Diff ## PR + guardrails -- Write the PR per `.github/copilot-pull-request-instructions.md`: WHY-first; 🛠️ (or 🌟 if it unlocks a user-facing feature); link the upstream release notes (real URL only); "Testing Performed" = the gradle-runner run including `allTests`. +- Write the PR per `CONTRIBUTING.md` → Pull Requests: WHY-first; 🧹 Chores for a routine catalog bump, since that is where `CONTRIBUTING.md` puts dependencies — 🛠️ only when the bump carries call-site adaptations of its own, 🌟 if it unlocks a user-facing feature; link the upstream release notes (real URL only); "Testing Performed" = the gradle-runner run including `allTests`. - Never hand-edit or vendor generated proto — this repo consumes the Maven artifact only. - Keep the change minimal: a bump PR is a catalog edit + the force block (Mode B) + necessary call-site adaptations, not a feature. - Branch off `main` unless told otherwise. diff --git a/.coderabbit.yaml b/.coderabbit.yaml index f651bab938..6889357ee8 100644 --- a/.coderabbit.yaml +++ b/.coderabbit.yaml @@ -13,10 +13,14 @@ reviews: auto_review: enabled: true drafts: false - # Review once when the PR goes ready, then on demand via `@coderabbitai review`. - # Re-reviewing every push turned 6-commit PRs into 8 review rounds, because each - # fix commit reopened a full pass. Batch the fixes, push, then ask for one re-review. - auto_incremental_review: false + # Review each push incrementally. With this off, a push got no review at all and the + # only way to re-review was `@coderabbitai full review`, which re-reads the whole diff + # and surfaces new findings on unchanged lines every round. + auto_incremental_review: true + # Never pause on our own. The default (5) stops automatic review after five + # reviewed commits and says so only inside the pinned summary comment — the + # check still goes green — so a long PR silently stops being reviewed. + auto_pause_after_reviewed_commits: 0 ignore_usernames: - renovate - renovate[bot] @@ -121,6 +125,17 @@ reviews: One problem per comment. Cite the exact line, symbol, or condition. Give a fix direction — a snippet when the fix is not obvious. Never restate a finding already raised in an existing review thread. + + + Scope. If a finding needs work beyond what this diff set out to do — a schema migration, a new + column, a change to a subsystem the PR only touches incidentally — say so and recommend a + follow-up issue instead of a change request. Prefer the smallest correct fix over the most + thorough one on a PR that is already several rounds deep. + + + Settled decisions. Do not re-raise a concern that an existing thread on this PR already resolved, + and do not ask for a change to be reverted that was made in response to an earlier finding of + yours without saying that is what you are proposing and why the trade-off changed. - path: "**/commonMain/**" instructions: > KMP common code. Flag any import of java.* or android.* — these break non-Android targets. Expect KMP equivalents instead (Okio, kotlinx Mutex/atomicfu, NumberFormatter.format() for floats). diff --git a/.github/actionlint.yaml b/.github/actionlint.yaml new file mode 100644 index 0000000000..9aec4c7709 --- /dev/null +++ b/.github/actionlint.yaml @@ -0,0 +1,10 @@ +# actionlint validates `runs-on` against a built-in list of GitHub-hosted labels, and +# the pin in pull-request.yml (1.7.12) predates ubuntu-26.04. Both labels below are +# real GitHub-hosted runners, not self-hosted; this key is simply actionlint's only +# mechanism for teaching it a label it does not yet know. +# +# Remove once the pinned actionlint recognises them on its own. +self-hosted-runner: + labels: + - ubuntu-26.04 + - ubuntu-26.04-arm diff --git a/.github/ci-gradle.properties b/.github/ci-gradle.properties index 07bd1a4dff..0a206f5188 100644 --- a/.github/ci-gradle.properties +++ b/.github/ci-gradle.properties @@ -12,10 +12,12 @@ org.gradle.daemon=false # ── Memory ──────────────────────────────────────────────────────────── -# Standard GitHub runners have 7 GB RAM. Keep Gradle + Kotlin daemon -# within budget (4g Gradle + 2g Kotlin daemon + 1g OS/tooling headroom). +# Public-repo ubuntu-24.04 runners have 16 GB RAM. Keep Gradle + Kotlin daemon +# within budget (4g Gradle + 6g Kotlin daemon, leaving room for lint and K/N). +# Only kotlin.daemon.jvmargs is read from a properties file; kotlin.daemon.jvm.options +# is a system property. Unset, the daemon inherits org.gradle.jvmargs' 4g and OOMs. org.gradle.jvmargs=-Xmx4g -XX:+UseParallelGC -XX:MaxMetaspaceSize=1g -Dfile.encoding=UTF-8 -kotlin.daemon.jvm.options=-Xmx2g -XX:+UseParallelGC +kotlin.daemon.jvmargs=-Xmx6g -XX:+UseG1GC # ── Parallelism ─────────────────────────────────────────────────────── org.gradle.parallel=true diff --git a/.github/copilot-pull-request-instructions.md b/.github/copilot-pull-request-instructions.md deleted file mode 100644 index eaf717bfea..0000000000 --- a/.github/copilot-pull-request-instructions.md +++ /dev/null @@ -1,25 +0,0 @@ -# GitHub Copilot Pull Request Instructions - - -You are an expert open-source maintainer. Your goal is to write clear, professional, and highly structured Pull Request descriptions based on the provided diffs. - - - -1. **Remove Boilerplate:** Always delete the "tips" section at the top of the `PULL_REQUEST_TEMPLATE.md` before generating your text. -2. **Context First:** Start with a clear, 1-2 sentence summary of *why* this change is being made. If the branch name or commits reference an issue (e.g., `fix-1234`), explicitly add `Fixes #1234` or `Resolves #1234`. -3. **Structured Changes:** Break down the code changes into bullet points categorized by: - - 🌟 **New Features** (UI, modules, logic) - - 🛠️ **Refactoring & Architecture** (KMP migrations, Koin DI updates) - - 🐛 **Bug Fixes** - - 🧹 **Chores** (Dependencies, formatting, docs) -4. **Architecture Callouts:** If the diff includes moving files from `androidMain` to `commonMain`, or migrating from Android Views to Compose, highlight this as a "KMP Migration Milestone". -5. **Testing Callouts:** If the diff includes changes to `commonTest` or mentions tests, add a section called "Testing Performed" and list the tests that were added/modified. -6. **Screenshots for UI changes:** If the change affects the UI (Compose composables, layouts, theming, navigation, or anything under `feature/**` / `core/ui/**`), add a **Screenshots** section when real images are available — for example generated `:screenshot-tests` reference PNGs committed in the PR, or images captured from a device/emulator. Prefer a **Before / After** table for visual changes and fixes: - - | Before | After | - |--------|-------| - | | | - - Reference committed images with a stable **commit-SHA** raw URL (`https://raw.githubusercontent.com////`, URL-encoding spaces as `%20`) so the links survive branch deletion, or use a GitHub-uploaded attachment. Only embed images that actually exist. -7. **No "Magic" Text:** Never invent URLs or insert fake/placeholder images. If no real screenshot is available for a UI change, leave the template's HTML image comment block intact so the author can add one — do not fabricate. - diff --git a/.github/lsp.json b/.github/lsp.json deleted file mode 100644 index 983ecf7850..0000000000 --- a/.github/lsp.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "lspServers": { - "kotlin": { - "command": "kotlin-language-server", - "args": [], - "fileExtensions": { - ".kt": "kotlin", - ".kts": "kotlin" - } - } - } -} diff --git a/.github/workflows/create-or-promote-release.yml b/.github/workflows/create-or-promote-release.yml index 14deddbec7..341f4f8f30 100644 --- a/.github/workflows/create-or-promote-release.yml +++ b/.github/workflows/create-or-promote-release.yml @@ -41,7 +41,7 @@ concurrency: jobs: determine-tags: - runs-on: ubuntu-24.04-arm + runs-on: ubuntu-26.04-arm timeout-minutes: 10 outputs: tag_to_process: ${{ steps.calculate_tags.outputs.tag_to_process }} @@ -182,7 +182,7 @@ jobs: cleanup-on-failure: needs: [determine-tags, call-release-workflow] if: ${{ (failure() || cancelled()) && !inputs.dry_run && inputs.channel == 'internal' }} - runs-on: ubuntu-24.04-arm + runs-on: ubuntu-26.04-arm timeout-minutes: 10 steps: - name: Checkout code diff --git a/.github/workflows/dependency-graph-submit.yml b/.github/workflows/dependency-graph-submit.yml index 47a1137485..62972ddffb 100644 --- a/.github/workflows/dependency-graph-submit.yml +++ b/.github/workflows/dependency-graph-submit.yml @@ -21,7 +21,7 @@ jobs: submit-dependency-graph: # failed runs may have partial graphs; skip if: github.repository == 'meshtastic/Meshtastic-Android' && github.event.workflow_run.conclusion == 'success' - runs-on: ubuntu-24.04-arm + runs-on: ubuntu-26.04-arm timeout-minutes: 10 steps: # skipped android-check (docs-only/bot PRs) uploads nothing — don't fail red on that diff --git a/.github/workflows/docs-deploy.yml b/.github/workflows/docs-deploy.yml index 5a3596bf7a..8d2525c1f3 100644 --- a/.github/workflows/docs-deploy.yml +++ b/.github/workflows/docs-deploy.yml @@ -40,7 +40,7 @@ concurrency: jobs: publish: if: github.repository == 'meshtastic/Meshtastic-Android' - runs-on: ubuntu-24.04 + runs-on: ubuntu-26.04 timeout-minutes: 45 steps: - name: Checkout @@ -56,9 +56,9 @@ jobs: develocity_access_key: ${{ secrets.DEVELOCITY_ACCESS_KEY }} - name: Setup Ruby - uses: ruby/setup-ruby@95ef2b042f9d7a56d8268cba8559e2842e2ad01b # v1.321.0 + uses: ruby/setup-ruby@a0102e0972be65f351c307e2d64b9314a57c8073 # v1.324.0 with: - ruby-version: '4.0.6' + ruby-version: '4.0.7' bundler-cache: true working-directory: docs diff --git a/.github/workflows/docs-quality.yml b/.github/workflows/docs-quality.yml index 9211a0da5f..bac2bf06e2 100644 --- a/.github/workflows/docs-quality.yml +++ b/.github/workflows/docs-quality.yml @@ -35,7 +35,7 @@ jobs: # Lightweight, but it fetches a Node toolchain, so it takes the ARM tier rather than # ubuntu-slim. No workflow currently runs Node; ubuntu-slim's suitability for setup-node # is untested and this is not the place to find out. - runs-on: ubuntu-24.04-arm + runs-on: ubuntu-26.04-arm timeout-minutes: 10 steps: diff --git a/.github/workflows/docs-release.yml b/.github/workflows/docs-release.yml index faa5a02a03..7363a1ee3b 100644 --- a/.github/workflows/docs-release.yml +++ b/.github/workflows/docs-release.yml @@ -41,7 +41,7 @@ concurrency: jobs: publish: if: github.repository == 'meshtastic/Meshtastic-Android' - runs-on: ubuntu-24.04 + runs-on: ubuntu-26.04 timeout-minutes: 45 steps: - name: Checkout @@ -86,9 +86,9 @@ jobs: develocity_access_key: ${{ secrets.DEVELOCITY_ACCESS_KEY }} - name: Setup Ruby - uses: ruby/setup-ruby@95ef2b042f9d7a56d8268cba8559e2842e2ad01b # v1.321.0 + uses: ruby/setup-ruby@a0102e0972be65f351c307e2d64b9314a57c8073 # v1.324.0 with: - ruby-version: '4.0.6' + ruby-version: '4.0.7' bundler-cache: true working-directory: docs diff --git a/.github/workflows/main-check.yml b/.github/workflows/main-check.yml index 6ecbc1f9e1..6a84f466ab 100644 --- a/.github/workflows/main-check.yml +++ b/.github/workflows/main-check.yml @@ -52,7 +52,7 @@ jobs: # Android APKs may still have built fine — attempt the snapshot regardless. If the # APK build itself failed, the artifact download below fails and this job goes red. if: github.repository == 'meshtastic/Meshtastic-Android' && !cancelled() - runs-on: ubuntu-24.04-arm + runs-on: ubuntu-26.04-arm # ~1.5 GB of assets, uploaded one at a time with up to 3 attempts each (see "Upload # snapshot assets"). Sized for the worst case that still succeeds -- every asset failing # twice before landing -- which is 45 uploads plus 7m30s of backoff. The 2026-08-26 diff --git a/.github/workflows/merge-queue.yml b/.github/workflows/merge-queue.yml index 71a2155039..8fbab45724 100644 --- a/.github/workflows/merge-queue.yml +++ b/.github/workflows/merge-queue.yml @@ -19,12 +19,12 @@ jobs: # new merge group but does NOT cancel the workflow runs of the destroyed one. Those stale # runs sit queued/running and starve the runner pool. Cancel any older merge-queue run # for the same PR — only the newest merge group per PR is ever valid. - # No checkout, no toolchain — just gh api + jq. ubuntu-slim starts sooner than a VM, which - # matters most here: every second before this runs is a second the superseded run keeps a slot. + # No checkout, no toolchain — just gh api + jq. Runs on the same label as check-changes: + # ubuntu-slim is a separate, smaller pool, and a gate job waiting on it holds the whole queue. cancel-superseded: name: Cancel Superseded Queue Runs if: github.repository == 'meshtastic/Meshtastic-Android' - runs-on: ubuntu-slim + runs-on: ubuntu-26.04-arm timeout-minutes: 5 permissions: actions: write @@ -44,7 +44,10 @@ jobs: --jq ".workflow_runs[] | select(.head_branch | startswith(\"$PR_PREFIX\")) | select(.id < ${{ github.run_id }}) | .id" done | sort -u | while read -r run_id; do echo "Cancelling superseded run $run_id" + # A graceful cancel is delivered to the runner, so a run still waiting for one + # never receives it. force-cancel ends the run at the API regardless. gh run cancel "$run_id" --repo "${{ github.repository }}" || true + gh api -X POST "repos/${{ github.repository }}/actions/runs/${run_id}/force-cancel" || true done # Docs-only queue entries (changelog updates, markdown fixes) cannot affect the build; @@ -53,7 +56,7 @@ jobs: check-changes: name: Check Changes if: github.repository == 'meshtastic/Meshtastic-Android' - runs-on: ubuntu-24.04-arm + runs-on: ubuntu-26.04-arm timeout-minutes: 5 outputs: android: ${{ steps.filter.outputs.android }} @@ -96,11 +99,12 @@ jobs: upload_artifacts: false secrets: inherit - # Pure gate job: no checkout, no toolchain, just reads `needs` results. ubuntu-slim is a - # container-backed single-CPU runner that starts faster than a VM (15-min job cap, x64 only). + # Pure gate job: no checkout, no toolchain, just reads `needs` results. It is the required + # check, so it runs on the label the rest of the workflow already gets slots on, not on + # ubuntu-slim's separate pool: an aggregator queued behind slim blocks a finished build. check-workflow-status: name: Check Workflow Status - runs-on: ubuntu-slim + runs-on: ubuntu-26.04-arm timeout-minutes: 5 permissions: {} needs: diff --git a/.github/workflows/play-listing.yml b/.github/workflows/play-listing.yml new file mode 100644 index 0000000000..052e7c745c --- /dev/null +++ b/.github/workflows/play-listing.yml @@ -0,0 +1,61 @@ +name: Play Store Listing + +# Uploads fastlane/metadata/android - every locale's title, descriptions, +# feature graphic, icon and screenshots - to the Google Play listing. Manual +# only: a listing commit is a Play submission, and a submission sent for review +# cancels and restarts any release review in flight. Nothing here touches a +# build or a track. +# +# The default is a dry run. Play validates the whole edit (unknown locales, +# overlength text, bad image dimensions) and discards it; tick `publish` to +# commit the edit instead. Even then it is held as "changes not sent for +# review" - send it from the Play Console when no release review is running. + +on: + workflow_dispatch: + inputs: + publish: + description: "Commit the listing to Play (unticked = validate only, nothing changes)" + type: boolean + default: false + +permissions: + contents: read + +# Two edits on the same listing race; the later one wins and the earlier one +# is silently discarded. +concurrency: + group: play-listing + cancel-in-progress: false + +jobs: + upload: + runs-on: ubuntu-slim + timeout-minutes: 15 + steps: + - name: Checkout code + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + + # Fails before any credential is written: a bad locale directory is + # rejected by the Play API mid-upload, after earlier locales have already + # been written, and an overlength string is rejected the same way. + - name: Check store metadata + run: python3 scripts/check-store-metadata.py + + - name: Set up Ruby + uses: ruby/setup-ruby@a0102e0972be65f351c307e2d64b9314a57c8073 # v1.324.0 + with: + ruby-version: '4.0.7' + bundler-cache: true + + - name: Decode Play Store credentials + run: echo '${{ secrets.GOOGLE_PLAY_JSON_KEY }}' > fastlane/play-store-credentials.json + + - name: Upload listing + env: + VALIDATE_ONLY: ${{ inputs.publish && 'false' || 'true' }} + run: bundle exec fastlane play_listing validate_only:"$VALIDATE_ONLY" + + - name: Clean up credentials + if: always() + run: rm -f fastlane/play-store-credentials.json diff --git a/.github/workflows/promote.yml b/.github/workflows/promote.yml index 41b09580cf..cca261a3c3 100644 --- a/.github/workflows/promote.yml +++ b/.github/workflows/promote.yml @@ -72,7 +72,7 @@ permissions: jobs: prepare-build-info: - runs-on: ubuntu-24.04-arm + runs-on: ubuntu-26.04-arm timeout-minutes: 10 outputs: APP_VERSION_NAME: ${{ steps.prep_version.outputs.APP_VERSION_NAME }} @@ -115,7 +115,7 @@ jobs: shell: bash promote-release: - runs-on: ubuntu-24.04-arm + runs-on: ubuntu-26.04-arm timeout-minutes: 30 needs: [ prepare-build-info ] env: @@ -129,9 +129,9 @@ jobs: ref: ${{ inputs.commit_sha || inputs.tag_name }} - name: Set up Ruby - uses: ruby/setup-ruby@95ef2b042f9d7a56d8268cba8559e2842e2ad01b # v1.321.0 + uses: ruby/setup-ruby@a0102e0972be65f351c307e2d64b9314a57c8073 # v1.324.0 with: - ruby-version: '4.0.6' + ruby-version: '4.0.7' bundler-cache: true - name: Decode Play Store credentials @@ -206,7 +206,7 @@ jobs: run: rm -f fastlane/play-store-credentials.json update-github-release: - runs-on: ubuntu-24.04-arm + runs-on: ubuntu-26.04-arm timeout-minutes: 10 needs: [ prepare-build-info, promote-release ] # actions: write is scoped here — only this job's publish-workflow @@ -408,7 +408,7 @@ jobs: # promoted to homebrew/cask, replace the tap PR with `brew bump-cask-pr`. update-homebrew-cask: if: ${{ inputs.channel == 'production' }} - runs-on: ubuntu-24.04-arm + runs-on: ubuntu-26.04-arm timeout-minutes: 15 needs: [ update-github-release ] steps: diff --git a/.github/workflows/pull-request.yml b/.github/workflows/pull-request.yml index 8c13908902..aa80b706d4 100644 --- a/.github/workflows/pull-request.yml +++ b/.github/workflows/pull-request.yml @@ -18,7 +18,7 @@ jobs: # compute, are the scarce resource during queue bursts). check-changes: if: github.repository == 'meshtastic/Meshtastic-Android' && !( github.head_ref == 'scheduled-updates' || github.head_ref == 'l10n_main' ) - runs-on: ubuntu-24.04-arm + runs-on: ubuntu-26.04-arm timeout-minutes: 10 outputs: android: ${{ steps.filter.outputs.android }} @@ -88,6 +88,7 @@ jobs: - 'feature/**' - 'screenshot-tests/**' - 'docs-screenshots/**' + - 'marketing-screenshots/**' # Shared build infrastructure - 'build-logic/**' - 'config/**' @@ -170,6 +171,7 @@ jobs: ':baselineprofile', ':core:konsist', ':docs-screenshots', + ':marketing-screenshots', ':screenshot-tests', } @@ -232,6 +234,7 @@ jobs: # and remove it from this list. no_tests_yet = { ':core:di', + ':marketing-screenshots', # store-listing screenshot generator, a JVM program ':core:nfc', ':core:resources', } @@ -277,7 +280,7 @@ jobs: check-metadata: name: Check Store Metadata if: github.repository == 'meshtastic/Meshtastic-Android' - runs-on: ubuntu-24.04-arm + runs-on: ubuntu-26.04-arm timeout-minutes: 5 permissions: contents: read @@ -303,8 +306,16 @@ jobs: run: ./scripts/verify-rb-selftest.sh - name: Self-test verify-abi-parity run: ./scripts/verify-abi-parity-selftest.sh - - name: Validate store listing metadata lengths - run: python3 scripts/check-metadata-length.py + # Locale codes, lengths and HTML for all three stores that read the tree. supply + # uploads every locale directory it finds and neither it nor the Crowdin CLI + # validates the names, so an unmapped code is only rejected by the Play API, partway + # through an upload; F-Droid and IzzyOnDroid read the tree straight from git. + - name: Validate store listing metadata + run: python3 scripts/check-store-metadata.py + # default.txt is rendered from metainfo.xml; a PR that edits one without the + # other ships a listing that no longer says what the release did. + - name: Check Play what's-new matches AppStream + run: python3 scripts/sync-play-changelog.py --check # Compose Multiplatform does not strip Android-style \" / \' escapes, so a # backslash written before a quote renders literally in the UI (PR #6357). # Guards the English source strings; locale mirrors are cleaned upstream by @@ -341,6 +352,18 @@ jobs: exit 1 fi + # Flathub runs this exact command (flatpak-builder-lint checks/metainfo.py) and turns any + # non-zero exit into appstream-failed-validation at publish time. Nothing here ran it, so a + # malformed reached Flathub before CI ever saw it. Catches a whitespace-only or + # empty-

description; a truly empty and a release with no + # description at all both still validate, so this is hygiene, not a prose gate. + - name: Validate AppStream metainfo + run: | + sudo apt-get update -qq + sudo apt-get install -y -qq appstream + appstreamcli validate --no-net \ + desktopApp/packaging/linux/org.meshtastic.MeshtasticDesktop.metainfo.xml + # 2. VALIDATION & BUILD: Delegate to reusable-check.yml # Coverage stays off for PRs to keep feedback fast (< 10 mins); mainline coverage comes # from main-check. Desktop *compilation* is covered on every PR by :desktopApp:test in the @@ -369,11 +392,12 @@ jobs: secrets: inherit # 3. WORKFLOW STATUS: Ensures required checks are satisfied - # Pure gate job: no checkout, no toolchain, just reads `needs` results. ubuntu-slim is a - # container-backed single-CPU runner that starts faster than a VM (15-min job cap, x64 only). + # Pure gate job: no checkout, no toolchain, just reads `needs` results. It is the required + # check, so it runs on the label the rest of the workflow already gets slots on, not on + # ubuntu-slim's separate pool: an aggregator queued behind slim blocks a finished build. check-workflow-status: name: Check Workflow Status - runs-on: ubuntu-slim + runs-on: ubuntu-26.04-arm timeout-minutes: 5 permissions: {} needs: [check-changes, check-metadata, validate-and-build] diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 748c69bb4a..eff31d604c 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -85,7 +85,7 @@ permissions: jobs: prepare-build-info: - runs-on: ubuntu-24.04-arm + runs-on: ubuntu-26.04-arm timeout-minutes: 10 outputs: APP_VERSION_NAME: ${{ steps.prep_version.outputs.APP_VERSION_NAME }} @@ -127,7 +127,7 @@ jobs: shell: bash release-google: - runs-on: ubuntu-24.04 + runs-on: ubuntu-26.04 timeout-minutes: 90 needs: [prepare-build-info] steps: @@ -168,9 +168,9 @@ jobs: echo "$GOOGLE_PLAY_JSON_KEY" > ./fastlane/play-store-credentials.json - name: Setup Fastlane - uses: ruby/setup-ruby@95ef2b042f9d7a56d8268cba8559e2842e2ad01b # v1.321.0 + uses: ruby/setup-ruby@a0102e0972be65f351c307e2d64b9314a57c8073 # v1.324.0 with: - ruby-version: '4.0.6' + ruby-version: '4.0.7' bundler-cache: true - name: Build and Deploy Google Play to Internal Track with Fastlane @@ -211,7 +211,7 @@ jobs: subject-path: androidApp/build/outputs/apk/google/release/*.apk release-fdroid: - runs-on: ubuntu-24.04 + runs-on: ubuntu-26.04 timeout-minutes: 90 needs: [prepare-build-info] steps: @@ -239,9 +239,9 @@ jobs: echo "$KEYSTORE_PROPERTIES" > ./keystore.properties - name: Setup Fastlane - uses: ruby/setup-ruby@95ef2b042f9d7a56d8268cba8559e2842e2ad01b # v1.321.0 + uses: ruby/setup-ruby@a0102e0972be65f351c307e2d64b9314a57c8073 # v1.324.0 with: - ruby-version: '4.0.6' + ruby-version: '4.0.7' bundler-cache: true - name: Build F-Droid with Fastlane @@ -485,7 +485,7 @@ jobs: release-flatpak-src: if: ${{ inputs.build_flatpak_src }} - runs-on: ubuntu-24.04 + runs-on: ubuntu-26.04 timeout-minutes: 30 needs: [create-flatpak-src] steps: @@ -523,7 +523,7 @@ jobs: github-release: if: ${{ !cancelled() && !failure() }} - runs-on: ubuntu-24.04-arm + runs-on: ubuntu-26.04-arm timeout-minutes: 15 needs: - prepare-build-info @@ -575,6 +575,21 @@ jobs: ${PREV:+-f previous_tag_name="$PREV"} \ --jq '.body' > release-notes.md + # The generated notes are a list of PR titles - accurate, and not what a user reads. + # metainfo.xml already carries one hand-written paragraph per version, required by + # pull-request.yml on the VERSION_NAME_BASE bump, so reuse it rather than asking for + # the same sentence twice. When it is missing the notes gain a rendered warning + # instead of silence, and every release here is created as a draft, so that warning + # is seen before anyone publishes. + - name: Prepend release highlights + run: | + python3 scripts/release-highlights.py \ + desktopApp/packaging/linux/org.meshtastic.MeshtasticDesktop.metainfo.xml \ + config.properties \ + release-notes.md + echo "--- release notes ---" + cat release-notes.md + - name: Create or Update GitHub Release uses: softprops/action-gh-release@efb35369e0ad2afab669f228072c1b0d510eae64 # v3 with: diff --git a/.github/workflows/reusable-check.yml b/.github/workflows/reusable-check.yml index cbe605ddfd..2c0f759da6 100644 --- a/.github/workflows/reusable-check.yml +++ b/.github/workflows/reusable-check.yml @@ -71,7 +71,7 @@ jobs: # ── Lint & Static Analysis ────────────────────────────────────────── lint-check: - runs-on: ubuntu-24.04 + runs-on: ubuntu-26.04 permissions: contents: read pull-requests: write @@ -106,11 +106,11 @@ jobs: install_jetbrains_jdk: 'true' - name: Lint, Analysis & KMP Smoke Compile - run: ./gradlew spotlessCheck detekt androidApp:lintFdroidDebug androidApp:lintGoogleDebug core:barcode:lintFdroidDebug core:barcode:lintGoogleDebug kmpSmokeCompile -Pci=true --continue + run: ./gradlew spotlessCheck detekt androidApp:lintFdroidDebug androidApp:lintGoogleDebug core:barcode:lintFdroidDebug core:barcode:lintGoogleDebug -Pci=true --continue # ── Screenshot Test Validation ────────────────────────────────────── screenshot-check: - runs-on: ubuntu-24.04 + runs-on: ubuntu-26.04 permissions: contents: read pull-requests: write @@ -159,7 +159,7 @@ jobs: # Only runs in the merge queue (not PRs, to keep PR feedback fast; not main pushes, # which are the same merge commit the queue just verified). rb-check: - runs-on: ubuntu-24.04 + runs-on: ubuntu-26.04 permissions: contents: read pull-requests: write @@ -197,13 +197,15 @@ jobs: # These lists are hand-maintained; pull-request.yml's check-changes job # guards them against drift (every module in settings.gradle.kts with test # sources must appear here or be explicitly exempted). - # shard-core: remaining core:* KMP module tests (allTests) + # shard-core: remaining core:* KMP module tests (allTests), plus kmpSmokeCompile, + # which is the only iOS compile :core:di, :core:nfc and :core:resources + # get. It lived in lint-check until a cold build there blew the 30m budget. # shard-feature: feature:* KMP module tests + :core:service # shard-app: Pure-Android/JVM tests (androidApp, desktopApp, # core:barcode, feature:widget) # + :core:database + :core:network test-shards: - runs-on: ubuntu-24.04 + runs-on: ubuntu-26.04 permissions: contents: read pull-requests: write @@ -230,6 +232,7 @@ jobs: :core:takserver:allTests :core:testing:allTests :core:ui:allTests + kmpSmokeCompile kover: >- :core:ble:koverXmlReport :core:common:koverXmlReport @@ -349,7 +352,7 @@ jobs: # which Codecov flag its coverage lands under. - name: Upload test results to Codecov (core) if: ${{ !cancelled() }} - uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7 + uses: codecov/codecov-action@303a32d7a59b442fa8d48b6a1cc6825c09c847a5 # v7 with: token: ${{ secrets.CODECOV_TOKEN }} slug: meshtastic/Meshtastic-Android @@ -361,7 +364,7 @@ jobs: - name: Upload test results to Codecov (feature) if: ${{ !cancelled() }} - uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7 + uses: codecov/codecov-action@303a32d7a59b442fa8d48b6a1cc6825c09c847a5 # v7 with: token: ${{ secrets.CODECOV_TOKEN }} slug: meshtastic/Meshtastic-Android @@ -373,7 +376,7 @@ jobs: - name: Upload test results to Codecov (app) if: ${{ !cancelled() }} - uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7 + uses: codecov/codecov-action@303a32d7a59b442fa8d48b6a1cc6825c09c847a5 # v7 with: token: ${{ secrets.CODECOV_TOKEN }} slug: meshtastic/Meshtastic-Android @@ -385,7 +388,7 @@ jobs: - name: Upload test results to Codecov (desktop) if: ${{ !cancelled() }} - uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7 + uses: codecov/codecov-action@303a32d7a59b442fa8d48b6a1cc6825c09c847a5 # v7 with: token: ${{ secrets.CODECOV_TOKEN }} slug: meshtastic/Meshtastic-Android @@ -397,7 +400,7 @@ jobs: - name: Upload coverage to Codecov (core) if: ${{ !cancelled() && inputs.run_coverage }} - uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7 + uses: codecov/codecov-action@303a32d7a59b442fa8d48b6a1cc6825c09c847a5 # v7 with: token: ${{ secrets.CODECOV_TOKEN }} slug: meshtastic/Meshtastic-Android @@ -408,7 +411,7 @@ jobs: - name: Upload coverage to Codecov (feature) if: ${{ !cancelled() && inputs.run_coverage }} - uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7 + uses: codecov/codecov-action@303a32d7a59b442fa8d48b6a1cc6825c09c847a5 # v7 with: token: ${{ secrets.CODECOV_TOKEN }} slug: meshtastic/Meshtastic-Android @@ -419,7 +422,7 @@ jobs: - name: Upload coverage to Codecov (app) if: ${{ !cancelled() && inputs.run_coverage }} - uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7 + uses: codecov/codecov-action@303a32d7a59b442fa8d48b6a1cc6825c09c847a5 # v7 with: token: ${{ secrets.CODECOV_TOKEN }} slug: meshtastic/Meshtastic-Android @@ -430,7 +433,7 @@ jobs: - name: Upload coverage to Codecov (desktop) if: ${{ !cancelled() && inputs.run_coverage }} - uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7 + uses: codecov/codecov-action@303a32d7a59b442fa8d48b6a1cc6825c09c847a5 # v7 with: token: ${{ secrets.CODECOV_TOKEN }} slug: meshtastic/Meshtastic-Android @@ -454,7 +457,7 @@ jobs: # ── Android Build ──────────────────────────────────────────────────── # Also generates the dependency graph — assembling both flavors resolves the widest set. android-check: - runs-on: ubuntu-24.04 + runs-on: ubuntu-26.04 # No permissions block on purpose: inherits the caller's. main grants contents: write # (graph submit); PRs grant read (upload only). timeout-minutes: 60 diff --git a/.github/workflows/scheduled-baseline.yml b/.github/workflows/scheduled-baseline.yml index e19c21f44b..a01dbad6e1 100644 --- a/.github/workflows/scheduled-baseline.yml +++ b/.github/workflows/scheduled-baseline.yml @@ -16,7 +16,7 @@ concurrency: jobs: regenerate: - runs-on: ubuntu-24.04 + runs-on: ubuntu-26.04 timeout-minutes: 90 if: github.repository == 'meshtastic/Meshtastic-Android' permissions: diff --git a/.github/workflows/scheduled-updates.yml b/.github/workflows/scheduled-updates.yml index 287d9c7fbc..6608419f76 100644 --- a/.github/workflows/scheduled-updates.yml +++ b/.github/workflows/scheduled-updates.yml @@ -15,7 +15,7 @@ concurrency: jobs: update_assets: - runs-on: ubuntu-24.04 + runs-on: ubuntu-26.04 timeout-minutes: 30 if: github.repository == 'meshtastic/Meshtastic-Android' permissions: @@ -265,7 +265,7 @@ jobs: fi - name: Sync with Crowdin - uses: crowdin/github-action@0d5670f539973aea2f01abce61a8989934df0025 # v3 + uses: crowdin/github-action@9af557de76d70c480f88065d336f445a362f402b # v3 with: base_url: 'https://meshtastic.crowdin.com/api/v2' config: 'crowdin.yml' @@ -286,13 +286,14 @@ jobs: - name: Fix file permissions run: sudo chown -R "$USER:$USER" . - # Early warning for overlength store-listing translations just pulled from - # Crowdin. Non-blocking on purpose: a hard failure here would abort the job - # and stop the PR from being created. The hard gate is the check-metadata - # job in pull-request.yml, which runs against the PR this workflow opens. - - name: Check store metadata lengths + # Early warning for store-listing translations just pulled from Crowdin + # (overlength text, unmapped locale codes, forbidden HTML). Non-blocking on + # purpose: a hard failure here would abort the job and stop the PR from + # being created. The hard gate is the same script in pull-request.yml, + # which runs against the PR this workflow opens. + - name: Check store metadata continue-on-error: true - run: python3 scripts/check-metadata-length.py + run: python3 scripts/check-store-metadata.py - name: Build PR body id: pr_body diff --git a/.github/workflows/update-changelog.yml b/.github/workflows/update-changelog.yml index 4d904e619f..c50a301ecf 100644 --- a/.github/workflows/update-changelog.yml +++ b/.github/workflows/update-changelog.yml @@ -1,11 +1,9 @@ name: Update Changelog +# Manual only: promote.yml stamps the released section at release time on its +# own. Dispatch this to refresh the [Unreleased] section between releases. on: - push: - branches: - - main - paths-ignore: - - 'CHANGELOG.md' + workflow_dispatch: permissions: contents: write @@ -18,6 +16,9 @@ concurrency: jobs: update-changelog: + # The content comes from main whatever ref was dispatched; refuse the rest so a + # stale-branch dispatch cannot open a PR built against a different tip. + if: github.ref == 'refs/heads/main' # ubuntu-slim: git + gh + coreutils + stdlib python3 is the whole toolchain here. Note this job checks out # full history below and slim is single-CPU, so the clone is slower than on a VM — and # slim's 15-minute platform cap is hard, matching the timeout already set here. A run that @@ -37,6 +38,8 @@ jobs: # Find the latest production tag (vX.Y.Z) PROD=$(git tag --list 'v[0-9]*.[0-9]*.[0-9]*' --sort=-v:refname | grep -E '^v[0-9]+\.[0-9]+\.[0-9]+$' | head -1) echo "prod=${PROD:-}" >> "$GITHUB_OUTPUT" + # The tip of the checked-out main; github.sha is the dispatched ref's tip. + echo "head=$(git rev-parse HEAD)" >> "$GITHUB_OUTPUT" if [ -z "$PROD" ]; then echo "No production tag found. Skipping." @@ -83,7 +86,7 @@ jobs: env: GH_TOKEN: ${{ github.token }} REPO: ${{ github.repository }} - HEAD_SHA: ${{ github.sha }} + HEAD_SHA: ${{ steps.tags.outputs.head }} PROD_TAG: ${{ steps.tags.outputs.prod }} INTERNAL_TAG: ${{ steps.tags.outputs.internal }} CLOSED_TAG: ${{ steps.tags.outputs.closed }} @@ -284,7 +287,7 @@ jobs: else gh pr create \ --title "docs: update CHANGELOG.md" \ - --body "Automated changelog update from push to main." \ + --body "Automated changelog refresh, dispatched from main." \ --head "$BRANCH" \ --base main \ --label "automation" \ diff --git a/.github/workflows/verify-flatpak.yml b/.github/workflows/verify-flatpak.yml index 55567c6c59..685ed57802 100644 --- a/.github/workflows/verify-flatpak.yml +++ b/.github/workflows/verify-flatpak.yml @@ -1,13 +1,24 @@ name: Verify Flatpak Offline Build on: + # Changing the generator or the manifest is the one case worth gating pre-merge: + # break it and every later run is red, with nothing else covering it. pull_request: branches: [ main ] paths: - 'scripts/verify-flatpak/**' + - '.github/workflows/verify-flatpak.yml' + # The dependency surface the manifest captures is verified post-merge. This is not a + # required check and never ran in the merge queue, so per-PR it cost ~2 runner slots + # for a signal that blocks nothing; post-merge still catches a break within one merge, + # and `main` itself was previously never verified at all. + push: + branches: [ main ] + paths: + - 'scripts/verify-flatpak/**' + - '.github/workflows/verify-flatpak.yml' - 'build.gradle.kts' - 'settings.gradle.kts' - - '.github/workflows/verify-flatpak.yml' # The desktop module's build config shapes the uber jar the flatpak wraps. - 'desktopApp/**' # The offline manifest pins the Gradle distribution independently of the wrapper — @@ -15,17 +26,22 @@ on: - 'gradle/wrapper/**' # build.gradle.kts reads compose-multiplatform from the catalog (#6911), so a # catalog-only bump changes the manifest's platform URLs. Deliberately the whole - # file and not a key filter: nearly every Renovate PR lands here so this over-runs, - # but a filter naming today's keys goes stale silently the moment the manifest - # reads another one — which is the failure #6911 existed to remove. + # file and not a key filter: a filter naming today's keys goes stale silently the + # moment the manifest reads another one — the failure #6911 existed to remove. - 'gradle/libs.versions.toml' + # Drift no path filter can see: a Flathub runtime bump, or an upstream artifact that + # moved or vanished. Nothing in this repo changes, so no other trigger would fire. + schedule: + - cron: '0 4 * * *' workflow_dispatch: permissions: contents: read concurrency: - group: flatpak-verify-${{ github.event.pull_request.number || github.ref }} + # Keyed on the event as well: a nightly and a push to main share github.ref, and + # without it cancel-in-progress lets one kill the other. + group: flatpak-verify-${{ github.event_name }}-${{ github.event.pull_request.number || github.ref }} cancel-in-progress: true jobs: @@ -34,6 +50,8 @@ jobs: # reusable-check.yml documents avoiding. Each leg now captures sources on its own arch. verify-flatpak: name: Verify Flatpak Offline Build (${{ matrix.arch }}) + # Matches the other scheduled workflows: without it every fork runs the nightly. + if: github.repository == 'meshtastic/Meshtastic-Android' runs-on: ${{ matrix.arch == 'aarch64' && 'ubuntu-24.04-arm' || 'ubuntu-24.04' }} timeout-minutes: 60 strategy: @@ -70,7 +88,7 @@ jobs: - uses: actions/setup-java@de7274f081f381c8f8158605e0321c36c376e2e6 # v6 with: distribution: temurin - java-version: 25 + java-version: 25.0.4+101.0.LTS token: ${{ github.token }} # JBR resolves its version list via the GitHub API — unauthenticated requests @@ -141,9 +159,9 @@ jobs: echo "### ✅ Offline Flatpak build succeeded (${{ matrix.arch }})" >> "$GITHUB_STEP_SUMMARY" # The build succeeding IS the verification; bundling costs ~2 min per arch for an - # artifact nobody installs from a PR. Kept for dispatch, where a tester wants it. + # artifact nobody installs from CI. Dispatch only, where a tester asked for it. - name: Export .flatpak bundle - if: github.event_name != 'pull_request' + if: github.event_name == 'workflow_dispatch' working-directory: ${{ runner.temp }}/org.meshtastic.MeshtasticDesktop env: ARCH: ${{ matrix.arch }} @@ -153,7 +171,7 @@ jobs: --runtime-repo=https://flathub.org/repo/flathub.flatpakrepo - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 - if: github.event_name != 'pull_request' + if: github.event_name == 'workflow_dispatch' with: name: meshtastic-desktop-flatpak-${{ matrix.arch }} path: ${{ runner.temp }}/org.meshtastic.MeshtasticDesktop/org.meshtastic.MeshtasticDesktop.${{ matrix.arch }}.flatpak diff --git a/.ruby-version b/.ruby-version index d13e837c8e..43beb4001b 100644 --- a/.ruby-version +++ b/.ruby-version @@ -1 +1 @@ -4.0.6 +4.0.7 diff --git a/.skills/compose-ui/strings-index.txt b/.skills/compose-ui/strings-index.txt index 0f686531c9..b00eef67ae 100644 --- a/.skills/compose-ui/strings-index.txt +++ b/.skills/compose-ui/strings-index.txt @@ -72,6 +72,7 @@ altitude always_point_north ambient_lighting ambient_lighting_config +ammonium analytics_notice analytics_okay analytics_platforms @@ -107,6 +108,8 @@ aqi_unhealthy aqi_unhealthy_sensitive aqi_value_with_severity aqi_very_unhealthy +archived_channel +archived_channel_read_only are_you_sure are_you_sure_change_default audio @@ -126,6 +129,7 @@ bandwidth_unsupported_summary baro_pressure battery battery_ina_2xx_i2c_address +biochemical_oxygen_demand ble_devices ble_rssi_threshold_defaults_to_80 ble_scan_needs_location_services @@ -196,6 +200,7 @@ channel_name channel_url channel_utilization channels +chemical_oxygen_demand ### CHIRPY ### chirpy chirpy_assistant_title @@ -452,6 +457,7 @@ discovery_restoring_preset discovery_scan_complete discovery_scan_failed discovery_scan_history +discovery_scan_in_progress discovery_scan_incomplete discovery_scan_progress discovery_scan_summary @@ -503,6 +509,7 @@ display_config display_mode display_time_in_12h_format display_units +dissolved_oxygen ### DISTANCE ### distance distance_filters @@ -534,6 +541,9 @@ doc_keywords_telemetry doc_keywords_translate doc_keywords_units doc_keywords_widget +doc_loading +doc_no_documentation +doc_no_results doc_search_placeholder doc_section_developer doc_section_user @@ -574,6 +584,7 @@ echo_enabled edit edit_custom_tile_source eight_hours +electrical_conductivity elevation_suffix ### EMOJI ### emoji_category_activities @@ -707,6 +718,7 @@ firmware_update_filename_unavailable firmware_update_flashing firmware_update_hang_tight firmware_update_hash_rejected +firmware_update_in_progress firmware_update_invalid_local_file_detail firmware_update_keep_device_close firmware_update_latest @@ -915,6 +927,8 @@ led_heartbeat led_state legacy_admin_channel library_count +license +license_notice ### LICENSED ### licensed_amateur_radio licensed_amateur_radio_text @@ -922,6 +936,8 @@ licensed_mode_enable_confirm licensed_mode_enable_title licensed_mode_enable_warning licensed_mode_enable_warning_signed +lightning_distance +lightning_strikes_1h ### LOAD ### load load_15_min @@ -1270,6 +1286,8 @@ new_node_seen new_nodes next nfc_disabled +nitrate +nitrogen ### NO ### no_bluetooth_devices_hint no_bluetooth_devices_seen @@ -1299,7 +1317,9 @@ node_filter_exclude_unheard node_filter_ignored node_filter_include_unknown node_filter_only_direct +node_filter_only_encrypted node_filter_only_online +node_filter_only_signed node_filter_placeholder node_filter_show_ignored node_filter_title @@ -1409,6 +1429,7 @@ open_source_libraries open_wifi_settings options orient_north +orp ### OUTPUT ### output_buzzer_gpio output_duration_milliseconds @@ -1471,11 +1492,22 @@ permissions_nothing_needed permissions_show phone_location phone_location_description +phosphorus pin_selected play plurals_hours plurals_minutes plurals_seconds +### PM ### +pm_sensor_status +pm_status_co2_error +pm_status_fan_error +pm_status_fan_speed_warning +pm_status_gas_error +pm_status_hcho_error +pm_status_pm_error +pm_status_rht_error +pm_status_unknown_flags pm1_0 pm10 pm2_5 @@ -1486,6 +1518,7 @@ position_enabled position_flags position_log position_packet +potassium ### POWER ### power power_config @@ -1657,6 +1690,7 @@ routing_error_too_large rssi rssi_definition rsyslog_server +salinity sample_message sats ### SAVE ### @@ -1701,11 +1735,16 @@ security_icon_insecure_no_precise security_icon_insecure_precise_only security_icon_secure security_icon_warning_precise_mqtt +security_legend_any_version +security_legend_legacy_firmware +security_legend_signing_firmware security_signed_message_info security_signed_node security_signed_node_desc security_signed_node_help security_signed_verified +security_verified_contact +security_verified_contact_help select select_all selected @@ -1732,11 +1771,18 @@ set_your_region settings ### SHARE ### share -share_channels_qr +share_action +share_by_tap_subtext +share_channels +share_channels_add_hint +share_channels_replace_hint +share_channels_subject share_connected_node share_contact +share_contact_subject share_location share_location_description +share_qr_subtext share_to share_your_location_in_real_time shared_contact_no_public_key @@ -1794,7 +1840,10 @@ smart_position snr snr_definition soil_moisture +soil_ph soil_temperature +soil_water_metrics +solar_irradiance speed speed_kmh speed_mph @@ -1949,6 +1998,7 @@ transport_tcp transport_udp transport_usb triple_click_adhoc_ping +turbidity twenty_four_hours two_days two_weeks @@ -2020,6 +2070,7 @@ voltage wait_for_bluetooth_duration_seconds wake_on_tap_or_motion warning +water_ph waypoint_delete waypoint_edit waypoint_new diff --git a/.skills/design-standards/SKILL.md b/.skills/design-standards/SKILL.md index 5d83f59821..61f4492841 100644 --- a/.skills/design-standards/SKILL.md +++ b/.skills/design-standards/SKILL.md @@ -4,7 +4,7 @@ Android-specific guidance for applying the Meshtastic design standards. All visual rules, color palettes, accessibility requirements, and cross-platform conventions live upstream. > **Source of truth:** [`meshtastic/design/standards/`](https://github.com/meshtastic/design/tree/master/standards) -> Read `meshtastic_design_standards_latest.md` (a pointer to the current versioned spec — `meshtastic_design_standards_v1_4.md` at time of writing) for the full spec (colors, M3 mapping, accessibility, units/locale, agent checklist). +> That index names the current version; open it from there for the full spec (colors, M3 mapping, accessibility, units/locale, agent checklist). Don't fetch `meshtastic_design_standards_latest.md` over HTTP — it is a symlink, and GitHub returns its target's filename, 35 bytes, with HTTP 200. > If this skill diverges from upstream, **upstream wins**. ## 1. How to Use the Standards diff --git a/.skills/navigation-and-di/SKILL.md b/.skills/navigation-and-di/SKILL.md index 4119a91a08..d4ac220849 100644 --- a/.skills/navigation-and-di/SKILL.md +++ b/.skills/navigation-and-di/SKILL.md @@ -12,7 +12,7 @@ This skill covers dependency injection (Koin Annotations 4.2.x) and JetBrains Na 4. **Resolution:** Resolve app-layer wrappers via `koinViewModel()` or injected bindings within Compose navigation graphs. ### Anti-Patterns -- **A1 Module Compile Safety:** Do **not** enable `compileSafety`. It is a single boolean that enables A1 per-module checks — there is no separate A3 full-graph mode. Runtime graph verification is handled by `KoinVerificationTest` and `DesktopKoinTest` instead. +- **Compile Safety Outside An Entry Point:** Do **not** enable `compileSafety` on a library module. Validation is whole-graph and runs at the `@KoinApplication` entry point, so a library validates against a graph it cannot see and reports `KOIN-D003` for definitions its consumers supply. `KoinConventionPlugin` enables it only for the modules in `KOIN_ENTRY_POINTS`. - **Default Parameters:** Do **not** expect Koin to inject default parameters automatically. The K2 plugin's `skipDefaultValues = true` behavior skips parameters with default Kotlin values. ### Koin Startup Pattern (K2 Compiler Plugin) @@ -31,7 +31,9 @@ startKoin { - `@KoinApplication` goes on a **dedicated bootstrap object**, not on a `@Module` class. - `startKoin()` (from `org.koin.plugin.module.dsl`) is a compiler plugin stub — if the plugin isn't applied, it throws `NotImplementedError`. - `stopKoin()` uses the standard runtime API (`org.koin.core.context.stopKoin`). -- `compileSafety` must stay **disabled** — it enables A1 per-module checks that break our inverted-dependency architecture. There is no separate A3 full-graph flag. +- `compileSafety` is **on at the entry points only** (`:androidApp`, `:desktopApp`). Plugin 1.1.0 replaced per-module validation with whole-graph validation, so the flag is only meaningful where the graph is assembled. A new app target must be added to `KOIN_ENTRY_POINTS` or it is never validated. +- A definition two `@Module(includes = ...)` levels below the entry point is invisible to the index. The flavor modules carry `@Configuration` as well as their `includes` for this reason; dropping the `includes` removes them from the **runtime** graph, which `KoinVerificationTest` catches. +- Hand-written DSL `module { }` definitions are not reachable by the assembled graph, which is why `:desktopApp` uses `@Module` classes. ## Navigation 3 diff --git a/.skills/new-branch/SKILL.md b/.skills/new-branch/SKILL.md index 3ab4bdc2ee..faa0da8546 100644 --- a/.skills/new-branch/SKILL.md +++ b/.skills/new-branch/SKILL.md @@ -33,8 +33,10 @@ git --no-pager log -1 --oneline ``` ## Branch Naming -Use conventional-commit style prefixes that match the PR title convention in -`.github/copilot-pull-request-instructions.md`: +Use conventional-commit style prefixes. `.specify/memory/constitution.md` +(Branch naming) is the canonical list **of prefixes**; `AGENTS.md` remains +authoritative for rules, architecture and workflow. `CONTRIBUTING.md` → +Pull Requests carries the same set contributor-facing: | Prefix | Use for | | :--- | :--- | @@ -43,6 +45,18 @@ Use conventional-commit style prefixes that match the PR title convention in | `refactor/` | Code structure changes, no behavior change | | `chore/` | Tooling, deps, CI, cleanup | | `docs/` | Documentation only | +| `build/` | Build system changes | +| `ci/` | CI workflow changes | +| `test/` | Test additions or fixes | +| `deps/` | Dependency updates | + +Spec-driven work is the exception and takes no prefix from that table: a +numeric spec prefix (`005-tak-v2-protocol`) or the timestamp form +`YYYYMMDD-HHMMSS-feature-name` that `/speckit.git.feature` creates. Both are +valid — don't reject one. + +`release/*` and `automation/*` are reserved for maintainers and automated +workflows. Keep the slug short and kebab-case, e.g. `fix/r8-animation-release`, `chore/koin-application-migration`. @@ -60,7 +74,7 @@ git push --force-with-lease Never use plain `--force`. Always `--force-with-lease` to avoid clobbering collaborator pushes. ## Post-Branch Checklist -- [ ] Branch name follows conventional prefix. +- [ ] Branch name carries a conventional prefix, or is a spec-driven name (numeric or `YYYYMMDD-HHMMSS-`) per Branch Naming above. - [ ] `ANDROID_HOME` exported (see AGENTS.md workspace bootstrap). - [ ] Optional: run `./gradlew assembleDebug` once to catch environment regressions before editing. diff --git a/.skills/speckit/SKILL.md b/.skills/speckit/SKILL.md index edf206ab25..2b4d091764 100644 --- a/.skills/speckit/SKILL.md +++ b/.skills/speckit/SKILL.md @@ -108,7 +108,7 @@ specs/ The project constitution at `.specify/memory/constitution.md` defines non-negotiable principles. All specs, plans, and tasks are validated against it during `/speckit.analyze`. -Current constitution (v1.3.6) enforces 7 principles: +Current constitution (v1.4.0) enforces 7 principles: 1. **KMP Core** — Business logic in `commonMain` only 2. **Zero Lint Tolerance** — `spotlessCheck` + `detekt` must pass diff --git a/.skills/testing-ci/SKILL.md b/.skills/testing-ci/SKILL.md index 18cf548834..6a71f078eb 100644 --- a/.skills/testing-ci/SKILL.md +++ b/.skills/testing-ci/SKILL.md @@ -106,13 +106,13 @@ CI is defined in `.github/workflows/reusable-check.yml` and structured as parall 4. **`build-desktop`** — Multi-OS matrix (`macos-latest`, `windows-latest`, `ubuntu-24.04`, `ubuntu-24.04-arm`) running `:desktopApp:packageDistributionForCurrentOS :desktopApp:proguardReleaseJars` (depends on `lint-check`). It packages the debug build type — real installers the snapshot release can ship — and pulls in `proguardReleaseJars` only so a jmods-less packaging JDK fails here rather than at release time. On Linux it then wraps jpackage's `app-image` directory into a real AppImage via `scripts/build-appimage.sh`. 5. **`screenshot-check`** — Runs `:screenshot-tests:validateDebugScreenshotTest` (the visual-regression gate) and uploads a diff report. Note: `:docs-screenshots` is intentionally NOT validated here (generate-only). 6. **`rb-check`** — Reproducible-build verification (`scripts/verify-rb.sh`). Runs **only** in the merge queue. -7. **`build-flatpak-src`** — Generates the Flatpak offline-build sources (`captureFlatpakSources`) on an `ubuntu-24.04` + `ubuntu-24.04-arm` matrix, gated on `run_desktop_flatpak_src`. Since #6919 the sources are generated inside each arch's own offline build rather than committed. +7. **`verify-flatpak`** — Lives in its own workflow (`.github/workflows/verify-flatpak.yml`), **not** in `reusable-check.yml`, and is not called by it. Generates the Flatpak offline-build sources (`captureFlatpakSources`) and then builds the flatpak fully offline, on an `ubuntu-24.04` + `ubuntu-24.04-arm` matrix. Since #6919 the sources are generated inside each arch's own offline build rather than committed. It is **not a required check** and never runs in the merge queue, so its triggers are scoped accordingly: a PR runs it only when it touches the flatpak tooling itself (`scripts/verify-flatpak/**`, the workflow), while the wider dependency surface it captures (`gradle/libs.versions.toml`, `desktopApp/**`, `gradle/wrapper/**`, the root build scripts) is verified on push to `main` plus a nightly cron. ### Runner Strategy (Four Tiers) - **`ubuntu-slim`** — The cheapest tier, and where lightweight jobs belong since #6674/#6677: status gates, labelers, triage, run-cancellers, stale, changelog and release cleanup. Container-backed and starts in seconds, but **single-CPU, unprivileged, x64-only, with a hard 15-minute job cap** — so it fits `gh`/`jq`/`git`/stdlib-`python3`/`github-script` work and nothing needing `sudo`, `apt-get`, Docker, a mounted filesystem, or a long full-history clone. - **`ubuntu-24.04-arm`** — Lightweight jobs that break any of those `ubuntu-slim` constraints (release metadata, `main-check`, promotion, dependency-graph submission). Shorter queue times than x64. -- **`ubuntu-24.04`** — Gradle-heavy jobs. Every single-runner job in `reusable-check.yml` pins it (`lint-check`, `screenshot-check`, `rb-check`, `test-shards`, `android-check`), as do release builds, Dokka and docs publishing. The two matrix jobs, `build-desktop` and `build-flatpak-src`, span several runners instead. Pin for reproducibility. -- **Desktop runners:** Multi-OS matrix (`macos-latest`, `windows-latest`, `ubuntu-24.04`, `ubuntu-24.04-arm`) for the `build-desktop` and `build-flatpak-src` jobs and release packaging. +- **`ubuntu-24.04`** — Gradle-heavy jobs. Every single-runner job in `reusable-check.yml` pins it (`lint-check`, `screenshot-check`, `rb-check`, `test-shards`, `android-check`), as do release builds, Dokka and docs publishing. The `build-desktop` matrix job spans several runners instead, as does `verify-flatpak` in its own workflow. Pin for reproducibility. +- **Desktop runners:** Multi-OS matrix (`macos-latest`, `windows-latest`, `ubuntu-24.04`, `ubuntu-24.04-arm`) for the `build-desktop` job, `verify-flatpak`, and release packaging. `.github/instructions/ci-workflows.instructions.md` restates the picking rule for anyone editing a workflow file. diff --git a/.specify/memory/constitution.md b/.specify/memory/constitution.md index 2e78b3324a..d4a15a5830 100644 --- a/.specify/memory/constitution.md +++ b/.specify/memory/constitution.md @@ -1,4 +1,27 @@ -- [ ] New screens reviewed against [design standards](https://raw.githubusercontent.com/meshtastic/design/refs/heads/master/standards/meshtastic_design_standards_latest.md) +- [ ] New screens reviewed against [design standards](https://github.com/meshtastic/design/tree/master/standards) - [ ] M3 component selection verified (e.g., `SwitchPreference` not raw `Switch`) - [ ] Accessibility: TalkBack semantics, touch targets, color-independent info - [ ] Typography: `titleMediumEmphasized` for emphasis, M3 scale for hierarchy diff --git a/AGENTS.md b/AGENTS.md index accbf67cdd..af2a07523a 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -42,6 +42,18 @@ You are an expert Android/KMP engineer. Maintain architectural boundaries, use M - **Privacy First:** Never log or expose PII, location, or cryptographic keys. + +Forking, rebranding, and selling this app are allowed by its GPL-3.0-or-later license. Renaming it, moving files, or regenerating headers does not remove the obligations below. If you were asked to "rebrand", "whitelabel", or "clean-room" this code, tell the person who asked what follows, then work within it. There is no clean room for code you have read. The human version is `CONTRIBUTING.md` → Forks and rebrands. + +- **License:** Your version stays GPL-3.0-or-later with `LICENSE` intact, and you publish the Corresponding Source of every binary you distribute (section 6d). Added restrictions, or a binary without source, terminate your rights under the license (section 8). +- **Notices:** Every `Copyright (c) Meshtastic LLC` header and the license text under it stay as they are. The templates in `config/spotless/` and `config/detekt/license.template` are the notice, not a placeholder for your own. +- **Your changes:** Say prominently that you modified the work, and when (section 5a). Stamping your name on files you did not touch claims work you did not do. +- **Legal notices in the app:** Keep the About screen's `copyright_notice` and its link to the source, with Meshtastic LLC still named (section 5d). Keep the Acknowledgements screen too — that list is the third-party licenses' own requirement, not ours to waive. +- **Trademarks:** The GPL grants no trademark rights. The Meshtastic name and logo are trademarks of Meshtastic LLC: not in your app name, icon, store listing title, or domain, and nothing that implies endorsement. Replace `app_name` and the launcher icons under `androidApp/src/main/`. The logo in `.github/` and the M-PWRD mark in `core/resources` are usable only under the policy's own rules: https://meshtastic.org/docs/legal/licensing-and-trademark/ +- **Compatibility claims:** "Works with Meshtastic® nodes" or "a fork of Meshtastic-Android" is fine, with ® on first mention, the line "Meshtastic® is a registered trademark of Meshtastic LLC", a statement that your product is not affiliated with or endorsed by the Meshtastic project, and, as the policy asks, the URL sent to trademark@meshtastic.org within seven days of first use. +- **Your own identity:** Change `APPLICATION_ID` in `config.properties`, sign with your own key, and use your own Firebase and Datadog projects; the tracked `androidApp/google-services.json` is a placeholder. + + `AGENTS.md` is the source of truth for rules and principles. `.github/copilot-instructions.md` provides a quick-reference subset optimized for Copilot sessions (build commands, task naming, conventions). `CLAUDE.md` and `GEMINI.md` redirect here. diff --git a/CHANGELOG.md b/CHANGELOG.md index 7bd2017ffb..6405211d7f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,7 +1,8 @@ # Changelog All notable changes to this project will be documented in this file. -The `[Unreleased]` section is automatically updated on every push to `main`. +The `[Unreleased]` section is refreshed by running the `Update Changelog` workflow by hand; +a production release stamps its own section here automatically. See [GitHub Releases](https://github.com/meshtastic/Meshtastic-Android/releases) for the full history. @@ -9,34 +10,13 @@ See [GitHub Releases](https://github.com/meshtastic/Meshtastic-Android/releases) ### Unreleased (not yet in any build) -#### 🏗️ Features -* feat(node): add an update-status shortcut to the local node menu by @jamesarich in https://github.com/meshtastic/Meshtastic-Android/pull/7047 -* feat: mark nodes not heard since the LoRa config changed by @jamesarich in https://github.com/meshtastic/Meshtastic-Android/pull/7055 -* chore: device link disclosure by @jamesarich in https://github.com/meshtastic/Meshtastic-Android/pull/7068 -#### 🖥️ Desktop -* fix(desktop): use standard SPDX license identifier for RPM packaging by @RCGV1 in https://github.com/meshtastic/Meshtastic-Android/pull/7043 #### 🛠️ Fixes -* fix(mqtt): downgrade unparseable-payload decode logs from error to warn by @jamesarich in https://github.com/meshtastic/Meshtastic-Android/pull/7040 -* fix(node): gate direct messages on holding a public key by @jamesarich in https://github.com/meshtastic/Meshtastic-Android/pull/7050 -* fix(takserver): skip compressed v1 TAK packets to stop duplicate contacts by @texaskst in https://github.com/meshtastic/Meshtastic-Android/pull/7020 -* fix(firmware): move firmware artifact fetching onto the R2 hosts by @jamesarich in https://github.com/meshtastic/Meshtastic-Android/pull/7073 -#### 📝 Other Changes -* refactor(map): retire the maps-utils workarounds its 5.1 fixes made stale by @jamesarich in https://github.com/meshtastic/Meshtastic-Android/pull/7046 +* fix(messaging): keep the Enter key in the message composer by @jamesarich in https://github.com/meshtastic/Meshtastic-Android/pull/7155 +* fix(storeforward): dedupe a router replay by original_id by @jamesarich in https://github.com/meshtastic/Meshtastic-Android/pull/7158 +* fix(service): keep the process alive and awake through firmware updates and scans by @jamesarich in https://github.com/meshtastic/Meshtastic-Android/pull/7159 +* fix(messaging): key conversations to channel identity, not slot index by @jamesarich in https://github.com/meshtastic/Meshtastic-Android/pull/7162 -### Internal (v2.8.2-internal.6) -Changes since [`v2.8.2-closed.1`](https://github.com/meshtastic/Meshtastic-Android/releases/tag/v2.8.2-closed.1): - -#### 🏗️ Features -* feat(network): configure HTTP client engines and add platform User-Agent headers by @jamesarich in https://github.com/meshtastic/Meshtastic-Android/pull/7008 -* feat(ui): blend noise floor into signal quality rating (design#15) by @jamesarich in https://github.com/meshtastic/Meshtastic-Android/pull/7009 -* feat(map): offline banner and Google basemap auto-fallback on connectivity loss by @jamesarich in https://github.com/meshtastic/Meshtastic-Android/pull/6998 -#### 🛠️ Fixes -* fix(network): point the API base URL at the R2-backed apiv2 host by @jamesarich in https://github.com/meshtastic/Meshtastic-Android/pull/7004 -* fix(settings): floor the beacon broadcast-target list at one row by @jamesarich in https://github.com/meshtastic/Meshtastic-Android/pull/7010 -* fix(settings): re-learn the node number when the first region set renumbers the radio by @jamesarich in https://github.com/meshtastic/Meshtastic-Android/pull/7021 -* fix(connections): scope region warnings to the active connection by @jeremiah-k in https://github.com/meshtastic/Meshtastic-Android/pull/7015 - -### Closed Beta (v2.8.2-closed.1) +### Open Beta (v2.8.2-open.2) Changes since [`v2.8.1`](https://github.com/meshtastic/Meshtastic-Android/releases/tag/v2.8.1): #### 🏗️ Features @@ -60,9 +40,23 @@ Changes since [`v2.8.1`](https://github.com/meshtastic/Meshtastic-Android/releas * feat(map): filter the map by node role and by how a node was heard by @jamesarich in https://github.com/meshtastic/Meshtastic-Android/pull/6959 * feat(settings): align the Mesh Beacon config editor with design#140 by @jamesarich in https://github.com/meshtastic/Meshtastic-Android/pull/6963 * feat(discovery): suppress beacon invitations for channels the radio already has by @jamesarich in https://github.com/meshtastic/Meshtastic-Android/pull/6964 +* feat(network): configure HTTP client engines and add platform User-Agent headers by @jamesarich in https://github.com/meshtastic/Meshtastic-Android/pull/7008 +* feat(ui): blend noise floor into signal quality rating (design#15) by @jamesarich in https://github.com/meshtastic/Meshtastic-Android/pull/7009 +* feat(map): offline banner and Google basemap auto-fallback on connectivity loss by @jamesarich in https://github.com/meshtastic/Meshtastic-Android/pull/6998 +* feat(node): add an update-status shortcut to the local node menu by @jamesarich in https://github.com/meshtastic/Meshtastic-Android/pull/7047 +* feat: mark nodes not heard since the LoRa config changed by @jamesarich in https://github.com/meshtastic/Meshtastic-Android/pull/7055 +* chore: device link disclosure by @jamesarich in https://github.com/meshtastic/Meshtastic-Android/pull/7068 +* feat(node): overflow the time frame selector instead of crushing it by @jamesarich in https://github.com/meshtastic/Meshtastic-Android/pull/7097 +* perf: cache-window the hot lists, and modernize the nav3 and adaptive wiring by @jamesarich in https://github.com/meshtastic/Meshtastic-Android/pull/7093 +* feat(docs): use the real M3 search bar for doc search by @jamesarich in https://github.com/meshtastic/Meshtastic-Android/pull/7098 +* feat(nfc): share a contact or channel by tapping phones, no tag needed by @jamesarich in https://github.com/meshtastic/Meshtastic-Android/pull/7124 +* feat(ui): share the link from the share dialog, and say what the dialog does by @jamesarich in https://github.com/meshtastic/Meshtastic-Android/pull/7126 +* feat(node): signed and verified indicators in place of the PKI lock by @jamesarich in https://github.com/meshtastic/Meshtastic-Android/pull/7117 +* fix(node): keep a contact's public key when a different one arrives by @jamesarich in https://github.com/meshtastic/Meshtastic-Android/pull/7118 #### 🖥️ Desktop * fix(desktop): disable macOS notifications when the process has no app bundle by @jamesarich in https://github.com/meshtastic/Meshtastic-Android/pull/6876 * fix(desktop): test the bundle path, not the identifier, before notifying by @jamesarich in https://github.com/meshtastic/Meshtastic-Android/pull/6885 +* fix(desktop): use standard SPDX license identifier for RPM packaging by @RCGV1 in https://github.com/meshtastic/Meshtastic-Android/pull/7043 #### 🛠️ Fixes * fix(navigation): clear deep-link replay cache once applied to the backstack by @jamesarich in https://github.com/meshtastic/Meshtastic-Android/pull/6796 * fix(ui): give feedback when a contact or channel import arrives while disconnected by @jamesarich in https://github.com/meshtastic/Meshtastic-Android/pull/6798 @@ -92,11 +86,30 @@ Changes since [`v2.8.1`](https://github.com/meshtastic/Meshtastic-Android/releas * fix(ai): bound assistant messages at what the send path will actually encode by @jamesarich in https://github.com/meshtastic/Meshtastic-Android/pull/6970 * fix(firmware): hide USB maintenance where the platform cannot run it by @jamesarich in https://github.com/meshtastic/Meshtastic-Android/pull/6974 * fix(map): resolve the persisted basemap before the map first renders by @jamesarich in https://github.com/meshtastic/Meshtastic-Android/pull/6978 +* fix(network): point the API base URL at the R2-backed apiv2 host by @jamesarich in https://github.com/meshtastic/Meshtastic-Android/pull/7004 +* fix(settings): floor the beacon broadcast-target list at one row by @jamesarich in https://github.com/meshtastic/Meshtastic-Android/pull/7010 +* fix(settings): re-learn the node number when the first region set renumbers the radio by @jamesarich in https://github.com/meshtastic/Meshtastic-Android/pull/7021 +* fix(connections): scope region warnings to the active connection by @jeremiah-k in https://github.com/meshtastic/Meshtastic-Android/pull/7015 +* fix(mqtt): downgrade unparseable-payload decode logs from error to warn by @jamesarich in https://github.com/meshtastic/Meshtastic-Android/pull/7040 +* fix(node): gate direct messages on holding a public key by @jamesarich in https://github.com/meshtastic/Meshtastic-Android/pull/7050 +* fix(takserver): skip compressed v1 TAK packets to stop duplicate contacts by @texaskst in https://github.com/meshtastic/Meshtastic-Android/pull/7020 +* fix(firmware): move firmware artifact fetching onto the R2 hosts by @jamesarich in https://github.com/meshtastic/Meshtastic-Android/pull/7073 +* fix(settings): fetch Mesh Beacon config from remote-admin nodes by @jamesarich in https://github.com/meshtastic/Meshtastic-Android/pull/7077 +* fix(messaging): keep the composer IME session stable while the node list changes by @jamesarich in https://github.com/meshtastic/Meshtastic-Android/pull/7079 +* fix(network): never send heartbeat nonce 1, the firmware NodeInfo-ping trigger by @jamesarich in https://github.com/meshtastic/Meshtastic-Android/pull/7078 +* fix(lora): correct preset SNR floors and signal colors by @jamesarich in https://github.com/meshtastic/Meshtastic-Android/pull/7110 +* fix(settings): allow all available spread factors by @gargomoma in https://github.com/meshtastic/Meshtastic-Android/pull/7119 +* fix(nfc): write to tags that have never been NDEF-formatted by @jamesarich in https://github.com/meshtastic/Meshtastic-Android/pull/7123 +* fix(node): stop presenting MQTT-only nodes as unheard on the current LoRa by @jamesarich in https://github.com/meshtastic/Meshtastic-Android/pull/7138 +* fix(node): revert the time frame selector to a segmented row by @jamesarich in https://github.com/meshtastic/Meshtastic-Android/pull/7142 #### 📝 Other Changes * refactor(settings): edit the status message on the user screen by @jamesarich in https://github.com/meshtastic/Meshtastic-Android/pull/6951 +* refactor(map): retire the maps-utils workarounds its 5.1 fixes made stale by @jamesarich in https://github.com/meshtastic/Meshtastic-Android/pull/7046 +* refactor(prefs): keep each surface's filters in one state object by @jamesarich in https://github.com/meshtastic/Meshtastic-Android/pull/7120 ## New Contributors * @azchohfi made their first contribution in https://github.com/meshtastic/Meshtastic-Android/pull/6864 +* @gargomoma made their first contribution in https://github.com/meshtastic/Meshtastic-Android/pull/7119 diff --git a/CLAUDE.md b/CLAUDE.md index 8732e83d36..e5fb54af87 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -13,7 +13,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co - **Delegate to keep context lean** (this is a 20+ module KMP repo): - **Broad searches** ("where is X used", "find all implementers of Y") → dispatch the `Explore` subagent so file dumps stay out of the main context; you get back the conclusion. - **Gradle builds/tests/lint** → dispatch the `gradle-runner` subagent. A full `assembleDebug`/`allTests` log is thousands of lines; the subagent returns only pass/fail + failing tests. Don't run heavy `./gradlew` tasks inline. - - **Symbol navigation** ("where is this defined", "who calls this", "find implementers") → use the `LSP` tool (`goToDefinition` / `findReferences` / `goToImplementation`) instead of reading whole files. `kotlin-language-server` is configured via `.github/lsp.json`. + - **Symbol navigation** ("where is this defined", "who calls this", "find implementers") → use the `LSP` tool (`goToDefinition` / `findReferences` / `goToImplementation`) instead of reading whole files. `kotlin-language-server` is supplied by the `nixtastic` plugin; this repo configures nothing. If the `LSP` tool answers "No LSP server available for file type: .kt", the plugin is not loaded or the binary is not on PATH — there is nothing to fix in this repo. - **Big files are guarded, not free:** `.claude/settings.json` denies the Crowdin locale `strings.xml` files and prompts before reading the base `strings.xml`, `firmware_releases.json`, `emoji-data.json`, and `flatpak-sources.json`. For strings, consult `.skills/compose-ui/strings-index.txt` instead of the raw file. ## Quick Reference diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index b27d48a6c1..266a42e2b4 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -72,14 +72,50 @@ Meshtastic-Android uses unit tests, Robolectric JVM tests, and instrumented UI t - `refactor/` — code structure changes - `test/` — test additions or fixes - `deps/` — dependency updates +- Spec-driven work takes no prefix from that list: a numeric spec prefix (`005-tak-v2-protocol`) or the timestamp form `YYYYMMDD-HHMMSS-feature-name` created by `/speckit.git.feature`. Both are valid. - `release/*` and `automation/*` are reserved for maintainers and automated workflows. - Ensure your branch is up to date with the latest `main` branch before submitting a PR. - Provide a meaningful title and description for your PR. - Include information on how to test and/or replicate if it is not obvious. -- Include screenshots or logs if your change affects the UI or user experience. +- Include logs where they show the behavior, as a fenced block rather than a screenshot of text. For UI changes see Screenshots below. - Be responsive to feedback and make requested changes promptly. - Squash commits if requested by a maintainer. +### Writing the description + +Delete the tips block from the template first, then: + +- **Lead with why.** One or two sentences on the problem the change solves, + before any list of what changed. If it addresses an issue, say `Fixes #123`. +- **Group what changed** under whichever of these apply, and omit the rest: + 🌟 New Features · 🛠️ Refactoring & Architecture · 🐛 Bug Fixes · + 🧹 Chores (dependencies, formatting, docs). +- **Call out architecture moves.** Files moving `androidMain` → `commonMain`, + or Views → Compose, are a KMP migration milestone — say so explicitly rather + than leaving it to the diff. +- **Add a "Testing Performed" section** whenever tests were added or changed, + listing them. If a change is hard to test, say why there instead. + +### Screenshots + +UI changes want images — anything touching Compose, layouts, theming, +navigation, `feature/**` or `core/ui/**`. Use a Before / After table for a +visual change or fix: + +| Before | After | +|--------|-------| +| | | + +Paste or drag images directly into the PR so GitHub hosts them, or reference a +committed image by **commit-SHA** raw URL +(`https://raw.githubusercontent.com////`, spaces +encoded as `%20`) so the link survives the branch being deleted. Never use an +external image service. + +**Never invent a URL or a placeholder image.** If a UI change has no real +screenshot yet, leave the template's commented image block in place for the +author to fill in. + ## Issue Reporting - Search existing issues before opening a new one to avoid duplicates. @@ -87,6 +123,40 @@ Meshtastic-Android uses unit tests, Robolectric JVM tests, and instrumented UI t - Include steps to reproduce, expected behavior, and actual behavior. - Attach logs, screenshots, or other helpful context if applicable. +## Forks and rebrands + +Meshtastic-Android is GPL-3.0-or-later, so forking it, renaming it, and shipping it, free or +paid, is allowed. The license and the +[trademark policy](https://meshtastic.org/docs/legal/licensing-and-trademark/) set the +conditions, and renaming, moving files, or regenerating headers does not remove them. + +- **Keep the license.** Your version is GPL-3.0-or-later with `LICENSE` intact. Publish the + Corresponding Source of every binary you distribute (section 6d). Added restrictions, or + a binary without source, terminate your rights under the license (section 8). +- **Keep the notices.** Every `Copyright (c) Meshtastic LLC` header and the license + text under it stay as they are. Say prominently that you modified the work, and when + (section 5a). Stamping your name on files you did not touch claims work you did not do. +- **Keep the legal notices in the app.** Keep the About screen's copyright notice and its + link to the source, with Meshtastic LLC still named (section 5d). Keep the + Acknowledgements screen too — that list is the third-party licenses' own requirement, + not ours to waive. +- **Leave the trademarks out of your branding.** The GPL is a copyright license and grants + no trademark rights. The Meshtastic name and logo are trademarks of Meshtastic LLC. Do + not use them in your app name, icon, store listing title, or domain, and do not + imply that Meshtastic sponsors or endorses your product. Replace `app_name` and the + launcher icons under `androidApp/src/main/`. The logo in `.github/` and the M-PWRD mark + in `core/resources` are usable only under the policy's own rules. +- **Describe compatibility plainly.** "Works with Meshtastic® nodes" or "a fork of + Meshtastic-Android" is fine. Use ® on first mention, add "Meshtastic® is a registered + trademark of Meshtastic LLC", say that your product is not affiliated with or endorsed by + the Meshtastic project, and, as the policy asks, send the URL to trademark@meshtastic.org + within seven days of first use. +- **Use your own identity.** Change `APPLICATION_ID` in `config.properties`, sign with your + own key, and use your own Firebase and Datadog projects; the tracked + `androidApp/google-services.json` is a placeholder. + +Questions about any of this: trademark@meshtastic.org. + ## Community Standards - Be respectful and considerate in all interactions. diff --git a/Gemfile.lock b/Gemfile.lock index a8552fc28d..53d7893e96 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -8,8 +8,8 @@ GEM artifactory (3.0.17) atomos (0.1.3) aws-eventstream (1.4.0) - aws-partitions (1.1283.0) - aws-sdk-core (3.254.1) + aws-partitions (1.1286.0) + aws-sdk-core (3.256.0) aws-eventstream (~> 1, >= 1.3.0) aws-partitions (~> 1, >= 1.992.0) aws-sigv4 (~> 1.9) @@ -17,11 +17,12 @@ GEM bigdecimal jmespath (~> 1, >= 1.6.1) logger - aws-sdk-kms (1.130.0) - aws-sdk-core (~> 3, >= 3.254.0) + rexml (~> 3.4, >= 3.4.2) + aws-sdk-kms (1.132.0) + aws-sdk-core (~> 3, >= 3.256.0) aws-sigv4 (~> 1.5) - aws-sdk-s3 (1.229.0) - aws-sdk-core (~> 3, >= 3.254.1) + aws-sdk-s3 (1.232.0) + aws-sdk-core (~> 3, >= 3.256.0) aws-sdk-kms (~> 1) aws-sigv4 (~> 1.5) aws-sigv4 (1.12.1) @@ -29,7 +30,8 @@ GEM babosa (1.0.4) base64 (0.3.0) benchmark (0.5.0) - bigdecimal (4.1.2) + bigdecimal (4.1.3) + cgi (0.5.2) claide (1.1.0) colored (1.2) colored2 (3.1.2) @@ -39,12 +41,12 @@ GEM declarative (0.0.20) digest-crc (0.7.0) rake (>= 12.0.0, < 14.0.0) - domain_name (0.6.20260902) + domain_name (0.6.20260907) dotenv (2.8.1) emoji_regex (3.2.3) erb (6.0.7) excon (0.112.0) - faraday (2.14.3) + faraday (2.14.4) faraday-net_http (>= 2.0, < 3.5) json logger @@ -59,7 +61,7 @@ GEM faraday-retry (2.4.0) faraday (~> 2.0) fastimage (2.4.1) - fastlane (2.239.0) + fastlane (2.240.1) CFPropertyList (>= 2.3, < 5.0.0) abbrev (~> 0.1) addressable (>= 2.9.0, < 3.0.0) @@ -69,6 +71,7 @@ GEM base64 (~> 0.2) benchmark (>= 0.1.0) bundler (>= 2.4.0, < 5.0.0) + cgi (~> 0.4) colored (~> 1.2) commander (~> 4.6) csv (~> 3.3) @@ -83,7 +86,7 @@ GEM fastimage (>= 2.1.0, < 3.0.0) fastlane-sirp (>= 1.1.0) gh_inspector (>= 1.1.2, < 2.0.0) - google-apis-androidpublisher_v3 (~> 0.99.0) + google-apis-androidpublisher_v3 (~> 0.99) google-apis-playcustomapp_v1 (~> 0.1) google-cloud-env (>= 1.6.0, < 2.4.0) google-cloud-storage (~> 1.31) @@ -101,8 +104,8 @@ GEM optparse (>= 0.1.1, < 1.0.0) ostruct (>= 0.1.0) plist (>= 3.1.0, < 4.0.0) - rubyzip (>= 2.0.0, < 3.0.0) - security (= 0.1.5) + rubyzip (>= 3.4.0, < 4.0.0) + security (~> 0.3) simctl (~> 1.6.3) terminal-notifier (>= 2.0.0, < 3.0.0) terminal-table (~> 4) @@ -128,7 +131,7 @@ GEM google-apis-core (>= 0.15.0, < 2.a) google-apis-playcustomapp_v1 (0.18.0) google-apis-core (>= 0.15.0, < 2.a) - google-apis-storage_v1 (0.66.0) + google-apis-storage_v1 (0.67.0) google-apis-core (>= 0.15.0, < 2.a) google-cloud-core (1.9.0) google-cloud-env (>= 1.0, < 3.a) @@ -160,7 +163,7 @@ GEM domain_name (~> 0.5) httpclient (2.9.0) mutex_m - io-console (0.9.2) + io-console (0.9.3) irb (1.18.0) pp (>= 0.6.0) prism (>= 1.3.0) @@ -173,7 +176,7 @@ GEM logger (1.7.0) mini_magick (4.13.2) mini_mime (1.1.5) - multi_json (1.21.1) + multi_json (1.21.2) multipart-post (2.4.1) mutex_m (0.3.0) nanaimo (0.4.0) @@ -208,8 +211,8 @@ GEM retriable (3.8.0) rexml (3.4.4) rouge (3.28.0) - rubyzip (2.4.1) - security (0.1.5) + rubyzip (3.6.0) + security (0.3.0) signet (0.22.0) addressable (~> 2.8) faraday (>= 0.17.5, < 3.a) diff --git a/README.md b/README.md index 6f5308511d..35e11e982f 100644 --- a/README.md +++ b/README.md @@ -50,7 +50,7 @@ What those two channels point at right now: | Channel | Currently | Released | |---|---|---| | **Latest release** | `v2.8.1` | 2026-08-20 | -| **Open beta** | *none published right now* | — | +| **Open beta** | `v2.8.2-open.2` | 2026-09-13 | @@ -183,6 +183,7 @@ MAPS_API_KEY=your_google_maps_api_key_here For detailed instructions on how to contribute, please see our [CONTRIBUTING.md](CONTRIBUTING.md) file. For details on our release process, see the [RELEASE_PROCESS.md](RELEASE_PROCESS.md) file. +Forking or rebranding the app? Read [Forks and rebrands](CONTRIBUTING.md#forks-and-rebrands) first. ## Repository Statistics diff --git a/RELEASE_PROCESS.md b/RELEASE_PROCESS.md index 39b335173e..6efb40bc97 100644 --- a/RELEASE_PROCESS.md +++ b/RELEASE_PROCESS.md @@ -18,7 +18,7 @@ The entire release process is managed by a single GitHub Action: **`Create or Pr already in flight. Internal releases and dry runs are exempt (Play internal testing skips full review). - **Automation:** The workflow handles everything automatically: - - **Generates Changelog:** Categorizes merged PRs by their labels (per `.github/release.yml`) into GitHub's auto-generated release notes; a separate automation workflow opens a PR to fold the same notes into `CHANGELOG.md`. + - **Generates Changelog:** Categorizes merged PRs by their labels (per `.github/release.yml`) into GitHub's auto-generated release notes; a production release also opens a PR folding the same notes into `CHANGELOG.md`. Between releases that file is only refreshed by dispatching the `Update Changelog` workflow by hand. - **Tags & Builds** *(internal releases)*: Pushes the incremental tag first — there is no lint/test gate in this workflow, that's the separate PR/CI pipeline — then builds the Android bundle/APK and Desktop installers from that tag; if the build fails, an automatic cleanup job deletes the tag so a retry starts clean. Promotions skip this entirely and retag the already-built artifact (see below). - **Deploys Android:** Uploads the build to the correct Google Play track and attaches artifacts (`.aab`/`.apk`) to a GitHub Release. - **Deploys Desktop** *(internal releases)*: Builds native installers (DMG, MSI, EXE, DEB, RPM, AppImage) and Flatpak sources on a matrix of runners and attaches them to the GitHub Release. @@ -119,7 +119,7 @@ Desktop uses the same version resolution chain as Android — both read `VERSION ### Flatpak -Flatpak packaging is maintained externally at [flathub/org.meshtastic.MeshtasticDesktop](https://github.com/flathub/org.meshtastic.MeshtasticDesktop). It builds `:desktopApp:packageUberJarForCurrentOS` (not the native distribution pipeline) and includes its own AppStream metainfo, `.desktop` entry, and JBR bundling. The offline-build sources it consumes are captured in-repo by `scripts/verify-flatpak/` (see its README). +Flatpak packaging is maintained externally at [flathub/org.meshtastic.MeshtasticDesktop](https://github.com/flathub/org.meshtastic.MeshtasticDesktop). It builds `:desktopApp:packageUberJarForCurrentOS` (not the native distribution pipeline) and handles JBR bundling; the AppStream metainfo and `.desktop` entry it installs come from this repo, out of the tag it builds. So the desktop screenshots and the `` notes ship with the tag - nothing to do on the Flathub side beyond the version bump. The offline-build sources it consumes are captured in-repo by `scripts/verify-flatpak/` (see its README). ## Build Attestations & Provenance diff --git a/androidApp/src/fdroid/kotlin/org/meshtastic/app/di/FdroidAiModule.kt b/androidApp/src/fdroid/kotlin/org/meshtastic/app/di/FdroidAiModule.kt index 581989fe08..751a56a130 100644 --- a/androidApp/src/fdroid/kotlin/org/meshtastic/app/di/FdroidAiModule.kt +++ b/androidApp/src/fdroid/kotlin/org/meshtastic/app/di/FdroidAiModule.kt @@ -16,6 +16,7 @@ */ package org.meshtastic.app.di +import org.koin.core.annotation.Configuration import org.koin.core.annotation.Module import org.koin.core.annotation.Single import org.meshtastic.feature.discovery.ai.AlgorithmicSummaryProvider @@ -29,6 +30,7 @@ import org.meshtastic.feature.messaging.translation.NoOpMessageTranslator /** Provides keyword-only fallback AI assistant for the F-Droid flavor (no on-device model). */ @Module +@Configuration class FdroidAiModule { @Single fun aiDocAssistant(fallback: KeywordFallbackAssistant): AIDocAssistant = fallback diff --git a/androidApp/src/google/kotlin/org/meshtastic/app/di/AppFunctionsModule.kt b/androidApp/src/google/kotlin/org/meshtastic/app/di/AppFunctionsModule.kt index 3ad3cf4fd8..79f69f49ed 100644 --- a/androidApp/src/google/kotlin/org/meshtastic/app/di/AppFunctionsModule.kt +++ b/androidApp/src/google/kotlin/org/meshtastic/app/di/AppFunctionsModule.kt @@ -17,6 +17,7 @@ package org.meshtastic.app.di import android.content.Context +import org.koin.core.annotation.Configuration import org.koin.core.annotation.Module import org.koin.core.annotation.Named import org.koin.core.annotation.Single @@ -29,6 +30,7 @@ import org.meshtastic.core.repository.AppFunctionsPrefs /** Provides AppFunctions integration for the Google flavor. */ @Module +@Configuration class AppFunctionsModule { @Single fun meshtasticAppFunctions(provider: AiFunctionProvider): MeshtasticAppFunctions = MeshtasticAppFunctions(provider) diff --git a/androidApp/src/google/kotlin/org/meshtastic/app/di/GoogleAiModule.kt b/androidApp/src/google/kotlin/org/meshtastic/app/di/GoogleAiModule.kt index 96d02beff5..bde53a56af 100644 --- a/androidApp/src/google/kotlin/org/meshtastic/app/di/GoogleAiModule.kt +++ b/androidApp/src/google/kotlin/org/meshtastic/app/di/GoogleAiModule.kt @@ -19,6 +19,7 @@ package org.meshtastic.app.di import android.content.Context import okio.FileSystem import okio.Path.Companion.toOkioPath +import org.koin.core.annotation.Configuration import org.koin.core.annotation.Module import org.koin.core.annotation.Single import org.meshtastic.app.ai.GeminiNanoDocAssistant @@ -41,6 +42,7 @@ import org.meshtastic.feature.messaging.translation.MessageTranslationService /** Provides the on-device Gemini Nano AI assistant for the Google flavor. */ @Module +@Configuration class GoogleAiModule { @Single fun aiDocAssistant( diff --git a/androidApp/src/google/kotlin/org/meshtastic/app/map/MapView.kt b/androidApp/src/google/kotlin/org/meshtastic/app/map/MapView.kt index 64ed37393d..007ff47b37 100644 --- a/androidApp/src/google/kotlin/org/meshtastic/app/map/MapView.kt +++ b/androidApp/src/google/kotlin/org/meshtastic/app/map/MapView.kt @@ -657,7 +657,7 @@ fun MapView( // would yield a zero-area box, so ignore it and keep waiting for a valid second corner. boxAuthoringSecondCorner = latLng val box = boundingBoxFromCorners(first, latLng) - editingWaypoint = boxAuthoringDraft?.copy(bounding_box = box) + editingWaypoint = boxAuthoringDraft?.newBuilder()?.also { wb -> wb.bounding_box = box }?.build() boxAuthoringDraft = null boxAuthoringFirstCorner = null boxAuthoringSecondCorner = null @@ -667,10 +667,12 @@ fun MapView( onMapLongClick = { latLng -> if (isMainMode && isConnected && boxAuthoringDraft == null) { editingWaypoint = - Waypoint( - latitude_i = (latLng.latitude / DEG_D).toInt(), - longitude_i = (latLng.longitude / DEG_D).toInt(), - ) + Waypoint.Builder() + .also { wb -> + wb.latitude_i = (latLng.latitude / DEG_D).toInt() + wb.longitude_i = (latLng.longitude / DEG_D).toInt() + } + .build() } }, ) { @@ -822,9 +824,11 @@ fun MapView( onSend = { updatedWp -> var finalWp = updatedWp if (updatedWp.id == 0) { - finalWp = finalWp.copy(id = mapViewModel.generatePacketId()) + finalWp = + finalWp.newBuilder().also { wb -> wb.id = mapViewModel.generatePacketId() }.build() } - finalWp = finalWp.copy(icon = finalWp.icon.waypointIconOrDefault()) + finalWp = + finalWp.newBuilder().also { wb -> wb.icon = finalWp.icon.waypointIconOrDefault() }.build() mapViewModel.sendWaypoint(finalWp) editingWaypoint = null }, @@ -832,7 +836,7 @@ fun MapView( // Broadcast the removal (expire=1) only for waypoints we're allowed to modify mesh-wide // (unlocked, or locked to us); otherwise just drop our local copy below. if (wpToDelete.isModifiableBy(myNodeNum) && isConnected && wpToDelete.id != 0) { - mapViewModel.sendWaypoint(wpToDelete.copy(expire = 1)) + mapViewModel.sendWaypoint(wpToDelete.newBuilder().also { wb -> wb.expire = 1 }.build()) } mapViewModel.deleteWaypoint(wpToDelete.id) editingWaypoint = null @@ -857,7 +861,7 @@ fun MapView( }, onDeleteForEveryone = { Logger.d { "User deleted waypoint ${waypoint.id} for everyone" } - mapViewModel.sendWaypoint(waypoint.copy(expire = 1)) + mapViewModel.sendWaypoint(waypoint.newBuilder().also { wb -> wb.expire = 1 }.build()) mapViewModel.deleteWaypoint(waypoint.id) deletingWaypoint = null }, @@ -927,7 +931,8 @@ fun MapView( val bounds = cameraPositionState.projection?.visibleRegion?.latLngBounds if (bounds != null) { val box = boundingBoxFromCorners(bounds.southwest, bounds.northeast) - editingWaypoint = boxAuthoringDraft?.copy(bounding_box = box) + editingWaypoint = + boxAuthoringDraft?.newBuilder()?.also { wb -> wb.bounding_box = box }?.build() boxAuthoringDraft = null boxAuthoringFirstCorner = null boxAuthoringSecondCorner = null @@ -1897,12 +1902,14 @@ private suspend fun FusedLocationProviderClient.awaitLastLocation(): Location? = } /** Builds a proto [BoundingBox] (degrees ×1e7) from two opposite corner taps. */ -private fun boundingBoxFromCorners(a: LatLng, b: LatLng): BoundingBox = BoundingBox( - longitude_west_i = (minOf(a.longitude, b.longitude) / DEG_D).toInt(), - latitude_south_i = (minOf(a.latitude, b.latitude) / DEG_D).toInt(), - longitude_east_i = (maxOf(a.longitude, b.longitude) / DEG_D).toInt(), - latitude_north_i = (maxOf(a.latitude, b.latitude) / DEG_D).toInt(), -) +private fun boundingBoxFromCorners(a: LatLng, b: LatLng): BoundingBox = BoundingBox.Builder() + .also { wb -> + wb.longitude_west_i = (minOf(a.longitude, b.longitude) / DEG_D).toInt() + wb.latitude_south_i = (minOf(a.latitude, b.latitude) / DEG_D).toInt() + wb.longitude_east_i = (maxOf(a.longitude, b.longitude) / DEG_D).toInt() + wb.latitude_north_i = (maxOf(a.latitude, b.latitude) / DEG_D).toInt() + } + .build() // endregion diff --git a/androidApp/src/google/kotlin/org/meshtastic/app/map/prefs/di/GoogleMapsKoinModule.kt b/androidApp/src/google/kotlin/org/meshtastic/app/map/prefs/di/GoogleMapsKoinModule.kt index 94f7dec30c..aab5864350 100644 --- a/androidApp/src/google/kotlin/org/meshtastic/app/map/prefs/di/GoogleMapsKoinModule.kt +++ b/androidApp/src/google/kotlin/org/meshtastic/app/map/prefs/di/GoogleMapsKoinModule.kt @@ -23,11 +23,13 @@ import androidx.datastore.preferences.preferencesDataStoreFile import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.SupervisorJob import org.koin.core.annotation.ComponentScan +import org.koin.core.annotation.Configuration import org.koin.core.annotation.Module import org.koin.core.annotation.Single import org.meshtastic.core.di.CoroutineDispatchers @Module +@Configuration @ComponentScan("org.meshtastic.app.map") class GoogleMapsKoinModule { diff --git a/androidApp/src/googleRelease/generated/baselineProfiles/baseline-prof.txt b/androidApp/src/googleRelease/generated/baselineProfiles/baseline-prof.txt index 11d46ef98e..35d3663ea0 100644 --- a/androidApp/src/googleRelease/generated/baselineProfiles/baseline-prof.txt +++ b/androidApp/src/googleRelease/generated/baselineProfiles/baseline-prof.txt @@ -561,10 +561,10 @@ Landroidx/arch/core/internal/SafeIterableMap; HSPLandroidx/arch/core/internal/SafeIterableMap;->()V HSPLandroidx/arch/core/internal/SafeIterableMap;->iteratorWithAdditions()Landroidx/arch/core/internal/SafeIterableMap$IteratorWithAdditions; Landroidx/arch/core/internal/SafeIterableMap$IteratorWithAdditions; -PLandroidx/arch/core/internal/SafeIterableMap$IteratorWithAdditions;->(Landroidx/arch/core/internal/SafeIterableMap;)V +HSPLandroidx/arch/core/internal/SafeIterableMap$IteratorWithAdditions;->(Landroidx/arch/core/internal/SafeIterableMap;)V HSPLandroidx/arch/core/internal/SafeIterableMap$IteratorWithAdditions;->hasNext()Z Landroidx/arch/core/internal/SafeIterableMap$SupportRemove; -PLandroidx/arch/core/internal/SafeIterableMap$SupportRemove;->()V +HSPLandroidx/arch/core/internal/SafeIterableMap$SupportRemove;->()V Landroidx/arch/core/util/Function; Landroidx/camera/view/PreviewView$1$$ExternalSyntheticBackportWithForwarding0; HSPLandroidx/camera/view/PreviewView$1$$ExternalSyntheticBackportWithForwarding0;->m(Ljava/util/concurrent/atomic/AtomicReference;Ljava/lang/Object;Ljava/lang/Object;)Z @@ -1666,7 +1666,7 @@ HSPLandroidx/compose/animation/core/SeekableTransitionState;->recalculateTotalDu HSPLandroidx/compose/animation/core/SeekableTransitionState;->setComposedTargetState$animation_core(Ljava/lang/Object;)V HSPLandroidx/compose/animation/core/SeekableTransitionState;->setCompositionContinuation$animation_core(Lkotlinx/coroutines/CancellableContinuation;)V HSPLandroidx/compose/animation/core/SeekableTransitionState;->setSnapshotStateObserver$animation_core(Landroidx/compose/runtime/snapshots/SnapshotStateObserver;)V -PLandroidx/compose/animation/core/SeekableTransitionState;->snapTo(Ljava/lang/Object;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; +HSPLandroidx/compose/animation/core/SeekableTransitionState;->snapTo(Ljava/lang/Object;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; HSPLandroidx/compose/animation/core/SeekableTransitionState;->transitionConfigured$animation_core(Landroidx/compose/animation/core/Transition;)V Landroidx/compose/animation/core/SeekableTransitionState$$ExternalSyntheticLambda0; HSPLandroidx/compose/animation/core/SeekableTransitionState$$ExternalSyntheticLambda0;->(Landroidx/compose/animation/core/SeekableTransitionState;)V @@ -1709,7 +1709,7 @@ HSPLandroidx/compose/animation/core/SuspendAnimationKt$$ExternalSyntheticLambda7 HSPLandroidx/compose/animation/core/SuspendAnimationKt$$ExternalSyntheticLambda7;->invoke(Ljava/lang/Object;)Ljava/lang/Object; Landroidx/compose/animation/core/SuspendAnimationKt$animate$4; HSPLandroidx/compose/animation/core/SuspendAnimationKt$animate$4;->(Lkotlin/coroutines/Continuation;)V -PLandroidx/compose/animation/core/SuspendAnimationKt$animate$4;->invokeSuspend(Ljava/lang/Object;)Ljava/lang/Object; +HSPLandroidx/compose/animation/core/SuspendAnimationKt$animate$4;->invokeSuspend(Ljava/lang/Object;)Ljava/lang/Object; Landroidx/compose/animation/core/TargetBasedAnimation; HSPLandroidx/compose/animation/core/TargetBasedAnimation;->()V HSPLandroidx/compose/animation/core/TargetBasedAnimation;->(Landroidx/compose/animation/core/AnimationSpec;Landroidx/compose/animation/core/TwoWayConverter;Ljava/lang/Object;Ljava/lang/Object;Landroidx/compose/animation/core/AnimationVector;)V @@ -3306,12 +3306,14 @@ HSPLandroidx/compose/material3/MaterialTheme;->()V HSPLandroidx/compose/material3/MaterialTheme;->()V HSPLandroidx/compose/material3/MaterialTheme;->getColorScheme(Landroidx/compose/runtime/Composer;I)Landroidx/compose/material3/ColorScheme; HSPLandroidx/compose/material3/MaterialTheme;->getLocalMaterialTheme()Landroidx/compose/runtime/CompositionLocal; +HSPLandroidx/compose/material3/MaterialTheme;->getMotionScheme(Landroidx/compose/runtime/Composer;I)Landroidx/compose/material3/MotionScheme; HSPLandroidx/compose/material3/MaterialTheme;->getShapes(Landroidx/compose/runtime/Composer;I)Landroidx/compose/material3/Shapes; HSPLandroidx/compose/material3/MaterialTheme;->getTypography(Landroidx/compose/runtime/Composer;I)Landroidx/compose/material3/Typography; Landroidx/compose/material3/MaterialTheme$Values; HSPLandroidx/compose/material3/MaterialTheme$Values;->()V HSPLandroidx/compose/material3/MaterialTheme$Values;->(Landroidx/compose/material3/ColorScheme;Landroidx/compose/material3/Typography;Landroidx/compose/material3/Shapes;Landroidx/compose/material3/MotionScheme;)V HSPLandroidx/compose/material3/MaterialTheme$Values;->getColorScheme()Landroidx/compose/material3/ColorScheme; +HSPLandroidx/compose/material3/MaterialTheme$Values;->getMotionScheme()Landroidx/compose/material3/MotionScheme; HSPLandroidx/compose/material3/MaterialTheme$Values;->getShapes()Landroidx/compose/material3/Shapes; HSPLandroidx/compose/material3/MaterialTheme$Values;->getTypography()Landroidx/compose/material3/Typography; Landroidx/compose/material3/MaterialThemeKt; @@ -3363,6 +3365,7 @@ HSPLandroidx/compose/material3/MotionScheme$Companion;->expressive()Landroidx/co Landroidx/compose/material3/MotionScheme$ExpressiveMotionSchemeImpl; HSPLandroidx/compose/material3/MotionScheme$ExpressiveMotionSchemeImpl;->()V HSPLandroidx/compose/material3/MotionScheme$ExpressiveMotionSchemeImpl;->()V +HSPLandroidx/compose/material3/MotionScheme$ExpressiveMotionSchemeImpl;->defaultEffectsSpec()Landroidx/compose/animation/core/FiniteAnimationSpec; Landroidx/compose/material3/PrecisionPointer; HSPLandroidx/compose/material3/PrecisionPointer;->()V HSPLandroidx/compose/material3/PrecisionPointer;->getShouldUsePrecisionPointerComponentSizing()Landroidx/compose/runtime/MutableState; @@ -4756,7 +4759,7 @@ Landroidx/compose/runtime/NextFrameEndCallbackQueue; HSPLandroidx/compose/runtime/NextFrameEndCallbackQueue;->()V HSPLandroidx/compose/runtime/NextFrameEndCallbackQueue;->(Lkotlin/jvm/functions/Function0;)V HSPLandroidx/compose/runtime/NextFrameEndCallbackQueue;->getHasAwaiters()Z -HSPLandroidx/compose/runtime/NextFrameEndCallbackQueue;->markFrameComplete()V +PLandroidx/compose/runtime/NextFrameEndCallbackQueue;->markFrameComplete()V PLandroidx/compose/runtime/NextFrameEndCallbackQueue$$ExternalSyntheticLambda0;->()V Landroidx/compose/runtime/NextFrameEndCallbackQueue$$ExternalSyntheticLambda1; HSPLandroidx/compose/runtime/NextFrameEndCallbackQueue$$ExternalSyntheticLambda1;->(Landroidx/compose/runtime/NextFrameEndCallbackQueue;Lkotlin/jvm/functions/Function0;)V @@ -4889,13 +4892,13 @@ HSPLandroidx/compose/runtime/Recomposer;->()V HSPLandroidx/compose/runtime/Recomposer;->(Lkotlin/coroutines/CoroutineContext;)V HSPLandroidx/compose/runtime/Recomposer;->access$awaitWorkAvailable(Landroidx/compose/runtime/Recomposer;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; HSPLandroidx/compose/runtime/Recomposer;->access$deriveStateLocked(Landroidx/compose/runtime/Recomposer;)Lkotlinx/coroutines/CancellableContinuation; -HSPLandroidx/compose/runtime/Recomposer;->access$discardUnusedMovableContentState(Landroidx/compose/runtime/Recomposer;)V +PLandroidx/compose/runtime/Recomposer;->access$discardUnusedMovableContentState(Landroidx/compose/runtime/Recomposer;)V HSPLandroidx/compose/runtime/Recomposer;->access$getBroadcastFrameClock$p(Landroidx/compose/runtime/Recomposer;)Landroidx/compose/runtime/BroadcastFrameClock; HSPLandroidx/compose/runtime/Recomposer;->access$getCompositionInvalidations$p(Landroidx/compose/runtime/Recomposer;)Landroidx/compose/runtime/collection/MutableVector; HSPLandroidx/compose/runtime/Recomposer;->access$getHasBroadcastFrameClockAwaiters(Landroidx/compose/runtime/Recomposer;)Z HSPLandroidx/compose/runtime/Recomposer;->access$getHasSchedulingWork(Landroidx/compose/runtime/Recomposer;)Z HSPLandroidx/compose/runtime/Recomposer;->access$getMovableContentAwaitingInsert$p(Landroidx/compose/runtime/Recomposer;)Ljava/util/List; -HSPLandroidx/compose/runtime/Recomposer;->access$getNextFrameEndCallbackQueue$p(Landroidx/compose/runtime/Recomposer;)Landroidx/compose/runtime/NextFrameEndCallbackQueue; +PLandroidx/compose/runtime/Recomposer;->access$getNextFrameEndCallbackQueue$p(Landroidx/compose/runtime/Recomposer;)Landroidx/compose/runtime/NextFrameEndCallbackQueue; HSPLandroidx/compose/runtime/Recomposer;->access$getRecomposerInfo$p(Landroidx/compose/runtime/Recomposer;)Landroidx/compose/runtime/Recomposer$RecomposerInfoImpl; HSPLandroidx/compose/runtime/Recomposer;->access$getShouldKeepRecomposing(Landroidx/compose/runtime/Recomposer;)Z HSPLandroidx/compose/runtime/Recomposer;->access$getSnapshotInvalidations$p(Landroidx/compose/runtime/Recomposer;)Landroidx/collection/MutableScatterSet; @@ -5829,7 +5832,6 @@ HSPLandroidx/compose/runtime/external/kotlinx/collections/immutable/implementati HSPLandroidx/compose/runtime/external/kotlinx/collections/immutable/implementations/immutableMap/TrieNode;->insertEntryAt(ILjava/lang/Object;Ljava/lang/Object;)Landroidx/compose/runtime/external/kotlinx/collections/immutable/implementations/immutableMap/TrieNode; HSPLandroidx/compose/runtime/external/kotlinx/collections/immutable/implementations/immutableMap/TrieNode;->keyAtIndex(I)Ljava/lang/Object; HSPLandroidx/compose/runtime/external/kotlinx/collections/immutable/implementations/immutableMap/TrieNode;->makeNode(ILjava/lang/Object;Ljava/lang/Object;ILjava/lang/Object;Ljava/lang/Object;ILandroidx/compose/runtime/external/kotlinx/collections/immutable/internal/MutabilityOwnership;)Landroidx/compose/runtime/external/kotlinx/collections/immutable/implementations/immutableMap/TrieNode; -HSPLandroidx/compose/runtime/external/kotlinx/collections/immutable/implementations/immutableMap/TrieNode;->moveEntryToNode(IIILjava/lang/Object;Ljava/lang/Object;I)Landroidx/compose/runtime/external/kotlinx/collections/immutable/implementations/immutableMap/TrieNode; HSPLandroidx/compose/runtime/external/kotlinx/collections/immutable/implementations/immutableMap/TrieNode;->mutableInsertEntryAt(ILjava/lang/Object;Ljava/lang/Object;Landroidx/compose/runtime/external/kotlinx/collections/immutable/internal/MutabilityOwnership;)Landroidx/compose/runtime/external/kotlinx/collections/immutable/implementations/immutableMap/TrieNode; HSPLandroidx/compose/runtime/external/kotlinx/collections/immutable/implementations/immutableMap/TrieNode;->mutableMoveEntryToNode(IIILjava/lang/Object;Ljava/lang/Object;ILandroidx/compose/runtime/external/kotlinx/collections/immutable/internal/MutabilityOwnership;)Landroidx/compose/runtime/external/kotlinx/collections/immutable/implementations/immutableMap/TrieNode; HSPLandroidx/compose/runtime/external/kotlinx/collections/immutable/implementations/immutableMap/TrieNode;->mutablePut(ILjava/lang/Object;Ljava/lang/Object;ILandroidx/compose/runtime/external/kotlinx/collections/immutable/implementations/immutableMap/PersistentHashMapBuilder;)Landroidx/compose/runtime/external/kotlinx/collections/immutable/implementations/immutableMap/TrieNode; @@ -5921,7 +5923,7 @@ HSPLandroidx/compose/runtime/internal/AtomicAwaitersCount$Companion;->(Lko Landroidx/compose/runtime/internal/AtomicBoolean; HSPLandroidx/compose/runtime/internal/AtomicBoolean;->constructor-impl(Landroidx/compose/runtime/internal/AtomicInt;)Landroidx/compose/runtime/internal/AtomicInt; HSPLandroidx/compose/runtime/internal/AtomicBoolean;->constructor-impl(Z)Landroidx/compose/runtime/internal/AtomicInt; -HSPLandroidx/compose/runtime/internal/AtomicBoolean;->set-impl(Landroidx/compose/runtime/internal/AtomicInt;Z)V +PLandroidx/compose/runtime/internal/AtomicBoolean;->set-impl(Landroidx/compose/runtime/internal/AtomicInt;Z)V Landroidx/compose/runtime/internal/AtomicInt; HSPLandroidx/compose/runtime/internal/AtomicInt;->()V HSPLandroidx/compose/runtime/internal/AtomicInt;->(I)V @@ -9915,8 +9917,8 @@ HSPLandroidx/compose/ui/platform/AndroidComposeView$$ExternalSyntheticLambda22;- Landroidx/compose/ui/platform/AndroidComposeView$$ExternalSyntheticLambda23; HSPLandroidx/compose/ui/platform/AndroidComposeView$$ExternalSyntheticLambda23;->(Landroidx/compose/ui/platform/AndroidComposeView;)V HSPLandroidx/compose/ui/platform/AndroidComposeView$$ExternalSyntheticLambda23;->invoke()Ljava/lang/Object; -Landroidx/compose/ui/platform/AndroidComposeView$$ExternalSyntheticLambda7; -HSPLandroidx/compose/ui/platform/AndroidComposeView$$ExternalSyntheticLambda7;->(Landroidx/compose/ui/platform/AndroidComposeView;)V +Landroidx/compose/ui/platform/AndroidComposeView$$ExternalSyntheticLambda8; +HSPLandroidx/compose/ui/platform/AndroidComposeView$$ExternalSyntheticLambda8;->(Landroidx/compose/ui/platform/AndroidComposeView;)V Landroidx/compose/ui/platform/AndroidComposeView$AndroidComposeViewNavigationSoundEffect; HSPLandroidx/compose/ui/platform/AndroidComposeView$AndroidComposeViewNavigationSoundEffect;->(Landroid/view/View;)V Landroidx/compose/ui/platform/AndroidComposeView$Companion; @@ -10038,7 +10040,7 @@ HSPLandroidx/compose/ui/platform/AndroidUiDispatcher;->access$getMain$delegate$c HSPLandroidx/compose/ui/platform/AndroidUiDispatcher;->access$getToRunOnFrame$p(Landroidx/compose/ui/platform/AndroidUiDispatcher;)Ljava/util/List; HSPLandroidx/compose/ui/platform/AndroidUiDispatcher;->access$performFrameDispatch(Landroidx/compose/ui/platform/AndroidUiDispatcher;J)V HSPLandroidx/compose/ui/platform/AndroidUiDispatcher;->access$performTrampolineDispatch(Landroidx/compose/ui/platform/AndroidUiDispatcher;)V -HSPLandroidx/compose/ui/platform/AndroidUiDispatcher;->access$setScheduledFrameDispatch$p(Landroidx/compose/ui/platform/AndroidUiDispatcher;Z)V +PLandroidx/compose/ui/platform/AndroidUiDispatcher;->access$setScheduledFrameDispatch$p(Landroidx/compose/ui/platform/AndroidUiDispatcher;Z)V HSPLandroidx/compose/ui/platform/AndroidUiDispatcher;->dispatch(Lkotlin/coroutines/CoroutineContext;Ljava/lang/Runnable;)V HSPLandroidx/compose/ui/platform/AndroidUiDispatcher;->getChoreographer()Landroid/view/Choreographer; HSPLandroidx/compose/ui/platform/AndroidUiDispatcher;->nextTask()Ljava/lang/Runnable; @@ -12353,15 +12355,15 @@ HSPLandroidx/concurrent/futures/AbstractResolvableFuture;->()V HSPLandroidx/concurrent/futures/AbstractResolvableFuture;->()V HSPLandroidx/concurrent/futures/AbstractResolvableFuture;->addListener(Ljava/lang/Runnable;Ljava/util/concurrent/Executor;)V HSPLandroidx/concurrent/futures/AbstractResolvableFuture;->afterDone()V -HSPLandroidx/concurrent/futures/AbstractResolvableFuture;->cancel(Z)Z +PLandroidx/concurrent/futures/AbstractResolvableFuture;->cancel(Z)Z HSPLandroidx/concurrent/futures/AbstractResolvableFuture;->checkNotNull(Ljava/lang/Object;)Ljava/lang/Object; HSPLandroidx/concurrent/futures/AbstractResolvableFuture;->clearListeners(Landroidx/concurrent/futures/AbstractResolvableFuture$Listener;)Landroidx/concurrent/futures/AbstractResolvableFuture$Listener; HSPLandroidx/concurrent/futures/AbstractResolvableFuture;->complete(Landroidx/concurrent/futures/AbstractResolvableFuture;)V -HSPLandroidx/concurrent/futures/AbstractResolvableFuture;->executeListener(Ljava/lang/Runnable;Ljava/util/concurrent/Executor;)V +PLandroidx/concurrent/futures/AbstractResolvableFuture;->executeListener(Ljava/lang/Runnable;Ljava/util/concurrent/Executor;)V PLandroidx/concurrent/futures/AbstractResolvableFuture;->get()Ljava/lang/Object; PLandroidx/concurrent/futures/AbstractResolvableFuture;->getDoneValue(Ljava/lang/Object;)Ljava/lang/Object; -HSPLandroidx/concurrent/futures/AbstractResolvableFuture;->isCancelled()Z -HSPLandroidx/concurrent/futures/AbstractResolvableFuture;->isDone()Z +PLandroidx/concurrent/futures/AbstractResolvableFuture;->isCancelled()Z +PLandroidx/concurrent/futures/AbstractResolvableFuture;->isDone()Z HSPLandroidx/concurrent/futures/AbstractResolvableFuture;->releaseWaiters()V HSPLandroidx/concurrent/futures/AbstractResolvableFuture;->set(Ljava/lang/Object;)Z Landroidx/concurrent/futures/AbstractResolvableFuture$AtomicHelper; @@ -12388,25 +12390,25 @@ Landroidx/concurrent/futures/CallbackToFutureAdapter$Completer; HSPLandroidx/concurrent/futures/CallbackToFutureAdapter$Completer;->()V HSPLandroidx/concurrent/futures/CallbackToFutureAdapter$Completer;->addCancellationListener(Ljava/lang/Runnable;Ljava/util/concurrent/Executor;)V PLandroidx/concurrent/futures/CallbackToFutureAdapter$Completer;->finalize()V -HSPLandroidx/concurrent/futures/CallbackToFutureAdapter$Completer;->fireCancellationListeners()V +PLandroidx/concurrent/futures/CallbackToFutureAdapter$Completer;->fireCancellationListeners()V HSPLandroidx/concurrent/futures/CallbackToFutureAdapter$Completer;->set(Ljava/lang/Object;)Z -HSPLandroidx/concurrent/futures/CallbackToFutureAdapter$Completer;->setCancelled()Z +PLandroidx/concurrent/futures/CallbackToFutureAdapter$Completer;->setCancelled()Z HSPLandroidx/concurrent/futures/CallbackToFutureAdapter$Completer;->setCompletedNormally()V Landroidx/concurrent/futures/CallbackToFutureAdapter$Resolver; Landroidx/concurrent/futures/CallbackToFutureAdapter$SafeFuture; HSPLandroidx/concurrent/futures/CallbackToFutureAdapter$SafeFuture;->(Landroidx/concurrent/futures/CallbackToFutureAdapter$Completer;)V HSPLandroidx/concurrent/futures/CallbackToFutureAdapter$SafeFuture;->addListener(Ljava/lang/Runnable;Ljava/util/concurrent/Executor;)V -HSPLandroidx/concurrent/futures/CallbackToFutureAdapter$SafeFuture;->cancel(Z)Z +PLandroidx/concurrent/futures/CallbackToFutureAdapter$SafeFuture;->cancel(Z)Z PLandroidx/concurrent/futures/CallbackToFutureAdapter$SafeFuture;->get()Ljava/lang/Object; -HSPLandroidx/concurrent/futures/CallbackToFutureAdapter$SafeFuture;->isCancelled()Z -HSPLandroidx/concurrent/futures/CallbackToFutureAdapter$SafeFuture;->isDone()Z +PLandroidx/concurrent/futures/CallbackToFutureAdapter$SafeFuture;->isCancelled()Z +PLandroidx/concurrent/futures/CallbackToFutureAdapter$SafeFuture;->isDone()Z HSPLandroidx/concurrent/futures/CallbackToFutureAdapter$SafeFuture;->set(Ljava/lang/Object;)Z Landroidx/concurrent/futures/CallbackToFutureAdapter$SafeFuture$1; HSPLandroidx/concurrent/futures/CallbackToFutureAdapter$SafeFuture$1;->(Landroidx/concurrent/futures/CallbackToFutureAdapter$SafeFuture;)V Landroidx/concurrent/futures/ResolvableFuture; HSPLandroidx/concurrent/futures/ResolvableFuture;->()V HSPLandroidx/concurrent/futures/ResolvableFuture;->create()Landroidx/concurrent/futures/ResolvableFuture; -HSPLandroidx/concurrent/futures/ResolvableFuture;->set(Ljava/lang/Object;)Z +PLandroidx/concurrent/futures/ResolvableFuture;->set(Ljava/lang/Object;)Z Landroidx/core/R$id; Landroidx/core/app/ActivityCompat; HSPLandroidx/core/app/ActivityCompat;->shouldShowRequestPermissionRationale(Landroid/app/Activity;Ljava/lang/String;)Z @@ -12527,22 +12529,18 @@ HSPLandroidx/core/graphics/Insets;->(IIII)V HSPLandroidx/core/graphics/Insets;->equals(Ljava/lang/Object;)Z HSPLandroidx/core/graphics/Insets;->of(IIII)Landroidx/core/graphics/Insets; HSPLandroidx/core/graphics/Insets;->toCompatInsets(Landroid/graphics/Insets;)Landroidx/core/graphics/Insets; -Landroidx/core/graphics/TypefaceCompat; -HSPLandroidx/core/graphics/TypefaceCompat;->()V +PLandroidx/core/graphics/TypefaceCompat;->()V PLandroidx/core/graphics/TypefaceCompat;->createFromFontInfo(Landroid/content/Context;Landroid/os/CancellationSignal;[Landroidx/core/provider/FontsContractCompat$FontInfo;I)Landroid/graphics/Typeface; -HSPLandroidx/core/graphics/TypefaceCompat;->getSystemFontFamily(Ljava/lang/String;)Landroid/graphics/Typeface; -Landroidx/core/graphics/TypefaceCompatApi29Impl; -HSPLandroidx/core/graphics/TypefaceCompatApi29Impl;->()V +PLandroidx/core/graphics/TypefaceCompat;->getSystemFontFamily(Ljava/lang/String;)Landroid/graphics/Typeface; +PLandroidx/core/graphics/TypefaceCompatApi29Impl;->()V PLandroidx/core/graphics/TypefaceCompatApi29Impl;->createFromFontInfo(Landroid/content/Context;Landroid/os/CancellationSignal;[Landroidx/core/provider/FontsContractCompat$FontInfo;I)Landroid/graphics/Typeface; PLandroidx/core/graphics/TypefaceCompatApi29Impl;->findBaseFont(Landroid/graphics/fonts/FontFamily;I)Landroid/graphics/fonts/Font; PLandroidx/core/graphics/TypefaceCompatApi29Impl;->getFont(Landroid/os/CancellationSignal;Landroidx/core/provider/FontsContractCompat$FontInfo;Landroid/content/ContentResolver;)Landroid/graphics/fonts/Font; PLandroidx/core/graphics/TypefaceCompatApi29Impl;->getFontFamily(Landroid/os/CancellationSignal;[Landroidx/core/provider/FontsContractCompat$FontInfo;Landroid/content/ContentResolver;)Landroid/graphics/fonts/FontFamily; PLandroidx/core/graphics/TypefaceCompatApi29Impl;->getFontFromProvider(Landroid/os/CancellationSignal;Landroidx/core/provider/FontsContractCompat$FontInfo;Landroid/content/ContentResolver;)Landroid/graphics/fonts/Font; PLandroidx/core/graphics/TypefaceCompatApi29Impl;->getMatchScore(Landroid/graphics/fonts/FontStyle;Landroid/graphics/fonts/FontStyle;)I -Landroidx/core/graphics/TypefaceCompatApi31Impl; -HSPLandroidx/core/graphics/TypefaceCompatApi31Impl;->()V -Landroidx/core/graphics/TypefaceCompatBaseImpl; -HSPLandroidx/core/graphics/TypefaceCompatBaseImpl;->()V +PLandroidx/core/graphics/TypefaceCompatApi31Impl;->()V +PLandroidx/core/graphics/TypefaceCompatBaseImpl;->()V PLandroidx/core/graphics/TypefaceCompatUtil;->mmap(Landroid/content/Context;Landroid/os/CancellationSignal;Landroid/net/Uri;)Ljava/nio/ByteBuffer; Landroidx/core/graphics/drawable/TintAwareDrawable; Landroidx/core/os/BundleCompat; @@ -12579,43 +12577,38 @@ HSPLandroidx/core/os/LocaleListPlatformWrapper;->isEmpty()Z HSPLandroidx/core/os/LocaleListPlatformWrapper;->size()I Landroidx/core/os/TraceCompat; HSPLandroidx/core/os/TraceCompat;->()V -PLandroidx/core/os/TraceCompat;->beginSection(Ljava/lang/String;)V -PLandroidx/core/os/TraceCompat;->endSection()V +HSPLandroidx/core/os/TraceCompat;->beginSection(Ljava/lang/String;)V +HSPLandroidx/core/os/TraceCompat;->endSection()V Landroidx/core/os/UserManagerCompat; HSPLandroidx/core/os/UserManagerCompat;->isUserUnlocked(Landroid/content/Context;)Z Landroidx/core/os/UserManagerCompat$Api24Impl; HSPLandroidx/core/os/UserManagerCompat$Api24Impl;->isUserUnlocked(Landroid/content/Context;)Z -Landroidx/core/provider/FontProvider; -HSPLandroidx/core/provider/FontProvider;->()V +PLandroidx/core/provider/FontProvider;->()V PLandroidx/core/provider/FontProvider;->convertToByteArrayList([Landroid/content/pm/Signature;)Ljava/util/List; PLandroidx/core/provider/FontProvider;->equalsByteArrayList(Ljava/util/List;Ljava/util/List;)Z -HSPLandroidx/core/provider/FontProvider;->getCertificates(Landroidx/core/provider/FontRequest;Landroid/content/res/Resources;)Ljava/util/List; -HSPLandroidx/core/provider/FontProvider;->getFontFamilyResult(Landroid/content/Context;Ljava/util/List;Landroid/os/CancellationSignal;)Landroidx/core/provider/FontsContractCompat$FontFamilyResult; -HSPLandroidx/core/provider/FontProvider;->getProvider(Landroid/content/pm/PackageManager;Landroidx/core/provider/FontRequest;Landroid/content/res/Resources;)Landroid/content/pm/ProviderInfo; +PLandroidx/core/provider/FontProvider;->getCertificates(Landroidx/core/provider/FontRequest;Landroid/content/res/Resources;)Ljava/util/List; +PLandroidx/core/provider/FontProvider;->getFontFamilyResult(Landroid/content/Context;Ljava/util/List;Landroid/os/CancellationSignal;)Landroidx/core/provider/FontsContractCompat$FontFamilyResult; +PLandroidx/core/provider/FontProvider;->getProvider(Landroid/content/pm/PackageManager;Landroidx/core/provider/FontRequest;Landroid/content/res/Resources;)Landroid/content/pm/ProviderInfo; PLandroidx/core/provider/FontProvider;->getSelectionArgs(Landroidx/core/provider/FontRequest;)[Ljava/lang/String; PLandroidx/core/provider/FontProvider;->query(Landroid/content/Context;Landroidx/core/provider/FontRequest;Ljava/lang/String;Landroid/os/CancellationSignal;)[Landroidx/core/provider/FontsContractCompat$FontInfo; -Landroidx/core/provider/FontProvider$$ExternalSyntheticLambda0; -HSPLandroidx/core/provider/FontProvider$$ExternalSyntheticLambda0;->()V +PLandroidx/core/provider/FontProvider$$ExternalSyntheticLambda0;->()V PLandroidx/core/provider/FontProvider$ContentQueryWrapper;->make(Landroid/content/Context;Landroid/net/Uri;)Landroidx/core/provider/FontProvider$ContentQueryWrapper; PLandroidx/core/provider/FontProvider$ContentQueryWrapperApi24Impl;->(Landroid/content/Context;Landroid/net/Uri;)V PLandroidx/core/provider/FontProvider$ContentQueryWrapperApi24Impl;->close()V PLandroidx/core/provider/FontProvider$ContentQueryWrapperApi24Impl;->query(Landroid/net/Uri;[Ljava/lang/String;Ljava/lang/String;[Ljava/lang/String;Ljava/lang/String;Landroid/os/CancellationSignal;)Landroid/database/Cursor; -Landroidx/core/provider/FontProvider$ProviderCacheKey; -HSPLandroidx/core/provider/FontProvider$ProviderCacheKey;->(Ljava/lang/String;Ljava/lang/String;Ljava/util/List;)V +PLandroidx/core/provider/FontProvider$ProviderCacheKey;->(Ljava/lang/String;Ljava/lang/String;Ljava/util/List;)V PLandroidx/core/provider/FontProvider$ProviderCacheKey;->hashCode()I -Landroidx/core/provider/FontRequest; -HSPLandroidx/core/provider/FontRequest;->(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/util/List;)V -HSPLandroidx/core/provider/FontRequest;->(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/util/List;Ljava/lang/String;Ljava/lang/String;)V -HSPLandroidx/core/provider/FontRequest;->createIdentifier(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String; -HSPLandroidx/core/provider/FontRequest;->getCertificates()Ljava/util/List; -HSPLandroidx/core/provider/FontRequest;->getProviderAuthority()Ljava/lang/String; -HSPLandroidx/core/provider/FontRequest;->getProviderPackage()Ljava/lang/String; +PLandroidx/core/provider/FontRequest;->(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/util/List;)V +PLandroidx/core/provider/FontRequest;->(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/util/List;Ljava/lang/String;Ljava/lang/String;)V +PLandroidx/core/provider/FontRequest;->createIdentifier(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String; +PLandroidx/core/provider/FontRequest;->getCertificates()Ljava/util/List; +PLandroidx/core/provider/FontRequest;->getProviderAuthority()Ljava/lang/String; +PLandroidx/core/provider/FontRequest;->getProviderPackage()Ljava/lang/String; PLandroidx/core/provider/FontRequest;->getQuery()Ljava/lang/String; -HSPLandroidx/core/provider/FontRequest;->getSystemFont()Ljava/lang/String; +PLandroidx/core/provider/FontRequest;->getSystemFont()Ljava/lang/String; PLandroidx/core/provider/FontRequest;->getVariationSettings()Ljava/lang/String; -Landroidx/core/provider/FontsContractCompat; PLandroidx/core/provider/FontsContractCompat;->buildTypeface(Landroid/content/Context;Landroid/os/CancellationSignal;[Landroidx/core/provider/FontsContractCompat$FontInfo;)Landroid/graphics/Typeface; -HSPLandroidx/core/provider/FontsContractCompat;->fetchFonts(Landroid/content/Context;Landroid/os/CancellationSignal;Landroidx/core/provider/FontRequest;)Landroidx/core/provider/FontsContractCompat$FontFamilyResult; +PLandroidx/core/provider/FontsContractCompat;->fetchFonts(Landroid/content/Context;Landroid/os/CancellationSignal;Landroidx/core/provider/FontRequest;)Landroidx/core/provider/FontsContractCompat$FontFamilyResult; PLandroidx/core/provider/FontsContractCompat$FontFamilyResult;->(ILjava/util/List;)V PLandroidx/core/provider/FontsContractCompat$FontFamilyResult;->create(ILjava/util/List;)Landroidx/core/provider/FontsContractCompat$FontFamilyResult; PLandroidx/core/provider/FontsContractCompat$FontFamilyResult;->getFonts()[Landroidx/core/provider/FontsContractCompat$FontInfo; @@ -12655,7 +12648,7 @@ Landroidx/core/util/Consumer; Landroidx/core/util/ObjectsCompat; HSPLandroidx/core/util/ObjectsCompat;->requireNonNull(Ljava/lang/Object;Ljava/lang/String;)Ljava/lang/Object; Landroidx/core/util/Preconditions; -PLandroidx/core/util/Preconditions;->checkArgument(ZLjava/lang/Object;)V +HSPLandroidx/core/util/Preconditions;->checkArgument(ZLjava/lang/Object;)V PLandroidx/core/util/Preconditions;->checkArgumentNonnegative(ILjava/lang/String;)I HSPLandroidx/core/util/Preconditions;->checkNotNull(Ljava/lang/Object;)Ljava/lang/Object; HSPLandroidx/core/util/Preconditions;->checkNotNull(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object; @@ -13024,6 +13017,7 @@ HSPLandroidx/datastore/core/DataStoreImpl$handleUpdate$2$1;->invoke(Lkotlinx/cor HSPLandroidx/datastore/core/DataStoreImpl$handleUpdate$2$1;->invokeSuspend(Ljava/lang/Object;)Ljava/lang/Object; Landroidx/datastore/core/DataStoreImpl$incrementCollector$1; HSPLandroidx/datastore/core/DataStoreImpl$incrementCollector$1;->(Landroidx/datastore/core/DataStoreImpl;Lkotlin/coroutines/Continuation;)V +HSPLandroidx/datastore/core/DataStoreImpl$incrementCollector$1;->invokeSuspend(Ljava/lang/Object;)Ljava/lang/Object; Landroidx/datastore/core/DataStoreImpl$incrementCollector$2$1; HSPLandroidx/datastore/core/DataStoreImpl$incrementCollector$2$1;->(Landroidx/datastore/core/DataStoreImpl;Lkotlin/coroutines/Continuation;)V HSPLandroidx/datastore/core/DataStoreImpl$incrementCollector$2$1;->create(Ljava/lang/Object;Lkotlin/coroutines/Continuation;)Lkotlin/coroutines/Continuation; @@ -13066,8 +13060,6 @@ HSPLandroidx/datastore/core/DataStoreImpl$transformAndWrite$2;->invokeSuspend(Lj Landroidx/datastore/core/DataStoreImpl$transformAndWrite$2$newData$1; HSPLandroidx/datastore/core/DataStoreImpl$transformAndWrite$2$newData$1;->(Lkotlin/jvm/functions/Function2;Landroidx/datastore/core/Data;Lkotlin/coroutines/Continuation;)V HSPLandroidx/datastore/core/DataStoreImpl$transformAndWrite$2$newData$1;->create(Ljava/lang/Object;Lkotlin/coroutines/Continuation;)Lkotlin/coroutines/Continuation; -HSPLandroidx/datastore/core/DataStoreImpl$transformAndWrite$2$newData$1;->invoke(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object; -HSPLandroidx/datastore/core/DataStoreImpl$transformAndWrite$2$newData$1;->invoke(Lkotlinx/coroutines/CoroutineScope;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; HSPLandroidx/datastore/core/DataStoreImpl$transformAndWrite$2$newData$1;->invokeSuspend(Ljava/lang/Object;)Ljava/lang/Object; Landroidx/datastore/core/DataStoreImpl$updateData$2; HSPLandroidx/datastore/core/DataStoreImpl$updateData$2;->(Landroidx/datastore/core/DataStoreImpl;Lkotlin/jvm/functions/Function2;Lkotlin/coroutines/Continuation;)V @@ -14112,31 +14104,26 @@ HSPLandroidx/emoji2/text/ConcurrencyHelpers;->createBackgroundPriorityExecutor(L HSPLandroidx/emoji2/text/ConcurrencyHelpers;->lambda$createBackgroundPriorityExecutor$0(Ljava/lang/String;Ljava/lang/Runnable;)Ljava/lang/Thread; HSPLandroidx/emoji2/text/ConcurrencyHelpers;->mainHandlerAsync()Landroid/os/Handler; Landroidx/emoji2/text/ConcurrencyHelpers$$ExternalSyntheticLambda1; -PLandroidx/emoji2/text/ConcurrencyHelpers$$ExternalSyntheticLambda1;->(Ljava/lang/String;)V +HSPLandroidx/emoji2/text/ConcurrencyHelpers$$ExternalSyntheticLambda1;->(Ljava/lang/String;)V HSPLandroidx/emoji2/text/ConcurrencyHelpers$$ExternalSyntheticLambda1;->newThread(Ljava/lang/Runnable;)Ljava/lang/Thread; Landroidx/emoji2/text/ConcurrencyHelpers$Handler28Impl; HSPLandroidx/emoji2/text/ConcurrencyHelpers$Handler28Impl;->createAsync(Landroid/os/Looper;)Landroid/os/Handler; -Landroidx/emoji2/text/DefaultEmojiCompatConfig; PLandroidx/emoji2/text/DefaultEmojiCompatConfig;->create(Landroid/content/Context;)Landroidx/emoji2/text/FontRequestEmojiCompatConfig; -Landroidx/emoji2/text/DefaultEmojiCompatConfig$DefaultEmojiCompatConfigFactory; PLandroidx/emoji2/text/DefaultEmojiCompatConfig$DefaultEmojiCompatConfigFactory;->(Landroidx/emoji2/text/DefaultEmojiCompatConfig$DefaultEmojiCompatConfigHelper;)V PLandroidx/emoji2/text/DefaultEmojiCompatConfig$DefaultEmojiCompatConfigFactory;->configOrNull(Landroid/content/Context;Landroidx/core/provider/FontRequest;)Landroidx/emoji2/text/EmojiCompat$Config; -HSPLandroidx/emoji2/text/DefaultEmojiCompatConfig$DefaultEmojiCompatConfigFactory;->convertToByteArray([Landroid/content/pm/Signature;)Ljava/util/List; +PLandroidx/emoji2/text/DefaultEmojiCompatConfig$DefaultEmojiCompatConfigFactory;->convertToByteArray([Landroid/content/pm/Signature;)Ljava/util/List; PLandroidx/emoji2/text/DefaultEmojiCompatConfig$DefaultEmojiCompatConfigFactory;->create(Landroid/content/Context;)Landroidx/emoji2/text/EmojiCompat$Config; -HSPLandroidx/emoji2/text/DefaultEmojiCompatConfig$DefaultEmojiCompatConfigFactory;->generateFontRequestFrom(Landroid/content/pm/ProviderInfo;Landroid/content/pm/PackageManager;)Landroidx/core/provider/FontRequest; +PLandroidx/emoji2/text/DefaultEmojiCompatConfig$DefaultEmojiCompatConfigFactory;->generateFontRequestFrom(Landroid/content/pm/ProviderInfo;Landroid/content/pm/PackageManager;)Landroidx/core/provider/FontRequest; PLandroidx/emoji2/text/DefaultEmojiCompatConfig$DefaultEmojiCompatConfigFactory;->getHelperForApi()Landroidx/emoji2/text/DefaultEmojiCompatConfig$DefaultEmojiCompatConfigHelper; -HSPLandroidx/emoji2/text/DefaultEmojiCompatConfig$DefaultEmojiCompatConfigFactory;->hasFlagSystem(Landroid/content/pm/ProviderInfo;)Z -HSPLandroidx/emoji2/text/DefaultEmojiCompatConfig$DefaultEmojiCompatConfigFactory;->queryDefaultInstalledContentProvider(Landroid/content/pm/PackageManager;)Landroid/content/pm/ProviderInfo; -HSPLandroidx/emoji2/text/DefaultEmojiCompatConfig$DefaultEmojiCompatConfigFactory;->queryForDefaultFontRequest(Landroid/content/Context;)Landroidx/core/provider/FontRequest; -Landroidx/emoji2/text/DefaultEmojiCompatConfig$DefaultEmojiCompatConfigHelper; +PLandroidx/emoji2/text/DefaultEmojiCompatConfig$DefaultEmojiCompatConfigFactory;->hasFlagSystem(Landroid/content/pm/ProviderInfo;)Z +PLandroidx/emoji2/text/DefaultEmojiCompatConfig$DefaultEmojiCompatConfigFactory;->queryDefaultInstalledContentProvider(Landroid/content/pm/PackageManager;)Landroid/content/pm/ProviderInfo; +PLandroidx/emoji2/text/DefaultEmojiCompatConfig$DefaultEmojiCompatConfigFactory;->queryForDefaultFontRequest(Landroid/content/Context;)Landroidx/core/provider/FontRequest; PLandroidx/emoji2/text/DefaultEmojiCompatConfig$DefaultEmojiCompatConfigHelper;->()V -Landroidx/emoji2/text/DefaultEmojiCompatConfig$DefaultEmojiCompatConfigHelper_API19; PLandroidx/emoji2/text/DefaultEmojiCompatConfig$DefaultEmojiCompatConfigHelper_API19;->()V -HSPLandroidx/emoji2/text/DefaultEmojiCompatConfig$DefaultEmojiCompatConfigHelper_API19;->getProviderInfo(Landroid/content/pm/ResolveInfo;)Landroid/content/pm/ProviderInfo; -HSPLandroidx/emoji2/text/DefaultEmojiCompatConfig$DefaultEmojiCompatConfigHelper_API19;->queryIntentContentProviders(Landroid/content/pm/PackageManager;Landroid/content/Intent;I)Ljava/util/List; -Landroidx/emoji2/text/DefaultEmojiCompatConfig$DefaultEmojiCompatConfigHelper_API28; +PLandroidx/emoji2/text/DefaultEmojiCompatConfig$DefaultEmojiCompatConfigHelper_API19;->getProviderInfo(Landroid/content/pm/ResolveInfo;)Landroid/content/pm/ProviderInfo; +PLandroidx/emoji2/text/DefaultEmojiCompatConfig$DefaultEmojiCompatConfigHelper_API19;->queryIntentContentProviders(Landroid/content/pm/PackageManager;Landroid/content/Intent;I)Ljava/util/List; PLandroidx/emoji2/text/DefaultEmojiCompatConfig$DefaultEmojiCompatConfigHelper_API28;->()V -HSPLandroidx/emoji2/text/DefaultEmojiCompatConfig$DefaultEmojiCompatConfigHelper_API28;->getSigningSignatures(Landroid/content/pm/PackageManager;Ljava/lang/String;)[Landroid/content/pm/Signature; +PLandroidx/emoji2/text/DefaultEmojiCompatConfig$DefaultEmojiCompatConfigHelper_API28;->getSigningSignatures(Landroid/content/pm/PackageManager;Ljava/lang/String;)[Landroid/content/pm/Signature; Landroidx/emoji2/text/DefaultGlyphChecker; HSPLandroidx/emoji2/text/DefaultGlyphChecker;->()V HSPLandroidx/emoji2/text/DefaultGlyphChecker;->()V @@ -14163,7 +14150,7 @@ HSPLandroidx/emoji2/text/EmojiCompat$CompatInternal19;->loadMetadata()V PLandroidx/emoji2/text/EmojiCompat$CompatInternal19;->onMetadataLoadSuccess(Landroidx/emoji2/text/MetadataRepo;)V PLandroidx/emoji2/text/EmojiCompat$CompatInternal19;->process(Ljava/lang/CharSequence;IIIZ)Ljava/lang/CharSequence; Landroidx/emoji2/text/EmojiCompat$CompatInternal19$1; -PLandroidx/emoji2/text/EmojiCompat$CompatInternal19$1;->(Landroidx/emoji2/text/EmojiCompat$CompatInternal19;)V +HSPLandroidx/emoji2/text/EmojiCompat$CompatInternal19$1;->(Landroidx/emoji2/text/EmojiCompat$CompatInternal19;)V PLandroidx/emoji2/text/EmojiCompat$CompatInternal19$1;->onLoaded(Landroidx/emoji2/text/MetadataRepo;)V Landroidx/emoji2/text/EmojiCompat$Config; HSPLandroidx/emoji2/text/EmojiCompat$Config;->(Landroidx/emoji2/text/EmojiCompat$MetadataRepoLoader;)V @@ -14179,7 +14166,7 @@ PLandroidx/emoji2/text/EmojiCompat$ListenerDispatcher;->(Ljava/util/Collec PLandroidx/emoji2/text/EmojiCompat$ListenerDispatcher;->run()V Landroidx/emoji2/text/EmojiCompat$MetadataRepoLoader; Landroidx/emoji2/text/EmojiCompat$MetadataRepoLoaderCallback; -PLandroidx/emoji2/text/EmojiCompat$MetadataRepoLoaderCallback;->()V +HSPLandroidx/emoji2/text/EmojiCompat$MetadataRepoLoaderCallback;->()V Landroidx/emoji2/text/EmojiCompat$SpanFactory; Landroidx/emoji2/text/EmojiCompatInitializer; HSPLandroidx/emoji2/text/EmojiCompatInitializer;->()V @@ -14199,9 +14186,8 @@ HSPLandroidx/emoji2/text/EmojiCompatInitializer$BackgroundDefaultLoader;->doLoad HSPLandroidx/emoji2/text/EmojiCompatInitializer$BackgroundDefaultLoader;->lambda$load$0$androidx-emoji2-text-EmojiCompatInitializer$BackgroundDefaultLoader(Landroidx/emoji2/text/EmojiCompat$MetadataRepoLoaderCallback;Ljava/util/concurrent/ThreadPoolExecutor;)V HSPLandroidx/emoji2/text/EmojiCompatInitializer$BackgroundDefaultLoader;->load(Landroidx/emoji2/text/EmojiCompat$MetadataRepoLoaderCallback;)V Landroidx/emoji2/text/EmojiCompatInitializer$BackgroundDefaultLoader$$ExternalSyntheticLambda0; -PLandroidx/emoji2/text/EmojiCompatInitializer$BackgroundDefaultLoader$$ExternalSyntheticLambda0;->(Landroidx/emoji2/text/EmojiCompatInitializer$BackgroundDefaultLoader;Landroidx/emoji2/text/EmojiCompat$MetadataRepoLoaderCallback;Ljava/util/concurrent/ThreadPoolExecutor;)V +HSPLandroidx/emoji2/text/EmojiCompatInitializer$BackgroundDefaultLoader$$ExternalSyntheticLambda0;->(Landroidx/emoji2/text/EmojiCompatInitializer$BackgroundDefaultLoader;Landroidx/emoji2/text/EmojiCompat$MetadataRepoLoaderCallback;Ljava/util/concurrent/ThreadPoolExecutor;)V HSPLandroidx/emoji2/text/EmojiCompatInitializer$BackgroundDefaultLoader$$ExternalSyntheticLambda0;->run()V -Landroidx/emoji2/text/EmojiCompatInitializer$BackgroundDefaultLoader$1; PLandroidx/emoji2/text/EmojiCompatInitializer$BackgroundDefaultLoader$1;->(Landroidx/emoji2/text/EmojiCompatInitializer$BackgroundDefaultLoader;Landroidx/emoji2/text/EmojiCompat$MetadataRepoLoaderCallback;Ljava/util/concurrent/ThreadPoolExecutor;)V PLandroidx/emoji2/text/EmojiCompatInitializer$BackgroundDefaultLoader$1;->onLoaded(Landroidx/emoji2/text/MetadataRepo;)V Landroidx/emoji2/text/EmojiCompatInitializer$LoadEmojiCompatRunnable; @@ -14221,25 +14207,21 @@ PLandroidx/emoji2/text/EmojiProcessor$ProcessorSm;->(Landroidx/emoji2/text PLandroidx/emoji2/text/EmojiProcessor$ProcessorSm;->check(I)I PLandroidx/emoji2/text/EmojiProcessor$ProcessorSm;->isInFlushableState()Z PLandroidx/emoji2/text/EmojiProcessor$ProcessorSm;->reset()I -Landroidx/emoji2/text/FontRequestEmojiCompatConfig; -HSPLandroidx/emoji2/text/FontRequestEmojiCompatConfig;->()V +PLandroidx/emoji2/text/FontRequestEmojiCompatConfig;->()V PLandroidx/emoji2/text/FontRequestEmojiCompatConfig;->(Landroid/content/Context;Landroidx/core/provider/FontRequest;)V -HSPLandroidx/emoji2/text/FontRequestEmojiCompatConfig;->setLoadingExecutor(Ljava/util/concurrent/Executor;)Landroidx/emoji2/text/FontRequestEmojiCompatConfig; -Landroidx/emoji2/text/FontRequestEmojiCompatConfig$FontProviderHelper; -HSPLandroidx/emoji2/text/FontRequestEmojiCompatConfig$FontProviderHelper;->()V +PLandroidx/emoji2/text/FontRequestEmojiCompatConfig;->setLoadingExecutor(Ljava/util/concurrent/Executor;)Landroidx/emoji2/text/FontRequestEmojiCompatConfig; +PLandroidx/emoji2/text/FontRequestEmojiCompatConfig$FontProviderHelper;->()V PLandroidx/emoji2/text/FontRequestEmojiCompatConfig$FontProviderHelper;->buildTypeface(Landroid/content/Context;Landroidx/core/provider/FontsContractCompat$FontInfo;)Landroid/graphics/Typeface; -HSPLandroidx/emoji2/text/FontRequestEmojiCompatConfig$FontProviderHelper;->fetchFonts(Landroid/content/Context;Landroidx/core/provider/FontRequest;)Landroidx/core/provider/FontsContractCompat$FontFamilyResult; -Landroidx/emoji2/text/FontRequestEmojiCompatConfig$FontRequestMetadataLoader; +PLandroidx/emoji2/text/FontRequestEmojiCompatConfig$FontProviderHelper;->fetchFonts(Landroid/content/Context;Landroidx/core/provider/FontRequest;)Landroidx/core/provider/FontsContractCompat$FontFamilyResult; PLandroidx/emoji2/text/FontRequestEmojiCompatConfig$FontRequestMetadataLoader;->(Landroid/content/Context;Landroidx/core/provider/FontRequest;Landroidx/emoji2/text/FontRequestEmojiCompatConfig$FontProviderHelper;)V PLandroidx/emoji2/text/FontRequestEmojiCompatConfig$FontRequestMetadataLoader;->cleanUp()V -HSPLandroidx/emoji2/text/FontRequestEmojiCompatConfig$FontRequestMetadataLoader;->createMetadata()V -HSPLandroidx/emoji2/text/FontRequestEmojiCompatConfig$FontRequestMetadataLoader;->load(Landroidx/emoji2/text/EmojiCompat$MetadataRepoLoaderCallback;)V -HSPLandroidx/emoji2/text/FontRequestEmojiCompatConfig$FontRequestMetadataLoader;->loadInternal()V -HSPLandroidx/emoji2/text/FontRequestEmojiCompatConfig$FontRequestMetadataLoader;->retrieveFontInfo()Landroidx/core/provider/FontsContractCompat$FontInfo; -HSPLandroidx/emoji2/text/FontRequestEmojiCompatConfig$FontRequestMetadataLoader;->setExecutor(Ljava/util/concurrent/Executor;)V -Landroidx/emoji2/text/FontRequestEmojiCompatConfig$FontRequestMetadataLoader$$ExternalSyntheticLambda0; -HSPLandroidx/emoji2/text/FontRequestEmojiCompatConfig$FontRequestMetadataLoader$$ExternalSyntheticLambda0;->(Landroidx/emoji2/text/FontRequestEmojiCompatConfig$FontRequestMetadataLoader;)V -HSPLandroidx/emoji2/text/FontRequestEmojiCompatConfig$FontRequestMetadataLoader$$ExternalSyntheticLambda0;->run()V +PLandroidx/emoji2/text/FontRequestEmojiCompatConfig$FontRequestMetadataLoader;->createMetadata()V +PLandroidx/emoji2/text/FontRequestEmojiCompatConfig$FontRequestMetadataLoader;->load(Landroidx/emoji2/text/EmojiCompat$MetadataRepoLoaderCallback;)V +PLandroidx/emoji2/text/FontRequestEmojiCompatConfig$FontRequestMetadataLoader;->loadInternal()V +PLandroidx/emoji2/text/FontRequestEmojiCompatConfig$FontRequestMetadataLoader;->retrieveFontInfo()Landroidx/core/provider/FontsContractCompat$FontInfo; +PLandroidx/emoji2/text/FontRequestEmojiCompatConfig$FontRequestMetadataLoader;->setExecutor(Ljava/util/concurrent/Executor;)V +PLandroidx/emoji2/text/FontRequestEmojiCompatConfig$FontRequestMetadataLoader$$ExternalSyntheticLambda0;->(Landroidx/emoji2/text/FontRequestEmojiCompatConfig$FontRequestMetadataLoader;)V +PLandroidx/emoji2/text/FontRequestEmojiCompatConfig$FontRequestMetadataLoader$$ExternalSyntheticLambda0;->run()V PLandroidx/emoji2/text/MetadataListReader;->findOffsetInfo(Landroidx/emoji2/text/MetadataListReader$OpenTypeReader;)Landroidx/emoji2/text/MetadataListReader$OffsetInfo; PLandroidx/emoji2/text/MetadataListReader;->read(Ljava/nio/ByteBuffer;)Landroidx/emoji2/text/flatbuffer/MetadataList; PLandroidx/emoji2/text/MetadataListReader;->toUnsignedInt(I)J @@ -15052,7 +15034,97 @@ HSPLandroidx/media3/muxer/AacWriter$$ExternalSyntheticBackport0;->m(Ljava/lang/O HSPLandroidx/media3/muxer/AacWriter$$ExternalSyntheticBackport0;->m(Ljava/lang/Object;)V HSPLandroidx/media3/muxer/AacWriter$$ExternalSyntheticBackport0;->m([Ljava/lang/Object;)Ljava/util/List; HSPLandroidx/media3/muxer/AacWriter$$ExternalSyntheticBackport0;->m([Ljava/lang/Object;)Ljava/util/Set; +Landroidx/metrics/performance/DelegatingFrameMetricsListener; +HSPLandroidx/metrics/performance/DelegatingFrameMetricsListener;->()V +HSPLandroidx/metrics/performance/DelegatingFrameMetricsListener;->(Ljava/util/List;)V +HSPLandroidx/metrics/performance/DelegatingFrameMetricsListener;->access$getFrameMetricsHandler$cp()Landroid/os/Handler; +HSPLandroidx/metrics/performance/DelegatingFrameMetricsListener;->access$setFrameMetricsHandler$cp(Landroid/os/Handler;)V +HSPLandroidx/metrics/performance/DelegatingFrameMetricsListener;->onFrameMetricsAvailable(Landroid/view/Window;Landroid/view/FrameMetrics;I)V +Landroidx/metrics/performance/DelegatingFrameMetricsListener$Companion; +HSPLandroidx/metrics/performance/DelegatingFrameMetricsListener$Companion;->()V +HSPLandroidx/metrics/performance/DelegatingFrameMetricsListener$Companion;->(Lkotlin/jvm/internal/DefaultConstructorMarker;)V +HSPLandroidx/metrics/performance/DelegatingFrameMetricsListener$Companion;->addDelegateToWindow(Landroid/view/Window;Landroid/view/Window$OnFrameMetricsAvailableListener;)Z +HSPLandroidx/metrics/performance/DelegatingFrameMetricsListener$Companion;->getFrameMetricsHandler$metrics_performance_release()Landroid/os/Handler; +HSPLandroidx/metrics/performance/DelegatingFrameMetricsListener$Companion;->setFrameMetricsHandler$metrics_performance_release(Landroid/os/Handler;)V +Landroidx/metrics/performance/FrameData; +HSPLandroidx/metrics/performance/FrameData;->(JJZLjava/util/List;)V +PLandroidx/metrics/performance/FrameData;->getFrameDurationUiNanos()J +PLandroidx/metrics/performance/FrameData;->getFrameStartNanos()J +PLandroidx/metrics/performance/FrameData;->isJank()Z +PLandroidx/metrics/performance/FrameData;->update$metrics_performance_release(JJZ)V +Landroidx/metrics/performance/FrameDataApi24; +HSPLandroidx/metrics/performance/FrameDataApi24;->(JJJZLjava/util/List;)V +PLandroidx/metrics/performance/FrameDataApi24;->update$metrics_performance_release(JJJZ)V +Landroidx/metrics/performance/FrameDataApi31; +HSPLandroidx/metrics/performance/FrameDataApi31;->(JJJJJZLjava/util/List;)V +PLandroidx/metrics/performance/FrameDataApi31;->update$metrics_performance_release(JJJJJZ)V +Landroidx/metrics/performance/JankStats; +HSPLandroidx/metrics/performance/JankStats;->()V +HSPLandroidx/metrics/performance/JankStats;->(Landroid/view/Window;Landroidx/metrics/performance/JankStats$OnFrameListener;)V +HSPLandroidx/metrics/performance/JankStats;->(Landroid/view/Window;Landroidx/metrics/performance/JankStats$OnFrameListener;Lkotlin/jvm/internal/DefaultConstructorMarker;)V +PLandroidx/metrics/performance/JankStats;->getJankHeuristicMultiplier()F +PLandroidx/metrics/performance/JankStats;->logFrameData$metrics_performance_release(Landroidx/metrics/performance/FrameData;)V +Landroidx/metrics/performance/JankStats$Companion; +HSPLandroidx/metrics/performance/JankStats$Companion;->()V +HSPLandroidx/metrics/performance/JankStats$Companion;->(Lkotlin/jvm/internal/DefaultConstructorMarker;)V +HSPLandroidx/metrics/performance/JankStats$Companion;->createAndTrack(Landroid/view/Window;Landroidx/metrics/performance/JankStats$OnFrameListener;)Landroidx/metrics/performance/JankStats; Landroidx/metrics/performance/JankStats$OnFrameListener; +Landroidx/metrics/performance/JankStatsApi16Impl; +HSPLandroidx/metrics/performance/JankStatsApi16Impl;->(Landroidx/metrics/performance/JankStats;Landroid/view/View;)V +PLandroidx/metrics/performance/JankStatsApi16Impl;->getMetricsStateHolder()Landroidx/metrics/performance/PerformanceMetricsState$Holder; +HSPLandroidx/metrics/performance/JankStatsApi16Impl;->getStateInfo()Ljava/util/List; +Landroidx/metrics/performance/JankStatsApi16Impl$onFrameListenerDelegate$1; +HSPLandroidx/metrics/performance/JankStatsApi16Impl$onFrameListenerDelegate$1;->(Landroidx/metrics/performance/JankStats;Landroidx/metrics/performance/JankStatsApi16Impl;)V +Landroidx/metrics/performance/JankStatsApi24Impl; +HSPLandroidx/metrics/performance/JankStatsApi24Impl;->(Landroidx/metrics/performance/JankStats;Landroid/view/View;Landroid/view/Window;)V +HSPLandroidx/metrics/performance/JankStatsApi24Impl;->frameMetricsAvailableListenerDelegate$lambda$0(Landroidx/metrics/performance/JankStatsApi24Impl;Landroidx/metrics/performance/JankStats;Landroid/view/Window;Landroid/view/FrameMetrics;I)V +PLandroidx/metrics/performance/JankStatsApi24Impl;->getPrevEnd()J +PLandroidx/metrics/performance/JankStatsApi24Impl;->setPrevEnd(J)V +HSPLandroidx/metrics/performance/JankStatsApi24Impl;->setupFrameTimer$lambda$1(ZLandroidx/metrics/performance/JankStatsApi24Impl;)V +HSPLandroidx/metrics/performance/JankStatsApi24Impl;->setupFrameTimer(Z)V +Landroidx/metrics/performance/JankStatsApi24Impl$$ExternalSyntheticLambda0; +HSPLandroidx/metrics/performance/JankStatsApi24Impl$$ExternalSyntheticLambda0;->(ZLandroidx/metrics/performance/JankStatsApi24Impl;)V +HSPLandroidx/metrics/performance/JankStatsApi24Impl$$ExternalSyntheticLambda0;->run()V +Landroidx/metrics/performance/JankStatsApi24Impl$$ExternalSyntheticLambda1; +HSPLandroidx/metrics/performance/JankStatsApi24Impl$$ExternalSyntheticLambda1;->(Landroidx/metrics/performance/JankStatsApi24Impl;Landroidx/metrics/performance/JankStats;)V +HSPLandroidx/metrics/performance/JankStatsApi24Impl$$ExternalSyntheticLambda1;->onFrameMetricsAvailable(Landroid/view/Window;Landroid/view/FrameMetrics;I)V +Landroidx/metrics/performance/JankStatsApi26Impl; +HSPLandroidx/metrics/performance/JankStatsApi26Impl;->(Landroidx/metrics/performance/JankStats;Landroid/view/View;Landroid/view/Window;)V +HSPLandroidx/metrics/performance/JankStatsApi26Impl;->getFrameStartTime$metrics_performance_release(Landroid/view/FrameMetrics;)J +Landroidx/metrics/performance/JankStatsApi31Impl; +HSPLandroidx/metrics/performance/JankStatsApi31Impl;->(Landroidx/metrics/performance/JankStats;Landroid/view/View;Landroid/view/Window;)V +PLandroidx/metrics/performance/JankStatsApi31Impl;->getExpectedFrameDuration(Landroid/view/FrameMetrics;)J +PLandroidx/metrics/performance/JankStatsApi31Impl;->getFrameData$metrics_performance_release(JJLandroid/view/FrameMetrics;)Landroidx/metrics/performance/FrameDataApi24; +PLandroidx/metrics/performance/JankStatsApi31Impl;->getFrameData$metrics_performance_release(JJLandroid/view/FrameMetrics;)Landroidx/metrics/performance/FrameDataApi31; +Landroidx/metrics/performance/JankStatsBaseImpl; +HSPLandroidx/metrics/performance/JankStatsBaseImpl;->()V +HSPLandroidx/metrics/performance/JankStatsBaseImpl;->(Landroidx/metrics/performance/JankStats;)V +Landroidx/metrics/performance/JankStatsBaseImpl$Companion; +HSPLandroidx/metrics/performance/JankStatsBaseImpl$Companion;->()V +HSPLandroidx/metrics/performance/JankStatsBaseImpl$Companion;->(Lkotlin/jvm/internal/DefaultConstructorMarker;)V +Landroidx/metrics/performance/OnFrameListenerDelegate; +HSPLandroidx/metrics/performance/OnFrameListenerDelegate;->()V +Landroidx/metrics/performance/PerformanceMetricsState; +HSPLandroidx/metrics/performance/PerformanceMetricsState;->()V +HSPLandroidx/metrics/performance/PerformanceMetricsState;->()V +HSPLandroidx/metrics/performance/PerformanceMetricsState;->(Lkotlin/jvm/internal/DefaultConstructorMarker;)V +PLandroidx/metrics/performance/PerformanceMetricsState;->addFrameState(JJLjava/util/List;Ljava/util/List;)V +PLandroidx/metrics/performance/PerformanceMetricsState;->getIntervalStates$metrics_performance_release(JJLjava/util/List;)V +Landroidx/metrics/performance/PerformanceMetricsState$Companion; +HSPLandroidx/metrics/performance/PerformanceMetricsState$Companion;->()V +HSPLandroidx/metrics/performance/PerformanceMetricsState$Companion;->(Lkotlin/jvm/internal/DefaultConstructorMarker;)V +HSPLandroidx/metrics/performance/PerformanceMetricsState$Companion;->create$metrics_performance_release(Landroid/view/View;)Landroidx/metrics/performance/PerformanceMetricsState$Holder; +HSPLandroidx/metrics/performance/PerformanceMetricsState$Companion;->getHolderForHierarchy(Landroid/view/View;)Landroidx/metrics/performance/PerformanceMetricsState$Holder; +Landroidx/metrics/performance/PerformanceMetricsState$Holder; +HSPLandroidx/metrics/performance/PerformanceMetricsState$Holder;->()V +HSPLandroidx/metrics/performance/PerformanceMetricsState$Holder;->getState()Landroidx/metrics/performance/PerformanceMetricsState; +HSPLandroidx/metrics/performance/PerformanceMetricsState$Holder;->setState$metrics_performance_release(Landroidx/metrics/performance/PerformanceMetricsState;)V +Landroidx/metrics/performance/R$id; +Landroidx/navigation/ActivityNavigator$Destination; +Landroidx/navigation/FloatingWindow; +Landroidx/navigation/NavDestination; +Landroidx/navigation/fragment/DialogFragmentNavigator$Destination; +Landroidx/navigation/fragment/FragmentNavigator$Destination; Landroidx/navigation3/ListUtilsKt; HSPLandroidx/navigation3/ListUtilsKt;->fastAnyOrAny(Ljava/util/List;Lkotlin/jvm/functions/Function1;)Z Landroidx/navigation3/runtime/DecoratedNavEntriesKt; @@ -15093,10 +15165,10 @@ Landroidx/navigation3/runtime/EntryProviderScope; HSPLandroidx/navigation3/runtime/EntryProviderScope;->()V HSPLandroidx/navigation3/runtime/EntryProviderScope;->(Lkotlin/jvm/functions/Function1;)V HSPLandroidx/navigation3/runtime/EntryProviderScope;->addEntryProvider(Lkotlin/reflect/KClass;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function3;)V -HSPLandroidx/navigation3/runtime/EntryProviderScope;->build$lambda$0(Landroidx/navigation3/runtime/EntryProviderScope;Ljava/lang/Object;)Landroidx/navigation3/runtime/NavEntry; +HSPLandroidx/navigation3/runtime/EntryProviderScope;->build$lambda$0(Landroidx/navigation3/runtime/EntryProviderScope;Ljava/util/Map;Ljava/lang/Object;)Landroidx/navigation3/runtime/NavEntry; HSPLandroidx/navigation3/runtime/EntryProviderScope;->build()Lkotlin/jvm/functions/Function1; Landroidx/navigation3/runtime/EntryProviderScope$$ExternalSyntheticLambda5; -HSPLandroidx/navigation3/runtime/EntryProviderScope$$ExternalSyntheticLambda5;->(Landroidx/navigation3/runtime/EntryProviderScope;)V +HSPLandroidx/navigation3/runtime/EntryProviderScope$$ExternalSyntheticLambda5;->(Landroidx/navigation3/runtime/EntryProviderScope;Ljava/util/Map;)V HSPLandroidx/navigation3/runtime/EntryProviderScope$$ExternalSyntheticLambda5;->invoke(Ljava/lang/Object;)Ljava/lang/Object; Landroidx/navigation3/runtime/ListUtilsKt; HSPLandroidx/navigation3/runtime/ListUtilsKt;->fastDistinctOrDistinct(Ljava/util/List;)Ljava/util/List; @@ -15210,7 +15282,15 @@ HSPLandroidx/navigation3/scene/DialogSceneStrategy$Companion;->(Lkotlin/jv Landroidx/navigation3/scene/DialogSceneStrategy$Companion$DialogKey; HSPLandroidx/navigation3/scene/DialogSceneStrategy$Companion$DialogKey;->()V HSPLandroidx/navigation3/scene/DialogSceneStrategy$Companion$DialogKey;->()V +Landroidx/navigation3/scene/NavigationEventHandlerKt; +HSPLandroidx/navigation3/scene/NavigationEventHandlerKt;->NavigationBackHandler(Landroidx/navigation3/scene/SceneState;Landroidx/navigationevent/compose/NavigationEventState;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function0;Landroidx/compose/runtime/Composer;II)V +Landroidx/navigation3/scene/NavigationEventHandlerKt$$ExternalSyntheticLambda0; +HSPLandroidx/navigation3/scene/NavigationEventHandlerKt$$ExternalSyntheticLambda0;->()V +Landroidx/navigation3/scene/NavigationEventHandlerKt$$ExternalSyntheticLambda1; +HSPLandroidx/navigation3/scene/NavigationEventHandlerKt$$ExternalSyntheticLambda1;->(Landroidx/navigation3/scene/SceneState;Lkotlin/jvm/functions/Function0;)V Landroidx/navigation3/scene/OverlayScene; +Landroidx/navigation3/scene/RememberNavigationEventStateKt; +HSPLandroidx/navigation3/scene/RememberNavigationEventStateKt;->rememberNavigationEventState(Landroidx/navigation3/scene/SceneState;Landroidx/compose/runtime/Composer;I)Landroidx/navigationevent/compose/NavigationEventState; Landroidx/navigation3/scene/Scene; HSPLandroidx/navigation3/scene/Scene;->getMetadata()Ljava/util/Map; Landroidx/navigation3/scene/SceneDecoratorStrategyScope; @@ -15309,16 +15389,16 @@ HSPLandroidx/navigation3/ui/NavDisplayKt;->NavDisplay(Landroidx/navigation3/scen HSPLandroidx/navigation3/ui/NavDisplayKt;->NavDisplay(Ljava/util/List;Landroidx/compose/ui/Modifier;Landroidx/compose/ui/Alignment;Ljava/util/List;Ljava/util/List;Landroidx/compose/animation/SharedTransitionScope;Landroidx/compose/animation/SizeTransform;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function0;Landroidx/compose/runtime/Composer;III)V HSPLandroidx/navigation3/ui/NavDisplayKt;->NavDisplay(Ljava/util/List;Landroidx/compose/ui/Modifier;Landroidx/compose/ui/Alignment;Lkotlin/jvm/functions/Function0;Ljava/util/List;Ljava/util/List;Ljava/util/List;Landroidx/compose/animation/SharedTransitionScope;Landroidx/compose/animation/SizeTransform;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function1;Landroidx/compose/runtime/Composer;III)V Landroidx/navigation3/ui/NavDisplayKt__NavDisplayKt; -HSPLandroidx/navigation3/ui/NavDisplayKt__NavDisplayKt;->$r8$lambda$6WUP_MmF2ACzmwTrp2aBzvnS0Ug(Landroidx/navigation3/scene/Scene;)Ljava/lang/Object; -HSPLandroidx/navigation3/ui/NavDisplayKt__NavDisplayKt;->$r8$lambda$6meK3rcdj_SbQiBBexE_CVd-d90(Landroidx/compose/animation/core/Transition;Landroidx/compose/runtime/snapshots/SnapshotStateList;Lkotlin/jvm/internal/Ref$ObjectRef;Ljava/util/Map;Landroidx/compose/animation/AnimatedContentScope;Landroidx/navigation3/scene/Scene;Landroidx/compose/runtime/Composer;I)Lkotlin/Unit; -HSPLandroidx/navigation3/ui/NavDisplayKt__NavDisplayKt;->$r8$lambda$DEpDt4b-_73nHf6rDOOBatOXNV8(ZLandroidx/navigation3/scene/Scene;ILkotlin/jvm/functions/Function2;ZLkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Landroidx/compose/animation/AnimatedContentTransitionScope;)Landroidx/compose/animation/ContentTransform; -HSPLandroidx/navigation3/ui/NavDisplayKt__NavDisplayKt;->$r8$lambda$OO9GgxnLAIBfMXURmhWq76Z3KmY(Lkotlin/jvm/functions/Function1;FLandroidx/compose/animation/SizeTransform;Landroidx/compose/animation/AnimatedContentTransitionScope;)Landroidx/compose/animation/ContentTransform; -HSPLandroidx/navigation3/ui/NavDisplayKt__NavDisplayKt;->$r8$lambda$XP_-QN-B82CWFFU8xBP8sTt5GYo(Landroidx/navigation3/scene/Scene;Landroidx/compose/runtime/Composer;I)Lkotlin/Unit; -HSPLandroidx/navigation3/ui/NavDisplayKt__NavDisplayKt;->NavDisplay$lambda$22$0$NavDisplayKt__NavDisplayKt(ZLandroidx/navigation3/scene/Scene;ILkotlin/jvm/functions/Function2;ZLkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Landroidx/compose/animation/AnimatedContentTransitionScope;)Landroidx/compose/animation/ContentTransform; -HSPLandroidx/navigation3/ui/NavDisplayKt__NavDisplayKt;->NavDisplay$lambda$23$0$NavDisplayKt__NavDisplayKt(Lkotlin/jvm/functions/Function1;FLandroidx/compose/animation/SizeTransform;Landroidx/compose/animation/AnimatedContentTransitionScope;)Landroidx/compose/animation/ContentTransform; -HSPLandroidx/navigation3/ui/NavDisplayKt__NavDisplayKt;->NavDisplay$lambda$24$0$NavDisplayKt__NavDisplayKt(Landroidx/navigation3/scene/Scene;)Ljava/lang/Object; -HSPLandroidx/navigation3/ui/NavDisplayKt__NavDisplayKt;->NavDisplay$lambda$25$1$NavDisplayKt__NavDisplayKt(Landroidx/navigation3/scene/Scene;Landroidx/compose/runtime/Composer;I)Lkotlin/Unit; -HSPLandroidx/navigation3/ui/NavDisplayKt__NavDisplayKt;->NavDisplay$lambda$25$NavDisplayKt__NavDisplayKt(Landroidx/compose/animation/core/Transition;Landroidx/compose/runtime/snapshots/SnapshotStateList;Lkotlin/jvm/internal/Ref$ObjectRef;Ljava/util/Map;Landroidx/compose/animation/AnimatedContentScope;Landroidx/navigation3/scene/Scene;Landroidx/compose/runtime/Composer;I)Lkotlin/Unit; +HSPLandroidx/navigation3/ui/NavDisplayKt__NavDisplayKt;->$r8$lambda$3YRipLXgCT84bSQWf9dBuZEeUw8(ZLandroidx/navigation3/scene/Scene;ILkotlin/jvm/functions/Function2;ZLkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Landroidx/compose/animation/AnimatedContentTransitionScope;)Landroidx/compose/animation/ContentTransform; +HSPLandroidx/navigation3/ui/NavDisplayKt__NavDisplayKt;->$r8$lambda$LZ9ASXP-YIdn-WuxMmn2u000sMA(Landroidx/navigation3/scene/Scene;)Ljava/lang/Object; +HSPLandroidx/navigation3/ui/NavDisplayKt__NavDisplayKt;->$r8$lambda$aySyOiJnnT1-CCIoFc3GO_djKFw(Landroidx/compose/animation/core/Transition;Landroidx/compose/runtime/snapshots/SnapshotStateList;Lkotlin/jvm/internal/Ref$ObjectRef;Ljava/util/Map;Landroidx/compose/animation/AnimatedContentScope;Landroidx/navigation3/scene/Scene;Landroidx/compose/runtime/Composer;I)Lkotlin/Unit; +HSPLandroidx/navigation3/ui/NavDisplayKt__NavDisplayKt;->$r8$lambda$hH0zCXP8JjnYbce4dkgo7nGOJNY(Landroidx/navigation3/scene/Scene;Landroidx/compose/runtime/Composer;I)Lkotlin/Unit; +HSPLandroidx/navigation3/ui/NavDisplayKt__NavDisplayKt;->$r8$lambda$vImyQbN8SR59TyvDeZ4bBt-ze1I(Lkotlin/jvm/functions/Function1;FLandroidx/compose/animation/SizeTransform;Landroidx/compose/animation/AnimatedContentTransitionScope;)Landroidx/compose/animation/ContentTransform; +HSPLandroidx/navigation3/ui/NavDisplayKt__NavDisplayKt;->NavDisplay$lambda$19$0$NavDisplayKt__NavDisplayKt(ZLandroidx/navigation3/scene/Scene;ILkotlin/jvm/functions/Function2;ZLkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Landroidx/compose/animation/AnimatedContentTransitionScope;)Landroidx/compose/animation/ContentTransform; +HSPLandroidx/navigation3/ui/NavDisplayKt__NavDisplayKt;->NavDisplay$lambda$20$0$NavDisplayKt__NavDisplayKt(Lkotlin/jvm/functions/Function1;FLandroidx/compose/animation/SizeTransform;Landroidx/compose/animation/AnimatedContentTransitionScope;)Landroidx/compose/animation/ContentTransform; +HSPLandroidx/navigation3/ui/NavDisplayKt__NavDisplayKt;->NavDisplay$lambda$21$0$NavDisplayKt__NavDisplayKt(Landroidx/navigation3/scene/Scene;)Ljava/lang/Object; +HSPLandroidx/navigation3/ui/NavDisplayKt__NavDisplayKt;->NavDisplay$lambda$22$1$NavDisplayKt__NavDisplayKt(Landroidx/navigation3/scene/Scene;Landroidx/compose/runtime/Composer;I)Lkotlin/Unit; +HSPLandroidx/navigation3/ui/NavDisplayKt__NavDisplayKt;->NavDisplay$lambda$22$NavDisplayKt__NavDisplayKt(Landroidx/compose/animation/core/Transition;Landroidx/compose/runtime/snapshots/SnapshotStateList;Lkotlin/jvm/internal/Ref$ObjectRef;Ljava/util/Map;Landroidx/compose/animation/AnimatedContentScope;Landroidx/navigation3/scene/Scene;Landroidx/compose/runtime/Composer;I)Lkotlin/Unit; HSPLandroidx/navigation3/ui/NavDisplayKt__NavDisplayKt;->NavDisplay(Landroidx/navigation3/scene/SceneState;Landroidx/navigationevent/compose/NavigationEventState;Landroidx/compose/ui/Modifier;Landroidx/compose/ui/Alignment;Landroidx/compose/animation/SizeTransform;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function2;Landroidx/compose/runtime/Composer;II)V HSPLandroidx/navigation3/ui/NavDisplayKt__NavDisplayKt;->NavDisplay(Ljava/util/List;Landroidx/compose/ui/Modifier;Landroidx/compose/ui/Alignment;Ljava/util/List;Ljava/util/List;Landroidx/compose/animation/SharedTransitionScope;Landroidx/compose/animation/SizeTransform;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function0;Landroidx/compose/runtime/Composer;III)V HSPLandroidx/navigation3/ui/NavDisplayKt__NavDisplayKt;->NavDisplay(Ljava/util/List;Landroidx/compose/ui/Modifier;Landroidx/compose/ui/Alignment;Lkotlin/jvm/functions/Function0;Ljava/util/List;Ljava/util/List;Ljava/util/List;Landroidx/compose/animation/SharedTransitionScope;Landroidx/compose/animation/SizeTransform;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function1;Landroidx/compose/runtime/Composer;III)V @@ -15327,62 +15407,60 @@ HSPLandroidx/navigation3/ui/NavDisplayKt__NavDisplayKt;->isPop$NavDisplayKt__Nav Landroidx/navigation3/ui/NavDisplayKt__NavDisplayKt$$ExternalSyntheticLambda1; HSPLandroidx/navigation3/ui/NavDisplayKt__NavDisplayKt$$ExternalSyntheticLambda1;->(Landroidx/navigation3/scene/SceneState;Landroidx/navigationevent/compose/NavigationEventState;Landroidx/compose/ui/Modifier;Landroidx/compose/ui/Alignment;Landroidx/compose/animation/SizeTransform;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function2;II)V Landroidx/navigation3/ui/NavDisplayKt__NavDisplayKt$$ExternalSyntheticLambda10; -HSPLandroidx/navigation3/ui/NavDisplayKt__NavDisplayKt$$ExternalSyntheticLambda10;->(Lkotlin/jvm/functions/Function1;FLandroidx/compose/animation/SizeTransform;)V +HSPLandroidx/navigation3/ui/NavDisplayKt__NavDisplayKt$$ExternalSyntheticLambda10;->()V HSPLandroidx/navigation3/ui/NavDisplayKt__NavDisplayKt$$ExternalSyntheticLambda10;->invoke(Ljava/lang/Object;)Ljava/lang/Object; Landroidx/navigation3/ui/NavDisplayKt__NavDisplayKt$$ExternalSyntheticLambda11; -HSPLandroidx/navigation3/ui/NavDisplayKt__NavDisplayKt$$ExternalSyntheticLambda11;->()V -HSPLandroidx/navigation3/ui/NavDisplayKt__NavDisplayKt$$ExternalSyntheticLambda11;->invoke(Ljava/lang/Object;)Ljava/lang/Object; -Landroidx/navigation3/ui/NavDisplayKt__NavDisplayKt$$ExternalSyntheticLambda12; -HSPLandroidx/navigation3/ui/NavDisplayKt__NavDisplayKt$$ExternalSyntheticLambda12;->(Landroidx/compose/animation/core/Transition;Landroidx/compose/runtime/snapshots/SnapshotStateList;Lkotlin/jvm/internal/Ref$ObjectRef;Ljava/util/Map;)V -HSPLandroidx/navigation3/ui/NavDisplayKt__NavDisplayKt$$ExternalSyntheticLambda12;->invoke(Ljava/lang/Object;Ljava/lang/Object;Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object; -Landroidx/navigation3/ui/NavDisplayKt__NavDisplayKt$$ExternalSyntheticLambda2; -HSPLandroidx/navigation3/ui/NavDisplayKt__NavDisplayKt$$ExternalSyntheticLambda2;->(Ljava/util/List;Landroidx/navigation3/scene/Scene;Lkotlin/jvm/functions/Function0;)V -Landroidx/navigation3/ui/NavDisplayKt__NavDisplayKt$$ExternalSyntheticLambda6; -HSPLandroidx/navigation3/ui/NavDisplayKt__NavDisplayKt$$ExternalSyntheticLambda6;->(Landroidx/navigation3/scene/Scene;)V -HSPLandroidx/navigation3/ui/NavDisplayKt__NavDisplayKt$$ExternalSyntheticLambda6;->invoke(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object; +HSPLandroidx/navigation3/ui/NavDisplayKt__NavDisplayKt$$ExternalSyntheticLambda11;->(Landroidx/compose/animation/core/Transition;Landroidx/compose/runtime/snapshots/SnapshotStateList;Lkotlin/jvm/internal/Ref$ObjectRef;Ljava/util/Map;)V +HSPLandroidx/navigation3/ui/NavDisplayKt__NavDisplayKt$$ExternalSyntheticLambda11;->invoke(Ljava/lang/Object;Ljava/lang/Object;Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object; +Landroidx/navigation3/ui/NavDisplayKt__NavDisplayKt$$ExternalSyntheticLambda5; +HSPLandroidx/navigation3/ui/NavDisplayKt__NavDisplayKt$$ExternalSyntheticLambda5;->(Landroidx/navigation3/scene/Scene;)V +HSPLandroidx/navigation3/ui/NavDisplayKt__NavDisplayKt$$ExternalSyntheticLambda5;->invoke(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object; +Landroidx/navigation3/ui/NavDisplayKt__NavDisplayKt$$ExternalSyntheticLambda7; +HSPLandroidx/navigation3/ui/NavDisplayKt__NavDisplayKt$$ExternalSyntheticLambda7;->(Ljava/util/List;Landroidx/compose/ui/Modifier;Landroidx/compose/ui/Alignment;Lkotlin/jvm/functions/Function0;Ljava/util/List;Ljava/util/List;Ljava/util/List;Landroidx/compose/animation/SharedTransitionScope;Landroidx/compose/animation/SizeTransform;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function1;III)V Landroidx/navigation3/ui/NavDisplayKt__NavDisplayKt$$ExternalSyntheticLambda8; -HSPLandroidx/navigation3/ui/NavDisplayKt__NavDisplayKt$$ExternalSyntheticLambda8;->(Ljava/util/List;Landroidx/compose/ui/Modifier;Landroidx/compose/ui/Alignment;Lkotlin/jvm/functions/Function0;Ljava/util/List;Ljava/util/List;Ljava/util/List;Landroidx/compose/animation/SharedTransitionScope;Landroidx/compose/animation/SizeTransform;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function1;III)V +HSPLandroidx/navigation3/ui/NavDisplayKt__NavDisplayKt$$ExternalSyntheticLambda8;->(ZLandroidx/navigation3/scene/Scene;ILkotlin/jvm/functions/Function2;ZLkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;)V +HSPLandroidx/navigation3/ui/NavDisplayKt__NavDisplayKt$$ExternalSyntheticLambda8;->invoke(Ljava/lang/Object;)Ljava/lang/Object; Landroidx/navigation3/ui/NavDisplayKt__NavDisplayKt$$ExternalSyntheticLambda9; -HSPLandroidx/navigation3/ui/NavDisplayKt__NavDisplayKt$$ExternalSyntheticLambda9;->(ZLandroidx/navigation3/scene/Scene;ILkotlin/jvm/functions/Function2;ZLkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;)V +HSPLandroidx/navigation3/ui/NavDisplayKt__NavDisplayKt$$ExternalSyntheticLambda9;->(Lkotlin/jvm/functions/Function1;FLandroidx/compose/animation/SizeTransform;)V HSPLandroidx/navigation3/ui/NavDisplayKt__NavDisplayKt$$ExternalSyntheticLambda9;->invoke(Ljava/lang/Object;)Ljava/lang/Object; -Landroidx/navigation3/ui/NavDisplayKt__NavDisplayKt$NavDisplay$10$1; -HSPLandroidx/navigation3/ui/NavDisplayKt__NavDisplayKt$NavDisplay$10$1;->(Ljava/util/List;Landroidx/compose/runtime/snapshots/SnapshotStateList;Lkotlin/coroutines/Continuation;)V -HSPLandroidx/navigation3/ui/NavDisplayKt__NavDisplayKt$NavDisplay$10$1;->create(Ljava/lang/Object;Lkotlin/coroutines/Continuation;)Lkotlin/coroutines/Continuation; -HSPLandroidx/navigation3/ui/NavDisplayKt__NavDisplayKt$NavDisplay$10$1;->invokeSuspend(Ljava/lang/Object;)Ljava/lang/Object; -Landroidx/navigation3/ui/NavDisplayKt__NavDisplayKt$NavDisplay$12$1; -HSPLandroidx/navigation3/ui/NavDisplayKt__NavDisplayKt$NavDisplay$12$1;->(Landroidx/compose/animation/core/SeekableTransitionState;Landroidx/navigation3/scene/Scene;Landroidx/compose/animation/core/Transition;Lkotlin/coroutines/Continuation;)V -HSPLandroidx/navigation3/ui/NavDisplayKt__NavDisplayKt$NavDisplay$12$1;->create(Ljava/lang/Object;Lkotlin/coroutines/Continuation;)Lkotlin/coroutines/Continuation; -HSPLandroidx/navigation3/ui/NavDisplayKt__NavDisplayKt$NavDisplay$12$1;->invokeSuspend$lambda$0(Lkotlinx/coroutines/CoroutineScope;FLandroidx/compose/animation/core/SeekableTransitionState;Landroidx/navigation3/scene/Scene;FF)Lkotlin/Unit; -HSPLandroidx/navigation3/ui/NavDisplayKt__NavDisplayKt$NavDisplay$12$1;->invokeSuspend(Ljava/lang/Object;)Ljava/lang/Object; -Landroidx/navigation3/ui/NavDisplayKt__NavDisplayKt$NavDisplay$12$1$$ExternalSyntheticLambda0; -HSPLandroidx/navigation3/ui/NavDisplayKt__NavDisplayKt$NavDisplay$12$1$$ExternalSyntheticLambda0;->(Lkotlinx/coroutines/CoroutineScope;FLandroidx/compose/animation/core/SeekableTransitionState;Landroidx/navigation3/scene/Scene;)V -HSPLandroidx/navigation3/ui/NavDisplayKt__NavDisplayKt$NavDisplay$12$1$$ExternalSyntheticLambda0;->invoke(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object; -Landroidx/navigation3/ui/NavDisplayKt__NavDisplayKt$NavDisplay$12$1$1$1; -HSPLandroidx/navigation3/ui/NavDisplayKt__NavDisplayKt$NavDisplay$12$1$1$1;->(FFLandroidx/compose/animation/core/SeekableTransitionState;Landroidx/navigation3/scene/Scene;Lkotlin/coroutines/Continuation;)V -HSPLandroidx/navigation3/ui/NavDisplayKt__NavDisplayKt$NavDisplay$12$1$1$1;->create(Ljava/lang/Object;Lkotlin/coroutines/Continuation;)Lkotlin/coroutines/Continuation; -HSPLandroidx/navigation3/ui/NavDisplayKt__NavDisplayKt$NavDisplay$12$1$1$1;->invokeSuspend(Ljava/lang/Object;)Ljava/lang/Object; -Landroidx/navigation3/ui/NavDisplayKt__NavDisplayKt$NavDisplay$16$1; -HSPLandroidx/navigation3/ui/NavDisplayKt__NavDisplayKt$NavDisplay$16$1;->(Landroidx/compose/animation/core/Transition;Landroidx/compose/runtime/snapshots/SnapshotStateMap;Landroidx/collection/MutableObjectFloatMap;Lkotlin/coroutines/Continuation;)V -HSPLandroidx/navigation3/ui/NavDisplayKt__NavDisplayKt$NavDisplay$16$1;->create(Ljava/lang/Object;Lkotlin/coroutines/Continuation;)Lkotlin/coroutines/Continuation; -HSPLandroidx/navigation3/ui/NavDisplayKt__NavDisplayKt$NavDisplay$16$1;->invokeSuspend$lambda$0(Landroidx/compose/animation/core/Transition;)Z -HSPLandroidx/navigation3/ui/NavDisplayKt__NavDisplayKt$NavDisplay$16$1;->invokeSuspend(Ljava/lang/Object;)Ljava/lang/Object; -Landroidx/navigation3/ui/NavDisplayKt__NavDisplayKt$NavDisplay$16$1$$ExternalSyntheticLambda0; -HSPLandroidx/navigation3/ui/NavDisplayKt__NavDisplayKt$NavDisplay$16$1$$ExternalSyntheticLambda0;->(Landroidx/compose/animation/core/Transition;)V -HSPLandroidx/navigation3/ui/NavDisplayKt__NavDisplayKt$NavDisplay$16$1$$ExternalSyntheticLambda0;->invoke()Ljava/lang/Object; -Landroidx/navigation3/ui/NavDisplayKt__NavDisplayKt$NavDisplay$16$1$3; -HSPLandroidx/navigation3/ui/NavDisplayKt__NavDisplayKt$NavDisplay$16$1$3;->(Landroidx/compose/animation/core/Transition;Landroidx/compose/runtime/snapshots/SnapshotStateMap;Landroidx/collection/MutableObjectFloatMap;)V -HSPLandroidx/navigation3/ui/NavDisplayKt__NavDisplayKt$NavDisplay$16$1$3;->emit(Ljava/lang/Object;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; -HSPLandroidx/navigation3/ui/NavDisplayKt__NavDisplayKt$NavDisplay$16$1$3;->emit(ZLkotlin/coroutines/Continuation;)Ljava/lang/Object; -Landroidx/navigation3/ui/NavDisplayKt__NavDisplayKt$NavDisplay$16$1$invokeSuspend$$inlined$filter$1; -HSPLandroidx/navigation3/ui/NavDisplayKt__NavDisplayKt$NavDisplay$16$1$invokeSuspend$$inlined$filter$1;->(Lkotlinx/coroutines/flow/Flow;)V -HSPLandroidx/navigation3/ui/NavDisplayKt__NavDisplayKt$NavDisplay$16$1$invokeSuspend$$inlined$filter$1;->collect(Lkotlinx/coroutines/flow/FlowCollector;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; -Landroidx/navigation3/ui/NavDisplayKt__NavDisplayKt$NavDisplay$16$1$invokeSuspend$$inlined$filter$1$2; -HSPLandroidx/navigation3/ui/NavDisplayKt__NavDisplayKt$NavDisplay$16$1$invokeSuspend$$inlined$filter$1$2;->(Lkotlinx/coroutines/flow/FlowCollector;)V -HSPLandroidx/navigation3/ui/NavDisplayKt__NavDisplayKt$NavDisplay$16$1$invokeSuspend$$inlined$filter$1$2;->emit(Ljava/lang/Object;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; -Landroidx/navigation3/ui/NavDisplayKt__NavDisplayKt$NavDisplay$16$1$invokeSuspend$$inlined$filter$1$2$1; -HSPLandroidx/navigation3/ui/NavDisplayKt__NavDisplayKt$NavDisplay$16$1$invokeSuspend$$inlined$filter$1$2$1;->(Landroidx/navigation3/ui/NavDisplayKt__NavDisplayKt$NavDisplay$16$1$invokeSuspend$$inlined$filter$1$2;Lkotlin/coroutines/Continuation;)V -Landroidx/navigation3/ui/NavDisplayKt__NavDisplayKt$NavDisplay$lambda$19$0$$inlined$sortedByDescending$1; -HSPLandroidx/navigation3/ui/NavDisplayKt__NavDisplayKt$NavDisplay$lambda$19$0$$inlined$sortedByDescending$1;->(Landroidx/collection/MutableObjectFloatMap;)V +Landroidx/navigation3/ui/NavDisplayKt__NavDisplayKt$NavDisplay$11$1; +HSPLandroidx/navigation3/ui/NavDisplayKt__NavDisplayKt$NavDisplay$11$1;->(Landroidx/compose/animation/core/SeekableTransitionState;Landroidx/navigation3/scene/Scene;Landroidx/compose/animation/core/Transition;Lkotlin/coroutines/Continuation;)V +HSPLandroidx/navigation3/ui/NavDisplayKt__NavDisplayKt$NavDisplay$11$1;->create(Ljava/lang/Object;Lkotlin/coroutines/Continuation;)Lkotlin/coroutines/Continuation; +HSPLandroidx/navigation3/ui/NavDisplayKt__NavDisplayKt$NavDisplay$11$1;->invokeSuspend$lambda$0(Lkotlinx/coroutines/CoroutineScope;FLandroidx/compose/animation/core/SeekableTransitionState;Landroidx/navigation3/scene/Scene;FF)Lkotlin/Unit; +HSPLandroidx/navigation3/ui/NavDisplayKt__NavDisplayKt$NavDisplay$11$1;->invokeSuspend(Ljava/lang/Object;)Ljava/lang/Object; +Landroidx/navigation3/ui/NavDisplayKt__NavDisplayKt$NavDisplay$11$1$$ExternalSyntheticLambda0; +HSPLandroidx/navigation3/ui/NavDisplayKt__NavDisplayKt$NavDisplay$11$1$$ExternalSyntheticLambda0;->(Lkotlinx/coroutines/CoroutineScope;FLandroidx/compose/animation/core/SeekableTransitionState;Landroidx/navigation3/scene/Scene;)V +HSPLandroidx/navigation3/ui/NavDisplayKt__NavDisplayKt$NavDisplay$11$1$$ExternalSyntheticLambda0;->invoke(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object; +Landroidx/navigation3/ui/NavDisplayKt__NavDisplayKt$NavDisplay$11$1$1$1; +HSPLandroidx/navigation3/ui/NavDisplayKt__NavDisplayKt$NavDisplay$11$1$1$1;->(FFLandroidx/compose/animation/core/SeekableTransitionState;Landroidx/navigation3/scene/Scene;Lkotlin/coroutines/Continuation;)V +HSPLandroidx/navigation3/ui/NavDisplayKt__NavDisplayKt$NavDisplay$11$1$1$1;->create(Ljava/lang/Object;Lkotlin/coroutines/Continuation;)Lkotlin/coroutines/Continuation; +HSPLandroidx/navigation3/ui/NavDisplayKt__NavDisplayKt$NavDisplay$11$1$1$1;->invokeSuspend(Ljava/lang/Object;)Ljava/lang/Object; +Landroidx/navigation3/ui/NavDisplayKt__NavDisplayKt$NavDisplay$15$1; +HSPLandroidx/navigation3/ui/NavDisplayKt__NavDisplayKt$NavDisplay$15$1;->(Landroidx/compose/animation/core/Transition;Landroidx/compose/runtime/snapshots/SnapshotStateMap;Landroidx/collection/MutableObjectFloatMap;Lkotlin/coroutines/Continuation;)V +HSPLandroidx/navigation3/ui/NavDisplayKt__NavDisplayKt$NavDisplay$15$1;->create(Ljava/lang/Object;Lkotlin/coroutines/Continuation;)Lkotlin/coroutines/Continuation; +HSPLandroidx/navigation3/ui/NavDisplayKt__NavDisplayKt$NavDisplay$15$1;->invokeSuspend$lambda$0(Landroidx/compose/animation/core/Transition;)Z +HSPLandroidx/navigation3/ui/NavDisplayKt__NavDisplayKt$NavDisplay$15$1;->invokeSuspend(Ljava/lang/Object;)Ljava/lang/Object; +Landroidx/navigation3/ui/NavDisplayKt__NavDisplayKt$NavDisplay$15$1$$ExternalSyntheticLambda0; +HSPLandroidx/navigation3/ui/NavDisplayKt__NavDisplayKt$NavDisplay$15$1$$ExternalSyntheticLambda0;->(Landroidx/compose/animation/core/Transition;)V +HSPLandroidx/navigation3/ui/NavDisplayKt__NavDisplayKt$NavDisplay$15$1$$ExternalSyntheticLambda0;->invoke()Ljava/lang/Object; +Landroidx/navigation3/ui/NavDisplayKt__NavDisplayKt$NavDisplay$15$1$3; +HSPLandroidx/navigation3/ui/NavDisplayKt__NavDisplayKt$NavDisplay$15$1$3;->(Landroidx/compose/animation/core/Transition;Landroidx/compose/runtime/snapshots/SnapshotStateMap;Landroidx/collection/MutableObjectFloatMap;)V +HSPLandroidx/navigation3/ui/NavDisplayKt__NavDisplayKt$NavDisplay$15$1$3;->emit(Ljava/lang/Object;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; +HSPLandroidx/navigation3/ui/NavDisplayKt__NavDisplayKt$NavDisplay$15$1$3;->emit(ZLkotlin/coroutines/Continuation;)Ljava/lang/Object; +Landroidx/navigation3/ui/NavDisplayKt__NavDisplayKt$NavDisplay$15$1$invokeSuspend$$inlined$filter$1; +HSPLandroidx/navigation3/ui/NavDisplayKt__NavDisplayKt$NavDisplay$15$1$invokeSuspend$$inlined$filter$1;->(Lkotlinx/coroutines/flow/Flow;)V +HSPLandroidx/navigation3/ui/NavDisplayKt__NavDisplayKt$NavDisplay$15$1$invokeSuspend$$inlined$filter$1;->collect(Lkotlinx/coroutines/flow/FlowCollector;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; +Landroidx/navigation3/ui/NavDisplayKt__NavDisplayKt$NavDisplay$15$1$invokeSuspend$$inlined$filter$1$2; +HSPLandroidx/navigation3/ui/NavDisplayKt__NavDisplayKt$NavDisplay$15$1$invokeSuspend$$inlined$filter$1$2;->(Lkotlinx/coroutines/flow/FlowCollector;)V +HSPLandroidx/navigation3/ui/NavDisplayKt__NavDisplayKt$NavDisplay$15$1$invokeSuspend$$inlined$filter$1$2;->emit(Ljava/lang/Object;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; +Landroidx/navigation3/ui/NavDisplayKt__NavDisplayKt$NavDisplay$15$1$invokeSuspend$$inlined$filter$1$2$1; +HSPLandroidx/navigation3/ui/NavDisplayKt__NavDisplayKt$NavDisplay$15$1$invokeSuspend$$inlined$filter$1$2$1;->(Landroidx/navigation3/ui/NavDisplayKt__NavDisplayKt$NavDisplay$15$1$invokeSuspend$$inlined$filter$1$2;Lkotlin/coroutines/Continuation;)V +Landroidx/navigation3/ui/NavDisplayKt__NavDisplayKt$NavDisplay$9$1; +HSPLandroidx/navigation3/ui/NavDisplayKt__NavDisplayKt$NavDisplay$9$1;->(Ljava/util/List;Landroidx/compose/runtime/snapshots/SnapshotStateList;Lkotlin/coroutines/Continuation;)V +HSPLandroidx/navigation3/ui/NavDisplayKt__NavDisplayKt$NavDisplay$9$1;->create(Ljava/lang/Object;Lkotlin/coroutines/Continuation;)Lkotlin/coroutines/Continuation; +HSPLandroidx/navigation3/ui/NavDisplayKt__NavDisplayKt$NavDisplay$9$1;->invokeSuspend(Ljava/lang/Object;)Ljava/lang/Object; +Landroidx/navigation3/ui/NavDisplayKt__NavDisplayKt$NavDisplay$lambda$16$0$$inlined$sortedByDescending$1; +HSPLandroidx/navigation3/ui/NavDisplayKt__NavDisplayKt$NavDisplay$lambda$16$0$$inlined$sortedByDescending$1;->(Landroidx/collection/MutableObjectFloatMap;)V Landroidx/navigationevent/NavigationEventDispatcher; HSPLandroidx/navigationevent/NavigationEventDispatcher;->()V HSPLandroidx/navigationevent/NavigationEventDispatcher;->(Landroidx/navigationevent/NavigationEventDispatcher;Landroidx/navigationevent/OnBackCompletedFallback;Landroidx/navigationevent/OnForwardCompletedFallback;)V @@ -15493,8 +15571,6 @@ HSPLandroidx/navigationevent/compose/NavigationEventHandlerKt;->NavigationBackHa HSPLandroidx/navigationevent/compose/NavigationEventHandlerKt;->NavigationEventHandler$lambda$6$0(Landroidx/navigationevent/compose/ComposeNavigationEventHandler;ZLkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function0;ZLkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function0;Landroidx/navigationevent/compose/NavigationEventState;)Lkotlin/Unit; HSPLandroidx/navigationevent/compose/NavigationEventHandlerKt;->NavigationEventHandler$lambda$7$0(Landroidx/navigationevent/compose/NavigationEventState;Landroidx/navigationevent/compose/ComposeNavigationEventHandler;Landroidx/navigationevent/NavigationEventDispatcher;Landroidx/compose/runtime/DisposableEffectScope;)Landroidx/compose/runtime/DisposableEffectResult; HSPLandroidx/navigationevent/compose/NavigationEventHandlerKt;->NavigationEventHandler(Landroidx/navigationevent/compose/NavigationEventState;ZLkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function0;ZLkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function0;Landroidx/compose/runtime/Composer;II)V -Landroidx/navigationevent/compose/NavigationEventHandlerKt$$ExternalSyntheticLambda10; -HSPLandroidx/navigationevent/compose/NavigationEventHandlerKt$$ExternalSyntheticLambda10;->()V Landroidx/navigationevent/compose/NavigationEventHandlerKt$$ExternalSyntheticLambda11; HSPLandroidx/navigationevent/compose/NavigationEventHandlerKt$$ExternalSyntheticLambda11;->()V Landroidx/navigationevent/compose/NavigationEventHandlerKt$$ExternalSyntheticLambda12; @@ -16146,7 +16222,7 @@ Landroidx/room3/ObservedTableStates; HSPLandroidx/room3/ObservedTableStates;->(I)V HSPLandroidx/room3/ObservedTableStates;->access$getLock$p(Landroidx/room3/ObservedTableStates;)Ljava/util/concurrent/locks/ReentrantLock; HSPLandroidx/room3/ObservedTableStates;->access$getNeedsSync$p(Landroidx/room3/ObservedTableStates;)Z -HSPLandroidx/room3/ObservedTableStates;->access$getOnSyncLock$p(Landroidx/room3/ObservedTableStates;)Ljava/util/concurrent/locks/ReentrantLock; +HSPLandroidx/room3/ObservedTableStates;->access$getOnSyncMutex$p(Landroidx/room3/ObservedTableStates;)Lkotlinx/coroutines/sync/Mutex; HSPLandroidx/room3/ObservedTableStates;->access$getTableObservedState$p(Landroidx/room3/ObservedTableStates;)[Z HSPLandroidx/room3/ObservedTableStates;->access$getTableObserversCount$p(Landroidx/room3/ObservedTableStates;)[J HSPLandroidx/room3/ObservedTableStates;->access$setInProgressSync$p(Landroidx/room3/ObservedTableStates;Z)V @@ -16427,6 +16503,12 @@ HSPLandroidx/room3/coroutines/ConnectionPoolImpl$useConnection$4;->create(Ljava/ HSPLandroidx/room3/coroutines/ConnectionPoolImpl$useConnection$4;->invoke(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object; HSPLandroidx/room3/coroutines/ConnectionPoolImpl$useConnection$4;->invoke(Lkotlinx/coroutines/CoroutineScope;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; HSPLandroidx/room3/coroutines/ConnectionPoolImpl$useConnection$4;->invokeSuspend(Ljava/lang/Object;)Ljava/lang/Object; +Landroidx/room3/coroutines/ConnectionPoolImpl$useConnection$5; +HSPLandroidx/room3/coroutines/ConnectionPoolImpl$useConnection$5;->(Lkotlin/jvm/internal/Ref$ObjectRef;Lkotlin/jvm/internal/Ref$ObjectRef;Landroidx/room3/coroutines/Pool;Lkotlin/coroutines/Continuation;)V +HSPLandroidx/room3/coroutines/ConnectionPoolImpl$useConnection$5;->create(Ljava/lang/Object;Lkotlin/coroutines/Continuation;)Lkotlin/coroutines/Continuation; +HSPLandroidx/room3/coroutines/ConnectionPoolImpl$useConnection$5;->invoke(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object; +HSPLandroidx/room3/coroutines/ConnectionPoolImpl$useConnection$5;->invoke(Lkotlinx/coroutines/CoroutineScope;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; +HSPLandroidx/room3/coroutines/ConnectionPoolImpl$useConnection$5;->invokeSuspend(Ljava/lang/Object;)Ljava/lang/Object; Landroidx/room3/coroutines/ConnectionPoolImpl_nonWebKt; HSPLandroidx/room3/coroutines/ConnectionPoolImpl_nonWebKt;->newConnectionWrapper(Landroidx/sqlite/SQLiteConnection;I)Landroidx/room3/coroutines/ConnectionWrapper; HSPLandroidx/room3/coroutines/ConnectionPoolImpl_nonWebKt;->newStatementWrapper(Landroidx/sqlite/SQLiteStatement;Lkotlin/jvm/functions/Function0;)Landroidx/room3/coroutines/StatementWrapper; @@ -16492,6 +16574,8 @@ HSPLandroidx/room3/coroutines/Pool$acquireWithTimeout$2;->invoke(Lkotlinx/corout HSPLandroidx/room3/coroutines/Pool$acquireWithTimeout$2;->invokeSuspend(Ljava/lang/Object;)Ljava/lang/Object; Landroidx/room3/coroutines/PooledConnectionImpl; HSPLandroidx/room3/coroutines/PooledConnectionImpl;->(Landroidx/room3/coroutines/ConnectionElementKey;Landroidx/room3/coroutines/ConnectionWrapper;Z)V +HSPLandroidx/room3/coroutines/PooledConnectionImpl;->access$beginTransaction(Landroidx/room3/coroutines/PooledConnectionImpl;Landroidx/room3/Transactor$SQLiteTransactionType;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; +HSPLandroidx/room3/coroutines/PooledConnectionImpl;->access$endTransaction(Landroidx/room3/coroutines/PooledConnectionImpl;ZLkotlin/coroutines/Continuation;)Ljava/lang/Object; HSPLandroidx/room3/coroutines/PooledConnectionImpl;->access$isRecycled$p(Landroidx/room3/coroutines/PooledConnectionImpl;)Z HSPLandroidx/room3/coroutines/PooledConnectionImpl;->beginTransaction(Landroidx/room3/Transactor$SQLiteTransactionType;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; HSPLandroidx/room3/coroutines/PooledConnectionImpl;->endTransaction(ZLkotlin/coroutines/Continuation;)Ljava/lang/Object; @@ -16519,6 +16603,18 @@ Landroidx/room3/coroutines/PooledConnectionImpl$endTransaction$1; HSPLandroidx/room3/coroutines/PooledConnectionImpl$endTransaction$1;->(Landroidx/room3/coroutines/PooledConnectionImpl;Lkotlin/coroutines/Continuation;)V Landroidx/room3/coroutines/PooledConnectionImpl$transaction$1; HSPLandroidx/room3/coroutines/PooledConnectionImpl$transaction$1;->(Landroidx/room3/coroutines/PooledConnectionImpl;Lkotlin/coroutines/Continuation;)V +Landroidx/room3/coroutines/PooledConnectionImpl$transaction$2; +HSPLandroidx/room3/coroutines/PooledConnectionImpl$transaction$2;->(Landroidx/room3/coroutines/PooledConnectionImpl;Landroidx/room3/Transactor$SQLiteTransactionType;Lkotlin/coroutines/Continuation;)V +HSPLandroidx/room3/coroutines/PooledConnectionImpl$transaction$2;->create(Ljava/lang/Object;Lkotlin/coroutines/Continuation;)Lkotlin/coroutines/Continuation; +HSPLandroidx/room3/coroutines/PooledConnectionImpl$transaction$2;->invoke(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object; +HSPLandroidx/room3/coroutines/PooledConnectionImpl$transaction$2;->invoke(Lkotlinx/coroutines/CoroutineScope;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; +HSPLandroidx/room3/coroutines/PooledConnectionImpl$transaction$2;->invokeSuspend(Ljava/lang/Object;)Ljava/lang/Object; +Landroidx/room3/coroutines/PooledConnectionImpl$transaction$3; +HSPLandroidx/room3/coroutines/PooledConnectionImpl$transaction$3;->(Landroidx/room3/coroutines/PooledConnectionImpl;Lkotlin/jvm/internal/Ref$BooleanRef;Lkotlin/jvm/internal/Ref$ObjectRef;Lkotlin/coroutines/Continuation;)V +HSPLandroidx/room3/coroutines/PooledConnectionImpl$transaction$3;->create(Ljava/lang/Object;Lkotlin/coroutines/Continuation;)Lkotlin/coroutines/Continuation; +HSPLandroidx/room3/coroutines/PooledConnectionImpl$transaction$3;->invoke(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object; +HSPLandroidx/room3/coroutines/PooledConnectionImpl$transaction$3;->invoke(Lkotlinx/coroutines/CoroutineScope;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; +HSPLandroidx/room3/coroutines/PooledConnectionImpl$transaction$3;->invokeSuspend(Ljava/lang/Object;)Ljava/lang/Object; Landroidx/room3/coroutines/PooledConnectionImpl$usePrepared$1; HSPLandroidx/room3/coroutines/PooledConnectionImpl$usePrepared$1;->(Landroidx/room3/coroutines/PooledConnectionImpl;Lkotlin/coroutines/Continuation;)V Landroidx/room3/coroutines/PooledConnectionImpl$usePrepared$2$1$1; @@ -17135,7 +17231,7 @@ HSPLandroidx/window/layout/WindowInfoTrackerImpl$windowLayoutInfo$1;->invoke(Lko HSPLandroidx/window/layout/WindowInfoTrackerImpl$windowLayoutInfo$1;->invokeSuspend$lambda$0(Lkotlinx/coroutines/channels/ProducerScope;Landroidx/window/layout/WindowLayoutInfo;)V HSPLandroidx/window/layout/WindowInfoTrackerImpl$windowLayoutInfo$1;->invokeSuspend(Ljava/lang/Object;)Ljava/lang/Object; Landroidx/window/layout/WindowInfoTrackerImpl$windowLayoutInfo$1$$ExternalSyntheticLambda0; -PLandroidx/window/layout/WindowInfoTrackerImpl$windowLayoutInfo$1$$ExternalSyntheticLambda0;->(Lkotlinx/coroutines/channels/ProducerScope;)V +HSPLandroidx/window/layout/WindowInfoTrackerImpl$windowLayoutInfo$1$$ExternalSyntheticLambda0;->(Lkotlinx/coroutines/channels/ProducerScope;)V HSPLandroidx/window/layout/WindowInfoTrackerImpl$windowLayoutInfo$1$$ExternalSyntheticLambda0;->accept(Ljava/lang/Object;)V PLandroidx/window/layout/WindowInfoTrackerImpl$windowLayoutInfo$1$$ExternalSyntheticLambda1;->(Landroidx/window/layout/WindowInfoTrackerImpl;Landroidx/core/util/Consumer;)V PLandroidx/window/layout/WindowLayoutInfo;->(Ljava/util/List;)V @@ -17178,7 +17274,7 @@ PLandroidx/window/layout/adapter/extensions/ExtensionsWindowLayoutInfoAdapter;-> PLandroidx/window/layout/adapter/extensions/ExtensionsWindowLayoutInfoAdapter;->translate$window_release(Landroid/content/Context;Landroidx/window/extensions/layout/WindowLayoutInfo;)Landroidx/window/layout/WindowLayoutInfo; PLandroidx/window/layout/adapter/extensions/ExtensionsWindowLayoutInfoAdapter;->translate$window_release(Landroidx/window/layout/WindowMetrics;Landroidx/window/extensions/layout/WindowLayoutInfo;)Landroidx/window/layout/WindowLayoutInfo; Landroidx/window/layout/adapter/extensions/MulticastConsumerApi2; -PLandroidx/window/layout/adapter/extensions/MulticastConsumerApi2;->(Landroid/content/Context;)V +HSPLandroidx/window/layout/adapter/extensions/MulticastConsumerApi2;->(Landroid/content/Context;)V HSPLandroidx/window/layout/adapter/extensions/MulticastConsumerApi2;->accept(Landroidx/window/extensions/layout/WindowLayoutInfo;)V HSPLandroidx/window/layout/adapter/extensions/MulticastConsumerApi2;->accept(Ljava/lang/Object;)V HSPLandroidx/window/layout/adapter/extensions/MulticastConsumerApi2;->addListener(Landroidx/core/util/Consumer;)V @@ -17297,9 +17393,9 @@ HSPLandroidx/work/Constraints$Companion;->()V HSPLandroidx/work/Constraints$Companion;->(Lkotlin/jvm/internal/DefaultConstructorMarker;)V Landroidx/work/CoroutineWorker; HSPLandroidx/work/CoroutineWorker;->(Landroid/content/Context;Landroidx/work/WorkerParameters;)V -HSPLandroidx/work/CoroutineWorker;->getCoroutineContext()Lkotlinx/coroutines/CoroutineDispatcher; -HSPLandroidx/work/CoroutineWorker;->onStopped()V -HSPLandroidx/work/CoroutineWorker;->startWork()Lcom/google/common/util/concurrent/ListenableFuture; +PLandroidx/work/CoroutineWorker;->getCoroutineContext()Lkotlinx/coroutines/CoroutineDispatcher; +PLandroidx/work/CoroutineWorker;->onStopped()V +PLandroidx/work/CoroutineWorker;->startWork()Lcom/google/common/util/concurrent/ListenableFuture; Landroidx/work/CoroutineWorker$DeprecatedDispatcher; HSPLandroidx/work/CoroutineWorker$DeprecatedDispatcher;->()V HSPLandroidx/work/CoroutineWorker$DeprecatedDispatcher;->()V @@ -17334,7 +17430,7 @@ Landroidx/work/DirectExecutor; HSPLandroidx/work/DirectExecutor;->$values()[Landroidx/work/DirectExecutor; HSPLandroidx/work/DirectExecutor;->()V HSPLandroidx/work/DirectExecutor;->(Ljava/lang/String;I)V -HSPLandroidx/work/DirectExecutor;->execute(Ljava/lang/Runnable;)V +PLandroidx/work/DirectExecutor;->execute(Ljava/lang/Runnable;)V Landroidx/work/ExistingPeriodicWorkPolicy; HSPLandroidx/work/ExistingPeriodicWorkPolicy;->$values()[Landroidx/work/ExistingPeriodicWorkPolicy; HSPLandroidx/work/ExistingPeriodicWorkPolicy;->()V @@ -17345,7 +17441,7 @@ Landroidx/work/InputMergerFactory; HSPLandroidx/work/InputMergerFactory;->()V Landroidx/work/ListenableFutureKt; HSPLandroidx/work/ListenableFutureKt;->launchFuture$default(Lkotlin/coroutines/CoroutineContext;Lkotlinx/coroutines/CoroutineStart;Lkotlin/jvm/functions/Function2;ILjava/lang/Object;)Lcom/google/common/util/concurrent/ListenableFuture; -HSPLandroidx/work/ListenableFutureKt;->launchFuture$lambda$1$lambda$0(Lkotlinx/coroutines/Job;)V +PLandroidx/work/ListenableFutureKt;->launchFuture$lambda$1$lambda$0(Lkotlinx/coroutines/Job;)V HSPLandroidx/work/ListenableFutureKt;->launchFuture$lambda$1(Lkotlin/coroutines/CoroutineContext;Lkotlinx/coroutines/CoroutineStart;Lkotlin/jvm/functions/Function2;Landroidx/concurrent/futures/CallbackToFutureAdapter$Completer;)Ljava/lang/Object; HSPLandroidx/work/ListenableFutureKt;->launchFuture(Lkotlin/coroutines/CoroutineContext;Lkotlinx/coroutines/CoroutineStart;Lkotlin/jvm/functions/Function2;)Lcom/google/common/util/concurrent/ListenableFuture; Landroidx/work/ListenableFutureKt$$ExternalSyntheticLambda1; @@ -17353,7 +17449,7 @@ HSPLandroidx/work/ListenableFutureKt$$ExternalSyntheticLambda1;->(Lkotlin/ HSPLandroidx/work/ListenableFutureKt$$ExternalSyntheticLambda1;->attachCompleter(Landroidx/concurrent/futures/CallbackToFutureAdapter$Completer;)Ljava/lang/Object; Landroidx/work/ListenableFutureKt$$ExternalSyntheticLambda4; HSPLandroidx/work/ListenableFutureKt$$ExternalSyntheticLambda4;->(Lkotlinx/coroutines/Job;)V -HSPLandroidx/work/ListenableFutureKt$$ExternalSyntheticLambda4;->run()V +PLandroidx/work/ListenableFutureKt$$ExternalSyntheticLambda4;->run()V Landroidx/work/ListenableFutureKt$launchFuture$1$2; HSPLandroidx/work/ListenableFutureKt$launchFuture$1$2;->(Lkotlin/jvm/functions/Function2;Landroidx/concurrent/futures/CallbackToFutureAdapter$Completer;Lkotlin/coroutines/Continuation;)V HSPLandroidx/work/ListenableFutureKt$launchFuture$1$2;->create(Ljava/lang/Object;Lkotlin/coroutines/Continuation;)Lkotlin/coroutines/Continuation; @@ -17361,9 +17457,13 @@ HSPLandroidx/work/ListenableFutureKt$launchFuture$1$2;->invokeSuspend(Ljava/lang Landroidx/work/ListenableWorker; HSPLandroidx/work/ListenableWorker;->(Landroid/content/Context;Landroidx/work/WorkerParameters;)V HSPLandroidx/work/ListenableWorker;->isUsed()Z -HSPLandroidx/work/ListenableWorker;->onStopped()V +PLandroidx/work/ListenableWorker;->onStopped()V HSPLandroidx/work/ListenableWorker;->setUsed()V -HSPLandroidx/work/ListenableWorker;->stop(I)V +PLandroidx/work/ListenableWorker;->stop(I)V +PLandroidx/work/ListenableWorker$Result;->()V +PLandroidx/work/ListenableWorker$Result;->success()Landroidx/work/ListenableWorker$Result; +PLandroidx/work/ListenableWorker$Result$Success;->()V +PLandroidx/work/ListenableWorker$Result$Success;->(Landroidx/work/Data;)V Landroidx/work/Logger; HSPLandroidx/work/Logger;->()V HSPLandroidx/work/Logger;->(I)V @@ -17374,7 +17474,7 @@ Landroidx/work/Logger$LogcatLogger; HSPLandroidx/work/Logger$LogcatLogger;->(I)V HSPLandroidx/work/Logger$LogcatLogger;->debug(Ljava/lang/String;Ljava/lang/String;)V PLandroidx/work/Logger$LogcatLogger;->info(Ljava/lang/String;Ljava/lang/String;)V -HSPLandroidx/work/Logger$LogcatLogger;->info(Ljava/lang/String;Ljava/lang/String;Ljava/lang/Throwable;)V +PLandroidx/work/Logger$LogcatLogger;->info(Ljava/lang/String;Ljava/lang/String;Ljava/lang/Throwable;)V Landroidx/work/NetworkType; HSPLandroidx/work/NetworkType;->$values()[Landroidx/work/NetworkType; HSPLandroidx/work/NetworkType;->()V @@ -17477,7 +17577,7 @@ HSPLandroidx/work/WorkerFactory;->createWorkerWithDefaultFallback(Landroid/conte Landroidx/work/WorkerParameters; HSPLandroidx/work/WorkerParameters;->(Ljava/util/UUID;Landroidx/work/Data;Ljava/util/Collection;Landroidx/work/WorkerParameters$RuntimeExtras;IILjava/util/concurrent/Executor;Lkotlin/coroutines/CoroutineContext;Landroidx/work/impl/utils/taskexecutor/TaskExecutor;Landroidx/work/WorkerFactory;Landroidx/work/ProgressUpdater;Landroidx/work/ForegroundUpdater;)V HSPLandroidx/work/WorkerParameters;->getForegroundUpdater()Landroidx/work/ForegroundUpdater; -HSPLandroidx/work/WorkerParameters;->getWorkerContext()Lkotlin/coroutines/CoroutineContext; +PLandroidx/work/WorkerParameters;->getWorkerContext()Lkotlin/coroutines/CoroutineContext; Landroidx/work/WorkerParameters$RuntimeExtras; HSPLandroidx/work/WorkerParameters$RuntimeExtras;->()V Landroidx/work/impl/AutoMigration_14_15; @@ -17528,12 +17628,12 @@ Landroidx/work/impl/Processor; HSPLandroidx/work/impl/Processor;->()V HSPLandroidx/work/impl/Processor;->(Landroid/content/Context;Landroidx/work/Configuration;Landroidx/work/impl/utils/taskexecutor/TaskExecutor;Landroidx/work/impl/WorkDatabase;)V HSPLandroidx/work/impl/Processor;->addExecutionListener(Landroidx/work/impl/ExecutionListener;)V -HSPLandroidx/work/impl/Processor;->cleanUpWorkerUnsafe(Ljava/lang/String;)Landroidx/work/impl/WorkerWrapper; +PLandroidx/work/impl/Processor;->cleanUpWorkerUnsafe(Ljava/lang/String;)Landroidx/work/impl/WorkerWrapper; HSPLandroidx/work/impl/Processor;->getWorkerWrapperUnsafe(Ljava/lang/String;)Landroidx/work/impl/WorkerWrapper; -HSPLandroidx/work/impl/Processor;->interrupt(Ljava/lang/String;Landroidx/work/impl/WorkerWrapper;I)Z +PLandroidx/work/impl/Processor;->interrupt(Ljava/lang/String;Landroidx/work/impl/WorkerWrapper;I)Z PLandroidx/work/impl/Processor;->isCancelled(Ljava/lang/String;)Z HSPLandroidx/work/impl/Processor;->isEnqueued(Ljava/lang/String;)Z -HSPLandroidx/work/impl/Processor;->lambda$runOnExecuted$2$androidx-work-impl-Processor(Landroidx/work/impl/model/WorkGenerationalId;Z)V +PLandroidx/work/impl/Processor;->lambda$runOnExecuted$2$androidx-work-impl-Processor(Landroidx/work/impl/model/WorkGenerationalId;Z)V HSPLandroidx/work/impl/Processor;->lambda$startWork$0$androidx-work-impl-Processor(Ljava/util/ArrayList;Ljava/lang/String;)Landroidx/work/impl/model/WorkSpec; PLandroidx/work/impl/Processor;->lambda$startWork$1$androidx-work-impl-Processor(Lcom/google/common/util/concurrent/ListenableFuture;Landroidx/work/impl/WorkerWrapper;)V PLandroidx/work/impl/Processor;->onExecuted(Landroidx/work/impl/WorkerWrapper;Z)V @@ -17543,27 +17643,27 @@ HSPLandroidx/work/impl/Processor;->startWork(Landroidx/work/impl/StartStopToken; HSPLandroidx/work/impl/Processor;->stopWork(Landroidx/work/impl/StartStopToken;I)Z Landroidx/work/impl/Processor$$ExternalSyntheticLambda0; HSPLandroidx/work/impl/Processor$$ExternalSyntheticLambda0;->(Landroidx/work/impl/Processor;Landroidx/work/impl/model/WorkGenerationalId;Z)V -HSPLandroidx/work/impl/Processor$$ExternalSyntheticLambda0;->run()V +PLandroidx/work/impl/Processor$$ExternalSyntheticLambda0;->run()V Landroidx/work/impl/Processor$$ExternalSyntheticLambda1; HSPLandroidx/work/impl/Processor$$ExternalSyntheticLambda1;->(Landroidx/work/impl/Processor;Ljava/util/ArrayList;Ljava/lang/String;)V HSPLandroidx/work/impl/Processor$$ExternalSyntheticLambda1;->call()Ljava/lang/Object; Landroidx/work/impl/Processor$$ExternalSyntheticLambda2; HSPLandroidx/work/impl/Processor$$ExternalSyntheticLambda2;->(Landroidx/work/impl/Processor;Lcom/google/common/util/concurrent/ListenableFuture;Landroidx/work/impl/WorkerWrapper;)V -HSPLandroidx/work/impl/Processor$$ExternalSyntheticLambda2;->run()V +PLandroidx/work/impl/Processor$$ExternalSyntheticLambda2;->run()V Landroidx/work/impl/RescheduleMigration; HSPLandroidx/work/impl/RescheduleMigration;->(Landroid/content/Context;II)V Landroidx/work/impl/Scheduler; Landroidx/work/impl/Schedulers; HSPLandroidx/work/impl/Schedulers;->()V HSPLandroidx/work/impl/Schedulers;->createBestAvailableBackgroundScheduler(Landroid/content/Context;Landroidx/work/impl/WorkDatabase;Landroidx/work/Configuration;)Landroidx/work/impl/Scheduler; -HSPLandroidx/work/impl/Schedulers;->lambda$registerRescheduling$0(Ljava/util/List;Landroidx/work/impl/model/WorkGenerationalId;Landroidx/work/Configuration;Landroidx/work/impl/WorkDatabase;)V -HSPLandroidx/work/impl/Schedulers;->lambda$registerRescheduling$1(Ljava/util/concurrent/Executor;Ljava/util/List;Landroidx/work/Configuration;Landroidx/work/impl/WorkDatabase;Landroidx/work/impl/model/WorkGenerationalId;Z)V +PLandroidx/work/impl/Schedulers;->lambda$registerRescheduling$0(Ljava/util/List;Landroidx/work/impl/model/WorkGenerationalId;Landroidx/work/Configuration;Landroidx/work/impl/WorkDatabase;)V +PLandroidx/work/impl/Schedulers;->lambda$registerRescheduling$1(Ljava/util/concurrent/Executor;Ljava/util/List;Landroidx/work/Configuration;Landroidx/work/impl/WorkDatabase;Landroidx/work/impl/model/WorkGenerationalId;Z)V HSPLandroidx/work/impl/Schedulers;->markScheduled(Landroidx/work/impl/model/WorkSpecDao;Landroidx/work/Clock;Ljava/util/List;)V HSPLandroidx/work/impl/Schedulers;->registerRescheduling(Ljava/util/List;Landroidx/work/impl/Processor;Ljava/util/concurrent/Executor;Landroidx/work/impl/WorkDatabase;Landroidx/work/Configuration;)V HSPLandroidx/work/impl/Schedulers;->schedule(Landroidx/work/Configuration;Landroidx/work/impl/WorkDatabase;Ljava/util/List;)V Landroidx/work/impl/Schedulers$$ExternalSyntheticLambda0; HSPLandroidx/work/impl/Schedulers$$ExternalSyntheticLambda0;->(Ljava/util/concurrent/Executor;Ljava/util/List;Landroidx/work/Configuration;Landroidx/work/impl/WorkDatabase;)V -HSPLandroidx/work/impl/Schedulers$$ExternalSyntheticLambda0;->onExecuted(Landroidx/work/impl/model/WorkGenerationalId;Z)V +PLandroidx/work/impl/Schedulers$$ExternalSyntheticLambda0;->onExecuted(Landroidx/work/impl/model/WorkGenerationalId;Z)V PLandroidx/work/impl/Schedulers$$ExternalSyntheticLambda1;->(Ljava/util/List;Landroidx/work/impl/model/WorkGenerationalId;Landroidx/work/Configuration;Landroidx/work/impl/WorkDatabase;)V PLandroidx/work/impl/Schedulers$$ExternalSyntheticLambda1;->run()V Landroidx/work/impl/StartStopToken; @@ -17583,13 +17683,13 @@ HSPLandroidx/work/impl/StartStopTokens$Companion;->create(Z)Landroidx/work/impl/ Landroidx/work/impl/StartStopTokensImpl; HSPLandroidx/work/impl/StartStopTokensImpl;->()V HSPLandroidx/work/impl/StartStopTokensImpl;->contains(Landroidx/work/impl/model/WorkGenerationalId;)Z -HSPLandroidx/work/impl/StartStopTokensImpl;->remove(Landroidx/work/impl/model/WorkGenerationalId;)Landroidx/work/impl/StartStopToken; +PLandroidx/work/impl/StartStopTokensImpl;->remove(Landroidx/work/impl/model/WorkGenerationalId;)Landroidx/work/impl/StartStopToken; HSPLandroidx/work/impl/StartStopTokensImpl;->remove(Ljava/lang/String;)Ljava/util/List; HSPLandroidx/work/impl/StartStopTokensImpl;->tokenFor(Landroidx/work/impl/model/WorkGenerationalId;)Landroidx/work/impl/StartStopToken; Landroidx/work/impl/SynchronizedStartStopTokensImpl; HSPLandroidx/work/impl/SynchronizedStartStopTokensImpl;->(Landroidx/work/impl/StartStopTokens;)V HSPLandroidx/work/impl/SynchronizedStartStopTokensImpl;->contains(Landroidx/work/impl/model/WorkGenerationalId;)Z -HSPLandroidx/work/impl/SynchronizedStartStopTokensImpl;->remove(Landroidx/work/impl/model/WorkGenerationalId;)Landroidx/work/impl/StartStopToken; +PLandroidx/work/impl/SynchronizedStartStopTokensImpl;->remove(Landroidx/work/impl/model/WorkGenerationalId;)Landroidx/work/impl/StartStopToken; HSPLandroidx/work/impl/SynchronizedStartStopTokensImpl;->remove(Ljava/lang/String;)Ljava/util/List; HSPLandroidx/work/impl/SynchronizedStartStopTokensImpl;->tokenFor(Landroidx/work/impl/model/WorkGenerationalId;)Landroidx/work/impl/StartStopToken; PLandroidx/work/impl/ToContinuation;->(Lcom/google/common/util/concurrent/ListenableFuture;Lkotlinx/coroutines/CancellableContinuation;)V @@ -17737,8 +17837,8 @@ HSPLandroidx/work/impl/WorkManagerImplExtKt$WorkManagerImpl$1;->invoke(Ljava/lan Landroidx/work/impl/WorkMigration9To10; HSPLandroidx/work/impl/WorkMigration9To10;->(Landroid/content/Context;)V Landroidx/work/impl/WorkerStoppedException; -HSPLandroidx/work/impl/WorkerStoppedException;->(I)V -HSPLandroidx/work/impl/WorkerStoppedException;->getReason()I +PLandroidx/work/impl/WorkerStoppedException;->(I)V +PLandroidx/work/impl/WorkerStoppedException;->getReason()I Landroidx/work/impl/WorkerUpdater; HSPLandroidx/work/impl/WorkerUpdater;->enqueueUniquelyNamedPeriodic$lambda$6(Landroidx/work/impl/WorkManagerImpl;Ljava/lang/String;Landroidx/work/WorkRequest;)Lkotlin/Unit; HSPLandroidx/work/impl/WorkerUpdater;->enqueueUniquelyNamedPeriodic(Landroidx/work/impl/WorkManagerImpl;Ljava/lang/String;Landroidx/work/WorkRequest;)Landroidx/work/Operation; @@ -17754,23 +17854,24 @@ HSPLandroidx/work/impl/WorkerUpdater$$ExternalSyntheticLambda4;->(Landroid HSPLandroidx/work/impl/WorkerUpdater$$ExternalSyntheticLambda4;->invoke()Ljava/lang/Object; Landroidx/work/impl/WorkerWrapper; HSPLandroidx/work/impl/WorkerWrapper;->(Landroidx/work/impl/WorkerWrapper$Builder;)V -HSPLandroidx/work/impl/WorkerWrapper;->access$getAppContext$p(Landroidx/work/impl/WorkerWrapper;)Landroid/content/Context; -HSPLandroidx/work/impl/WorkerWrapper;->access$getWorkDatabase$p(Landroidx/work/impl/WorkerWrapper;)Landroidx/work/impl/WorkDatabase; -HSPLandroidx/work/impl/WorkerWrapper;->access$getWorkTaskExecutor$p(Landroidx/work/impl/WorkerWrapper;)Landroidx/work/impl/utils/taskexecutor/TaskExecutor; +PLandroidx/work/impl/WorkerWrapper;->access$getAppContext$p(Landroidx/work/impl/WorkerWrapper;)Landroid/content/Context; +PLandroidx/work/impl/WorkerWrapper;->access$getWorkDatabase$p(Landroidx/work/impl/WorkerWrapper;)Landroidx/work/impl/WorkDatabase; +PLandroidx/work/impl/WorkerWrapper;->access$getWorkTaskExecutor$p(Landroidx/work/impl/WorkerWrapper;)Landroidx/work/impl/utils/taskexecutor/TaskExecutor; HSPLandroidx/work/impl/WorkerWrapper;->access$getWorkerJob$p(Landroidx/work/impl/WorkerWrapper;)Lkotlinx/coroutines/CompletableJob; -HSPLandroidx/work/impl/WorkerWrapper;->access$resetWorkerStatus(Landroidx/work/impl/WorkerWrapper;I)Z +PLandroidx/work/impl/WorkerWrapper;->access$onWorkFinished(Landroidx/work/impl/WorkerWrapper;Landroidx/work/ListenableWorker$Result;)Z +PLandroidx/work/impl/WorkerWrapper;->access$resetWorkerStatus(Landroidx/work/impl/WorkerWrapper;I)Z HSPLandroidx/work/impl/WorkerWrapper;->access$runWorker(Landroidx/work/impl/WorkerWrapper;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; HSPLandroidx/work/impl/WorkerWrapper;->createWorkDescription(Ljava/util/List;)Ljava/lang/String; -HSPLandroidx/work/impl/WorkerWrapper;->getWorkGenerationalId()Landroidx/work/impl/model/WorkGenerationalId; -HSPLandroidx/work/impl/WorkerWrapper;->getWorkSpec()Landroidx/work/impl/model/WorkSpec; +PLandroidx/work/impl/WorkerWrapper;->getWorkGenerationalId()Landroidx/work/impl/model/WorkGenerationalId; +PLandroidx/work/impl/WorkerWrapper;->getWorkSpec()Landroidx/work/impl/model/WorkSpec; PLandroidx/work/impl/WorkerWrapper;->handleResult(Landroidx/work/ListenableWorker$Result;)Z -HSPLandroidx/work/impl/WorkerWrapper;->interrupt(I)V +PLandroidx/work/impl/WorkerWrapper;->interrupt(I)V HSPLandroidx/work/impl/WorkerWrapper;->launch()Lcom/google/common/util/concurrent/ListenableFuture; PLandroidx/work/impl/WorkerWrapper;->onWorkFinished(Landroidx/work/ListenableWorker$Result;)Z PLandroidx/work/impl/WorkerWrapper;->resetPeriodic()Z -HSPLandroidx/work/impl/WorkerWrapper;->resetWorkerStatus(I)Z +PLandroidx/work/impl/WorkerWrapper;->resetWorkerStatus(I)Z HSPLandroidx/work/impl/WorkerWrapper;->runWorker$lambda$1(Landroidx/work/impl/WorkerWrapper;)Ljava/lang/Boolean; -HSPLandroidx/work/impl/WorkerWrapper;->runWorker$lambda$4(Landroidx/work/ListenableWorker;ZLjava/lang/String;Landroidx/work/impl/WorkerWrapper;Ljava/lang/Throwable;)Lkotlin/Unit; +PLandroidx/work/impl/WorkerWrapper;->runWorker$lambda$4(Landroidx/work/ListenableWorker;ZLjava/lang/String;Landroidx/work/impl/WorkerWrapper;Ljava/lang/Throwable;)Lkotlin/Unit; HSPLandroidx/work/impl/WorkerWrapper;->runWorker(Lkotlin/coroutines/Continuation;)Ljava/lang/Object; HSPLandroidx/work/impl/WorkerWrapper;->trySetRunning$lambda$14(Landroidx/work/impl/WorkerWrapper;)Ljava/lang/Boolean; HSPLandroidx/work/impl/WorkerWrapper;->trySetRunning()Z @@ -17779,7 +17880,7 @@ HSPLandroidx/work/impl/WorkerWrapper$$ExternalSyntheticLambda0;->(Landroid HSPLandroidx/work/impl/WorkerWrapper$$ExternalSyntheticLambda0;->call()Ljava/lang/Object; Landroidx/work/impl/WorkerWrapper$$ExternalSyntheticLambda1; HSPLandroidx/work/impl/WorkerWrapper$$ExternalSyntheticLambda1;->(Landroidx/work/ListenableWorker;ZLjava/lang/String;Landroidx/work/impl/WorkerWrapper;)V -HSPLandroidx/work/impl/WorkerWrapper$$ExternalSyntheticLambda1;->invoke(Ljava/lang/Object;)Ljava/lang/Object; +PLandroidx/work/impl/WorkerWrapper$$ExternalSyntheticLambda1;->invoke(Ljava/lang/Object;)Ljava/lang/Object; Landroidx/work/impl/WorkerWrapper$$ExternalSyntheticLambda2; HSPLandroidx/work/impl/WorkerWrapper$$ExternalSyntheticLambda2;->(Landroidx/work/impl/WorkerWrapper;)V HSPLandroidx/work/impl/WorkerWrapper$$ExternalSyntheticLambda2;->call()Ljava/lang/Object; @@ -17798,6 +17899,7 @@ HSPLandroidx/work/impl/WorkerWrapper$Builder;->getWorker()Landroidx/work/Listena HSPLandroidx/work/impl/WorkerWrapper$Builder;->withRuntimeExtras(Landroidx/work/WorkerParameters$RuntimeExtras;)Landroidx/work/impl/WorkerWrapper$Builder; PLandroidx/work/impl/WorkerWrapper$Resolution;->()V PLandroidx/work/impl/WorkerWrapper$Resolution;->(Lkotlin/jvm/internal/DefaultConstructorMarker;)V +PLandroidx/work/impl/WorkerWrapper$Resolution$Finished;->getResult()Landroidx/work/ListenableWorker$Result; PLandroidx/work/impl/WorkerWrapper$Resolution$ResetWorkerStatus;->(I)V PLandroidx/work/impl/WorkerWrapper$Resolution$ResetWorkerStatus;->(IILkotlin/jvm/internal/DefaultConstructorMarker;)V PLandroidx/work/impl/WorkerWrapper$Resolution$ResetWorkerStatus;->getReason()I @@ -17806,7 +17908,7 @@ HSPLandroidx/work/impl/WorkerWrapper$launch$1;->(Landroidx/work/impl/Worke HSPLandroidx/work/impl/WorkerWrapper$launch$1;->create(Ljava/lang/Object;Lkotlin/coroutines/Continuation;)Lkotlin/coroutines/Continuation; HSPLandroidx/work/impl/WorkerWrapper$launch$1;->invoke(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object; HSPLandroidx/work/impl/WorkerWrapper$launch$1;->invoke(Lkotlinx/coroutines/CoroutineScope;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; -HSPLandroidx/work/impl/WorkerWrapper$launch$1;->invokeSuspend$lambda$1(Landroidx/work/impl/WorkerWrapper$Resolution;Landroidx/work/impl/WorkerWrapper;)Ljava/lang/Boolean; +PLandroidx/work/impl/WorkerWrapper$launch$1;->invokeSuspend$lambda$1(Landroidx/work/impl/WorkerWrapper$Resolution;Landroidx/work/impl/WorkerWrapper;)Ljava/lang/Boolean; HSPLandroidx/work/impl/WorkerWrapper$launch$1;->invokeSuspend(Ljava/lang/Object;)Ljava/lang/Object; PLandroidx/work/impl/WorkerWrapper$launch$1$$ExternalSyntheticLambda0;->(Landroidx/work/impl/WorkerWrapper$Resolution;Landroidx/work/impl/WorkerWrapper;)V PLandroidx/work/impl/WorkerWrapper$launch$1$$ExternalSyntheticLambda0;->call()Ljava/lang/Object; @@ -17818,11 +17920,11 @@ HSPLandroidx/work/impl/WorkerWrapper$launch$1$resolution$1;->invoke(Lkotlinx/cor HSPLandroidx/work/impl/WorkerWrapper$launch$1$resolution$1;->invokeSuspend(Ljava/lang/Object;)Ljava/lang/Object; Landroidx/work/impl/WorkerWrapper$runWorker$1; HSPLandroidx/work/impl/WorkerWrapper$runWorker$1;->(Landroidx/work/impl/WorkerWrapper;Lkotlin/coroutines/Continuation;)V -HSPLandroidx/work/impl/WorkerWrapper$runWorker$1;->invokeSuspend(Ljava/lang/Object;)Ljava/lang/Object; +PLandroidx/work/impl/WorkerWrapper$runWorker$1;->invokeSuspend(Ljava/lang/Object;)Ljava/lang/Object; Landroidx/work/impl/WorkerWrapper$runWorker$result$1; HSPLandroidx/work/impl/WorkerWrapper$runWorker$result$1;->(Landroidx/work/impl/WorkerWrapper;Landroidx/work/ListenableWorker;Landroidx/work/ForegroundUpdater;Lkotlin/coroutines/Continuation;)V HSPLandroidx/work/impl/WorkerWrapper$runWorker$result$1;->create(Ljava/lang/Object;Lkotlin/coroutines/Continuation;)Lkotlin/coroutines/Continuation; -HSPLandroidx/work/impl/WorkerWrapper$runWorker$result$1;->invokeSuspend(Ljava/lang/Object;)Ljava/lang/Object; +PLandroidx/work/impl/WorkerWrapper$runWorker$result$1;->invokeSuspend(Ljava/lang/Object;)Ljava/lang/Object; PLandroidx/work/impl/WorkerWrapperKt;->()V PLandroidx/work/impl/WorkerWrapperKt;->access$getTAG$p()Ljava/lang/String; PLandroidx/work/impl/WorkerWrapperKt;->access$getUninterruptibly(Ljava/util/concurrent/Future;)Ljava/lang/Object; @@ -17845,9 +17947,9 @@ HSPLandroidx/work/impl/background/greedy/GreedyScheduler;->(Landroid/conte HSPLandroidx/work/impl/background/greedy/GreedyScheduler;->cancel(Ljava/lang/String;)V HSPLandroidx/work/impl/background/greedy/GreedyScheduler;->checkDefaultProcess()V HSPLandroidx/work/impl/background/greedy/GreedyScheduler;->hasLimitedSchedulingSlots()Z -HSPLandroidx/work/impl/background/greedy/GreedyScheduler;->onExecuted(Landroidx/work/impl/model/WorkGenerationalId;Z)V +PLandroidx/work/impl/background/greedy/GreedyScheduler;->onExecuted(Landroidx/work/impl/model/WorkGenerationalId;Z)V HSPLandroidx/work/impl/background/greedy/GreedyScheduler;->registerExecutionListenerIfNeeded()V -HSPLandroidx/work/impl/background/greedy/GreedyScheduler;->removeConstraintTrackingFor(Landroidx/work/impl/model/WorkGenerationalId;)V +PLandroidx/work/impl/background/greedy/GreedyScheduler;->removeConstraintTrackingFor(Landroidx/work/impl/model/WorkGenerationalId;)V HSPLandroidx/work/impl/background/greedy/GreedyScheduler;->schedule([Landroidx/work/impl/model/WorkSpec;)V HSPLandroidx/work/impl/background/greedy/GreedyScheduler;->throttleIfNeeded(Landroidx/work/impl/model/WorkSpec;)J Landroidx/work/impl/background/greedy/GreedyScheduler$AttemptData; @@ -17899,13 +18001,13 @@ HSPLandroidx/work/impl/background/systemjob/SystemJobScheduler;->scheduleInterna Landroidx/work/impl/background/systemjob/SystemJobService; HSPLandroidx/work/impl/background/systemjob/SystemJobService;->()V HSPLandroidx/work/impl/background/systemjob/SystemJobService;->()V -HSPLandroidx/work/impl/background/systemjob/SystemJobService;->assertMainThread(Ljava/lang/String;)V +PLandroidx/work/impl/background/systemjob/SystemJobService;->assertMainThread(Ljava/lang/String;)V HSPLandroidx/work/impl/background/systemjob/SystemJobService;->onCreate()V PLandroidx/work/impl/background/systemjob/SystemJobService;->onDestroy()V -HSPLandroidx/work/impl/background/systemjob/SystemJobService;->onExecuted(Landroidx/work/impl/model/WorkGenerationalId;Z)V -HSPLandroidx/work/impl/background/systemjob/SystemJobService;->onStartJob(Landroid/app/job/JobParameters;)Z +PLandroidx/work/impl/background/systemjob/SystemJobService;->onExecuted(Landroidx/work/impl/model/WorkGenerationalId;Z)V +PLandroidx/work/impl/background/systemjob/SystemJobService;->onStartJob(Landroid/app/job/JobParameters;)Z PLandroidx/work/impl/background/systemjob/SystemJobService;->onStopJob(Landroid/app/job/JobParameters;)Z -HSPLandroidx/work/impl/background/systemjob/SystemJobService;->workGenerationalIdFromJobParameters(Landroid/app/job/JobParameters;)Landroidx/work/impl/model/WorkGenerationalId; +PLandroidx/work/impl/background/systemjob/SystemJobService;->workGenerationalIdFromJobParameters(Landroid/app/job/JobParameters;)Landroidx/work/impl/model/WorkGenerationalId; PLandroidx/work/impl/background/systemjob/SystemJobService$Api24Impl;->getTriggeredContentAuthorities(Landroid/app/job/JobParameters;)[Ljava/lang/String; PLandroidx/work/impl/background/systemjob/SystemJobService$Api24Impl;->getTriggeredContentUris(Landroid/app/job/JobParameters;)[Landroid/net/Uri; PLandroidx/work/impl/background/systemjob/SystemJobService$Api28Impl;->getNetwork(Landroid/app/job/JobParameters;)Landroid/net/Network; @@ -18386,7 +18488,7 @@ HSPLco/touchlab/kermit/LogcatWriter$WhenMappings;->()V Lco/touchlab/kermit/Logger; HSPLco/touchlab/kermit/Logger;->()V HSPLco/touchlab/kermit/Logger;->(Lco/touchlab/kermit/LoggerConfig;Ljava/lang/String;)V -HSPLco/touchlab/kermit/Logger;->getTag()Ljava/lang/String; +PLco/touchlab/kermit/Logger;->getTag()Ljava/lang/String; HSPLco/touchlab/kermit/Logger;->withTag(Ljava/lang/String;)Lco/touchlab/kermit/Logger; Lco/touchlab/kermit/Logger$Companion; HSPLco/touchlab/kermit/Logger$Companion;->()V @@ -18456,7 +18558,7 @@ HSPLcom/datadog/android/api/InternalLogger$Target;->values()[Lcom/datadog/androi Lcom/datadog/android/api/SdkCore; Lcom/datadog/android/api/context/AccountInfo; Lcom/datadog/android/api/context/DatadogContext; -HSPLcom/datadog/android/api/context/DatadogContext;->(Lcom/datadog/android/DatadogSite;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;ILjava/lang/String;Ljava/lang/String;Ljava/lang/String;Lcom/datadog/android/api/context/TimeInfo;Lcom/datadog/android/api/context/ProcessInfo;Lcom/datadog/android/api/context/NetworkInfo;Lcom/datadog/android/api/context/DeviceInfo;Lcom/datadog/android/api/context/UserInfo;Lcom/datadog/android/api/context/AccountInfo;Lcom/datadog/android/privacy/TrackingConsent;Ljava/lang/String;Ljava/util/Map;)V +HSPLcom/datadog/android/api/context/DatadogContext;->(Lcom/datadog/android/DatadogSite;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;ILjava/lang/String;Ljava/lang/String;Ljava/lang/String;Lcom/datadog/android/api/context/TimeInfo;Lcom/datadog/android/api/context/ProcessInfo;Lcom/datadog/android/api/context/NetworkInfo;Lcom/datadog/android/api/context/DeviceInfo;Lcom/datadog/android/api/context/UserInfo;Lcom/datadog/android/api/context/AccountInfo;Lcom/datadog/android/privacy/TrackingConsent;Ljava/lang/String;Ljava/lang/String;Ljava/util/Map;)V HSPLcom/datadog/android/api/context/DatadogContext;->getAccountInfo()Lcom/datadog/android/api/context/AccountInfo; HSPLcom/datadog/android/api/context/DatadogContext;->getAppBuildId()Ljava/lang/String; HSPLcom/datadog/android/api/context/DatadogContext;->getClientToken()Ljava/lang/String; @@ -18464,7 +18566,7 @@ HSPLcom/datadog/android/api/context/DatadogContext;->getDeviceInfo()Lcom/datadog HSPLcom/datadog/android/api/context/DatadogContext;->getEnv()Ljava/lang/String; HSPLcom/datadog/android/api/context/DatadogContext;->getFeaturesContext()Ljava/util/Map; HSPLcom/datadog/android/api/context/DatadogContext;->getNetworkInfo()Lcom/datadog/android/api/context/NetworkInfo; -PLcom/datadog/android/api/context/DatadogContext;->getProcessInfo()Lcom/datadog/android/api/context/ProcessInfo; +HSPLcom/datadog/android/api/context/DatadogContext;->getRemoteConfigurationId()Ljava/lang/String; HSPLcom/datadog/android/api/context/DatadogContext;->getSdkVersion()Ljava/lang/String; HSPLcom/datadog/android/api/context/DatadogContext;->getService()Ljava/lang/String; HSPLcom/datadog/android/api/context/DatadogContext;->getSite()Lcom/datadog/android/DatadogSite; @@ -18479,13 +18581,11 @@ Lcom/datadog/android/api/context/DeviceInfo; HSPLcom/datadog/android/api/context/DeviceInfo;->(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Lcom/datadog/android/api/context/DeviceType;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/Integer;Lcom/datadog/android/api/context/LocaleInfo;ILjava/lang/Integer;Ljava/lang/Boolean;)V HSPLcom/datadog/android/api/context/DeviceInfo;->getArchitecture()Ljava/lang/String; HSPLcom/datadog/android/api/context/DeviceInfo;->getDeviceBrand()Ljava/lang/String; -PLcom/datadog/android/api/context/DeviceInfo;->getDeviceBuildId()Ljava/lang/String; HSPLcom/datadog/android/api/context/DeviceInfo;->getDeviceModel()Ljava/lang/String; HSPLcom/datadog/android/api/context/DeviceInfo;->getDeviceName()Ljava/lang/String; HSPLcom/datadog/android/api/context/DeviceInfo;->getDeviceType()Lcom/datadog/android/api/context/DeviceType; HSPLcom/datadog/android/api/context/DeviceInfo;->getLocaleInfo()Lcom/datadog/android/api/context/LocaleInfo; HSPLcom/datadog/android/api/context/DeviceInfo;->getLogicalCpuCount()I -PLcom/datadog/android/api/context/DeviceInfo;->getNumberOfDisplays()Ljava/lang/Integer; HSPLcom/datadog/android/api/context/DeviceInfo;->getOsMajorVersion()Ljava/lang/String; HSPLcom/datadog/android/api/context/DeviceInfo;->getOsName()Ljava/lang/String; HSPLcom/datadog/android/api/context/DeviceInfo;->getOsVersion()Ljava/lang/String; @@ -18521,7 +18621,6 @@ HSPLcom/datadog/android/api/context/NetworkInfo$Connectivity$Companion;->( HSPLcom/datadog/android/api/context/NetworkInfo$Connectivity$Companion;->(Lkotlin/jvm/internal/DefaultConstructorMarker;)V Lcom/datadog/android/api/context/ProcessInfo; HSPLcom/datadog/android/api/context/ProcessInfo;->(Z)V -PLcom/datadog/android/api/context/ProcessInfo;->isMainProcess()Z Lcom/datadog/android/api/context/TimeInfo; HSPLcom/datadog/android/api/context/TimeInfo;->()V HSPLcom/datadog/android/api/context/TimeInfo;->(JJJJ)V @@ -18549,6 +18648,7 @@ Lcom/datadog/android/api/feature/FeatureScope; Lcom/datadog/android/api/feature/FeatureScopeExtKt; HSPLcom/datadog/android/api/feature/FeatureScopeExtKt;->measureMethodCallPerf(Lcom/datadog/android/api/InternalLogger;Ljava/lang/Class;Ljava/lang/String;FLkotlin/jvm/functions/Function0;)Ljava/lang/Object; Lcom/datadog/android/api/feature/FeatureSdkCore; +Lcom/datadog/android/api/feature/FeatureSdkCore$DefaultImpls; Lcom/datadog/android/api/feature/StorageBackedFeature; Lcom/datadog/android/api/net/Request; HSPLcom/datadog/android/api/net/Request;->(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/util/Map;[BLjava/lang/String;)V @@ -18606,9 +18706,8 @@ HSPLcom/datadog/android/core/SdkReference;->(Ljava/lang/String;Lkotlin/jvm HSPLcom/datadog/android/core/SdkReference;->(Ljava/lang/String;Lkotlin/jvm/functions/Function1;ILkotlin/jvm/internal/DefaultConstructorMarker;)V HSPLcom/datadog/android/core/SdkReference;->get()Lcom/datadog/android/api/SdkCore; HSPLcom/datadog/android/core/SdkReference;->tryAcquire()Lcom/datadog/android/api/SdkCore; -Lcom/datadog/android/core/SdkReference$1; -HSPLcom/datadog/android/core/SdkReference$1;->()V -HSPLcom/datadog/android/core/SdkReference$1;->()V +Lcom/datadog/android/core/SdkReference$$ExternalSyntheticLambda0; +HSPLcom/datadog/android/core/SdkReference$$ExternalSyntheticLambda0;->()V Lcom/datadog/android/core/configuration/BackPressureMitigation; HSPLcom/datadog/android/core/configuration/BackPressureMitigation;->$values()[Lcom/datadog/android/core/configuration/BackPressureMitigation; HSPLcom/datadog/android/core/configuration/BackPressureMitigation;->()V @@ -18641,6 +18740,10 @@ HSPLcom/datadog/android/core/configuration/Configuration;->getEnv$dd_sdk_android HSPLcom/datadog/android/core/configuration/Configuration;->getService$dd_sdk_android_core_release()Ljava/lang/String; HSPLcom/datadog/android/core/configuration/Configuration;->getVariant$dd_sdk_android_core_release()Ljava/lang/String; HSPLcom/datadog/android/core/configuration/Configuration;->getVersion$dd_sdk_android_core_release()Ljava/lang/String; +Lcom/datadog/android/core/configuration/Configuration$$ExternalSyntheticLambda0; +HSPLcom/datadog/android/core/configuration/Configuration$$ExternalSyntheticLambda0;->()V +Lcom/datadog/android/core/configuration/Configuration$$ExternalSyntheticLambda1; +HSPLcom/datadog/android/core/configuration/Configuration$$ExternalSyntheticLambda1;->()V Lcom/datadog/android/core/configuration/Configuration$Builder; HSPLcom/datadog/android/core/configuration/Configuration$Builder;->(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)V HSPLcom/datadog/android/core/configuration/Configuration$Builder;->build()Lcom/datadog/android/core/configuration/Configuration; @@ -18651,16 +18754,10 @@ Lcom/datadog/android/core/configuration/Configuration$Companion; HSPLcom/datadog/android/core/configuration/Configuration$Companion;->()V HSPLcom/datadog/android/core/configuration/Configuration$Companion;->(Lkotlin/jvm/internal/DefaultConstructorMarker;)V HSPLcom/datadog/android/core/configuration/Configuration$Companion;->getDEFAULT_CORE_CONFIG$dd_sdk_android_core_release()Lcom/datadog/android/core/configuration/Configuration$Core; -Lcom/datadog/android/core/configuration/Configuration$Companion$DEFAULT_BACKPRESSURE_STRATEGY$1; -HSPLcom/datadog/android/core/configuration/Configuration$Companion$DEFAULT_BACKPRESSURE_STRATEGY$1;->()V -HSPLcom/datadog/android/core/configuration/Configuration$Companion$DEFAULT_BACKPRESSURE_STRATEGY$1;->()V -Lcom/datadog/android/core/configuration/Configuration$Companion$DEFAULT_BACKPRESSURE_STRATEGY$2; -HSPLcom/datadog/android/core/configuration/Configuration$Companion$DEFAULT_BACKPRESSURE_STRATEGY$2;->()V -HSPLcom/datadog/android/core/configuration/Configuration$Companion$DEFAULT_BACKPRESSURE_STRATEGY$2;->()V Lcom/datadog/android/core/configuration/Configuration$Core; -HSPLcom/datadog/android/core/configuration/Configuration$Core;->(ZZLjava/util/Map;Lcom/datadog/android/core/configuration/BatchSize;Lcom/datadog/android/core/configuration/UploadFrequency;Ljava/net/Proxy;Lokhttp3/Authenticator;Lcom/datadog/android/security/Encryption;Lcom/datadog/android/DatadogSite;Lcom/datadog/android/core/configuration/BatchProcessingLevel;Lcom/datadog/android/core/persistence/PersistenceStrategy$Factory;Lcom/datadog/android/core/configuration/BackPressureStrategy;Lcom/datadog/android/core/configuration/UploadSchedulerStrategy;)V -HSPLcom/datadog/android/core/configuration/Configuration$Core;->copy$default(Lcom/datadog/android/core/configuration/Configuration$Core;ZZLjava/util/Map;Lcom/datadog/android/core/configuration/BatchSize;Lcom/datadog/android/core/configuration/UploadFrequency;Ljava/net/Proxy;Lokhttp3/Authenticator;Lcom/datadog/android/security/Encryption;Lcom/datadog/android/DatadogSite;Lcom/datadog/android/core/configuration/BatchProcessingLevel;Lcom/datadog/android/core/persistence/PersistenceStrategy$Factory;Lcom/datadog/android/core/configuration/BackPressureStrategy;Lcom/datadog/android/core/configuration/UploadSchedulerStrategy;ILjava/lang/Object;)Lcom/datadog/android/core/configuration/Configuration$Core; -HSPLcom/datadog/android/core/configuration/Configuration$Core;->copy(ZZLjava/util/Map;Lcom/datadog/android/core/configuration/BatchSize;Lcom/datadog/android/core/configuration/UploadFrequency;Ljava/net/Proxy;Lokhttp3/Authenticator;Lcom/datadog/android/security/Encryption;Lcom/datadog/android/DatadogSite;Lcom/datadog/android/core/configuration/BatchProcessingLevel;Lcom/datadog/android/core/persistence/PersistenceStrategy$Factory;Lcom/datadog/android/core/configuration/BackPressureStrategy;Lcom/datadog/android/core/configuration/UploadSchedulerStrategy;)Lcom/datadog/android/core/configuration/Configuration$Core; +HSPLcom/datadog/android/core/configuration/Configuration$Core;->(ZZLjava/util/Map;Lcom/datadog/android/core/configuration/BatchSize;Lcom/datadog/android/core/configuration/UploadFrequency;Ljava/net/Proxy;Lokhttp3/Authenticator;Lcom/datadog/android/security/Encryption;Lcom/datadog/android/DatadogSite;Lcom/datadog/android/core/configuration/BatchProcessingLevel;Lcom/datadog/android/core/persistence/PersistenceStrategy$Factory;Lcom/datadog/android/core/configuration/BackPressureStrategy;Lcom/datadog/android/core/configuration/UploadSchedulerStrategy;Ljava/lang/String;)V +HSPLcom/datadog/android/core/configuration/Configuration$Core;->copy$default(Lcom/datadog/android/core/configuration/Configuration$Core;ZZLjava/util/Map;Lcom/datadog/android/core/configuration/BatchSize;Lcom/datadog/android/core/configuration/UploadFrequency;Ljava/net/Proxy;Lokhttp3/Authenticator;Lcom/datadog/android/security/Encryption;Lcom/datadog/android/DatadogSite;Lcom/datadog/android/core/configuration/BatchProcessingLevel;Lcom/datadog/android/core/persistence/PersistenceStrategy$Factory;Lcom/datadog/android/core/configuration/BackPressureStrategy;Lcom/datadog/android/core/configuration/UploadSchedulerStrategy;Ljava/lang/String;ILjava/lang/Object;)Lcom/datadog/android/core/configuration/Configuration$Core; +HSPLcom/datadog/android/core/configuration/Configuration$Core;->copy(ZZLjava/util/Map;Lcom/datadog/android/core/configuration/BatchSize;Lcom/datadog/android/core/configuration/UploadFrequency;Ljava/net/Proxy;Lokhttp3/Authenticator;Lcom/datadog/android/security/Encryption;Lcom/datadog/android/DatadogSite;Lcom/datadog/android/core/configuration/BatchProcessingLevel;Lcom/datadog/android/core/persistence/PersistenceStrategy$Factory;Lcom/datadog/android/core/configuration/BackPressureStrategy;Lcom/datadog/android/core/configuration/UploadSchedulerStrategy;Ljava/lang/String;)Lcom/datadog/android/core/configuration/Configuration$Core; HSPLcom/datadog/android/core/configuration/Configuration$Core;->getBackpressureStrategy()Lcom/datadog/android/core/configuration/BackPressureStrategy; PLcom/datadog/android/core/configuration/Configuration$Core;->getBatchProcessingLevel()Lcom/datadog/android/core/configuration/BatchProcessingLevel; HSPLcom/datadog/android/core/configuration/Configuration$Core;->getBatchSize()Lcom/datadog/android/core/configuration/BatchSize; @@ -18670,6 +18767,7 @@ HSPLcom/datadog/android/core/configuration/Configuration$Core;->getFirstPartyHos HSPLcom/datadog/android/core/configuration/Configuration$Core;->getNeedsClearTextHttp()Z HSPLcom/datadog/android/core/configuration/Configuration$Core;->getPersistenceStrategyFactory()Lcom/datadog/android/core/persistence/PersistenceStrategy$Factory; HSPLcom/datadog/android/core/configuration/Configuration$Core;->getProxy()Ljava/net/Proxy; +HSPLcom/datadog/android/core/configuration/Configuration$Core;->getRemoteConfigurationId()Ljava/lang/String; HSPLcom/datadog/android/core/configuration/Configuration$Core;->getSite()Lcom/datadog/android/DatadogSite; HSPLcom/datadog/android/core/configuration/Configuration$Core;->getUploadFrequency()Lcom/datadog/android/core/configuration/UploadFrequency; HSPLcom/datadog/android/core/configuration/Configuration$Core;->getUploadSchedulerStrategy()Lcom/datadog/android/core/configuration/UploadSchedulerStrategy; @@ -18702,48 +18800,42 @@ HSPLcom/datadog/android/core/constraints/DataConstraints$DefaultImpls;->validate Lcom/datadog/android/core/constraints/DatadogDataConstraints; HSPLcom/datadog/android/core/constraints/DatadogDataConstraints;->()V HSPLcom/datadog/android/core/constraints/DatadogDataConstraints;->(Lcom/datadog/android/api/InternalLogger;)V -HSPLcom/datadog/android/core/constraints/DatadogDataConstraints;->access$isKeyReserved(Lcom/datadog/android/core/constraints/DatadogDataConstraints;Ljava/lang/String;)Z HSPLcom/datadog/android/core/constraints/DatadogDataConstraints;->convertAttributeKey(Ljava/lang/String;I)Ljava/lang/String; HSPLcom/datadog/android/core/constraints/DatadogDataConstraints;->convertTag(Ljava/lang/String;)Ljava/lang/String; HSPLcom/datadog/android/core/constraints/DatadogDataConstraints;->isKeyReserved(Ljava/lang/String;)Z +HSPLcom/datadog/android/core/constraints/DatadogDataConstraints;->tagTransforms$lambda$13(Ljava/lang/String;)Ljava/lang/String; +HSPLcom/datadog/android/core/constraints/DatadogDataConstraints;->tagTransforms$lambda$14(Ljava/lang/String;)Ljava/lang/String; +HSPLcom/datadog/android/core/constraints/DatadogDataConstraints;->tagTransforms$lambda$15(Ljava/lang/String;)Ljava/lang/String; +HSPLcom/datadog/android/core/constraints/DatadogDataConstraints;->tagTransforms$lambda$16(Ljava/lang/String;)Ljava/lang/String; +HSPLcom/datadog/android/core/constraints/DatadogDataConstraints;->tagTransforms$lambda$17(Ljava/lang/String;)Ljava/lang/String; +HSPLcom/datadog/android/core/constraints/DatadogDataConstraints;->tagTransforms$lambda$18(Lcom/datadog/android/core/constraints/DatadogDataConstraints;Ljava/lang/String;)Ljava/lang/String; HSPLcom/datadog/android/core/constraints/DatadogDataConstraints;->validateAttributes(Ljava/util/Map;Ljava/lang/String;Ljava/lang/String;Ljava/util/Set;)Ljava/util/Map; +HSPLcom/datadog/android/core/constraints/DatadogDataConstraints;->validateTags$lambda$2$lambda$1(Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String; HSPLcom/datadog/android/core/constraints/DatadogDataConstraints;->validateTags(Ljava/util/List;)Ljava/util/List; +Lcom/datadog/android/core/constraints/DatadogDataConstraints$$ExternalSyntheticLambda0; +HSPLcom/datadog/android/core/constraints/DatadogDataConstraints$$ExternalSyntheticLambda0;->()V +HSPLcom/datadog/android/core/constraints/DatadogDataConstraints$$ExternalSyntheticLambda0;->invoke(Ljava/lang/Object;)Ljava/lang/Object; +Lcom/datadog/android/core/constraints/DatadogDataConstraints$$ExternalSyntheticLambda11; +HSPLcom/datadog/android/core/constraints/DatadogDataConstraints$$ExternalSyntheticLambda11;->(Ljava/lang/String;Ljava/lang/String;)V +HSPLcom/datadog/android/core/constraints/DatadogDataConstraints$$ExternalSyntheticLambda11;->invoke()Ljava/lang/Object; +Lcom/datadog/android/core/constraints/DatadogDataConstraints$$ExternalSyntheticLambda5; +HSPLcom/datadog/android/core/constraints/DatadogDataConstraints$$ExternalSyntheticLambda5;->()V +HSPLcom/datadog/android/core/constraints/DatadogDataConstraints$$ExternalSyntheticLambda5;->invoke(Ljava/lang/Object;)Ljava/lang/Object; +Lcom/datadog/android/core/constraints/DatadogDataConstraints$$ExternalSyntheticLambda6; +HSPLcom/datadog/android/core/constraints/DatadogDataConstraints$$ExternalSyntheticLambda6;->()V +HSPLcom/datadog/android/core/constraints/DatadogDataConstraints$$ExternalSyntheticLambda6;->invoke(Ljava/lang/Object;)Ljava/lang/Object; +Lcom/datadog/android/core/constraints/DatadogDataConstraints$$ExternalSyntheticLambda7; +HSPLcom/datadog/android/core/constraints/DatadogDataConstraints$$ExternalSyntheticLambda7;->()V +HSPLcom/datadog/android/core/constraints/DatadogDataConstraints$$ExternalSyntheticLambda7;->invoke(Ljava/lang/Object;)Ljava/lang/Object; +Lcom/datadog/android/core/constraints/DatadogDataConstraints$$ExternalSyntheticLambda8; +HSPLcom/datadog/android/core/constraints/DatadogDataConstraints$$ExternalSyntheticLambda8;->()V +HSPLcom/datadog/android/core/constraints/DatadogDataConstraints$$ExternalSyntheticLambda8;->invoke(Ljava/lang/Object;)Ljava/lang/Object; +Lcom/datadog/android/core/constraints/DatadogDataConstraints$$ExternalSyntheticLambda9; +HSPLcom/datadog/android/core/constraints/DatadogDataConstraints$$ExternalSyntheticLambda9;->(Lcom/datadog/android/core/constraints/DatadogDataConstraints;)V +HSPLcom/datadog/android/core/constraints/DatadogDataConstraints$$ExternalSyntheticLambda9;->invoke(Ljava/lang/Object;)Ljava/lang/Object; Lcom/datadog/android/core/constraints/DatadogDataConstraints$Companion; HSPLcom/datadog/android/core/constraints/DatadogDataConstraints$Companion;->()V HSPLcom/datadog/android/core/constraints/DatadogDataConstraints$Companion;->(Lkotlin/jvm/internal/DefaultConstructorMarker;)V -Lcom/datadog/android/core/constraints/DatadogDataConstraints$tagTransforms$1; -HSPLcom/datadog/android/core/constraints/DatadogDataConstraints$tagTransforms$1;->()V -HSPLcom/datadog/android/core/constraints/DatadogDataConstraints$tagTransforms$1;->()V -HSPLcom/datadog/android/core/constraints/DatadogDataConstraints$tagTransforms$1;->invoke(Ljava/lang/Object;)Ljava/lang/Object; -HSPLcom/datadog/android/core/constraints/DatadogDataConstraints$tagTransforms$1;->invoke(Ljava/lang/String;)Ljava/lang/String; -Lcom/datadog/android/core/constraints/DatadogDataConstraints$tagTransforms$2; -HSPLcom/datadog/android/core/constraints/DatadogDataConstraints$tagTransforms$2;->()V -HSPLcom/datadog/android/core/constraints/DatadogDataConstraints$tagTransforms$2;->()V -HSPLcom/datadog/android/core/constraints/DatadogDataConstraints$tagTransforms$2;->invoke(Ljava/lang/Object;)Ljava/lang/Object; -HSPLcom/datadog/android/core/constraints/DatadogDataConstraints$tagTransforms$2;->invoke(Ljava/lang/String;)Ljava/lang/String; -Lcom/datadog/android/core/constraints/DatadogDataConstraints$tagTransforms$3; -HSPLcom/datadog/android/core/constraints/DatadogDataConstraints$tagTransforms$3;->()V -HSPLcom/datadog/android/core/constraints/DatadogDataConstraints$tagTransforms$3;->()V -HSPLcom/datadog/android/core/constraints/DatadogDataConstraints$tagTransforms$3;->invoke(Ljava/lang/Object;)Ljava/lang/Object; -HSPLcom/datadog/android/core/constraints/DatadogDataConstraints$tagTransforms$3;->invoke(Ljava/lang/String;)Ljava/lang/String; -Lcom/datadog/android/core/constraints/DatadogDataConstraints$tagTransforms$4; -HSPLcom/datadog/android/core/constraints/DatadogDataConstraints$tagTransforms$4;->()V -HSPLcom/datadog/android/core/constraints/DatadogDataConstraints$tagTransforms$4;->()V -HSPLcom/datadog/android/core/constraints/DatadogDataConstraints$tagTransforms$4;->invoke(Ljava/lang/Object;)Ljava/lang/Object; -HSPLcom/datadog/android/core/constraints/DatadogDataConstraints$tagTransforms$4;->invoke(Ljava/lang/String;)Ljava/lang/String; -Lcom/datadog/android/core/constraints/DatadogDataConstraints$tagTransforms$5; -HSPLcom/datadog/android/core/constraints/DatadogDataConstraints$tagTransforms$5;->()V -HSPLcom/datadog/android/core/constraints/DatadogDataConstraints$tagTransforms$5;->()V -HSPLcom/datadog/android/core/constraints/DatadogDataConstraints$tagTransforms$5;->invoke(Ljava/lang/Object;)Ljava/lang/Object; -HSPLcom/datadog/android/core/constraints/DatadogDataConstraints$tagTransforms$5;->invoke(Ljava/lang/String;)Ljava/lang/String; -Lcom/datadog/android/core/constraints/DatadogDataConstraints$tagTransforms$6; -HSPLcom/datadog/android/core/constraints/DatadogDataConstraints$tagTransforms$6;->(Lcom/datadog/android/core/constraints/DatadogDataConstraints;)V -HSPLcom/datadog/android/core/constraints/DatadogDataConstraints$tagTransforms$6;->invoke(Ljava/lang/Object;)Ljava/lang/Object; -HSPLcom/datadog/android/core/constraints/DatadogDataConstraints$tagTransforms$6;->invoke(Ljava/lang/String;)Ljava/lang/String; -Lcom/datadog/android/core/constraints/DatadogDataConstraints$validateTags$convertedTags$1$2; -HSPLcom/datadog/android/core/constraints/DatadogDataConstraints$validateTags$convertedTags$1$2;->(Ljava/lang/String;Ljava/lang/String;)V -HSPLcom/datadog/android/core/constraints/DatadogDataConstraints$validateTags$convertedTags$1$2;->invoke()Ljava/lang/Object; -HSPLcom/datadog/android/core/constraints/DatadogDataConstraints$validateTags$convertedTags$1$2;->invoke()Ljava/lang/String; Lcom/datadog/android/core/feature/event/JvmCrash; Lcom/datadog/android/core/feature/event/JvmCrash$Rum; Lcom/datadog/android/core/internal/ContextProvider; @@ -18751,13 +18843,9 @@ Lcom/datadog/android/core/internal/CoreFeature; HSPLcom/datadog/android/core/internal/CoreFeature;->()V HSPLcom/datadog/android/core/internal/CoreFeature;->(Lcom/datadog/android/api/InternalLogger;Lcom/datadog/android/core/internal/time/AppStartTimeProvider;Lcom/datadog/android/core/thread/FlushableExecutorService$Factory;Lcom/datadog/android/core/internal/thread/ScheduledExecutorServiceFactory;Lcom/datadog/android/internal/system/BuildSdkVersionProvider;)V HSPLcom/datadog/android/core/internal/CoreFeature;->(Lcom/datadog/android/api/InternalLogger;Lcom/datadog/android/core/internal/time/AppStartTimeProvider;Lcom/datadog/android/core/thread/FlushableExecutorService$Factory;Lcom/datadog/android/core/internal/thread/ScheduledExecutorServiceFactory;Lcom/datadog/android/internal/system/BuildSdkVersionProvider;ILkotlin/jvm/internal/DefaultConstructorMarker;)V -HSPLcom/datadog/android/core/internal/CoreFeature;->DEFAULT_FLUSHABLE_EXECUTOR_SERVICE_FACTORY$lambda$11(Lcom/datadog/android/api/InternalLogger;Ljava/lang/String;Lcom/datadog/android/core/configuration/BackPressureStrategy;Lcom/datadog/android/internal/time/TimeProvider;)Lcom/datadog/android/core/thread/FlushableExecutorService; +HSPLcom/datadog/android/core/internal/CoreFeature;->DEFAULT_FLUSHABLE_EXECUTOR_SERVICE_FACTORY$lambda$28(Lcom/datadog/android/api/InternalLogger;Ljava/lang/String;Lcom/datadog/android/core/configuration/BackPressureStrategy;Lcom/datadog/android/internal/time/TimeProvider;)Lcom/datadog/android/core/thread/FlushableExecutorService; HSPLcom/datadog/android/core/internal/CoreFeature;->access$getDEFAULT_FLUSHABLE_EXECUTOR_SERVICE_FACTORY$cp()Lcom/datadog/android/core/thread/FlushableExecutorService$Factory; HSPLcom/datadog/android/core/internal/CoreFeature;->access$getDEFAULT_SCHEDULED_EXECUTOR_SERVICE_FACTORY$cp()Lcom/datadog/android/core/internal/thread/ScheduledExecutorServiceFactory; -HSPLcom/datadog/android/core/internal/CoreFeature;->access$getInternalLogger$p(Lcom/datadog/android/core/internal/CoreFeature;)Lcom/datadog/android/api/InternalLogger; -HSPLcom/datadog/android/core/internal/CoreFeature;->access$getLazySharedOkHttpClient(Lcom/datadog/android/core/internal/CoreFeature;)Lokhttp3/OkHttpClient; -HSPLcom/datadog/android/core/internal/CoreFeature;->access$getNETWORK_TIMEOUT_MS$cp()J -HSPLcom/datadog/android/core/internal/CoreFeature;->access$getRESTRICTED_CIPHER_SUITES$cp()[Lokhttp3/CipherSuite; HSPLcom/datadog/android/core/internal/CoreFeature;->buildFilePersistenceConfig()Lcom/datadog/android/core/internal/persistence/file/FilePersistenceConfig; HSPLcom/datadog/android/core/internal/CoreFeature;->createExecutorService(Ljava/lang/String;)Ljava/util/concurrent/ExecutorService; HSPLcom/datadog/android/core/internal/CoreFeature;->getAccountInfoProvider$dd_sdk_android_core_release()Lcom/datadog/android/core/internal/account/MutableAccountInfoProvider; @@ -18786,6 +18874,7 @@ HSPLcom/datadog/android/core/internal/CoreFeature;->getPackageInfo(Landroid/cont HSPLcom/datadog/android/core/internal/CoreFeature;->getPackageVersionProvider$dd_sdk_android_core_release()Lcom/datadog/android/core/internal/system/AppVersionProvider; HSPLcom/datadog/android/core/internal/CoreFeature;->getPersistenceExecutorService$dd_sdk_android_core_release()Lcom/datadog/android/core/thread/FlushableExecutorService; HSPLcom/datadog/android/core/internal/CoreFeature;->getPersistenceStrategyFactory$dd_sdk_android_core_release()Lcom/datadog/android/core/persistence/PersistenceStrategy$Factory; +HSPLcom/datadog/android/core/internal/CoreFeature;->getRemoteConfigurationId$dd_sdk_android_core_release()Ljava/lang/String; HSPLcom/datadog/android/core/internal/CoreFeature;->getSafeContext(Landroid/content/Context;)Landroid/content/Context; HSPLcom/datadog/android/core/internal/CoreFeature;->getSdkVersion$dd_sdk_android_core_release()Ljava/lang/String; HSPLcom/datadog/android/core/internal/CoreFeature;->getServiceName$dd_sdk_android_core_release()Ljava/lang/String; @@ -18799,10 +18888,14 @@ HSPLcom/datadog/android/core/internal/CoreFeature;->getUploadExecutorService$dd_ HSPLcom/datadog/android/core/internal/CoreFeature;->getUploadFrequency$dd_sdk_android_core_release()Lcom/datadog/android/core/configuration/UploadFrequency; HSPLcom/datadog/android/core/internal/CoreFeature;->getUserInfoProvider$dd_sdk_android_core_release()Lcom/datadog/android/core/internal/user/MutableUserInfoProvider; HSPLcom/datadog/android/core/internal/CoreFeature;->getVariant$dd_sdk_android_core_release()Ljava/lang/String; -HSPLcom/datadog/android/core/internal/CoreFeature;->initialize$lambda$0(Lcom/datadog/android/core/internal/CoreFeature;Landroid/content/Context;)V +HSPLcom/datadog/android/core/internal/CoreFeature;->initialize$lambda$4(Lcom/datadog/android/core/internal/CoreFeature;Landroid/content/Context;)V +HSPLcom/datadog/android/core/internal/CoreFeature;->initialize$lambda$5(Landroid/content/Context;Ljava/lang/String;)Ljava/io/File; HSPLcom/datadog/android/core/internal/CoreFeature;->initialize(Landroid/content/Context;Ljava/lang/String;Lcom/datadog/android/core/configuration/Configuration;Lcom/datadog/android/privacy/TrackingConsent;)V HSPLcom/datadog/android/core/internal/CoreFeature;->initializeClockSync(Landroid/content/Context;)V HSPLcom/datadog/android/core/internal/CoreFeature;->isMainProcess$dd_sdk_android_core_release()Z +HSPLcom/datadog/android/core/internal/CoreFeature;->lastViewEventFileWriter_delegate$lambda$3(Lcom/datadog/android/core/internal/CoreFeature;)Lcom/datadog/android/core/internal/persistence/file/batch/BatchFileReaderWriter; +HSPLcom/datadog/android/core/internal/CoreFeature;->lastViewEventFile_delegate$lambda$2(Lcom/datadog/android/core/internal/CoreFeature;)Ljava/io/File; +HSPLcom/datadog/android/core/internal/CoreFeature;->lazySharedOkHttpClient_delegate$lambda$0(Lcom/datadog/android/core/internal/CoreFeature;)Lokhttp3/OkHttpClient; HSPLcom/datadog/android/core/internal/CoreFeature;->prepareNdkCrashData(Ljava/lang/String;)V HSPLcom/datadog/android/core/internal/CoreFeature;->readApplicationInformation(Landroid/content/Context;Lcom/datadog/android/core/configuration/Configuration;)V HSPLcom/datadog/android/core/internal/CoreFeature;->readBuildId(Landroid/content/Context;)Ljava/lang/String; @@ -18818,142 +18911,137 @@ HSPLcom/datadog/android/core/internal/CoreFeature;->setUploadExecutorService$dd_ HSPLcom/datadog/android/core/internal/CoreFeature;->setupExecutors()V HSPLcom/datadog/android/core/internal/CoreFeature;->setupInfoProviders(Landroid/content/Context;Lcom/datadog/android/privacy/TrackingConsent;)V HSPLcom/datadog/android/core/internal/CoreFeature;->setupNetworkInfoProviders(Landroid/content/Context;Landroid/os/Handler;)V +HSPLcom/datadog/android/core/internal/CoreFeature;->setupOkHttpClient$lambda$22(Lcom/datadog/android/core/internal/CoreFeature;Lcom/datadog/android/core/configuration/Configuration$Core;)Lokhttp3/OkHttpClient; HSPLcom/datadog/android/core/internal/CoreFeature;->setupOkHttpClient(Lcom/datadog/android/core/configuration/Configuration$Core;)V HSPLcom/datadog/android/core/internal/CoreFeature;->writeLastViewEvent$dd_sdk_android_core_release([B)V -Lcom/datadog/android/core/internal/CoreFeature$$ExternalSyntheticLambda0; -HSPLcom/datadog/android/core/internal/CoreFeature$$ExternalSyntheticLambda0;->(Lcom/datadog/android/core/internal/CoreFeature;Landroid/content/Context;)V -HSPLcom/datadog/android/core/internal/CoreFeature$$ExternalSyntheticLambda0;->run()V -Lcom/datadog/android/core/internal/CoreFeature$$ExternalSyntheticLambda1; -HSPLcom/datadog/android/core/internal/CoreFeature$$ExternalSyntheticLambda1;->()V -HSPLcom/datadog/android/core/internal/CoreFeature$$ExternalSyntheticLambda1;->create(Lcom/datadog/android/api/InternalLogger;Ljava/lang/String;Lcom/datadog/android/core/configuration/BackPressureStrategy;Lcom/datadog/android/internal/time/TimeProvider;)Lcom/datadog/android/core/thread/FlushableExecutorService; -Lcom/datadog/android/core/internal/CoreFeature$$ExternalSyntheticLambda2; -HSPLcom/datadog/android/core/internal/CoreFeature$$ExternalSyntheticLambda2;->()V +Lcom/datadog/android/core/internal/CoreFeature$$ExternalSyntheticLambda10; +HSPLcom/datadog/android/core/internal/CoreFeature$$ExternalSyntheticLambda10;->()V +Lcom/datadog/android/core/internal/CoreFeature$$ExternalSyntheticLambda13; +HSPLcom/datadog/android/core/internal/CoreFeature$$ExternalSyntheticLambda13;->(Lcom/datadog/android/core/internal/CoreFeature;)V +Lcom/datadog/android/core/internal/CoreFeature$$ExternalSyntheticLambda14; +HSPLcom/datadog/android/core/internal/CoreFeature$$ExternalSyntheticLambda14;->(Lcom/datadog/android/core/internal/CoreFeature;)V +HSPLcom/datadog/android/core/internal/CoreFeature$$ExternalSyntheticLambda14;->invoke()Ljava/lang/Object; +Lcom/datadog/android/core/internal/CoreFeature$$ExternalSyntheticLambda15; +HSPLcom/datadog/android/core/internal/CoreFeature$$ExternalSyntheticLambda15;->(Lcom/datadog/android/core/internal/CoreFeature;)V +Lcom/datadog/android/core/internal/CoreFeature$$ExternalSyntheticLambda16; +HSPLcom/datadog/android/core/internal/CoreFeature$$ExternalSyntheticLambda16;->(Lcom/datadog/android/core/internal/CoreFeature;)V +HSPLcom/datadog/android/core/internal/CoreFeature$$ExternalSyntheticLambda16;->invoke()Ljava/lang/Object; +Lcom/datadog/android/core/internal/CoreFeature$$ExternalSyntheticLambda17; +HSPLcom/datadog/android/core/internal/CoreFeature$$ExternalSyntheticLambda17;->(Lcom/datadog/android/core/internal/CoreFeature;)V +HSPLcom/datadog/android/core/internal/CoreFeature$$ExternalSyntheticLambda17;->invoke()Ljava/lang/Object; +Lcom/datadog/android/core/internal/CoreFeature$$ExternalSyntheticLambda19; +HSPLcom/datadog/android/core/internal/CoreFeature$$ExternalSyntheticLambda19;->()V +Lcom/datadog/android/core/internal/CoreFeature$$ExternalSyntheticLambda4; +HSPLcom/datadog/android/core/internal/CoreFeature$$ExternalSyntheticLambda4;->(Lcom/datadog/android/core/internal/CoreFeature;Landroid/content/Context;)V +HSPLcom/datadog/android/core/internal/CoreFeature$$ExternalSyntheticLambda4;->run()V +Lcom/datadog/android/core/internal/CoreFeature$$ExternalSyntheticLambda5; +HSPLcom/datadog/android/core/internal/CoreFeature$$ExternalSyntheticLambda5;->(Landroid/content/Context;Ljava/lang/String;)V +HSPLcom/datadog/android/core/internal/CoreFeature$$ExternalSyntheticLambda5;->invoke()Ljava/lang/Object; +Lcom/datadog/android/core/internal/CoreFeature$$ExternalSyntheticLambda6; +HSPLcom/datadog/android/core/internal/CoreFeature$$ExternalSyntheticLambda6;->(Lcom/datadog/android/core/internal/CoreFeature;Lcom/datadog/android/core/configuration/Configuration$Core;)V +HSPLcom/datadog/android/core/internal/CoreFeature$$ExternalSyntheticLambda6;->invoke()Ljava/lang/Object; +Lcom/datadog/android/core/internal/CoreFeature$$ExternalSyntheticLambda7; +HSPLcom/datadog/android/core/internal/CoreFeature$$ExternalSyntheticLambda7;->()V +Lcom/datadog/android/core/internal/CoreFeature$$ExternalSyntheticLambda8; +HSPLcom/datadog/android/core/internal/CoreFeature$$ExternalSyntheticLambda8;->()V +Lcom/datadog/android/core/internal/CoreFeature$$ExternalSyntheticLambda9; +HSPLcom/datadog/android/core/internal/CoreFeature$$ExternalSyntheticLambda9;->()V +HSPLcom/datadog/android/core/internal/CoreFeature$$ExternalSyntheticLambda9;->create(Lcom/datadog/android/api/InternalLogger;Ljava/lang/String;Lcom/datadog/android/core/configuration/BackPressureStrategy;Lcom/datadog/android/internal/time/TimeProvider;)Lcom/datadog/android/core/thread/FlushableExecutorService; Lcom/datadog/android/core/internal/CoreFeature$Companion; HSPLcom/datadog/android/core/internal/CoreFeature$Companion;->()V HSPLcom/datadog/android/core/internal/CoreFeature$Companion;->(Lkotlin/jvm/internal/DefaultConstructorMarker;)V HSPLcom/datadog/android/core/internal/CoreFeature$Companion;->getDEFAULT_FLUSHABLE_EXECUTOR_SERVICE_FACTORY$dd_sdk_android_core_release()Lcom/datadog/android/core/thread/FlushableExecutorService$Factory; HSPLcom/datadog/android/core/internal/CoreFeature$Companion;->getDEFAULT_SCHEDULED_EXECUTOR_SERVICE_FACTORY$dd_sdk_android_core_release()Lcom/datadog/android/core/internal/thread/ScheduledExecutorServiceFactory; -HSPLcom/datadog/android/core/internal/CoreFeature$Companion;->getNETWORK_TIMEOUT_MS$dd_sdk_android_core_release()J -HSPLcom/datadog/android/core/internal/CoreFeature$Companion;->getRESTRICTED_CIPHER_SUITES$dd_sdk_android_core_release()[Lokhttp3/CipherSuite; Lcom/datadog/android/core/internal/CoreFeature$OkHttpCallFactory; HSPLcom/datadog/android/core/internal/CoreFeature$OkHttpCallFactory;->(Lkotlin/jvm/functions/Function0;)V HSPLcom/datadog/android/core/internal/CoreFeature$OkHttpCallFactory;->getOkhttpClient()Lokhttp3/OkHttpClient; HSPLcom/datadog/android/core/internal/CoreFeature$OkHttpCallFactory;->newCall(Lokhttp3/Request;)Lokhttp3/Call; -Lcom/datadog/android/core/internal/CoreFeature$initialize$2; -HSPLcom/datadog/android/core/internal/CoreFeature$initialize$2;->(Landroid/content/Context;Ljava/lang/String;)V -HSPLcom/datadog/android/core/internal/CoreFeature$initialize$2;->invoke()Ljava/io/File; -HSPLcom/datadog/android/core/internal/CoreFeature$initialize$2;->invoke()Ljava/lang/Object; -Lcom/datadog/android/core/internal/CoreFeature$lastViewEvent$2; -HSPLcom/datadog/android/core/internal/CoreFeature$lastViewEvent$2;->(Lcom/datadog/android/core/internal/CoreFeature;)V -Lcom/datadog/android/core/internal/CoreFeature$lastViewEventFile$2; -HSPLcom/datadog/android/core/internal/CoreFeature$lastViewEventFile$2;->(Lcom/datadog/android/core/internal/CoreFeature;)V -HSPLcom/datadog/android/core/internal/CoreFeature$lastViewEventFile$2;->invoke()Ljava/io/File; -HSPLcom/datadog/android/core/internal/CoreFeature$lastViewEventFile$2;->invoke()Ljava/lang/Object; -Lcom/datadog/android/core/internal/CoreFeature$lastViewEventFileWriter$2; -HSPLcom/datadog/android/core/internal/CoreFeature$lastViewEventFileWriter$2;->(Lcom/datadog/android/core/internal/CoreFeature;)V -HSPLcom/datadog/android/core/internal/CoreFeature$lastViewEventFileWriter$2;->invoke()Lcom/datadog/android/core/internal/persistence/file/batch/BatchFileReaderWriter; -HSPLcom/datadog/android/core/internal/CoreFeature$lastViewEventFileWriter$2;->invoke()Ljava/lang/Object; -Lcom/datadog/android/core/internal/CoreFeature$lazySharedOkHttpClient$2; -HSPLcom/datadog/android/core/internal/CoreFeature$lazySharedOkHttpClient$2;->(Lcom/datadog/android/core/internal/CoreFeature;)V -HSPLcom/datadog/android/core/internal/CoreFeature$lazySharedOkHttpClient$2;->invoke()Ljava/lang/Object; -HSPLcom/datadog/android/core/internal/CoreFeature$lazySharedOkHttpClient$2;->invoke()Lokhttp3/OkHttpClient; -Lcom/datadog/android/core/internal/CoreFeature$prepareNdkCrashData$1; -HSPLcom/datadog/android/core/internal/CoreFeature$prepareNdkCrashData$1;->(Lcom/datadog/android/core/internal/CoreFeature;)V -Lcom/datadog/android/core/internal/CoreFeature$readBuildId$1$2; -HSPLcom/datadog/android/core/internal/CoreFeature$readBuildId$1$2;->()V -HSPLcom/datadog/android/core/internal/CoreFeature$readBuildId$1$2;->()V -Lcom/datadog/android/core/internal/CoreFeature$setupExecutors$contextQueue$1; -HSPLcom/datadog/android/core/internal/CoreFeature$setupExecutors$contextQueue$1;->()V -HSPLcom/datadog/android/core/internal/CoreFeature$setupExecutors$contextQueue$1;->()V -Lcom/datadog/android/core/internal/CoreFeature$setupExecutors$contextQueue$2; -HSPLcom/datadog/android/core/internal/CoreFeature$setupExecutors$contextQueue$2;->()V -HSPLcom/datadog/android/core/internal/CoreFeature$setupExecutors$contextQueue$2;->()V -Lcom/datadog/android/core/internal/CoreFeature$setupOkHttpClient$1; -HSPLcom/datadog/android/core/internal/CoreFeature$setupOkHttpClient$1;->(Lcom/datadog/android/core/internal/CoreFeature;Lcom/datadog/android/core/configuration/Configuration$Core;)V -HSPLcom/datadog/android/core/internal/CoreFeature$setupOkHttpClient$1;->invoke()Ljava/lang/Object; -HSPLcom/datadog/android/core/internal/CoreFeature$setupOkHttpClient$1;->invoke()Lokhttp3/OkHttpClient; Lcom/datadog/android/core/internal/DatadogContextProvider; HSPLcom/datadog/android/core/internal/DatadogContextProvider;->(Lcom/datadog/android/core/internal/CoreFeature;Lcom/datadog/android/core/internal/FeatureContextProvider;)V HSPLcom/datadog/android/core/internal/DatadogContextProvider;->getContext(Ljava/util/Set;)Lcom/datadog/android/api/context/DatadogContext; Lcom/datadog/android/core/internal/DatadogCore; HSPLcom/datadog/android/core/internal/DatadogCore;->()V -HSPLcom/datadog/android/core/internal/DatadogCore;->(Landroid/content/Context;Ljava/lang/String;Ljava/lang/String;Lkotlin/jvm/functions/Function1;Lcom/datadog/android/core/thread/FlushableExecutorService$Factory;Lcom/datadog/android/internal/system/BuildSdkVersionProvider;)V -HSPLcom/datadog/android/core/internal/DatadogCore;->(Landroid/content/Context;Ljava/lang/String;Ljava/lang/String;Lkotlin/jvm/functions/Function1;Lcom/datadog/android/core/thread/FlushableExecutorService$Factory;Lcom/datadog/android/internal/system/BuildSdkVersionProvider;ILkotlin/jvm/internal/DefaultConstructorMarker;)V +HSPLcom/datadog/android/core/internal/DatadogCore;->(Landroid/content/Context;Ljava/lang/String;Ljava/lang/String;Lkotlin/jvm/functions/Function1;Lcom/datadog/android/core/thread/FlushableExecutorService$Factory;Lcom/datadog/android/internal/system/BuildSdkVersionProvider;Lcom/datadog/android/core/internal/remote/RemoteConfigService$Factory;)V +HSPLcom/datadog/android/core/internal/DatadogCore;->(Landroid/content/Context;Ljava/lang/String;Ljava/lang/String;Lkotlin/jvm/functions/Function1;Lcom/datadog/android/core/thread/FlushableExecutorService$Factory;Lcom/datadog/android/internal/system/BuildSdkVersionProvider;Lcom/datadog/android/core/internal/remote/RemoteConfigService$Factory;ILkotlin/jvm/internal/DefaultConstructorMarker;)V +HSPLcom/datadog/android/core/internal/DatadogCore;->_init_$lambda$0(Lcom/datadog/android/api/feature/FeatureSdkCore;)Lcom/datadog/android/core/internal/logger/SdkInternalLogger; HSPLcom/datadog/android/core/internal/DatadogCore;->applyAdditionalConfiguration(Ljava/util/Map;)V HSPLcom/datadog/android/core/internal/DatadogCore;->createSingleThreadExecutorService(Ljava/lang/String;)Ljava/util/concurrent/ExecutorService; HSPLcom/datadog/android/core/internal/DatadogCore;->getAppStartTimeNs()J HSPLcom/datadog/android/core/internal/DatadogCore;->getCoreFeature$dd_sdk_android_core_release()Lcom/datadog/android/core/internal/CoreFeature; HSPLcom/datadog/android/core/internal/DatadogCore;->getFeature(Ljava/lang/String;)Lcom/datadog/android/api/feature/FeatureScope; -HSPLcom/datadog/android/core/internal/DatadogCore;->getFeatureContext$lambda$11(Lcom/datadog/android/core/internal/DatadogCore;Ljava/lang/String;)Ljava/util/Map; +HSPLcom/datadog/android/core/internal/DatadogCore;->getFeatureContext$lambda$16$lambda$15(Lcom/datadog/android/core/internal/SdkFeature;)Ljava/util/HashMap; +HSPLcom/datadog/android/core/internal/DatadogCore;->getFeatureContext$lambda$16(Lcom/datadog/android/core/internal/DatadogCore;Ljava/lang/String;)Ljava/util/Map; HSPLcom/datadog/android/core/internal/DatadogCore;->getFeatureContext(Ljava/lang/String;Z)Ljava/util/Map; -HSPLcom/datadog/android/core/internal/DatadogCore;->getFeatureContextUpdateReceivers$dd_sdk_android_core_release()Ljava/util/Set; -HSPLcom/datadog/android/core/internal/DatadogCore;->getFeatures$dd_sdk_android_core_release()Ljava/util/Map; HSPLcom/datadog/android/core/internal/DatadogCore;->getFirstPartyHostResolver()Lcom/datadog/android/core/internal/net/FirstPartyHostHeaderTypeResolver; HSPLcom/datadog/android/core/internal/DatadogCore;->getInternalLogger()Lcom/datadog/android/api/InternalLogger; HSPLcom/datadog/android/core/internal/DatadogCore;->getName()Ljava/lang/String; +HSPLcom/datadog/android/core/internal/DatadogCore;->getRemoteConfiguration()Lcom/datadog/android/core/internal/remote/model/RemoteConfiguration; HSPLcom/datadog/android/core/internal/DatadogCore;->getService()Ljava/lang/String; HSPLcom/datadog/android/core/internal/DatadogCore;->getTime()Lcom/datadog/android/api/context/TimeInfo; HSPLcom/datadog/android/core/internal/DatadogCore;->getTimeProvider()Lcom/datadog/android/internal/time/TimeProvider; HSPLcom/datadog/android/core/internal/DatadogCore;->initialize$dd_sdk_android_core_release(Lcom/datadog/android/core/configuration/Configuration;)V -HSPLcom/datadog/android/core/internal/DatadogCore;->initialize$lambda$17(Lcom/datadog/android/core/internal/DatadogCore;Ljava/lang/String;)Ljava/util/Map; +HSPLcom/datadog/android/core/internal/DatadogCore;->initialize$lambda$26(Lcom/datadog/android/core/internal/DatadogCore;)Lcom/datadog/android/internal/time/TimeProvider; +HSPLcom/datadog/android/core/internal/DatadogCore;->initialize$lambda$27(Lcom/datadog/android/core/internal/DatadogCore;Ljava/lang/String;)Ljava/util/Map; HSPLcom/datadog/android/core/internal/DatadogCore;->initializeCrashReportFeature()V HSPLcom/datadog/android/core/internal/DatadogCore;->isAppDebuggable(Landroid/content/Context;)Z HSPLcom/datadog/android/core/internal/DatadogCore;->isDeveloperModeEnabled()Z HSPLcom/datadog/android/core/internal/DatadogCore;->isEnvironmentNameValid(Ljava/lang/String;)Z HSPLcom/datadog/android/core/internal/DatadogCore;->registerFeature(Lcom/datadog/android/api/feature/Feature;)V -PLcom/datadog/android/core/internal/DatadogCore;->sendCoreConfigurationTelemetryEvent$lambda$23(Lcom/datadog/android/core/internal/DatadogCore;Lcom/datadog/android/core/configuration/Configuration;)V +PLcom/datadog/android/core/internal/DatadogCore;->sendCoreConfigurationTelemetryEvent$lambda$41(Lcom/datadog/android/core/internal/DatadogCore;Lcom/datadog/android/core/configuration/Configuration;)V HSPLcom/datadog/android/core/internal/DatadogCore;->sendCoreConfigurationTelemetryEvent(Lcom/datadog/android/core/configuration/Configuration;)V -HSPLcom/datadog/android/core/internal/DatadogCore;->setAnonymousId$lambda$13(Lcom/datadog/android/core/internal/DatadogCore;Ljava/util/UUID;)V +HSPLcom/datadog/android/core/internal/DatadogCore;->setAnonymousId$lambda$21(Lcom/datadog/android/core/internal/DatadogCore;Ljava/util/UUID;)V HSPLcom/datadog/android/core/internal/DatadogCore;->setAnonymousId(Ljava/util/UUID;)V HSPLcom/datadog/android/core/internal/DatadogCore;->setCoreFeature$dd_sdk_android_core_release(Lcom/datadog/android/core/internal/CoreFeature;)V HSPLcom/datadog/android/core/internal/DatadogCore;->setEventReceiver(Ljava/lang/String;Lcom/datadog/android/api/feature/FeatureEventReceiver;)V -HSPLcom/datadog/android/core/internal/DatadogCore;->setTrackingConsent$lambda$0(Lcom/datadog/android/core/internal/DatadogCore;Lcom/datadog/android/privacy/TrackingConsent;)V +HSPLcom/datadog/android/core/internal/DatadogCore;->setTrackingConsent$lambda$1(Lcom/datadog/android/core/internal/DatadogCore;Lcom/datadog/android/privacy/TrackingConsent;)V HSPLcom/datadog/android/core/internal/DatadogCore;->setTrackingConsent(Lcom/datadog/android/privacy/TrackingConsent;)V HSPLcom/datadog/android/core/internal/DatadogCore;->setupLifecycleMonitorCallback(Landroid/content/Context;)V +HSPLcom/datadog/android/core/internal/DatadogCore;->setupRemoteConfiguration$dd_sdk_android_core_release(Lcom/datadog/android/core/configuration/Configuration;)V HSPLcom/datadog/android/core/internal/DatadogCore;->setupShutdownHook()V +HSPLcom/datadog/android/core/internal/DatadogCore;->updateFeatureContext$lambda$13$lambda$12(Lcom/datadog/android/core/internal/SdkFeature;Lkotlin/jvm/functions/Function1;Lcom/datadog/android/core/internal/DatadogCore;Ljava/lang/String;)Lkotlin/Unit; +HSPLcom/datadog/android/core/internal/DatadogCore;->updateFeatureContext$lambda$13(Lcom/datadog/android/core/internal/DatadogCore;Ljava/lang/String;Lkotlin/jvm/functions/Function1;)Lkotlin/Unit; +HSPLcom/datadog/android/core/internal/DatadogCore;->updateFeatureContext$lambda$14(Lkotlin/jvm/functions/Function0;)V HSPLcom/datadog/android/core/internal/DatadogCore;->updateFeatureContext(Ljava/lang/String;ZLkotlin/jvm/functions/Function1;)V HSPLcom/datadog/android/core/internal/DatadogCore;->writeLastViewEvent([B)V -Lcom/datadog/android/core/internal/DatadogCore$$ExternalSyntheticLambda10; -HSPLcom/datadog/android/core/internal/DatadogCore$$ExternalSyntheticLambda10;->(Lcom/datadog/android/core/internal/DatadogCore;)V -HSPLcom/datadog/android/core/internal/DatadogCore$$ExternalSyntheticLambda10;->getFeatureContext(Ljava/lang/String;)Ljava/util/Map; -Lcom/datadog/android/core/internal/DatadogCore$$ExternalSyntheticLambda11; -HSPLcom/datadog/android/core/internal/DatadogCore$$ExternalSyntheticLambda11;->(Lcom/datadog/android/core/internal/DatadogCore;Ljava/lang/String;)V -HSPLcom/datadog/android/core/internal/DatadogCore$$ExternalSyntheticLambda11;->call()Ljava/lang/Object; +Lcom/datadog/android/core/internal/DatadogCore$$ExternalSyntheticLambda0; +HSPLcom/datadog/android/core/internal/DatadogCore$$ExternalSyntheticLambda0;->()V +HSPLcom/datadog/android/core/internal/DatadogCore$$ExternalSyntheticLambda0;->invoke(Ljava/lang/Object;)Ljava/lang/Object; +Lcom/datadog/android/core/internal/DatadogCore$$ExternalSyntheticLambda1; +HSPLcom/datadog/android/core/internal/DatadogCore$$ExternalSyntheticLambda1;->(Lcom/datadog/android/core/internal/DatadogCore;Ljava/lang/String;)V +HSPLcom/datadog/android/core/internal/DatadogCore$$ExternalSyntheticLambda1;->call()Ljava/lang/Object; Lcom/datadog/android/core/internal/DatadogCore$$ExternalSyntheticLambda12; -HSPLcom/datadog/android/core/internal/DatadogCore$$ExternalSyntheticLambda12;->(Lcom/datadog/android/core/internal/DatadogCore;Lcom/datadog/android/core/configuration/Configuration;)V -PLcom/datadog/android/core/internal/DatadogCore$$ExternalSyntheticLambda12;->run()V -Lcom/datadog/android/core/internal/DatadogCore$$ExternalSyntheticLambda15; -HSPLcom/datadog/android/core/internal/DatadogCore$$ExternalSyntheticLambda15;->(Lcom/datadog/android/core/internal/DatadogCore;)V -Lcom/datadog/android/core/internal/DatadogCore$$ExternalSyntheticLambda16; -HSPLcom/datadog/android/core/internal/DatadogCore$$ExternalSyntheticLambda16;->(Lcom/datadog/android/core/internal/DatadogCore;Ljava/util/UUID;)V -HSPLcom/datadog/android/core/internal/DatadogCore$$ExternalSyntheticLambda16;->run()V -Lcom/datadog/android/core/internal/DatadogCore$$ExternalSyntheticLambda4; -HSPLcom/datadog/android/core/internal/DatadogCore$$ExternalSyntheticLambda4;->(Lcom/datadog/android/core/internal/DatadogCore;Lcom/datadog/android/privacy/TrackingConsent;)V -HSPLcom/datadog/android/core/internal/DatadogCore$$ExternalSyntheticLambda4;->run()V -Lcom/datadog/android/core/internal/DatadogCore$1; -HSPLcom/datadog/android/core/internal/DatadogCore$1;->()V -HSPLcom/datadog/android/core/internal/DatadogCore$1;->()V -HSPLcom/datadog/android/core/internal/DatadogCore$1;->invoke(Lcom/datadog/android/api/feature/FeatureSdkCore;)Lcom/datadog/android/core/internal/logger/SdkInternalLogger; -HSPLcom/datadog/android/core/internal/DatadogCore$1;->invoke(Ljava/lang/Object;)Ljava/lang/Object; +HSPLcom/datadog/android/core/internal/DatadogCore$$ExternalSyntheticLambda12;->(Lcom/datadog/android/core/internal/DatadogCore;Ljava/util/UUID;)V +HSPLcom/datadog/android/core/internal/DatadogCore$$ExternalSyntheticLambda12;->run()V +Lcom/datadog/android/core/internal/DatadogCore$$ExternalSyntheticLambda13; +HSPLcom/datadog/android/core/internal/DatadogCore$$ExternalSyntheticLambda13;->(Lcom/datadog/android/core/internal/SdkFeature;Lkotlin/jvm/functions/Function1;Lcom/datadog/android/core/internal/DatadogCore;Ljava/lang/String;)V +HSPLcom/datadog/android/core/internal/DatadogCore$$ExternalSyntheticLambda13;->invoke()Ljava/lang/Object; +Lcom/datadog/android/core/internal/DatadogCore$$ExternalSyntheticLambda14; +HSPLcom/datadog/android/core/internal/DatadogCore$$ExternalSyntheticLambda14;->()V +Lcom/datadog/android/core/internal/DatadogCore$$ExternalSyntheticLambda19; +HSPLcom/datadog/android/core/internal/DatadogCore$$ExternalSyntheticLambda19;->(Lcom/datadog/android/core/internal/SdkFeature;)V +HSPLcom/datadog/android/core/internal/DatadogCore$$ExternalSyntheticLambda19;->invoke()Ljava/lang/Object; +Lcom/datadog/android/core/internal/DatadogCore$$ExternalSyntheticLambda2; +HSPLcom/datadog/android/core/internal/DatadogCore$$ExternalSyntheticLambda2;->(Lcom/datadog/android/core/internal/DatadogCore;Lcom/datadog/android/core/configuration/Configuration;)V +PLcom/datadog/android/core/internal/DatadogCore$$ExternalSyntheticLambda2;->run()V +Lcom/datadog/android/core/internal/DatadogCore$$ExternalSyntheticLambda20; +HSPLcom/datadog/android/core/internal/DatadogCore$$ExternalSyntheticLambda20;->(Lcom/datadog/android/core/internal/DatadogCore;Lcom/datadog/android/privacy/TrackingConsent;)V +HSPLcom/datadog/android/core/internal/DatadogCore$$ExternalSyntheticLambda20;->run()V +Lcom/datadog/android/core/internal/DatadogCore$$ExternalSyntheticLambda32; +HSPLcom/datadog/android/core/internal/DatadogCore$$ExternalSyntheticLambda32;->(Lcom/datadog/android/core/internal/DatadogCore;)V +HSPLcom/datadog/android/core/internal/DatadogCore$$ExternalSyntheticLambda32;->invoke()Ljava/lang/Object; +Lcom/datadog/android/core/internal/DatadogCore$$ExternalSyntheticLambda33; +HSPLcom/datadog/android/core/internal/DatadogCore$$ExternalSyntheticLambda33;->(Lcom/datadog/android/core/internal/DatadogCore;)V +HSPLcom/datadog/android/core/internal/DatadogCore$$ExternalSyntheticLambda33;->getFeatureContext(Ljava/lang/String;)Ljava/util/Map; +Lcom/datadog/android/core/internal/DatadogCore$$ExternalSyntheticLambda5; +HSPLcom/datadog/android/core/internal/DatadogCore$$ExternalSyntheticLambda5;->(Lcom/datadog/android/core/internal/DatadogCore;Ljava/lang/String;Lkotlin/jvm/functions/Function1;)V +HSPLcom/datadog/android/core/internal/DatadogCore$$ExternalSyntheticLambda5;->invoke()Ljava/lang/Object; +Lcom/datadog/android/core/internal/DatadogCore$$ExternalSyntheticLambda6; +HSPLcom/datadog/android/core/internal/DatadogCore$$ExternalSyntheticLambda6;->run()V +Lcom/datadog/android/core/internal/DatadogCore$$ExternalSyntheticLambda7; +HSPLcom/datadog/android/core/internal/DatadogCore$$ExternalSyntheticLambda7;->(Lcom/datadog/android/core/internal/DatadogCore;)V Lcom/datadog/android/core/internal/DatadogCore$Companion; HSPLcom/datadog/android/core/internal/DatadogCore$Companion;->()V HSPLcom/datadog/android/core/internal/DatadogCore$Companion;->(Lkotlin/jvm/internal/DefaultConstructorMarker;)V -Lcom/datadog/android/core/internal/DatadogCore$getFeatureContext$callable$1$1; -HSPLcom/datadog/android/core/internal/DatadogCore$getFeatureContext$callable$1$1;->(Lcom/datadog/android/core/internal/SdkFeature;)V -HSPLcom/datadog/android/core/internal/DatadogCore$getFeatureContext$callable$1$1;->invoke()Ljava/lang/Object; -HSPLcom/datadog/android/core/internal/DatadogCore$getFeatureContext$callable$1$1;->invoke()Ljava/util/HashMap; -Lcom/datadog/android/core/internal/DatadogCore$initialize$1; -HSPLcom/datadog/android/core/internal/DatadogCore$initialize$1;->(Lcom/datadog/android/core/internal/DatadogCore;)V -HSPLcom/datadog/android/core/internal/DatadogCore$initialize$1;->invoke()Lcom/datadog/android/internal/time/TimeProvider; -HSPLcom/datadog/android/core/internal/DatadogCore$initialize$1;->invoke()Ljava/lang/Object; -Lcom/datadog/android/core/internal/DatadogCore$updateFeatureContext$runnable$1; -HSPLcom/datadog/android/core/internal/DatadogCore$updateFeatureContext$runnable$1;->(Lcom/datadog/android/core/internal/DatadogCore;Ljava/lang/String;Lkotlin/jvm/functions/Function1;)V -HSPLcom/datadog/android/core/internal/DatadogCore$updateFeatureContext$runnable$1;->invoke()Ljava/lang/Object; -HSPLcom/datadog/android/core/internal/DatadogCore$updateFeatureContext$runnable$1;->invoke()V -Lcom/datadog/android/core/internal/DatadogCore$updateFeatureContext$runnable$1$1; -HSPLcom/datadog/android/core/internal/DatadogCore$updateFeatureContext$runnable$1$1;->(Lcom/datadog/android/core/internal/SdkFeature;Lkotlin/jvm/functions/Function1;Lcom/datadog/android/core/internal/DatadogCore;Ljava/lang/String;)V -HSPLcom/datadog/android/core/internal/DatadogCore$updateFeatureContext$runnable$1$1;->invoke()Ljava/lang/Object; -HSPLcom/datadog/android/core/internal/DatadogCore$updateFeatureContext$runnable$1$1;->invoke()V Lcom/datadog/android/core/internal/FeatureContextProvider; Lcom/datadog/android/core/internal/HashGenerator; Lcom/datadog/android/core/internal/NoOpContextProvider; @@ -18985,28 +19073,27 @@ HSPLcom/datadog/android/core/internal/SdkFeature;->setDataStore(Lcom/datadog/and HSPLcom/datadog/android/core/internal/SdkFeature;->setupMetricsDispatcher(Lcom/datadog/android/core/internal/configuration/DataUploadConfiguration;Lcom/datadog/android/core/internal/persistence/file/FilePersistenceConfig;Landroid/content/Context;)V HSPLcom/datadog/android/core/internal/SdkFeature;->setupUploader(Lcom/datadog/android/api/feature/StorageBackedFeature;Lcom/datadog/android/core/configuration/UploadSchedulerStrategy;I)V HSPLcom/datadog/android/core/internal/SdkFeature;->unwrap()Lcom/datadog/android/api/feature/Feature; -HSPLcom/datadog/android/core/internal/SdkFeature;->withWriteContext$lambda$0(Lcom/datadog/android/core/internal/SdkFeature;Ljava/util/Set;Lkotlin/jvm/functions/Function2;)V +HSPLcom/datadog/android/core/internal/SdkFeature;->withWriteContext$lambda$1(Lcom/datadog/android/core/internal/SdkFeature;Ljava/util/Set;Lkotlin/jvm/functions/Function2;)V HSPLcom/datadog/android/core/internal/SdkFeature;->withWriteContext(Ljava/util/Set;Lkotlin/jvm/functions/Function2;)V -Lcom/datadog/android/core/internal/SdkFeature$$ExternalSyntheticLambda3; -HSPLcom/datadog/android/core/internal/SdkFeature$$ExternalSyntheticLambda3;->(Lcom/datadog/android/core/internal/SdkFeature;Ljava/util/Set;Lkotlin/jvm/functions/Function2;)V -HSPLcom/datadog/android/core/internal/SdkFeature$$ExternalSyntheticLambda3;->run()V +Lcom/datadog/android/core/internal/SdkFeature$$ExternalSyntheticLambda2; +HSPLcom/datadog/android/core/internal/SdkFeature$$ExternalSyntheticLambda2;->(Lcom/datadog/android/core/internal/SdkFeature;)V +Lcom/datadog/android/core/internal/SdkFeature$$ExternalSyntheticLambda5; +HSPLcom/datadog/android/core/internal/SdkFeature$$ExternalSyntheticLambda5;->(Lcom/datadog/android/core/internal/SdkFeature;Ljava/util/Set;Lkotlin/jvm/functions/Function2;)V +HSPLcom/datadog/android/core/internal/SdkFeature$$ExternalSyntheticLambda5;->run()V Lcom/datadog/android/core/internal/SdkFeature$Companion; HSPLcom/datadog/android/core/internal/SdkFeature$Companion;->()V HSPLcom/datadog/android/core/internal/SdkFeature$Companion;->(Lkotlin/jvm/internal/DefaultConstructorMarker;)V -Lcom/datadog/android/core/internal/SdkFeature$createBatchCountBenchmark$1; -HSPLcom/datadog/android/core/internal/SdkFeature$createBatchCountBenchmark$1;->(Lcom/datadog/android/core/internal/SdkFeature;)V Lcom/datadog/android/core/internal/Sha256HashGenerator; HSPLcom/datadog/android/core/internal/Sha256HashGenerator;->()V HSPLcom/datadog/android/core/internal/Sha256HashGenerator;->()V +HSPLcom/datadog/android/core/internal/Sha256HashGenerator;->generate$lambda$0(B)Ljava/lang/CharSequence; HSPLcom/datadog/android/core/internal/Sha256HashGenerator;->generate(Ljava/lang/String;)Ljava/lang/String; +Lcom/datadog/android/core/internal/Sha256HashGenerator$$ExternalSyntheticLambda0; +HSPLcom/datadog/android/core/internal/Sha256HashGenerator$$ExternalSyntheticLambda0;->()V +HSPLcom/datadog/android/core/internal/Sha256HashGenerator$$ExternalSyntheticLambda0;->invoke(Ljava/lang/Object;)Ljava/lang/Object; Lcom/datadog/android/core/internal/Sha256HashGenerator$Companion; HSPLcom/datadog/android/core/internal/Sha256HashGenerator$Companion;->()V HSPLcom/datadog/android/core/internal/Sha256HashGenerator$Companion;->(Lkotlin/jvm/internal/DefaultConstructorMarker;)V -Lcom/datadog/android/core/internal/Sha256HashGenerator$generate$1; -HSPLcom/datadog/android/core/internal/Sha256HashGenerator$generate$1;->()V -HSPLcom/datadog/android/core/internal/Sha256HashGenerator$generate$1;->()V -HSPLcom/datadog/android/core/internal/Sha256HashGenerator$generate$1;->invoke(B)Ljava/lang/CharSequence; -HSPLcom/datadog/android/core/internal/Sha256HashGenerator$generate$1;->invoke(Ljava/lang/Object;)Ljava/lang/Object; Lcom/datadog/android/core/internal/account/AccountInfoProvider; Lcom/datadog/android/core/internal/account/DatadogAccountInfoProvider; HSPLcom/datadog/android/core/internal/account/DatadogAccountInfoProvider;->()V @@ -19031,8 +19118,6 @@ HSPLcom/datadog/android/core/internal/configuration/DataUploadConfiguration$Comp Lcom/datadog/android/core/internal/data/upload/DataOkHttpUploader; HSPLcom/datadog/android/core/internal/data/upload/DataOkHttpUploader;->()V HSPLcom/datadog/android/core/internal/data/upload/DataOkHttpUploader;->(Lcom/datadog/android/api/net/RequestFactory;Lcom/datadog/android/api/InternalLogger;Lokhttp3/Call$Factory;Ljava/lang/String;Lcom/datadog/android/core/internal/system/AndroidInfoProvider;Lcom/datadog/android/internal/profiler/ExecutionTimer;)V -HSPLcom/datadog/android/core/internal/data/upload/DataOkHttpUploader;->access$executeUploadRequest(Lcom/datadog/android/core/internal/data/upload/DataOkHttpUploader;Lcom/datadog/android/api/net/Request;)Lcom/datadog/android/core/internal/data/upload/UploadStatus; -HSPLcom/datadog/android/core/internal/data/upload/DataOkHttpUploader;->access$sanitizeHeaderValue(Lcom/datadog/android/core/internal/data/upload/DataOkHttpUploader;Ljava/lang/String;)Ljava/lang/String; HSPLcom/datadog/android/core/internal/data/upload/DataOkHttpUploader;->buildOkHttpRequest(Lcom/datadog/android/api/net/Request;)Lokhttp3/Request; HSPLcom/datadog/android/core/internal/data/upload/DataOkHttpUploader;->executeUploadRequest(Lcom/datadog/android/api/net/Request;)Lcom/datadog/android/core/internal/data/upload/UploadStatus; HSPLcom/datadog/android/core/internal/data/upload/DataOkHttpUploader;->getUserAgent()Ljava/lang/String; @@ -19041,34 +19126,43 @@ HSPLcom/datadog/android/core/internal/data/upload/DataOkHttpUploader;->isValidHe HSPLcom/datadog/android/core/internal/data/upload/DataOkHttpUploader;->resolveExecutionContext(Lcom/datadog/android/core/internal/persistence/BatchId;)Lcom/datadog/android/api/net/RequestExecutionContext; HSPLcom/datadog/android/core/internal/data/upload/DataOkHttpUploader;->responseCodeToUploadStatus(ILcom/datadog/android/api/net/Request;)Lcom/datadog/android/core/internal/data/upload/UploadStatus; HSPLcom/datadog/android/core/internal/data/upload/DataOkHttpUploader;->sanitizeHeaderValue(Ljava/lang/String;)Ljava/lang/String; +HSPLcom/datadog/android/core/internal/data/upload/DataOkHttpUploader;->upload$lambda$4(Lcom/datadog/android/core/internal/data/upload/DataOkHttpUploader;Lcom/datadog/android/api/net/Request;Lcom/datadog/android/api/context/DatadogContext;)Lcom/datadog/android/core/internal/data/upload/UploadStatus; HSPLcom/datadog/android/core/internal/data/upload/DataOkHttpUploader;->upload(Lcom/datadog/android/api/context/DatadogContext;Ljava/util/List;[BLcom/datadog/android/core/internal/persistence/BatchId;)Lcom/datadog/android/core/internal/data/upload/UploadStatus; +HSPLcom/datadog/android/core/internal/data/upload/DataOkHttpUploader;->userAgent_delegate$lambda$6(Lcom/datadog/android/core/internal/data/upload/DataOkHttpUploader;)Ljava/lang/String; +Lcom/datadog/android/core/internal/data/upload/DataOkHttpUploader$$ExternalSyntheticLambda4; +HSPLcom/datadog/android/core/internal/data/upload/DataOkHttpUploader$$ExternalSyntheticLambda4;->(Lcom/datadog/android/core/internal/data/upload/DataOkHttpUploader;Lcom/datadog/android/api/net/Request;Lcom/datadog/android/api/context/DatadogContext;)V +HSPLcom/datadog/android/core/internal/data/upload/DataOkHttpUploader$$ExternalSyntheticLambda4;->invoke()Ljava/lang/Object; +Lcom/datadog/android/core/internal/data/upload/DataOkHttpUploader$$ExternalSyntheticLambda7; +HSPLcom/datadog/android/core/internal/data/upload/DataOkHttpUploader$$ExternalSyntheticLambda7;->(Lcom/datadog/android/core/internal/data/upload/DataOkHttpUploader;)V +HSPLcom/datadog/android/core/internal/data/upload/DataOkHttpUploader$$ExternalSyntheticLambda7;->invoke()Ljava/lang/Object; Lcom/datadog/android/core/internal/data/upload/DataOkHttpUploader$Companion; HSPLcom/datadog/android/core/internal/data/upload/DataOkHttpUploader$Companion;->()V HSPLcom/datadog/android/core/internal/data/upload/DataOkHttpUploader$Companion;->(Lkotlin/jvm/internal/DefaultConstructorMarker;)V -Lcom/datadog/android/core/internal/data/upload/DataOkHttpUploader$upload$uploadStatus$1; -HSPLcom/datadog/android/core/internal/data/upload/DataOkHttpUploader$upload$uploadStatus$1;->(Lcom/datadog/android/core/internal/data/upload/DataOkHttpUploader;Lcom/datadog/android/api/net/Request;Lcom/datadog/android/api/context/DatadogContext;)V -HSPLcom/datadog/android/core/internal/data/upload/DataOkHttpUploader$upload$uploadStatus$1;->invoke()Lcom/datadog/android/core/internal/data/upload/UploadStatus; -HSPLcom/datadog/android/core/internal/data/upload/DataOkHttpUploader$upload$uploadStatus$1;->invoke()Ljava/lang/Object; -Lcom/datadog/android/core/internal/data/upload/DataOkHttpUploader$userAgent$2; -HSPLcom/datadog/android/core/internal/data/upload/DataOkHttpUploader$userAgent$2;->(Lcom/datadog/android/core/internal/data/upload/DataOkHttpUploader;)V -HSPLcom/datadog/android/core/internal/data/upload/DataOkHttpUploader$userAgent$2;->invoke()Ljava/lang/Object; -HSPLcom/datadog/android/core/internal/data/upload/DataOkHttpUploader$userAgent$2;->invoke()Ljava/lang/String; -Lcom/datadog/android/core/internal/data/upload/DataUploadRunnable; -HSPLcom/datadog/android/core/internal/data/upload/DataUploadRunnable;->()V -HSPLcom/datadog/android/core/internal/data/upload/DataUploadRunnable;->(Ljava/lang/String;Ljava/util/concurrent/ScheduledThreadPoolExecutor;Lcom/datadog/android/core/internal/persistence/Storage;Lcom/datadog/android/core/internal/data/upload/DataUploader;Lcom/datadog/android/core/internal/ContextProvider;Lcom/datadog/android/core/internal/net/info/NetworkInfoProvider;Lcom/datadog/android/core/internal/system/SystemInfoProvider;Lcom/datadog/android/core/configuration/UploadSchedulerStrategy;ILcom/datadog/android/api/InternalLogger;Lcom/datadog/android/core/internal/metrics/BenchmarkUploads;)V -HSPLcom/datadog/android/core/internal/data/upload/DataUploadRunnable;->(Ljava/lang/String;Ljava/util/concurrent/ScheduledThreadPoolExecutor;Lcom/datadog/android/core/internal/persistence/Storage;Lcom/datadog/android/core/internal/data/upload/DataUploader;Lcom/datadog/android/core/internal/ContextProvider;Lcom/datadog/android/core/internal/net/info/NetworkInfoProvider;Lcom/datadog/android/core/internal/system/SystemInfoProvider;Lcom/datadog/android/core/configuration/UploadSchedulerStrategy;ILcom/datadog/android/api/InternalLogger;Lcom/datadog/android/core/internal/metrics/BenchmarkUploads;ILkotlin/jvm/internal/DefaultConstructorMarker;)V -HSPLcom/datadog/android/core/internal/data/upload/DataUploadRunnable;->consumeBatch(Lcom/datadog/android/api/context/DatadogContext;Lcom/datadog/android/core/internal/persistence/BatchId;Ljava/util/List;[B)Lcom/datadog/android/core/internal/data/upload/UploadStatus; -HSPLcom/datadog/android/core/internal/data/upload/DataUploadRunnable;->handleNextBatch(Lcom/datadog/android/api/context/DatadogContext;)Lcom/datadog/android/core/internal/data/upload/UploadStatus; -HSPLcom/datadog/android/core/internal/data/upload/DataUploadRunnable;->isNetworkAvailable()Z -HSPLcom/datadog/android/core/internal/data/upload/DataUploadRunnable;->isSystemReady()Z -HSPLcom/datadog/android/core/internal/data/upload/DataUploadRunnable;->run()V -HSPLcom/datadog/android/core/internal/data/upload/DataUploadRunnable;->scheduleNextUpload(J)V -Lcom/datadog/android/core/internal/data/upload/DataUploadRunnable$Companion; -HSPLcom/datadog/android/core/internal/data/upload/DataUploadRunnable$Companion;->()V -HSPLcom/datadog/android/core/internal/data/upload/DataUploadRunnable$Companion;->(Lkotlin/jvm/internal/DefaultConstructorMarker;)V Lcom/datadog/android/core/internal/data/upload/DataUploadScheduler; -HSPLcom/datadog/android/core/internal/data/upload/DataUploadScheduler;->(Ljava/lang/String;Lcom/datadog/android/core/internal/persistence/Storage;Lcom/datadog/android/core/internal/data/upload/DataUploader;Lcom/datadog/android/core/internal/ContextProvider;Lcom/datadog/android/core/internal/net/info/NetworkInfoProvider;Lcom/datadog/android/core/internal/system/SystemInfoProvider;Lcom/datadog/android/core/configuration/UploadSchedulerStrategy;ILjava/util/concurrent/ScheduledThreadPoolExecutor;Lcom/datadog/android/api/InternalLogger;)V +HSPLcom/datadog/android/core/internal/data/upload/DataUploadScheduler;->(Ljava/lang/String;Lcom/datadog/android/core/internal/persistence/Storage;Lcom/datadog/android/core/internal/data/upload/DataUploader;Lcom/datadog/android/core/internal/ContextProvider;Lcom/datadog/android/core/internal/net/info/NetworkInfoProvider;Lcom/datadog/android/core/internal/system/SystemInfoProvider;Lcom/datadog/android/core/configuration/UploadSchedulerStrategy;ILjava/util/concurrent/ScheduledThreadPoolExecutor;Lcom/datadog/android/api/InternalLogger;Ljava/util/concurrent/Callable;)V +HSPLcom/datadog/android/core/internal/data/upload/DataUploadScheduler;->(Ljava/lang/String;Lcom/datadog/android/core/internal/persistence/Storage;Lcom/datadog/android/core/internal/data/upload/DataUploader;Lcom/datadog/android/core/internal/ContextProvider;Lcom/datadog/android/core/internal/net/info/NetworkInfoProvider;Lcom/datadog/android/core/internal/system/SystemInfoProvider;Lcom/datadog/android/core/configuration/UploadSchedulerStrategy;ILjava/util/concurrent/ScheduledThreadPoolExecutor;Lcom/datadog/android/api/InternalLogger;Ljava/util/concurrent/Callable;ILkotlin/jvm/internal/DefaultConstructorMarker;)V +HSPLcom/datadog/android/core/internal/data/upload/DataUploadScheduler;->access$getScheduleLock$p(Lcom/datadog/android/core/internal/data/upload/DataUploadScheduler;)Ljava/lang/Object; +HSPLcom/datadog/android/core/internal/data/upload/DataUploadScheduler;->access$getStopped$p(Lcom/datadog/android/core/internal/data/upload/DataUploadScheduler;)Z +HSPLcom/datadog/android/core/internal/data/upload/DataUploadScheduler;->access$scheduleNext(Lcom/datadog/android/core/internal/data/upload/DataUploadScheduler;J)V +HSPLcom/datadog/android/core/internal/data/upload/DataUploadScheduler;->getRunnable$dd_sdk_android_core_release()Ljava/util/concurrent/Callable; +HSPLcom/datadog/android/core/internal/data/upload/DataUploadScheduler;->scheduleNext(J)V HSPLcom/datadog/android/core/internal/data/upload/DataUploadScheduler;->startScheduling()V +Lcom/datadog/android/core/internal/data/upload/DataUploadScheduler$uploadTask$1; +HSPLcom/datadog/android/core/internal/data/upload/DataUploadScheduler$uploadTask$1;->(Lcom/datadog/android/core/internal/data/upload/DataUploadScheduler;)V +HSPLcom/datadog/android/core/internal/data/upload/DataUploadScheduler$uploadTask$1;->run()V +Lcom/datadog/android/core/internal/data/upload/DataUploadTask; +HSPLcom/datadog/android/core/internal/data/upload/DataUploadTask;->()V +HSPLcom/datadog/android/core/internal/data/upload/DataUploadTask;->(Ljava/lang/String;Lcom/datadog/android/core/internal/persistence/Storage;Lcom/datadog/android/core/internal/data/upload/DataUploader;Lcom/datadog/android/core/internal/ContextProvider;Lcom/datadog/android/core/internal/net/info/NetworkInfoProvider;Lcom/datadog/android/core/internal/system/SystemInfoProvider;Lcom/datadog/android/core/configuration/UploadSchedulerStrategy;ILcom/datadog/android/core/internal/metrics/BenchmarkUploads;)V +HSPLcom/datadog/android/core/internal/data/upload/DataUploadTask;->(Ljava/lang/String;Lcom/datadog/android/core/internal/persistence/Storage;Lcom/datadog/android/core/internal/data/upload/DataUploader;Lcom/datadog/android/core/internal/ContextProvider;Lcom/datadog/android/core/internal/net/info/NetworkInfoProvider;Lcom/datadog/android/core/internal/system/SystemInfoProvider;Lcom/datadog/android/core/configuration/UploadSchedulerStrategy;ILcom/datadog/android/core/internal/metrics/BenchmarkUploads;ILkotlin/jvm/internal/DefaultConstructorMarker;)V +HSPLcom/datadog/android/core/internal/data/upload/DataUploadTask;->call()Ljava/lang/Long; +HSPLcom/datadog/android/core/internal/data/upload/DataUploadTask;->call()Ljava/lang/Object; +HSPLcom/datadog/android/core/internal/data/upload/DataUploadTask;->consumeBatch(Lcom/datadog/android/api/context/DatadogContext;Lcom/datadog/android/core/internal/persistence/BatchId;Ljava/util/List;[B)Lcom/datadog/android/core/internal/data/upload/UploadStatus; +HSPLcom/datadog/android/core/internal/data/upload/DataUploadTask;->handleNextBatch(Lcom/datadog/android/api/context/DatadogContext;)Lcom/datadog/android/core/internal/data/upload/UploadStatus; +HSPLcom/datadog/android/core/internal/data/upload/DataUploadTask;->isNetworkAvailable()Z +HSPLcom/datadog/android/core/internal/data/upload/DataUploadTask;->isSystemReady()Z +Lcom/datadog/android/core/internal/data/upload/DataUploadTask$Companion; +HSPLcom/datadog/android/core/internal/data/upload/DataUploadTask$Companion;->()V +HSPLcom/datadog/android/core/internal/data/upload/DataUploadTask$Companion;->(Lkotlin/jvm/internal/DefaultConstructorMarker;)V Lcom/datadog/android/core/internal/data/upload/DataUploader; Lcom/datadog/android/core/internal/data/upload/DefaultUploadSchedulerStrategy; HSPLcom/datadog/android/core/internal/data/upload/DefaultUploadSchedulerStrategy;->()V @@ -19108,21 +19202,22 @@ HSPLcom/datadog/android/core/internal/data/upload/RotatingDnsResolver$Companion; HSPLcom/datadog/android/core/internal/data/upload/RotatingDnsResolver$Companion;->(Lkotlin/jvm/internal/DefaultConstructorMarker;)V Lcom/datadog/android/core/internal/data/upload/RotatingDnsResolver$ResolvedHost; HSPLcom/datadog/android/core/internal/data/upload/RotatingDnsResolver$ResolvedHost;->(Ljava/lang/String;Ljava/util/List;J)V -Lcom/datadog/android/core/internal/data/upload/UploadRunnable; Lcom/datadog/android/core/internal/data/upload/UploadScheduler; Lcom/datadog/android/core/internal/data/upload/UploadStatus; HSPLcom/datadog/android/core/internal/data/upload/UploadStatus;->()V HSPLcom/datadog/android/core/internal/data/upload/UploadStatus;->(ZILjava/lang/Throwable;)V HSPLcom/datadog/android/core/internal/data/upload/UploadStatus;->(ZILjava/lang/Throwable;ILkotlin/jvm/internal/DefaultConstructorMarker;)V HSPLcom/datadog/android/core/internal/data/upload/UploadStatus;->(ZILjava/lang/Throwable;Lkotlin/jvm/internal/DefaultConstructorMarker;)V -HSPLcom/datadog/android/core/internal/data/upload/UploadStatus;->access$buildStatusMessage(Lcom/datadog/android/core/internal/data/upload/UploadStatus;Ljava/lang/String;ILjava/lang/String;Ljava/lang/Throwable;I)Ljava/lang/String; HSPLcom/datadog/android/core/internal/data/upload/UploadStatus;->buildStatusMessage(Ljava/lang/String;ILjava/lang/String;Ljava/lang/Throwable;I)Ljava/lang/String; HSPLcom/datadog/android/core/internal/data/upload/UploadStatus;->getCode()I HSPLcom/datadog/android/core/internal/data/upload/UploadStatus;->getShouldRetry()Z -HSPLcom/datadog/android/core/internal/data/upload/UploadStatus;->getThrowable()Ljava/lang/Throwable; +HSPLcom/datadog/android/core/internal/data/upload/UploadStatus;->logStatus$lambda$0(Lcom/datadog/android/core/internal/data/upload/UploadStatus;Ljava/lang/String;ILjava/lang/String;I)Ljava/lang/String; HSPLcom/datadog/android/core/internal/data/upload/UploadStatus;->logStatus(Ljava/lang/String;ILcom/datadog/android/api/InternalLogger;ILjava/lang/String;)V HSPLcom/datadog/android/core/internal/data/upload/UploadStatus;->resolveInternalLogLevel()Lcom/datadog/android/api/InternalLogger$Level; HSPLcom/datadog/android/core/internal/data/upload/UploadStatus;->resolveInternalLogTarget()Ljava/util/List; +Lcom/datadog/android/core/internal/data/upload/UploadStatus$$ExternalSyntheticLambda0; +HSPLcom/datadog/android/core/internal/data/upload/UploadStatus$$ExternalSyntheticLambda0;->(Lcom/datadog/android/core/internal/data/upload/UploadStatus;Ljava/lang/String;ILjava/lang/String;I)V +HSPLcom/datadog/android/core/internal/data/upload/UploadStatus$$ExternalSyntheticLambda0;->invoke()Ljava/lang/Object; Lcom/datadog/android/core/internal/data/upload/UploadStatus$Companion; HSPLcom/datadog/android/core/internal/data/upload/UploadStatus$Companion;->()V HSPLcom/datadog/android/core/internal/data/upload/UploadStatus$Companion;->(Lkotlin/jvm/internal/DefaultConstructorMarker;)V @@ -19136,10 +19231,6 @@ HSPLcom/datadog/android/core/internal/data/upload/UploadStatus$InvalidTokenError Lcom/datadog/android/core/internal/data/upload/UploadStatus$RequestCreationError; Lcom/datadog/android/core/internal/data/upload/UploadStatus$Success; Lcom/datadog/android/core/internal/data/upload/UploadStatus$UnknownStatus; -Lcom/datadog/android/core/internal/data/upload/UploadStatus$logStatus$1; -HSPLcom/datadog/android/core/internal/data/upload/UploadStatus$logStatus$1;->(Lcom/datadog/android/core/internal/data/upload/UploadStatus;Ljava/lang/String;ILjava/lang/String;I)V -HSPLcom/datadog/android/core/internal/data/upload/UploadStatus$logStatus$1;->invoke()Ljava/lang/Object; -HSPLcom/datadog/android/core/internal/data/upload/UploadStatus$logStatus$1;->invoke()Ljava/lang/String; Lcom/datadog/android/core/internal/lifecycle/ProcessLifecycleCallback; HSPLcom/datadog/android/core/internal/lifecycle/ProcessLifecycleCallback;->(Landroid/content/Context;Ljava/lang/String;Lcom/datadog/android/api/InternalLogger;)V HSPLcom/datadog/android/core/internal/lifecycle/ProcessLifecycleCallback;->onResumed()V @@ -19157,6 +19248,9 @@ Lcom/datadog/android/core/internal/logger/SdkInternalLogger; HSPLcom/datadog/android/core/internal/logger/SdkInternalLogger;->()V HSPLcom/datadog/android/core/internal/logger/SdkInternalLogger;->(Lcom/datadog/android/api/feature/FeatureSdkCore;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function0;)V HSPLcom/datadog/android/core/internal/logger/SdkInternalLogger;->(Lcom/datadog/android/api/feature/FeatureSdkCore;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function0;ILkotlin/jvm/internal/DefaultConstructorMarker;)V +HSPLcom/datadog/android/core/internal/logger/SdkInternalLogger;->_init_$lambda$1$lambda$0(I)Z +HSPLcom/datadog/android/core/internal/logger/SdkInternalLogger;->_init_$lambda$1()Lcom/datadog/android/core/internal/logger/LogcatLogHandler; +HSPLcom/datadog/android/core/internal/logger/SdkInternalLogger;->_init_$lambda$2()Lcom/datadog/android/core/internal/logger/LogcatLogHandler; HSPLcom/datadog/android/core/internal/logger/SdkInternalLogger;->log(Lcom/datadog/android/api/InternalLogger$Level;Lcom/datadog/android/api/InternalLogger$Target;Lkotlin/jvm/functions/Function0;Ljava/lang/Throwable;ZLjava/util/Map;)V HSPLcom/datadog/android/core/internal/logger/SdkInternalLogger;->log(Lcom/datadog/android/api/InternalLogger$Level;Ljava/util/List;Lkotlin/jvm/functions/Function0;Ljava/lang/Throwable;ZLjava/util/Map;)V HSPLcom/datadog/android/core/internal/logger/SdkInternalLogger;->logApiUsage(FLkotlin/jvm/functions/Function0;)V @@ -19168,21 +19262,15 @@ HSPLcom/datadog/android/core/internal/logger/SdkInternalLogger;->sendToLogHandle HSPLcom/datadog/android/core/internal/logger/SdkInternalLogger;->startPerformanceMeasure(Ljava/lang/String;Lcom/datadog/android/core/metrics/TelemetryMetricType;FLjava/lang/String;)Lcom/datadog/android/core/metrics/PerformanceMetric; HSPLcom/datadog/android/core/internal/logger/SdkInternalLogger;->toLogLevel(Lcom/datadog/android/api/InternalLogger$Level;)I HSPLcom/datadog/android/core/internal/logger/SdkInternalLogger;->withSdkName(Ljava/lang/String;)Ljava/lang/String; -Lcom/datadog/android/core/internal/logger/SdkInternalLogger$1; -HSPLcom/datadog/android/core/internal/logger/SdkInternalLogger$1;->()V -HSPLcom/datadog/android/core/internal/logger/SdkInternalLogger$1;->()V -HSPLcom/datadog/android/core/internal/logger/SdkInternalLogger$1;->invoke()Lcom/datadog/android/core/internal/logger/LogcatLogHandler; -HSPLcom/datadog/android/core/internal/logger/SdkInternalLogger$1;->invoke()Ljava/lang/Object; -Lcom/datadog/android/core/internal/logger/SdkInternalLogger$1$1; -HSPLcom/datadog/android/core/internal/logger/SdkInternalLogger$1$1;->()V -HSPLcom/datadog/android/core/internal/logger/SdkInternalLogger$1$1;->()V -HSPLcom/datadog/android/core/internal/logger/SdkInternalLogger$1$1;->invoke(I)Ljava/lang/Boolean; -HSPLcom/datadog/android/core/internal/logger/SdkInternalLogger$1$1;->invoke(Ljava/lang/Object;)Ljava/lang/Object; -Lcom/datadog/android/core/internal/logger/SdkInternalLogger$2; -HSPLcom/datadog/android/core/internal/logger/SdkInternalLogger$2;->()V -HSPLcom/datadog/android/core/internal/logger/SdkInternalLogger$2;->()V -HSPLcom/datadog/android/core/internal/logger/SdkInternalLogger$2;->invoke()Lcom/datadog/android/core/internal/logger/LogcatLogHandler; -HSPLcom/datadog/android/core/internal/logger/SdkInternalLogger$2;->invoke()Ljava/lang/Object; +Lcom/datadog/android/core/internal/logger/SdkInternalLogger$$ExternalSyntheticLambda0; +HSPLcom/datadog/android/core/internal/logger/SdkInternalLogger$$ExternalSyntheticLambda0;->()V +HSPLcom/datadog/android/core/internal/logger/SdkInternalLogger$$ExternalSyntheticLambda0;->invoke()Ljava/lang/Object; +Lcom/datadog/android/core/internal/logger/SdkInternalLogger$$ExternalSyntheticLambda1; +HSPLcom/datadog/android/core/internal/logger/SdkInternalLogger$$ExternalSyntheticLambda1;->()V +HSPLcom/datadog/android/core/internal/logger/SdkInternalLogger$$ExternalSyntheticLambda1;->invoke()Ljava/lang/Object; +Lcom/datadog/android/core/internal/logger/SdkInternalLogger$$ExternalSyntheticLambda2; +HSPLcom/datadog/android/core/internal/logger/SdkInternalLogger$$ExternalSyntheticLambda2;->()V +HSPLcom/datadog/android/core/internal/logger/SdkInternalLogger$$ExternalSyntheticLambda2;->invoke(Ljava/lang/Object;)Ljava/lang/Object; Lcom/datadog/android/core/internal/logger/SdkInternalLogger$Companion; HSPLcom/datadog/android/core/internal/logger/SdkInternalLogger$Companion;->()V HSPLcom/datadog/android/core/internal/logger/SdkInternalLogger$Companion;->(Lkotlin/jvm/internal/DefaultConstructorMarker;)V @@ -19193,16 +19281,16 @@ HSPLcom/datadog/android/core/internal/metrics/BatchMetricsDispatcher;->( HSPLcom/datadog/android/core/internal/metrics/BatchMetricsDispatcher;->(Ljava/lang/String;Lcom/datadog/android/core/internal/configuration/DataUploadConfiguration;Lcom/datadog/android/core/internal/persistence/file/FilePersistenceConfig;Lcom/datadog/android/api/InternalLogger;Lcom/datadog/android/internal/time/TimeProvider;)V HSPLcom/datadog/android/core/internal/metrics/BatchMetricsDispatcher;->nameAsTimestampSafe(Ljava/io/File;Lcom/datadog/android/api/InternalLogger;)Ljava/lang/Long; HSPLcom/datadog/android/core/internal/metrics/BatchMetricsDispatcher;->onResumed()V +HSPLcom/datadog/android/core/internal/metrics/BatchMetricsDispatcher;->onStarted()V HSPLcom/datadog/android/core/internal/metrics/BatchMetricsDispatcher;->resolveBatchDeletedMetricAttributes(Ljava/io/File;Lcom/datadog/android/core/internal/metrics/RemovalReason;I)Ljava/util/Map; HSPLcom/datadog/android/core/internal/metrics/BatchMetricsDispatcher;->resolveFileOriginAsConsent(Ljava/io/File;)Ljava/lang/String; HSPLcom/datadog/android/core/internal/metrics/BatchMetricsDispatcher;->resolveTrackName(Ljava/lang/String;)Ljava/lang/String; HSPLcom/datadog/android/core/internal/metrics/BatchMetricsDispatcher;->sendBatchDeletedMetric(Ljava/io/File;Lcom/datadog/android/core/internal/metrics/RemovalReason;I)V +Lcom/datadog/android/core/internal/metrics/BatchMetricsDispatcher$$ExternalSyntheticLambda0; +HSPLcom/datadog/android/core/internal/metrics/BatchMetricsDispatcher$$ExternalSyntheticLambda0;->()V Lcom/datadog/android/core/internal/metrics/BatchMetricsDispatcher$Companion; HSPLcom/datadog/android/core/internal/metrics/BatchMetricsDispatcher$Companion;->()V HSPLcom/datadog/android/core/internal/metrics/BatchMetricsDispatcher$Companion;->(Lkotlin/jvm/internal/DefaultConstructorMarker;)V -Lcom/datadog/android/core/internal/metrics/BatchMetricsDispatcher$sendBatchDeletedMetric$1$1; -HSPLcom/datadog/android/core/internal/metrics/BatchMetricsDispatcher$sendBatchDeletedMetric$1$1;->()V -HSPLcom/datadog/android/core/internal/metrics/BatchMetricsDispatcher$sendBatchDeletedMetric$1$1;->()V Lcom/datadog/android/core/internal/metrics/BenchmarkUploads; HSPLcom/datadog/android/core/internal/metrics/BenchmarkUploads;->()V HSPLcom/datadog/android/core/internal/metrics/BenchmarkUploads;->(Lcom/datadog/android/internal/profiler/BenchmarkSdkUploads;)V @@ -19228,6 +19316,7 @@ HSPLcom/datadog/android/core/internal/metrics/RemovalReason$IntakeCode;->toStrin Lcom/datadog/android/core/internal/net/DefaultFirstPartyHostHeaderTypeResolver; HSPLcom/datadog/android/core/internal/net/DefaultFirstPartyHostHeaderTypeResolver;->(Ljava/util/Map;)V HSPLcom/datadog/android/core/internal/net/DefaultFirstPartyHostHeaderTypeResolver;->addKnownHostsWithHeaderTypes$dd_sdk_android_core_release(Ljava/util/Map;)V +HSPLcom/datadog/android/core/internal/net/DefaultFirstPartyHostHeaderTypeResolver;->getAllHeaderTypes()Ljava/util/Set; HSPLcom/datadog/android/core/internal/net/DefaultFirstPartyHostHeaderTypeResolver;->isEmpty()Z HSPLcom/datadog/android/core/internal/net/DefaultFirstPartyHostHeaderTypeResolver;->isWildcardPattern(Ljava/lang/String;)Z HSPLcom/datadog/android/core/internal/net/DefaultFirstPartyHostHeaderTypeResolver;->updateKnownHosts(Ljava/util/Map;)V @@ -19347,53 +19436,47 @@ HSPLcom/datadog/android/core/internal/persistence/datastore/DatastoreFileWriter$ Lcom/datadog/android/core/internal/persistence/datastore/NoOpDataStoreHandler; HSPLcom/datadog/android/core/internal/persistence/datastore/NoOpDataStoreHandler;->()V Lcom/datadog/android/core/internal/persistence/file/FileExtKt; +HSPLcom/datadog/android/core/internal/persistence/file/FileExtKt;->canWriteSafe$lambda$2(Ljava/io/File;)Z HSPLcom/datadog/android/core/internal/persistence/file/FileExtKt;->canWriteSafe(Ljava/io/File;Lcom/datadog/android/api/InternalLogger;)Z +HSPLcom/datadog/android/core/internal/persistence/file/FileExtKt;->existsSafe$lambda$5(Ljava/io/File;)Z HSPLcom/datadog/android/core/internal/persistence/file/FileExtKt;->existsSafe(Ljava/io/File;Lcom/datadog/android/api/InternalLogger;)Z +HSPLcom/datadog/android/core/internal/persistence/file/FileExtKt;->isDirectorySafe$lambda$7(Ljava/io/File;)Z HSPLcom/datadog/android/core/internal/persistence/file/FileExtKt;->isDirectorySafe(Ljava/io/File;Lcom/datadog/android/api/InternalLogger;)Z +HSPLcom/datadog/android/core/internal/persistence/file/FileExtKt;->lengthSafe$lambda$11(Ljava/io/File;)J HSPLcom/datadog/android/core/internal/persistence/file/FileExtKt;->lengthSafe(Ljava/io/File;Lcom/datadog/android/api/InternalLogger;)J +HSPLcom/datadog/android/core/internal/persistence/file/FileExtKt;->listFilesSafe$lambda$8(Ljava/io/File;)[Ljava/io/File; +HSPLcom/datadog/android/core/internal/persistence/file/FileExtKt;->listFilesSafe$lambda$9(Ljava/io/FileFilter;Ljava/io/File;)[Ljava/io/File; HSPLcom/datadog/android/core/internal/persistence/file/FileExtKt;->listFilesSafe(Ljava/io/File;Lcom/datadog/android/api/InternalLogger;)[Ljava/io/File; HSPLcom/datadog/android/core/internal/persistence/file/FileExtKt;->listFilesSafe(Ljava/io/File;Ljava/io/FileFilter;Lcom/datadog/android/api/InternalLogger;)[Ljava/io/File; +HSPLcom/datadog/android/core/internal/persistence/file/FileExtKt;->mkdirsSafe$lambda$12(Ljava/io/File;)Z HSPLcom/datadog/android/core/internal/persistence/file/FileExtKt;->mkdirsSafe(Ljava/io/File;Lcom/datadog/android/api/InternalLogger;)Z +HSPLcom/datadog/android/core/internal/persistence/file/FileExtKt;->renameToSafe$lambda$13(Ljava/io/File;Ljava/io/File;)Z HSPLcom/datadog/android/core/internal/persistence/file/FileExtKt;->renameToSafe(Ljava/io/File;Ljava/io/File;Lcom/datadog/android/api/InternalLogger;)Z HSPLcom/datadog/android/core/internal/persistence/file/FileExtKt;->safeCall(Ljava/io/File;Ljava/lang/Object;Lcom/datadog/android/api/InternalLogger;Lkotlin/jvm/functions/Function1;)Ljava/lang/Object; -Lcom/datadog/android/core/internal/persistence/file/FileExtKt$canWriteSafe$1; -HSPLcom/datadog/android/core/internal/persistence/file/FileExtKt$canWriteSafe$1;->()V -HSPLcom/datadog/android/core/internal/persistence/file/FileExtKt$canWriteSafe$1;->()V -HSPLcom/datadog/android/core/internal/persistence/file/FileExtKt$canWriteSafe$1;->invoke(Ljava/io/File;)Ljava/lang/Boolean; -HSPLcom/datadog/android/core/internal/persistence/file/FileExtKt$canWriteSafe$1;->invoke(Ljava/lang/Object;)Ljava/lang/Object; -Lcom/datadog/android/core/internal/persistence/file/FileExtKt$existsSafe$1; -HSPLcom/datadog/android/core/internal/persistence/file/FileExtKt$existsSafe$1;->()V -HSPLcom/datadog/android/core/internal/persistence/file/FileExtKt$existsSafe$1;->()V -HSPLcom/datadog/android/core/internal/persistence/file/FileExtKt$existsSafe$1;->invoke(Ljava/io/File;)Ljava/lang/Boolean; -HSPLcom/datadog/android/core/internal/persistence/file/FileExtKt$existsSafe$1;->invoke(Ljava/lang/Object;)Ljava/lang/Object; -Lcom/datadog/android/core/internal/persistence/file/FileExtKt$isDirectorySafe$1; -HSPLcom/datadog/android/core/internal/persistence/file/FileExtKt$isDirectorySafe$1;->()V -HSPLcom/datadog/android/core/internal/persistence/file/FileExtKt$isDirectorySafe$1;->()V -HSPLcom/datadog/android/core/internal/persistence/file/FileExtKt$isDirectorySafe$1;->invoke(Ljava/io/File;)Ljava/lang/Boolean; -HSPLcom/datadog/android/core/internal/persistence/file/FileExtKt$isDirectorySafe$1;->invoke(Ljava/lang/Object;)Ljava/lang/Object; -Lcom/datadog/android/core/internal/persistence/file/FileExtKt$lengthSafe$1; -HSPLcom/datadog/android/core/internal/persistence/file/FileExtKt$lengthSafe$1;->()V -HSPLcom/datadog/android/core/internal/persistence/file/FileExtKt$lengthSafe$1;->()V -HSPLcom/datadog/android/core/internal/persistence/file/FileExtKt$lengthSafe$1;->invoke(Ljava/io/File;)Ljava/lang/Long; -HSPLcom/datadog/android/core/internal/persistence/file/FileExtKt$lengthSafe$1;->invoke(Ljava/lang/Object;)Ljava/lang/Object; -Lcom/datadog/android/core/internal/persistence/file/FileExtKt$listFilesSafe$1; -HSPLcom/datadog/android/core/internal/persistence/file/FileExtKt$listFilesSafe$1;->()V -HSPLcom/datadog/android/core/internal/persistence/file/FileExtKt$listFilesSafe$1;->()V -HSPLcom/datadog/android/core/internal/persistence/file/FileExtKt$listFilesSafe$1;->invoke(Ljava/io/File;)[Ljava/io/File; -HSPLcom/datadog/android/core/internal/persistence/file/FileExtKt$listFilesSafe$1;->invoke(Ljava/lang/Object;)Ljava/lang/Object; -Lcom/datadog/android/core/internal/persistence/file/FileExtKt$listFilesSafe$2; -HSPLcom/datadog/android/core/internal/persistence/file/FileExtKt$listFilesSafe$2;->(Ljava/io/FileFilter;)V -HSPLcom/datadog/android/core/internal/persistence/file/FileExtKt$listFilesSafe$2;->invoke(Ljava/io/File;)[Ljava/io/File; -HSPLcom/datadog/android/core/internal/persistence/file/FileExtKt$listFilesSafe$2;->invoke(Ljava/lang/Object;)Ljava/lang/Object; -Lcom/datadog/android/core/internal/persistence/file/FileExtKt$mkdirsSafe$1; -HSPLcom/datadog/android/core/internal/persistence/file/FileExtKt$mkdirsSafe$1;->()V -HSPLcom/datadog/android/core/internal/persistence/file/FileExtKt$mkdirsSafe$1;->()V -HSPLcom/datadog/android/core/internal/persistence/file/FileExtKt$mkdirsSafe$1;->invoke(Ljava/io/File;)Ljava/lang/Boolean; -HSPLcom/datadog/android/core/internal/persistence/file/FileExtKt$mkdirsSafe$1;->invoke(Ljava/lang/Object;)Ljava/lang/Object; -Lcom/datadog/android/core/internal/persistence/file/FileExtKt$renameToSafe$1; -HSPLcom/datadog/android/core/internal/persistence/file/FileExtKt$renameToSafe$1;->(Ljava/io/File;)V -HSPLcom/datadog/android/core/internal/persistence/file/FileExtKt$renameToSafe$1;->invoke(Ljava/io/File;)Ljava/lang/Boolean; -HSPLcom/datadog/android/core/internal/persistence/file/FileExtKt$renameToSafe$1;->invoke(Ljava/lang/Object;)Ljava/lang/Object; +Lcom/datadog/android/core/internal/persistence/file/FileExtKt$$ExternalSyntheticLambda1; +HSPLcom/datadog/android/core/internal/persistence/file/FileExtKt$$ExternalSyntheticLambda1;->()V +HSPLcom/datadog/android/core/internal/persistence/file/FileExtKt$$ExternalSyntheticLambda1;->invoke(Ljava/lang/Object;)Ljava/lang/Object; +Lcom/datadog/android/core/internal/persistence/file/FileExtKt$$ExternalSyntheticLambda11; +HSPLcom/datadog/android/core/internal/persistence/file/FileExtKt$$ExternalSyntheticLambda11;->(Ljava/io/FileFilter;)V +HSPLcom/datadog/android/core/internal/persistence/file/FileExtKt$$ExternalSyntheticLambda11;->invoke(Ljava/lang/Object;)Ljava/lang/Object; +Lcom/datadog/android/core/internal/persistence/file/FileExtKt$$ExternalSyntheticLambda13; +HSPLcom/datadog/android/core/internal/persistence/file/FileExtKt$$ExternalSyntheticLambda13;->()V +HSPLcom/datadog/android/core/internal/persistence/file/FileExtKt$$ExternalSyntheticLambda13;->invoke(Ljava/lang/Object;)Ljava/lang/Object; +Lcom/datadog/android/core/internal/persistence/file/FileExtKt$$ExternalSyntheticLambda17; +HSPLcom/datadog/android/core/internal/persistence/file/FileExtKt$$ExternalSyntheticLambda17;->(Ljava/io/File;)V +HSPLcom/datadog/android/core/internal/persistence/file/FileExtKt$$ExternalSyntheticLambda17;->invoke(Ljava/lang/Object;)Ljava/lang/Object; +Lcom/datadog/android/core/internal/persistence/file/FileExtKt$$ExternalSyntheticLambda3; +HSPLcom/datadog/android/core/internal/persistence/file/FileExtKt$$ExternalSyntheticLambda3;->()V +HSPLcom/datadog/android/core/internal/persistence/file/FileExtKt$$ExternalSyntheticLambda3;->invoke(Ljava/lang/Object;)Ljava/lang/Object; +Lcom/datadog/android/core/internal/persistence/file/FileExtKt$$ExternalSyntheticLambda5; +HSPLcom/datadog/android/core/internal/persistence/file/FileExtKt$$ExternalSyntheticLambda5;->()V +HSPLcom/datadog/android/core/internal/persistence/file/FileExtKt$$ExternalSyntheticLambda5;->invoke(Ljava/lang/Object;)Ljava/lang/Object; +Lcom/datadog/android/core/internal/persistence/file/FileExtKt$$ExternalSyntheticLambda6; +HSPLcom/datadog/android/core/internal/persistence/file/FileExtKt$$ExternalSyntheticLambda6;->()V +HSPLcom/datadog/android/core/internal/persistence/file/FileExtKt$$ExternalSyntheticLambda6;->invoke(Ljava/lang/Object;)Ljava/lang/Object; +Lcom/datadog/android/core/internal/persistence/file/FileExtKt$$ExternalSyntheticLambda8; +HSPLcom/datadog/android/core/internal/persistence/file/FileExtKt$$ExternalSyntheticLambda8;->()V +HSPLcom/datadog/android/core/internal/persistence/file/FileExtKt$$ExternalSyntheticLambda8;->invoke(Ljava/lang/Object;)Ljava/lang/Object; Lcom/datadog/android/core/internal/persistence/file/FileMover; HSPLcom/datadog/android/core/internal/persistence/file/FileMover;->()V HSPLcom/datadog/android/core/internal/persistence/file/FileMover;->(Lcom/datadog/android/api/InternalLogger;)V @@ -19479,30 +19562,25 @@ HSPLcom/datadog/android/core/internal/persistence/file/advanced/FeatureFileOrche Lcom/datadog/android/core/internal/persistence/file/advanced/MoveDataMigrationOperation; HSPLcom/datadog/android/core/internal/persistence/file/advanced/MoveDataMigrationOperation;->()V HSPLcom/datadog/android/core/internal/persistence/file/advanced/MoveDataMigrationOperation;->(Ljava/io/File;Ljava/io/File;Lcom/datadog/android/core/internal/persistence/file/FileMover;Lcom/datadog/android/api/InternalLogger;)V -HSPLcom/datadog/android/core/internal/persistence/file/advanced/MoveDataMigrationOperation;->getFileMover$dd_sdk_android_core_release()Lcom/datadog/android/core/internal/persistence/file/FileMover; -HSPLcom/datadog/android/core/internal/persistence/file/advanced/MoveDataMigrationOperation;->getFromDir$dd_sdk_android_core_release()Ljava/io/File; -HSPLcom/datadog/android/core/internal/persistence/file/advanced/MoveDataMigrationOperation;->getToDir$dd_sdk_android_core_release()Ljava/io/File; +HSPLcom/datadog/android/core/internal/persistence/file/advanced/MoveDataMigrationOperation;->run$lambda$2(Lcom/datadog/android/core/internal/persistence/file/advanced/MoveDataMigrationOperation;)Z HSPLcom/datadog/android/core/internal/persistence/file/advanced/MoveDataMigrationOperation;->run()V +Lcom/datadog/android/core/internal/persistence/file/advanced/MoveDataMigrationOperation$$ExternalSyntheticLambda2; +HSPLcom/datadog/android/core/internal/persistence/file/advanced/MoveDataMigrationOperation$$ExternalSyntheticLambda2;->(Lcom/datadog/android/core/internal/persistence/file/advanced/MoveDataMigrationOperation;)V +HSPLcom/datadog/android/core/internal/persistence/file/advanced/MoveDataMigrationOperation$$ExternalSyntheticLambda2;->invoke()Ljava/lang/Object; Lcom/datadog/android/core/internal/persistence/file/advanced/MoveDataMigrationOperation$Companion; HSPLcom/datadog/android/core/internal/persistence/file/advanced/MoveDataMigrationOperation$Companion;->()V HSPLcom/datadog/android/core/internal/persistence/file/advanced/MoveDataMigrationOperation$Companion;->(Lkotlin/jvm/internal/DefaultConstructorMarker;)V -Lcom/datadog/android/core/internal/persistence/file/advanced/MoveDataMigrationOperation$run$3; -HSPLcom/datadog/android/core/internal/persistence/file/advanced/MoveDataMigrationOperation$run$3;->(Lcom/datadog/android/core/internal/persistence/file/advanced/MoveDataMigrationOperation;)V -HSPLcom/datadog/android/core/internal/persistence/file/advanced/MoveDataMigrationOperation$run$3;->invoke()Ljava/lang/Boolean; -HSPLcom/datadog/android/core/internal/persistence/file/advanced/MoveDataMigrationOperation$run$3;->invoke()Ljava/lang/Object; Lcom/datadog/android/core/internal/persistence/file/advanced/WipeDataMigrationOperation; HSPLcom/datadog/android/core/internal/persistence/file/advanced/WipeDataMigrationOperation;->()V HSPLcom/datadog/android/core/internal/persistence/file/advanced/WipeDataMigrationOperation;->(Ljava/io/File;Lcom/datadog/android/core/internal/persistence/file/FileMover;Lcom/datadog/android/api/InternalLogger;)V -HSPLcom/datadog/android/core/internal/persistence/file/advanced/WipeDataMigrationOperation;->getFileMover$dd_sdk_android_core_release()Lcom/datadog/android/core/internal/persistence/file/FileMover; -HSPLcom/datadog/android/core/internal/persistence/file/advanced/WipeDataMigrationOperation;->getTargetDir$dd_sdk_android_core_release()Ljava/io/File; +HSPLcom/datadog/android/core/internal/persistence/file/advanced/WipeDataMigrationOperation;->run$lambda$1(Lcom/datadog/android/core/internal/persistence/file/advanced/WipeDataMigrationOperation;)Z HSPLcom/datadog/android/core/internal/persistence/file/advanced/WipeDataMigrationOperation;->run()V +Lcom/datadog/android/core/internal/persistence/file/advanced/WipeDataMigrationOperation$$ExternalSyntheticLambda1; +HSPLcom/datadog/android/core/internal/persistence/file/advanced/WipeDataMigrationOperation$$ExternalSyntheticLambda1;->(Lcom/datadog/android/core/internal/persistence/file/advanced/WipeDataMigrationOperation;)V +HSPLcom/datadog/android/core/internal/persistence/file/advanced/WipeDataMigrationOperation$$ExternalSyntheticLambda1;->invoke()Ljava/lang/Object; Lcom/datadog/android/core/internal/persistence/file/advanced/WipeDataMigrationOperation$Companion; HSPLcom/datadog/android/core/internal/persistence/file/advanced/WipeDataMigrationOperation$Companion;->()V HSPLcom/datadog/android/core/internal/persistence/file/advanced/WipeDataMigrationOperation$Companion;->(Lkotlin/jvm/internal/DefaultConstructorMarker;)V -Lcom/datadog/android/core/internal/persistence/file/advanced/WipeDataMigrationOperation$run$2; -HSPLcom/datadog/android/core/internal/persistence/file/advanced/WipeDataMigrationOperation$run$2;->(Lcom/datadog/android/core/internal/persistence/file/advanced/WipeDataMigrationOperation;)V -HSPLcom/datadog/android/core/internal/persistence/file/advanced/WipeDataMigrationOperation$run$2;->invoke()Ljava/lang/Boolean; -HSPLcom/datadog/android/core/internal/persistence/file/advanced/WipeDataMigrationOperation$run$2;->invoke()Ljava/lang/Object; Lcom/datadog/android/core/internal/persistence/file/batch/BatchFileOrchestrator; HSPLcom/datadog/android/core/internal/persistence/file/batch/BatchFileOrchestrator;->()V HSPLcom/datadog/android/core/internal/persistence/file/batch/BatchFileOrchestrator;->(Ljava/io/File;Lcom/datadog/android/core/internal/persistence/file/FilePersistenceConfig;Lcom/datadog/android/api/InternalLogger;Lcom/datadog/android/core/internal/metrics/MetricsDispatcher;Lcom/datadog/android/internal/time/TimeProvider;Ljava/util/concurrent/atomic/AtomicInteger;)V @@ -19591,8 +19669,8 @@ HSPLcom/datadog/android/core/internal/persistence/tlvformat/TLVBlockType;->$valu HSPLcom/datadog/android/core/internal/persistence/tlvformat/TLVBlockType;->()V HSPLcom/datadog/android/core/internal/persistence/tlvformat/TLVBlockType;->(Ljava/lang/String;IS)V HSPLcom/datadog/android/core/internal/persistence/tlvformat/TLVBlockType;->access$getMap$cp()Ljava/util/Map; +HSPLcom/datadog/android/core/internal/persistence/tlvformat/TLVBlockType;->getEntries()Lkotlin/enums/EnumEntries; HSPLcom/datadog/android/core/internal/persistence/tlvformat/TLVBlockType;->getRawValue-Mh2AYeg()S -HSPLcom/datadog/android/core/internal/persistence/tlvformat/TLVBlockType;->values()[Lcom/datadog/android/core/internal/persistence/tlvformat/TLVBlockType; Lcom/datadog/android/core/internal/persistence/tlvformat/TLVBlockType$Companion; HSPLcom/datadog/android/core/internal/persistence/tlvformat/TLVBlockType$Companion;->()V HSPLcom/datadog/android/core/internal/persistence/tlvformat/TLVBlockType$Companion;->(Lkotlin/jvm/internal/DefaultConstructorMarker;)V @@ -19614,6 +19692,7 @@ HSPLcom/datadog/android/core/internal/receiver/ThreadSafeReceiver;->registerRece Lcom/datadog/android/core/internal/receiver/ThreadSafeReceiver$Companion; HSPLcom/datadog/android/core/internal/receiver/ThreadSafeReceiver$Companion;->()V HSPLcom/datadog/android/core/internal/receiver/ThreadSafeReceiver$Companion;->(Lkotlin/jvm/internal/DefaultConstructorMarker;)V +Lcom/datadog/android/core/internal/remote/RemoteConfigService$Factory; Lcom/datadog/android/core/internal/storage/TelemetryAwareEventBatchWriter; Lcom/datadog/android/core/internal/system/AndroidInfoProvider; Lcom/datadog/android/core/internal/system/AppVersionProvider; @@ -19633,7 +19712,11 @@ HSPLcom/datadog/android/core/internal/system/DefaultAndroidInfoProvider;->(Landroid/content/Context;)V HSPLcom/datadog/android/core/internal/system/DefaultAndroidInfoProvider;->(Landroid/content/Context;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Lcom/datadog/android/internal/system/BuildSdkVersionProvider;)V HSPLcom/datadog/android/core/internal/system/DefaultAndroidInfoProvider;->(Landroid/content/Context;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Lcom/datadog/android/internal/system/BuildSdkVersionProvider;ILkotlin/jvm/internal/DefaultConstructorMarker;)V -HSPLcom/datadog/android/core/internal/system/DefaultAndroidInfoProvider;->access$getBuildSdkVersionProvider$p(Lcom/datadog/android/core/internal/system/DefaultAndroidInfoProvider;)Lcom/datadog/android/internal/system/BuildSdkVersionProvider; +HSPLcom/datadog/android/core/internal/system/DefaultAndroidInfoProvider;->architecture_delegate$lambda$10()Ljava/lang/String; +HSPLcom/datadog/android/core/internal/system/DefaultAndroidInfoProvider;->currentLocale_delegate$lambda$5(Landroid/content/Context;Lcom/datadog/android/core/internal/system/DefaultAndroidInfoProvider;)Ljava/lang/String; +HSPLcom/datadog/android/core/internal/system/DefaultAndroidInfoProvider;->deviceBrand_delegate$lambda$8(Ljava/lang/String;)Ljava/lang/String; +HSPLcom/datadog/android/core/internal/system/DefaultAndroidInfoProvider;->deviceName_delegate$lambda$1(Lcom/datadog/android/core/internal/system/DefaultAndroidInfoProvider;)Ljava/lang/String; +HSPLcom/datadog/android/core/internal/system/DefaultAndroidInfoProvider;->deviceType_delegate$lambda$0(Ljava/lang/String;Landroid/content/Context;)Lcom/datadog/android/api/context/DeviceType; HSPLcom/datadog/android/core/internal/system/DefaultAndroidInfoProvider;->getArchitecture()Ljava/lang/String; HSPLcom/datadog/android/core/internal/system/DefaultAndroidInfoProvider;->getCurrentLocale()Ljava/lang/String; HSPLcom/datadog/android/core/internal/system/DefaultAndroidInfoProvider;->getDeviceBrand()Ljava/lang/String; @@ -19650,6 +19733,49 @@ HSPLcom/datadog/android/core/internal/system/DefaultAndroidInfoProvider;->getOsV HSPLcom/datadog/android/core/internal/system/DefaultAndroidInfoProvider;->getTimeZone()Ljava/lang/String; HSPLcom/datadog/android/core/internal/system/DefaultAndroidInfoProvider;->getTotalRam()Ljava/lang/Integer; HSPLcom/datadog/android/core/internal/system/DefaultAndroidInfoProvider;->isLowRam()Ljava/lang/Boolean; +HSPLcom/datadog/android/core/internal/system/DefaultAndroidInfoProvider;->isLowRam_delegate$lambda$15(Landroid/content/Context;)Ljava/lang/Boolean; +HSPLcom/datadog/android/core/internal/system/DefaultAndroidInfoProvider;->locales_delegate$lambda$4(Landroid/content/Context;Lcom/datadog/android/core/internal/system/DefaultAndroidInfoProvider;)Ljava/util/List; +HSPLcom/datadog/android/core/internal/system/DefaultAndroidInfoProvider;->logicalCpuCount_delegate$lambda$13()I +HSPLcom/datadog/android/core/internal/system/DefaultAndroidInfoProvider;->numberOfDisplays_delegate$lambda$12(Landroid/content/Context;)Ljava/lang/Integer; +HSPLcom/datadog/android/core/internal/system/DefaultAndroidInfoProvider;->osMajorVersion_delegate$lambda$9(Lcom/datadog/android/core/internal/system/DefaultAndroidInfoProvider;)Ljava/lang/String; +HSPLcom/datadog/android/core/internal/system/DefaultAndroidInfoProvider;->timeZone_delegate$lambda$6()Ljava/lang/String; +HSPLcom/datadog/android/core/internal/system/DefaultAndroidInfoProvider;->totalRam_delegate$lambda$14(Landroid/content/Context;)Ljava/lang/Integer; +Lcom/datadog/android/core/internal/system/DefaultAndroidInfoProvider$$ExternalSyntheticLambda0; +HSPLcom/datadog/android/core/internal/system/DefaultAndroidInfoProvider$$ExternalSyntheticLambda0;->(Ljava/lang/String;Landroid/content/Context;)V +HSPLcom/datadog/android/core/internal/system/DefaultAndroidInfoProvider$$ExternalSyntheticLambda0;->invoke()Ljava/lang/Object; +Lcom/datadog/android/core/internal/system/DefaultAndroidInfoProvider$$ExternalSyntheticLambda1; +HSPLcom/datadog/android/core/internal/system/DefaultAndroidInfoProvider$$ExternalSyntheticLambda1;->(Landroid/content/Context;)V +HSPLcom/datadog/android/core/internal/system/DefaultAndroidInfoProvider$$ExternalSyntheticLambda1;->invoke()Ljava/lang/Object; +Lcom/datadog/android/core/internal/system/DefaultAndroidInfoProvider$$ExternalSyntheticLambda10; +HSPLcom/datadog/android/core/internal/system/DefaultAndroidInfoProvider$$ExternalSyntheticLambda10;->(Lcom/datadog/android/core/internal/system/DefaultAndroidInfoProvider;)V +HSPLcom/datadog/android/core/internal/system/DefaultAndroidInfoProvider$$ExternalSyntheticLambda10;->invoke()Ljava/lang/Object; +Lcom/datadog/android/core/internal/system/DefaultAndroidInfoProvider$$ExternalSyntheticLambda11; +HSPLcom/datadog/android/core/internal/system/DefaultAndroidInfoProvider$$ExternalSyntheticLambda11;->()V +HSPLcom/datadog/android/core/internal/system/DefaultAndroidInfoProvider$$ExternalSyntheticLambda11;->invoke()Ljava/lang/Object; +Lcom/datadog/android/core/internal/system/DefaultAndroidInfoProvider$$ExternalSyntheticLambda2; +HSPLcom/datadog/android/core/internal/system/DefaultAndroidInfoProvider$$ExternalSyntheticLambda2;->()V +HSPLcom/datadog/android/core/internal/system/DefaultAndroidInfoProvider$$ExternalSyntheticLambda2;->invoke()Ljava/lang/Object; +Lcom/datadog/android/core/internal/system/DefaultAndroidInfoProvider$$ExternalSyntheticLambda3; +HSPLcom/datadog/android/core/internal/system/DefaultAndroidInfoProvider$$ExternalSyntheticLambda3;->(Landroid/content/Context;)V +HSPLcom/datadog/android/core/internal/system/DefaultAndroidInfoProvider$$ExternalSyntheticLambda3;->invoke()Ljava/lang/Object; +Lcom/datadog/android/core/internal/system/DefaultAndroidInfoProvider$$ExternalSyntheticLambda4; +HSPLcom/datadog/android/core/internal/system/DefaultAndroidInfoProvider$$ExternalSyntheticLambda4;->(Landroid/content/Context;)V +HSPLcom/datadog/android/core/internal/system/DefaultAndroidInfoProvider$$ExternalSyntheticLambda4;->invoke()Ljava/lang/Object; +Lcom/datadog/android/core/internal/system/DefaultAndroidInfoProvider$$ExternalSyntheticLambda5; +HSPLcom/datadog/android/core/internal/system/DefaultAndroidInfoProvider$$ExternalSyntheticLambda5;->(Lcom/datadog/android/core/internal/system/DefaultAndroidInfoProvider;)V +HSPLcom/datadog/android/core/internal/system/DefaultAndroidInfoProvider$$ExternalSyntheticLambda5;->invoke()Ljava/lang/Object; +Lcom/datadog/android/core/internal/system/DefaultAndroidInfoProvider$$ExternalSyntheticLambda6; +HSPLcom/datadog/android/core/internal/system/DefaultAndroidInfoProvider$$ExternalSyntheticLambda6;->(Landroid/content/Context;Lcom/datadog/android/core/internal/system/DefaultAndroidInfoProvider;)V +HSPLcom/datadog/android/core/internal/system/DefaultAndroidInfoProvider$$ExternalSyntheticLambda6;->invoke()Ljava/lang/Object; +Lcom/datadog/android/core/internal/system/DefaultAndroidInfoProvider$$ExternalSyntheticLambda7; +HSPLcom/datadog/android/core/internal/system/DefaultAndroidInfoProvider$$ExternalSyntheticLambda7;->(Landroid/content/Context;Lcom/datadog/android/core/internal/system/DefaultAndroidInfoProvider;)V +HSPLcom/datadog/android/core/internal/system/DefaultAndroidInfoProvider$$ExternalSyntheticLambda7;->invoke()Ljava/lang/Object; +Lcom/datadog/android/core/internal/system/DefaultAndroidInfoProvider$$ExternalSyntheticLambda8; +HSPLcom/datadog/android/core/internal/system/DefaultAndroidInfoProvider$$ExternalSyntheticLambda8;->()V +HSPLcom/datadog/android/core/internal/system/DefaultAndroidInfoProvider$$ExternalSyntheticLambda8;->invoke()Ljava/lang/Object; +Lcom/datadog/android/core/internal/system/DefaultAndroidInfoProvider$$ExternalSyntheticLambda9; +HSPLcom/datadog/android/core/internal/system/DefaultAndroidInfoProvider$$ExternalSyntheticLambda9;->(Ljava/lang/String;)V +HSPLcom/datadog/android/core/internal/system/DefaultAndroidInfoProvider$$ExternalSyntheticLambda9;->invoke()Ljava/lang/Object; Lcom/datadog/android/core/internal/system/DefaultAndroidInfoProvider$Companion; HSPLcom/datadog/android/core/internal/system/DefaultAndroidInfoProvider$Companion;->()V HSPLcom/datadog/android/core/internal/system/DefaultAndroidInfoProvider$Companion;->(Lkotlin/jvm/internal/DefaultConstructorMarker;)V @@ -19659,57 +19785,6 @@ HSPLcom/datadog/android/core/internal/system/DefaultAndroidInfoProvider$Companio HSPLcom/datadog/android/core/internal/system/DefaultAndroidInfoProvider$Companion;->isTablet(Ljava/lang/String;Landroid/content/Context;)Z HSPLcom/datadog/android/core/internal/system/DefaultAndroidInfoProvider$Companion;->isTv(Landroid/content/Context;)Z HSPLcom/datadog/android/core/internal/system/DefaultAndroidInfoProvider$Companion;->resolveDeviceType(Ljava/lang/String;Landroid/content/Context;)Lcom/datadog/android/api/context/DeviceType; -Lcom/datadog/android/core/internal/system/DefaultAndroidInfoProvider$architecture$2; -HSPLcom/datadog/android/core/internal/system/DefaultAndroidInfoProvider$architecture$2;->()V -HSPLcom/datadog/android/core/internal/system/DefaultAndroidInfoProvider$architecture$2;->()V -HSPLcom/datadog/android/core/internal/system/DefaultAndroidInfoProvider$architecture$2;->invoke()Ljava/lang/Object; -HSPLcom/datadog/android/core/internal/system/DefaultAndroidInfoProvider$architecture$2;->invoke()Ljava/lang/String; -Lcom/datadog/android/core/internal/system/DefaultAndroidInfoProvider$currentLocale$2; -HSPLcom/datadog/android/core/internal/system/DefaultAndroidInfoProvider$currentLocale$2;->(Landroid/content/Context;Lcom/datadog/android/core/internal/system/DefaultAndroidInfoProvider;)V -HSPLcom/datadog/android/core/internal/system/DefaultAndroidInfoProvider$currentLocale$2;->invoke()Ljava/lang/Object; -HSPLcom/datadog/android/core/internal/system/DefaultAndroidInfoProvider$currentLocale$2;->invoke()Ljava/lang/String; -Lcom/datadog/android/core/internal/system/DefaultAndroidInfoProvider$deviceBrand$2; -HSPLcom/datadog/android/core/internal/system/DefaultAndroidInfoProvider$deviceBrand$2;->(Ljava/lang/String;)V -HSPLcom/datadog/android/core/internal/system/DefaultAndroidInfoProvider$deviceBrand$2;->invoke()Ljava/lang/Object; -HSPLcom/datadog/android/core/internal/system/DefaultAndroidInfoProvider$deviceBrand$2;->invoke()Ljava/lang/String; -Lcom/datadog/android/core/internal/system/DefaultAndroidInfoProvider$deviceName$2; -HSPLcom/datadog/android/core/internal/system/DefaultAndroidInfoProvider$deviceName$2;->(Lcom/datadog/android/core/internal/system/DefaultAndroidInfoProvider;)V -HSPLcom/datadog/android/core/internal/system/DefaultAndroidInfoProvider$deviceName$2;->invoke()Ljava/lang/Object; -HSPLcom/datadog/android/core/internal/system/DefaultAndroidInfoProvider$deviceName$2;->invoke()Ljava/lang/String; -Lcom/datadog/android/core/internal/system/DefaultAndroidInfoProvider$deviceType$2; -HSPLcom/datadog/android/core/internal/system/DefaultAndroidInfoProvider$deviceType$2;->(Ljava/lang/String;Landroid/content/Context;)V -HSPLcom/datadog/android/core/internal/system/DefaultAndroidInfoProvider$deviceType$2;->invoke()Lcom/datadog/android/api/context/DeviceType; -HSPLcom/datadog/android/core/internal/system/DefaultAndroidInfoProvider$deviceType$2;->invoke()Ljava/lang/Object; -Lcom/datadog/android/core/internal/system/DefaultAndroidInfoProvider$isLowRam$2; -HSPLcom/datadog/android/core/internal/system/DefaultAndroidInfoProvider$isLowRam$2;->(Landroid/content/Context;)V -HSPLcom/datadog/android/core/internal/system/DefaultAndroidInfoProvider$isLowRam$2;->invoke()Ljava/lang/Boolean; -HSPLcom/datadog/android/core/internal/system/DefaultAndroidInfoProvider$isLowRam$2;->invoke()Ljava/lang/Object; -Lcom/datadog/android/core/internal/system/DefaultAndroidInfoProvider$locales$2; -HSPLcom/datadog/android/core/internal/system/DefaultAndroidInfoProvider$locales$2;->(Landroid/content/Context;Lcom/datadog/android/core/internal/system/DefaultAndroidInfoProvider;)V -HSPLcom/datadog/android/core/internal/system/DefaultAndroidInfoProvider$locales$2;->invoke()Ljava/lang/Object; -HSPLcom/datadog/android/core/internal/system/DefaultAndroidInfoProvider$locales$2;->invoke()Ljava/util/List; -Lcom/datadog/android/core/internal/system/DefaultAndroidInfoProvider$logicalCpuCount$2; -HSPLcom/datadog/android/core/internal/system/DefaultAndroidInfoProvider$logicalCpuCount$2;->()V -HSPLcom/datadog/android/core/internal/system/DefaultAndroidInfoProvider$logicalCpuCount$2;->()V -HSPLcom/datadog/android/core/internal/system/DefaultAndroidInfoProvider$logicalCpuCount$2;->invoke()Ljava/lang/Integer; -HSPLcom/datadog/android/core/internal/system/DefaultAndroidInfoProvider$logicalCpuCount$2;->invoke()Ljava/lang/Object; -Lcom/datadog/android/core/internal/system/DefaultAndroidInfoProvider$numberOfDisplays$2; -HSPLcom/datadog/android/core/internal/system/DefaultAndroidInfoProvider$numberOfDisplays$2;->(Landroid/content/Context;)V -HSPLcom/datadog/android/core/internal/system/DefaultAndroidInfoProvider$numberOfDisplays$2;->invoke()Ljava/lang/Integer; -HSPLcom/datadog/android/core/internal/system/DefaultAndroidInfoProvider$numberOfDisplays$2;->invoke()Ljava/lang/Object; -Lcom/datadog/android/core/internal/system/DefaultAndroidInfoProvider$osMajorVersion$2; -HSPLcom/datadog/android/core/internal/system/DefaultAndroidInfoProvider$osMajorVersion$2;->(Lcom/datadog/android/core/internal/system/DefaultAndroidInfoProvider;)V -HSPLcom/datadog/android/core/internal/system/DefaultAndroidInfoProvider$osMajorVersion$2;->invoke()Ljava/lang/Object; -HSPLcom/datadog/android/core/internal/system/DefaultAndroidInfoProvider$osMajorVersion$2;->invoke()Ljava/lang/String; -Lcom/datadog/android/core/internal/system/DefaultAndroidInfoProvider$timeZone$2; -HSPLcom/datadog/android/core/internal/system/DefaultAndroidInfoProvider$timeZone$2;->()V -HSPLcom/datadog/android/core/internal/system/DefaultAndroidInfoProvider$timeZone$2;->()V -HSPLcom/datadog/android/core/internal/system/DefaultAndroidInfoProvider$timeZone$2;->invoke()Ljava/lang/Object; -HSPLcom/datadog/android/core/internal/system/DefaultAndroidInfoProvider$timeZone$2;->invoke()Ljava/lang/String; -Lcom/datadog/android/core/internal/system/DefaultAndroidInfoProvider$totalRam$2; -HSPLcom/datadog/android/core/internal/system/DefaultAndroidInfoProvider$totalRam$2;->(Landroid/content/Context;)V -HSPLcom/datadog/android/core/internal/system/DefaultAndroidInfoProvider$totalRam$2;->invoke()Ljava/lang/Integer; -HSPLcom/datadog/android/core/internal/system/DefaultAndroidInfoProvider$totalRam$2;->invoke()Ljava/lang/Object; Lcom/datadog/android/core/internal/system/DefaultAppVersionProvider; HSPLcom/datadog/android/core/internal/system/DefaultAppVersionProvider;->(Ljava/lang/String;I)V HSPLcom/datadog/android/core/internal/system/DefaultAppVersionProvider;->getVersion()Ljava/lang/String; @@ -19747,13 +19822,12 @@ HSPLcom/datadog/android/core/internal/thread/BackPressureExecutorService$Compani Lcom/datadog/android/core/internal/thread/BackPressuredBlockingQueue; HSPLcom/datadog/android/core/internal/thread/BackPressuredBlockingQueue;->(Lcom/datadog/android/api/InternalLogger;Ljava/lang/String;IILkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function1;Lcom/datadog/android/core/configuration/BackPressureMitigation;Lcom/datadog/android/internal/time/TimeProvider;)V HSPLcom/datadog/android/core/internal/thread/BackPressuredBlockingQueue;->(Lcom/datadog/android/api/InternalLogger;Ljava/lang/String;Lcom/datadog/android/core/configuration/BackPressureStrategy;Lcom/datadog/android/internal/time/TimeProvider;)V -HSPLcom/datadog/android/core/internal/thread/BackPressuredBlockingQueue;->access$offer$s-727953952(Lcom/datadog/android/core/internal/thread/BackPressuredBlockingQueue;Ljava/lang/Object;)Z HSPLcom/datadog/android/core/internal/thread/BackPressuredBlockingQueue;->addWithBackPressure(Ljava/lang/Object;Lkotlin/jvm/functions/Function1;)Z +HSPLcom/datadog/android/core/internal/thread/BackPressuredBlockingQueue;->offer$lambda$0(Lcom/datadog/android/core/internal/thread/BackPressuredBlockingQueue;Ljava/lang/Object;)Z HSPLcom/datadog/android/core/internal/thread/BackPressuredBlockingQueue;->offer(Ljava/lang/Object;)Z -Lcom/datadog/android/core/internal/thread/BackPressuredBlockingQueue$offer$1; -HSPLcom/datadog/android/core/internal/thread/BackPressuredBlockingQueue$offer$1;->(Lcom/datadog/android/core/internal/thread/BackPressuredBlockingQueue;)V -HSPLcom/datadog/android/core/internal/thread/BackPressuredBlockingQueue$offer$1;->invoke(Ljava/lang/Object;)Ljava/lang/Boolean; -HSPLcom/datadog/android/core/internal/thread/BackPressuredBlockingQueue$offer$1;->invoke(Ljava/lang/Object;)Ljava/lang/Object; +Lcom/datadog/android/core/internal/thread/BackPressuredBlockingQueue$$ExternalSyntheticLambda2; +HSPLcom/datadog/android/core/internal/thread/BackPressuredBlockingQueue$$ExternalSyntheticLambda2;->(Lcom/datadog/android/core/internal/thread/BackPressuredBlockingQueue;)V +HSPLcom/datadog/android/core/internal/thread/BackPressuredBlockingQueue$$ExternalSyntheticLambda2;->invoke(Ljava/lang/Object;)Ljava/lang/Object; Lcom/datadog/android/core/internal/thread/BroadcastReceiverThread; HSPLcom/datadog/android/core/internal/thread/BroadcastReceiverThread;->()V Lcom/datadog/android/core/internal/thread/DatadogThreadFactory; @@ -19762,8 +19836,8 @@ HSPLcom/datadog/android/core/internal/thread/DatadogThreadFactory;->newThread(Lj Lcom/datadog/android/core/internal/thread/LoggingScheduledThreadPoolExecutor; HSPLcom/datadog/android/core/internal/thread/LoggingScheduledThreadPoolExecutor;->(ILjava/lang/String;Lcom/datadog/android/api/InternalLogger;Lcom/datadog/android/core/configuration/BackPressureStrategy;)V HSPLcom/datadog/android/core/internal/thread/LoggingScheduledThreadPoolExecutor;->afterExecute(Ljava/lang/Runnable;Ljava/lang/Throwable;)V -Lcom/datadog/android/core/internal/thread/LoggingScheduledThreadPoolExecutor$$ExternalSyntheticLambda0; -HSPLcom/datadog/android/core/internal/thread/LoggingScheduledThreadPoolExecutor$$ExternalSyntheticLambda0;->(Lcom/datadog/android/api/InternalLogger;Ljava/lang/String;Lcom/datadog/android/core/configuration/BackPressureStrategy;)V +Lcom/datadog/android/core/internal/thread/LoggingScheduledThreadPoolExecutor$$ExternalSyntheticLambda1; +HSPLcom/datadog/android/core/internal/thread/LoggingScheduledThreadPoolExecutor$$ExternalSyntheticLambda1;->(Lcom/datadog/android/api/InternalLogger;Ljava/lang/String;Lcom/datadog/android/core/configuration/BackPressureStrategy;)V Lcom/datadog/android/core/internal/thread/NamedFutureTask; HSPLcom/datadog/android/core/internal/thread/NamedFutureTask;->(Ljava/lang/String;Ljava/util/concurrent/Callable;)V Lcom/datadog/android/core/internal/thread/ObservableLinkedBlockingQueue; @@ -19787,17 +19861,14 @@ Lcom/datadog/android/core/internal/time/DefaultAppStartTimeProvider; HSPLcom/datadog/android/core/internal/time/DefaultAppStartTimeProvider;->()V HSPLcom/datadog/android/core/internal/time/DefaultAppStartTimeProvider;->(Lkotlin/jvm/functions/Function0;Lcom/datadog/android/internal/system/BuildSdkVersionProvider;)V HSPLcom/datadog/android/core/internal/time/DefaultAppStartTimeProvider;->(Lkotlin/jvm/functions/Function0;Lcom/datadog/android/internal/system/BuildSdkVersionProvider;ILkotlin/jvm/internal/DefaultConstructorMarker;)V -HSPLcom/datadog/android/core/internal/time/DefaultAppStartTimeProvider;->access$getPROCESS_START_TO_CP_START_DIFF_THRESHOLD_NS$cp()J -HSPLcom/datadog/android/core/internal/time/DefaultAppStartTimeProvider;->access$getTimeProviderFactory$p(Lcom/datadog/android/core/internal/time/DefaultAppStartTimeProvider;)Lkotlin/jvm/functions/Function0; +HSPLcom/datadog/android/core/internal/time/DefaultAppStartTimeProvider;->appStartTimeNs_delegate$lambda$0(Lcom/datadog/android/internal/system/BuildSdkVersionProvider;Lcom/datadog/android/core/internal/time/DefaultAppStartTimeProvider;)J HSPLcom/datadog/android/core/internal/time/DefaultAppStartTimeProvider;->getAppStartTimeNs()J +Lcom/datadog/android/core/internal/time/DefaultAppStartTimeProvider$$ExternalSyntheticLambda0; +HSPLcom/datadog/android/core/internal/time/DefaultAppStartTimeProvider$$ExternalSyntheticLambda0;->(Lcom/datadog/android/internal/system/BuildSdkVersionProvider;Lcom/datadog/android/core/internal/time/DefaultAppStartTimeProvider;)V +HSPLcom/datadog/android/core/internal/time/DefaultAppStartTimeProvider$$ExternalSyntheticLambda0;->invoke()Ljava/lang/Object; Lcom/datadog/android/core/internal/time/DefaultAppStartTimeProvider$Companion; HSPLcom/datadog/android/core/internal/time/DefaultAppStartTimeProvider$Companion;->()V HSPLcom/datadog/android/core/internal/time/DefaultAppStartTimeProvider$Companion;->(Lkotlin/jvm/internal/DefaultConstructorMarker;)V -HSPLcom/datadog/android/core/internal/time/DefaultAppStartTimeProvider$Companion;->getPROCESS_START_TO_CP_START_DIFF_THRESHOLD_NS$dd_sdk_android_core_release()J -Lcom/datadog/android/core/internal/time/DefaultAppStartTimeProvider$appStartTimeNs$2; -HSPLcom/datadog/android/core/internal/time/DefaultAppStartTimeProvider$appStartTimeNs$2;->(Lcom/datadog/android/internal/system/BuildSdkVersionProvider;Lcom/datadog/android/core/internal/time/DefaultAppStartTimeProvider;)V -HSPLcom/datadog/android/core/internal/time/DefaultAppStartTimeProvider$appStartTimeNs$2;->invoke()Ljava/lang/Long; -HSPLcom/datadog/android/core/internal/time/DefaultAppStartTimeProvider$appStartTimeNs$2;->invoke()Ljava/lang/Object; Lcom/datadog/android/core/internal/time/KronosTimeProvider; HSPLcom/datadog/android/core/internal/time/KronosTimeProvider;->()V HSPLcom/datadog/android/core/internal/time/KronosTimeProvider;->(Lcom/lyft/kronos/Clock;Lcom/datadog/android/api/InternalLogger;)V @@ -19876,12 +19947,12 @@ Lcom/datadog/android/core/sampling/DeterministicSampler; HSPLcom/datadog/android/core/sampling/DeterministicSampler;->()V HSPLcom/datadog/android/core/sampling/DeterministicSampler;->(Lkotlin/jvm/functions/Function1;F)V HSPLcom/datadog/android/core/sampling/DeterministicSampler;->(Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function0;)V +HSPLcom/datadog/android/core/sampling/DeterministicSampler;->_init_$lambda$0(F)F HSPLcom/datadog/android/core/sampling/DeterministicSampler;->getSampleRate()Ljava/lang/Float; HSPLcom/datadog/android/core/sampling/DeterministicSampler;->sample(Ljava/lang/Object;)Z -Lcom/datadog/android/core/sampling/DeterministicSampler$1; -HSPLcom/datadog/android/core/sampling/DeterministicSampler$1;->(F)V -HSPLcom/datadog/android/core/sampling/DeterministicSampler$1;->invoke()Ljava/lang/Float; -HSPLcom/datadog/android/core/sampling/DeterministicSampler$1;->invoke()Ljava/lang/Object; +Lcom/datadog/android/core/sampling/DeterministicSampler$$ExternalSyntheticLambda0; +HSPLcom/datadog/android/core/sampling/DeterministicSampler$$ExternalSyntheticLambda0;->(F)V +HSPLcom/datadog/android/core/sampling/DeterministicSampler$$ExternalSyntheticLambda0;->invoke()Ljava/lang/Object; Lcom/datadog/android/core/sampling/DeterministicSampler$Companion; HSPLcom/datadog/android/core/sampling/DeterministicSampler$Companion;->()V HSPLcom/datadog/android/core/sampling/DeterministicSampler$Companion;->(Lkotlin/jvm/internal/DefaultConstructorMarker;)V @@ -19889,21 +19960,20 @@ Lcom/datadog/android/core/sampling/RateBasedSampler; HSPLcom/datadog/android/core/sampling/RateBasedSampler;->()V HSPLcom/datadog/android/core/sampling/RateBasedSampler;->(F)V HSPLcom/datadog/android/core/sampling/RateBasedSampler;->(Lkotlin/jvm/functions/Function0;)V +HSPLcom/datadog/android/core/sampling/RateBasedSampler;->_init_$lambda$0(F)F HSPLcom/datadog/android/core/sampling/RateBasedSampler;->getRandom()Ljava/security/SecureRandom; HSPLcom/datadog/android/core/sampling/RateBasedSampler;->getSampleRate()Ljava/lang/Float; +HSPLcom/datadog/android/core/sampling/RateBasedSampler;->random_delegate$lambda$1()Ljava/security/SecureRandom; HSPLcom/datadog/android/core/sampling/RateBasedSampler;->sample(Ljava/lang/Object;)Z -Lcom/datadog/android/core/sampling/RateBasedSampler$1; -HSPLcom/datadog/android/core/sampling/RateBasedSampler$1;->(F)V -HSPLcom/datadog/android/core/sampling/RateBasedSampler$1;->invoke()Ljava/lang/Float; -HSPLcom/datadog/android/core/sampling/RateBasedSampler$1;->invoke()Ljava/lang/Object; +Lcom/datadog/android/core/sampling/RateBasedSampler$$ExternalSyntheticLambda2; +HSPLcom/datadog/android/core/sampling/RateBasedSampler$$ExternalSyntheticLambda2;->(F)V +HSPLcom/datadog/android/core/sampling/RateBasedSampler$$ExternalSyntheticLambda2;->invoke()Ljava/lang/Object; +Lcom/datadog/android/core/sampling/RateBasedSampler$$ExternalSyntheticLambda3; +HSPLcom/datadog/android/core/sampling/RateBasedSampler$$ExternalSyntheticLambda3;->()V +HSPLcom/datadog/android/core/sampling/RateBasedSampler$$ExternalSyntheticLambda3;->invoke()Ljava/lang/Object; Lcom/datadog/android/core/sampling/RateBasedSampler$Companion; HSPLcom/datadog/android/core/sampling/RateBasedSampler$Companion;->()V HSPLcom/datadog/android/core/sampling/RateBasedSampler$Companion;->(Lkotlin/jvm/internal/DefaultConstructorMarker;)V -Lcom/datadog/android/core/sampling/RateBasedSampler$random$2; -HSPLcom/datadog/android/core/sampling/RateBasedSampler$random$2;->()V -HSPLcom/datadog/android/core/sampling/RateBasedSampler$random$2;->()V -HSPLcom/datadog/android/core/sampling/RateBasedSampler$random$2;->invoke()Ljava/lang/Object; -HSPLcom/datadog/android/core/sampling/RateBasedSampler$random$2;->invoke()Ljava/security/SecureRandom; Lcom/datadog/android/core/sampling/Sampler; Lcom/datadog/android/core/thread/FlushableExecutorService; Lcom/datadog/android/core/thread/FlushableExecutorService$Factory; @@ -19936,12 +20006,15 @@ Lcom/datadog/android/internal/attributes/LocalAttribute$Key; HSPLcom/datadog/android/internal/attributes/LocalAttribute$Key;->$values()[Lcom/datadog/android/internal/attributes/LocalAttribute$Key; HSPLcom/datadog/android/internal/attributes/LocalAttribute$Key;->()V HSPLcom/datadog/android/internal/attributes/LocalAttribute$Key;->(Ljava/lang/String;ILjava/lang/String;)V +HSPLcom/datadog/android/internal/attributes/LocalAttribute$Key;->toString()Ljava/lang/String; Lcom/datadog/android/internal/attributes/LocalAttributeKt; Lcom/datadog/android/internal/attributes/ViewScopeInstrumentationType; Lcom/datadog/android/internal/attributes/ViewScopeInstrumentationType$Native; HSPLcom/datadog/android/internal/attributes/ViewScopeInstrumentationType$Native;->$values()[Lcom/datadog/android/internal/attributes/ViewScopeInstrumentationType$Native; HSPLcom/datadog/android/internal/attributes/ViewScopeInstrumentationType$Native;->()V HSPLcom/datadog/android/internal/attributes/ViewScopeInstrumentationType$Native;->(Ljava/lang/String;ILjava/lang/String;)V +HSPLcom/datadog/android/internal/attributes/ViewScopeInstrumentationType$Native;->getValue()Ljava/lang/String; +PLcom/datadog/android/internal/collections/EvictingQueue;->(ILjava/util/Deque;)V Lcom/datadog/android/internal/heatmaps/HeatmapIdentifierRegistry; HSPLcom/datadog/android/internal/heatmaps/HeatmapIdentifierRegistry;->()V Lcom/datadog/android/internal/heatmaps/HeatmapIdentifierRegistry$Companion; @@ -19996,9 +20069,11 @@ HSPLcom/datadog/android/internal/system/BuildSdkVersionProvider$Companion;->getD Lcom/datadog/android/internal/system/BuildSdkVersionProvider$Companion$DEFAULT$1; HSPLcom/datadog/android/internal/system/BuildSdkVersionProvider$Companion$DEFAULT$1;->()V HSPLcom/datadog/android/internal/system/BuildSdkVersionProvider$Companion$DEFAULT$1;->isAtLeastN()Z +HSPLcom/datadog/android/internal/system/BuildSdkVersionProvider$Companion$DEFAULT$1;->isAtLeastO()Z HSPLcom/datadog/android/internal/system/BuildSdkVersionProvider$Companion$DEFAULT$1;->isAtLeastP()Z HSPLcom/datadog/android/internal/system/BuildSdkVersionProvider$Companion$DEFAULT$1;->isAtLeastQ()Z HSPLcom/datadog/android/internal/system/BuildSdkVersionProvider$Companion$DEFAULT$1;->isAtLeastR()Z +HSPLcom/datadog/android/internal/system/BuildSdkVersionProvider$Companion$DEFAULT$1;->isAtLeastS()Z HSPLcom/datadog/android/internal/system/BuildSdkVersionProvider$Companion$DEFAULT$1;->isAtLeastTiramisu()Z Lcom/datadog/android/internal/telemetry/InternalTelemetryEvent; HSPLcom/datadog/android/internal/telemetry/InternalTelemetryEvent;->()V @@ -20015,12 +20090,6 @@ HSPLcom/datadog/android/internal/telemetry/InternalTelemetryEvent$ApiUsage$Netwo HSPLcom/datadog/android/internal/telemetry/InternalTelemetryEvent$ApiUsage$NetworkInstrumentation$LibraryType;->(Ljava/lang/String;I)V Lcom/datadog/android/internal/telemetry/InternalTelemetryEvent$Configuration; PLcom/datadog/android/internal/telemetry/InternalTelemetryEvent$Configuration;->(ZJJZZI)V -PLcom/datadog/android/internal/telemetry/InternalTelemetryEvent$Configuration;->getBatchProcessingLevel()I -PLcom/datadog/android/internal/telemetry/InternalTelemetryEvent$Configuration;->getBatchSize()J -PLcom/datadog/android/internal/telemetry/InternalTelemetryEvent$Configuration;->getBatchUploadFrequency()J -PLcom/datadog/android/internal/telemetry/InternalTelemetryEvent$Configuration;->getTrackErrors()Z -PLcom/datadog/android/internal/telemetry/InternalTelemetryEvent$Configuration;->getUseLocalEncryption()Z -PLcom/datadog/android/internal/telemetry/InternalTelemetryEvent$Configuration;->getUseProxy()Z Lcom/datadog/android/internal/telemetry/InternalTelemetryEvent$InterceptorInstantiated; HSPLcom/datadog/android/internal/telemetry/InternalTelemetryEvent$InterceptorInstantiated;->()V HSPLcom/datadog/android/internal/telemetry/InternalTelemetryEvent$InterceptorInstantiated;->()V @@ -20039,10 +20108,8 @@ Lcom/datadog/android/internal/telemetry/TracingHeaderType; HSPLcom/datadog/android/internal/telemetry/TracingHeaderType;->$values()[Lcom/datadog/android/internal/telemetry/TracingHeaderType; HSPLcom/datadog/android/internal/telemetry/TracingHeaderType;->()V HSPLcom/datadog/android/internal/telemetry/TracingHeaderType;->(Ljava/lang/String;I)V -PLcom/datadog/android/internal/telemetry/TracingHeaderType;->values()[Lcom/datadog/android/internal/telemetry/TracingHeaderType; Lcom/datadog/android/internal/telemetry/TracingHeaderTypesSet; HSPLcom/datadog/android/internal/telemetry/TracingHeaderTypesSet;->(Ljava/util/Set;)V -PLcom/datadog/android/internal/telemetry/TracingHeaderTypesSet;->getTypes()Ljava/util/Set; Lcom/datadog/android/internal/thread/NamedCallable; HSPLcom/datadog/android/internal/thread/NamedCallable;->(Ljava/lang/String;Ljava/util/concurrent/Callable;)V HSPLcom/datadog/android/internal/thread/NamedCallable;->call()Ljava/lang/Object; @@ -20057,11 +20124,13 @@ HSPLcom/datadog/android/internal/thread/NamedRunnable;->run()V Lcom/datadog/android/internal/time/BaseTimeProvider; HSPLcom/datadog/android/internal/time/BaseTimeProvider;->()V HSPLcom/datadog/android/internal/time/BaseTimeProvider;->getDeviceElapsedRealtimeMillis()J +HSPLcom/datadog/android/internal/time/BaseTimeProvider;->getDeviceElapsedRealtimeNanos()J HSPLcom/datadog/android/internal/time/BaseTimeProvider;->getDeviceElapsedTimeNanos()J HSPLcom/datadog/android/internal/time/BaseTimeProvider;->getDeviceTimestampMillis()J HSPLcom/datadog/android/internal/time/BaseTimeProvider;->getDeviceUptimeMillis()J Lcom/datadog/android/internal/time/DefaultTimeProvider; HSPLcom/datadog/android/internal/time/DefaultTimeProvider;->()V +HSPLcom/datadog/android/internal/time/DefaultTimeProvider;->getServerTimestampMillis()J Lcom/datadog/android/internal/time/TimeProvider; Lcom/datadog/android/internal/utils/ByteArrayExtKt; HSPLcom/datadog/android/internal/utils/ByteArrayExtKt;->toHexString([B)Ljava/lang/String; @@ -20106,7 +20175,6 @@ HSPLcom/datadog/android/log/LogsConfiguration$Builder;->build()Lcom/datadog/andr Lcom/datadog/android/log/internal/LogsFeature; HSPLcom/datadog/android/log/internal/LogsFeature;->()V HSPLcom/datadog/android/log/internal/LogsFeature;->(Lcom/datadog/android/api/feature/FeatureSdkCore;Ljava/lang/String;Lcom/datadog/android/event/EventMapper;)V -HSPLcom/datadog/android/log/internal/LogsFeature;->access$getSdkCore$p(Lcom/datadog/android/log/internal/LogsFeature;)Lcom/datadog/android/api/feature/FeatureSdkCore; HSPLcom/datadog/android/log/internal/LogsFeature;->createDataWriter(Lcom/datadog/android/event/EventMapper;)Lcom/datadog/android/api/storage/DataWriter; HSPLcom/datadog/android/log/internal/LogsFeature;->getAttributes$dd_sdk_android_logs_release()Ljava/util/Map; HSPLcom/datadog/android/log/internal/LogsFeature;->getDataWriter$dd_sdk_android_logs_release()Lcom/datadog/android/api/storage/DataWriter; @@ -20115,13 +20183,13 @@ HSPLcom/datadog/android/log/internal/LogsFeature;->getPackageName$dd_sdk_android HSPLcom/datadog/android/log/internal/LogsFeature;->getRequestFactory()Lcom/datadog/android/api/net/RequestFactory; HSPLcom/datadog/android/log/internal/LogsFeature;->getStorageConfiguration()Lcom/datadog/android/api/storage/FeatureStorageConfiguration; HSPLcom/datadog/android/log/internal/LogsFeature;->onInitialize(Landroid/content/Context;)V +HSPLcom/datadog/android/log/internal/LogsFeature;->requestFactory_delegate$lambda$0(Ljava/lang/String;Lcom/datadog/android/log/internal/LogsFeature;)Lcom/datadog/android/log/internal/net/LogsRequestFactory; +Lcom/datadog/android/log/internal/LogsFeature$$ExternalSyntheticLambda2; +HSPLcom/datadog/android/log/internal/LogsFeature$$ExternalSyntheticLambda2;->(Ljava/lang/String;Lcom/datadog/android/log/internal/LogsFeature;)V +HSPLcom/datadog/android/log/internal/LogsFeature$$ExternalSyntheticLambda2;->invoke()Ljava/lang/Object; Lcom/datadog/android/log/internal/LogsFeature$Companion; HSPLcom/datadog/android/log/internal/LogsFeature$Companion;->()V HSPLcom/datadog/android/log/internal/LogsFeature$Companion;->(Lkotlin/jvm/internal/DefaultConstructorMarker;)V -Lcom/datadog/android/log/internal/LogsFeature$requestFactory$2; -HSPLcom/datadog/android/log/internal/LogsFeature$requestFactory$2;->(Ljava/lang/String;Lcom/datadog/android/log/internal/LogsFeature;)V -HSPLcom/datadog/android/log/internal/LogsFeature$requestFactory$2;->invoke()Lcom/datadog/android/log/internal/net/LogsRequestFactory; -HSPLcom/datadog/android/log/internal/LogsFeature$requestFactory$2;->invoke()Ljava/lang/Object; Lcom/datadog/android/log/internal/domain/DatadogLogGenerator; HSPLcom/datadog/android/log/internal/domain/DatadogLogGenerator;->()V HSPLcom/datadog/android/log/internal/domain/DatadogLogGenerator;->(Ljava/lang/String;Lcom/datadog/android/api/InternalLogger;)V @@ -20169,21 +20237,19 @@ HSPLcom/datadog/android/log/internal/domain/event/LogEventSerializer$Companion;- Lcom/datadog/android/log/internal/logger/DatadogLogHandler; HSPLcom/datadog/android/log/internal/logger/DatadogLogHandler;->()V HSPLcom/datadog/android/log/internal/logger/DatadogLogHandler;->(Ljava/lang/String;Lcom/datadog/android/log/internal/domain/LogGenerator;Lcom/datadog/android/api/feature/FeatureSdkCore;Lcom/datadog/android/api/storage/DataWriter;ZZZLcom/datadog/android/core/sampling/Sampler;I)V -HSPLcom/datadog/android/log/internal/logger/DatadogLogHandler;->access$createLog(Lcom/datadog/android/log/internal/logger/DatadogLogHandler;ILcom/datadog/android/api/context/DatadogContext;Ljava/lang/String;Ljava/lang/Throwable;Ljava/util/Map;Ljava/util/Set;Ljava/lang/String;J)Lcom/datadog/android/log/model/LogEvent; HSPLcom/datadog/android/log/internal/logger/DatadogLogHandler;->createLog(ILcom/datadog/android/api/context/DatadogContext;Ljava/lang/String;Ljava/lang/Throwable;Ljava/util/Map;Ljava/util/Set;Ljava/lang/String;J)Lcom/datadog/android/log/model/LogEvent; -HSPLcom/datadog/android/log/internal/logger/DatadogLogHandler;->getWriter$dd_sdk_android_logs_release()Lcom/datadog/android/api/storage/DataWriter; +HSPLcom/datadog/android/log/internal/logger/DatadogLogHandler;->handleLog$lambda$1$lambda$0(Lcom/datadog/android/log/internal/logger/DatadogLogHandler;Lcom/datadog/android/log/model/LogEvent;Lcom/datadog/android/api/storage/EventBatchWriter;)Lkotlin/Unit; +HSPLcom/datadog/android/log/internal/logger/DatadogLogHandler;->handleLog$lambda$1(Lcom/datadog/android/log/internal/logger/DatadogLogHandler;ILjava/lang/String;Ljava/lang/Throwable;Ljava/util/Map;Ljava/util/Set;Ljava/lang/String;JLcom/datadog/android/api/context/DatadogContext;Lkotlin/jvm/functions/Function1;)Lkotlin/Unit; HSPLcom/datadog/android/log/internal/logger/DatadogLogHandler;->handleLog(ILjava/lang/String;Ljava/lang/Throwable;Ljava/util/Map;Ljava/util/Set;Ljava/lang/Long;)V +Lcom/datadog/android/log/internal/logger/DatadogLogHandler$$ExternalSyntheticLambda2; +HSPLcom/datadog/android/log/internal/logger/DatadogLogHandler$$ExternalSyntheticLambda2;->(Lcom/datadog/android/log/internal/logger/DatadogLogHandler;Lcom/datadog/android/log/model/LogEvent;)V +HSPLcom/datadog/android/log/internal/logger/DatadogLogHandler$$ExternalSyntheticLambda2;->invoke(Ljava/lang/Object;)Ljava/lang/Object; +Lcom/datadog/android/log/internal/logger/DatadogLogHandler$$ExternalSyntheticLambda5; +HSPLcom/datadog/android/log/internal/logger/DatadogLogHandler$$ExternalSyntheticLambda5;->(Lcom/datadog/android/log/internal/logger/DatadogLogHandler;ILjava/lang/String;Ljava/lang/Throwable;Ljava/util/Map;Ljava/util/Set;Ljava/lang/String;J)V +HSPLcom/datadog/android/log/internal/logger/DatadogLogHandler$$ExternalSyntheticLambda5;->invoke(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object; Lcom/datadog/android/log/internal/logger/DatadogLogHandler$Companion; HSPLcom/datadog/android/log/internal/logger/DatadogLogHandler$Companion;->()V HSPLcom/datadog/android/log/internal/logger/DatadogLogHandler$Companion;->(Lkotlin/jvm/internal/DefaultConstructorMarker;)V -Lcom/datadog/android/log/internal/logger/DatadogLogHandler$handleLog$1; -HSPLcom/datadog/android/log/internal/logger/DatadogLogHandler$handleLog$1;->(Lcom/datadog/android/log/internal/logger/DatadogLogHandler;ILjava/lang/String;Ljava/lang/Throwable;Ljava/util/Map;Ljava/util/Set;Ljava/lang/String;J)V -HSPLcom/datadog/android/log/internal/logger/DatadogLogHandler$handleLog$1;->invoke(Lcom/datadog/android/api/context/DatadogContext;Lkotlin/jvm/functions/Function1;)V -HSPLcom/datadog/android/log/internal/logger/DatadogLogHandler$handleLog$1;->invoke(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object; -Lcom/datadog/android/log/internal/logger/DatadogLogHandler$handleLog$1$1; -HSPLcom/datadog/android/log/internal/logger/DatadogLogHandler$handleLog$1$1;->(Lcom/datadog/android/log/internal/logger/DatadogLogHandler;Lcom/datadog/android/log/model/LogEvent;)V -HSPLcom/datadog/android/log/internal/logger/DatadogLogHandler$handleLog$1$1;->invoke(Lcom/datadog/android/api/storage/EventBatchWriter;)V -HSPLcom/datadog/android/log/internal/logger/DatadogLogHandler$handleLog$1$1;->invoke(Ljava/lang/Object;)Ljava/lang/Object; Lcom/datadog/android/log/internal/logger/LogHandler; Lcom/datadog/android/log/internal/net/LogsRequestFactory; HSPLcom/datadog/android/log/internal/net/LogsRequestFactory;->()V @@ -20288,12 +20354,12 @@ HSPLcom/datadog/android/ndk/internal/DatadogNdkCrashHandler;->handleNdkCrash(Lco HSPLcom/datadog/android/ndk/internal/DatadogNdkCrashHandler;->prepareData$lambda$0(Lcom/datadog/android/ndk/internal/DatadogNdkCrashHandler;)V HSPLcom/datadog/android/ndk/internal/DatadogNdkCrashHandler;->prepareData()V HSPLcom/datadog/android/ndk/internal/DatadogNdkCrashHandler;->readCrashData()V -Lcom/datadog/android/ndk/internal/DatadogNdkCrashHandler$$ExternalSyntheticLambda0; -HSPLcom/datadog/android/ndk/internal/DatadogNdkCrashHandler$$ExternalSyntheticLambda0;->(Lcom/datadog/android/ndk/internal/DatadogNdkCrashHandler;)V -HSPLcom/datadog/android/ndk/internal/DatadogNdkCrashHandler$$ExternalSyntheticLambda0;->run()V Lcom/datadog/android/ndk/internal/DatadogNdkCrashHandler$$ExternalSyntheticLambda1; -HSPLcom/datadog/android/ndk/internal/DatadogNdkCrashHandler$$ExternalSyntheticLambda1;->(Lcom/datadog/android/ndk/internal/DatadogNdkCrashHandler;Lcom/datadog/android/api/feature/FeatureSdkCore;)V +HSPLcom/datadog/android/ndk/internal/DatadogNdkCrashHandler$$ExternalSyntheticLambda1;->(Lcom/datadog/android/ndk/internal/DatadogNdkCrashHandler;)V HSPLcom/datadog/android/ndk/internal/DatadogNdkCrashHandler$$ExternalSyntheticLambda1;->run()V +Lcom/datadog/android/ndk/internal/DatadogNdkCrashHandler$$ExternalSyntheticLambda3; +HSPLcom/datadog/android/ndk/internal/DatadogNdkCrashHandler$$ExternalSyntheticLambda3;->(Lcom/datadog/android/ndk/internal/DatadogNdkCrashHandler;Lcom/datadog/android/api/feature/FeatureSdkCore;)V +HSPLcom/datadog/android/ndk/internal/DatadogNdkCrashHandler$$ExternalSyntheticLambda3;->run()V Lcom/datadog/android/ndk/internal/DatadogNdkCrashHandler$Companion; HSPLcom/datadog/android/ndk/internal/DatadogNdkCrashHandler$Companion;->()V HSPLcom/datadog/android/ndk/internal/DatadogNdkCrashHandler$Companion;->(Lkotlin/jvm/internal/DefaultConstructorMarker;)V @@ -20319,8 +20385,11 @@ Lcom/datadog/android/okhttp/DatadogEventListener$Factory$Companion$NO_OP_EVENT_L HSPLcom/datadog/android/okhttp/DatadogEventListener$Factory$Companion$NO_OP_EVENT_LISTENER$1;->()V Lcom/datadog/android/okhttp/DatadogInterceptor; HSPLcom/datadog/android/okhttp/DatadogInterceptor;->()V -HSPLcom/datadog/android/okhttp/DatadogInterceptor;->(Ljava/lang/String;Ljava/util/Map;Lcom/datadog/android/okhttp/trace/TracedRequestListener;Lcom/datadog/android/rum/RumResourceAttributesProvider;Lcom/datadog/android/core/sampling/Sampler;Lcom/datadog/android/trace/TraceContextInjection;ZLkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function0;Lcom/datadog/android/rum/resource/ResourceHeadersExtractor;)V +HSPLcom/datadog/android/okhttp/DatadogInterceptor;->(Ljava/lang/String;Ljava/util/Map;Lcom/datadog/android/okhttp/trace/TracedRequestListener;Lcom/datadog/android/rum/RumResourceAttributesProvider;Lcom/datadog/android/core/sampling/Sampler;Lcom/datadog/android/trace/TraceContextInjection;ZLkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function0;Lcom/datadog/android/rum/resource/ResourceHeadersExtractor;Lkotlin/jvm/functions/Function1;)V +HSPLcom/datadog/android/okhttp/DatadogInterceptor;->(Ljava/lang/String;Ljava/util/Map;Lcom/datadog/android/okhttp/trace/TracedRequestListener;Lcom/datadog/android/rum/RumResourceAttributesProvider;Lcom/datadog/android/core/sampling/Sampler;Lcom/datadog/android/trace/TraceContextInjection;ZLkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function0;Lcom/datadog/android/rum/resource/ResourceHeadersExtractor;Lkotlin/jvm/functions/Function1;ILkotlin/jvm/internal/DefaultConstructorMarker;)V HSPLcom/datadog/android/okhttp/DatadogInterceptor;->onSdkInstanceReady$dd_sdk_android_okhttp_release(Lcom/datadog/android/core/InternalSdkCore;)V +Lcom/datadog/android/okhttp/DatadogInterceptor$1; +HSPLcom/datadog/android/okhttp/DatadogInterceptor$1;->(Ljava/lang/Object;)V Lcom/datadog/android/okhttp/DatadogInterceptor$Builder; HSPLcom/datadog/android/okhttp/DatadogInterceptor$Builder;->(Ljava/util/List;)V HSPLcom/datadog/android/okhttp/DatadogInterceptor$Builder;->(Ljava/util/Map;)V @@ -20344,12 +20413,22 @@ HSPLcom/datadog/android/okhttp/trace/NoOpTracedRequestListener;->()V Lcom/datadog/android/okhttp/trace/TracedRequestListener; Lcom/datadog/android/okhttp/trace/TracingInterceptor; HSPLcom/datadog/android/okhttp/trace/TracingInterceptor;->()V -HSPLcom/datadog/android/okhttp/trace/TracingInterceptor;->(Ljava/lang/String;Ljava/util/Map;Lcom/datadog/android/okhttp/trace/TracedRequestListener;Ljava/lang/String;Lcom/datadog/android/core/sampling/Sampler;Lcom/datadog/android/trace/TraceContextInjection;ZLkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function0;)V +HSPLcom/datadog/android/okhttp/trace/TracingInterceptor;->(Ljava/lang/String;Ljava/util/Map;Lcom/datadog/android/okhttp/trace/TracedRequestListener;Ljava/lang/String;Lcom/datadog/android/core/sampling/Sampler;Lcom/datadog/android/trace/TraceContextInjection;ZLkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function1;)V HSPLcom/datadog/android/okhttp/trace/TracingInterceptor;->access$getDEFAULT_LOCAL_TRACER_FACTORY$cp()Lkotlin/jvm/functions/Function2; -HSPLcom/datadog/android/okhttp/trace/TracingInterceptor;->getTraceSampler$dd_sdk_android_okhttp_release()Lcom/datadog/android/core/sampling/Sampler; -HSPLcom/datadog/android/okhttp/trace/TracingInterceptor;->getTracedHosts$dd_sdk_android_okhttp_release()Ljava/util/Map; HSPLcom/datadog/android/okhttp/trace/TracingInterceptor;->onSdkInstanceReady$dd_sdk_android_okhttp_release(Lcom/datadog/android/core/InternalSdkCore;)V +HSPLcom/datadog/android/okhttp/trace/TracingInterceptor;->sdkCoreReference$lambda$1(Lcom/datadog/android/okhttp/trace/TracingInterceptor;Lcom/datadog/android/api/SdkCore;)Lkotlin/Unit; +HSPLcom/datadog/android/okhttp/trace/TracingInterceptor;->updateTracingFeatureContext$lambda$10(Lcom/datadog/android/okhttp/trace/TracingInterceptor;Ljava/util/Map;)Lkotlin/Unit; HSPLcom/datadog/android/okhttp/trace/TracingInterceptor;->updateTracingFeatureContext(Lcom/datadog/android/api/feature/FeatureSdkCore;)V +Lcom/datadog/android/okhttp/trace/TracingInterceptor$$ExternalSyntheticLambda0; +HSPLcom/datadog/android/okhttp/trace/TracingInterceptor$$ExternalSyntheticLambda0;->(Lcom/datadog/android/okhttp/trace/TracingInterceptor;)V +Lcom/datadog/android/okhttp/trace/TracingInterceptor$$ExternalSyntheticLambda11; +HSPLcom/datadog/android/okhttp/trace/TracingInterceptor$$ExternalSyntheticLambda11;->()V +Lcom/datadog/android/okhttp/trace/TracingInterceptor$$ExternalSyntheticLambda12; +HSPLcom/datadog/android/okhttp/trace/TracingInterceptor$$ExternalSyntheticLambda12;->(Lcom/datadog/android/okhttp/trace/TracingInterceptor;)V +HSPLcom/datadog/android/okhttp/trace/TracingInterceptor$$ExternalSyntheticLambda12;->invoke(Ljava/lang/Object;)Ljava/lang/Object; +Lcom/datadog/android/okhttp/trace/TracingInterceptor$$ExternalSyntheticLambda2; +HSPLcom/datadog/android/okhttp/trace/TracingInterceptor$$ExternalSyntheticLambda2;->(Lcom/datadog/android/okhttp/trace/TracingInterceptor;)V +HSPLcom/datadog/android/okhttp/trace/TracingInterceptor$$ExternalSyntheticLambda2;->invoke(Ljava/lang/Object;)Ljava/lang/Object; Lcom/datadog/android/okhttp/trace/TracingInterceptor$BaseBuilder; HSPLcom/datadog/android/okhttp/trace/TracingInterceptor$BaseBuilder;->(Ljava/util/Map;)V HSPLcom/datadog/android/okhttp/trace/TracingInterceptor$BaseBuilder;->getGlobalTracerProvider$dd_sdk_android_okhttp_release()Lkotlin/jvm/functions/Function0; @@ -20360,25 +20439,11 @@ HSPLcom/datadog/android/okhttp/trace/TracingInterceptor$BaseBuilder;->getTraceCo HSPLcom/datadog/android/okhttp/trace/TracingInterceptor$BaseBuilder;->getTraceSampler$dd_sdk_android_okhttp_release()Lcom/datadog/android/core/sampling/Sampler; HSPLcom/datadog/android/okhttp/trace/TracingInterceptor$BaseBuilder;->getTracedHostsWithHeaderType$dd_sdk_android_okhttp_release()Ljava/util/Map; HSPLcom/datadog/android/okhttp/trace/TracingInterceptor$BaseBuilder;->getTracedRequestListener$dd_sdk_android_okhttp_release()Lcom/datadog/android/okhttp/trace/TracedRequestListener; -Lcom/datadog/android/okhttp/trace/TracingInterceptor$BaseBuilder$globalTracerProvider$1; -HSPLcom/datadog/android/okhttp/trace/TracingInterceptor$BaseBuilder$globalTracerProvider$1;->()V -HSPLcom/datadog/android/okhttp/trace/TracingInterceptor$BaseBuilder$globalTracerProvider$1;->()V +Lcom/datadog/android/okhttp/trace/TracingInterceptor$BaseBuilder$$ExternalSyntheticLambda0; +HSPLcom/datadog/android/okhttp/trace/TracingInterceptor$BaseBuilder$$ExternalSyntheticLambda0;->()V Lcom/datadog/android/okhttp/trace/TracingInterceptor$Companion; HSPLcom/datadog/android/okhttp/trace/TracingInterceptor$Companion;->()V HSPLcom/datadog/android/okhttp/trace/TracingInterceptor$Companion;->(Lkotlin/jvm/internal/DefaultConstructorMarker;)V -Lcom/datadog/android/okhttp/trace/TracingInterceptor$Companion$DEFAULT_LOCAL_TRACER_FACTORY$1; -HSPLcom/datadog/android/okhttp/trace/TracingInterceptor$Companion$DEFAULT_LOCAL_TRACER_FACTORY$1;->()V -HSPLcom/datadog/android/okhttp/trace/TracingInterceptor$Companion$DEFAULT_LOCAL_TRACER_FACTORY$1;->()V -Lcom/datadog/android/okhttp/trace/TracingInterceptor$rumContextPropagator$1; -HSPLcom/datadog/android/okhttp/trace/TracingInterceptor$rumContextPropagator$1;->(Lcom/datadog/android/okhttp/trace/TracingInterceptor;)V -Lcom/datadog/android/okhttp/trace/TracingInterceptor$sdkCoreReference$1; -HSPLcom/datadog/android/okhttp/trace/TracingInterceptor$sdkCoreReference$1;->(Lcom/datadog/android/okhttp/trace/TracingInterceptor;)V -HSPLcom/datadog/android/okhttp/trace/TracingInterceptor$sdkCoreReference$1;->invoke(Lcom/datadog/android/api/SdkCore;)V -HSPLcom/datadog/android/okhttp/trace/TracingInterceptor$sdkCoreReference$1;->invoke(Ljava/lang/Object;)Ljava/lang/Object; -Lcom/datadog/android/okhttp/trace/TracingInterceptor$updateTracingFeatureContext$1; -HSPLcom/datadog/android/okhttp/trace/TracingInterceptor$updateTracingFeatureContext$1;->(Lcom/datadog/android/okhttp/trace/TracingInterceptor;)V -HSPLcom/datadog/android/okhttp/trace/TracingInterceptor$updateTracingFeatureContext$1;->invoke(Ljava/lang/Object;)Ljava/lang/Object; -HSPLcom/datadog/android/okhttp/trace/TracingInterceptor$updateTracingFeatureContext$1;->invoke(Ljava/util/Map;)V Lcom/datadog/android/privacy/TrackingConsent; HSPLcom/datadog/android/privacy/TrackingConsent;->$values()[Lcom/datadog/android/privacy/TrackingConsent; HSPLcom/datadog/android/privacy/TrackingConsent;->()V @@ -20408,15 +20473,15 @@ HSPLcom/datadog/android/rum/NoOpRumResourceAttributesProvider;->()V Lcom/datadog/android/rum/Rum; HSPLcom/datadog/android/rum/Rum;->()V HSPLcom/datadog/android/rum/Rum;->()V +HSPLcom/datadog/android/rum/Rum;->createMonitor$lambda$5(Lcom/datadog/android/rum/internal/domain/scope/RumVitalAppLaunchEventHelper;Lcom/datadog/android/core/InternalSdkCore;Lcom/datadog/android/rum/internal/startup/RumAppStartupTelemetryReporter;)Lcom/datadog/android/rum/internal/startup/RumSessionScopeStartupManager; HSPLcom/datadog/android/rum/Rum;->createMonitor(Lcom/datadog/android/core/InternalSdkCore;Lcom/datadog/android/rum/internal/RumFeature;)Lcom/datadog/android/rum/internal/monitor/DatadogRumMonitor; HSPLcom/datadog/android/rum/Rum;->createTelemetryEventHandler(Lcom/datadog/android/core/InternalSdkCore;Lcom/datadog/android/rum/internal/RumFeature;Lcom/datadog/android/rum/internal/metric/SessionEndedMetricDispatcher;)Lcom/datadog/android/telemetry/internal/TelemetryEventHandler; HSPLcom/datadog/android/rum/Rum;->enable$default(Lcom/datadog/android/rum/RumConfiguration;Lcom/datadog/android/api/SdkCore;ILjava/lang/Object;)V HSPLcom/datadog/android/rum/Rum;->enable(Lcom/datadog/android/rum/RumConfiguration;Lcom/datadog/android/api/SdkCore;)V HSPLcom/datadog/android/rum/Rum;->isTimeseriesConfigured(Lcom/datadog/android/rum/internal/RumFeature$Configuration;)Z -Lcom/datadog/android/rum/Rum$createMonitor$1; -HSPLcom/datadog/android/rum/Rum$createMonitor$1;->(Lcom/datadog/android/rum/internal/domain/scope/RumVitalAppLaunchEventHelper;Lcom/datadog/android/core/InternalSdkCore;Lcom/datadog/android/rum/internal/startup/RumAppStartupTelemetryReporter;)V -HSPLcom/datadog/android/rum/Rum$createMonitor$1;->invoke()Lcom/datadog/android/rum/internal/startup/RumSessionScopeStartupManager; -HSPLcom/datadog/android/rum/Rum$createMonitor$1;->invoke()Ljava/lang/Object; +Lcom/datadog/android/rum/Rum$$ExternalSyntheticLambda4; +HSPLcom/datadog/android/rum/Rum$$ExternalSyntheticLambda4;->(Lcom/datadog/android/rum/internal/domain/scope/RumVitalAppLaunchEventHelper;Lcom/datadog/android/core/InternalSdkCore;Lcom/datadog/android/rum/internal/startup/RumAppStartupTelemetryReporter;)V +HSPLcom/datadog/android/rum/Rum$$ExternalSyntheticLambda4;->invoke()Ljava/lang/Object; Lcom/datadog/android/rum/Rum$createMonitor$sessionSampler$1; HSPLcom/datadog/android/rum/Rum$createMonitor$sessionSampler$1;->(Ljava/lang/Object;)V Lcom/datadog/android/rum/RumActionType; @@ -20472,12 +20537,19 @@ HSPLcom/datadog/android/rum/_RumInternalProxy;->(Lcom/datadog/android/rum/ Lcom/datadog/android/rum/_RumInternalProxy$Companion; HSPLcom/datadog/android/rum/_RumInternalProxy$Companion;->()V HSPLcom/datadog/android/rum/_RumInternalProxy$Companion;->(Lkotlin/jvm/internal/DefaultConstructorMarker;)V +Lcom/datadog/android/rum/configuration/RumViewEventWriteConfig; +Lcom/datadog/android/rum/configuration/RumViewEventWriteConfig$AlwaysFullView; +HSPLcom/datadog/android/rum/configuration/RumViewEventWriteConfig$AlwaysFullView;->()V +HSPLcom/datadog/android/rum/configuration/RumViewEventWriteConfig$AlwaysFullView;->()V Lcom/datadog/android/rum/configuration/SlowFramesConfiguration; HSPLcom/datadog/android/rum/configuration/SlowFramesConfiguration;->()V HSPLcom/datadog/android/rum/configuration/SlowFramesConfiguration;->(IJJJJ)V HSPLcom/datadog/android/rum/configuration/SlowFramesConfiguration;->(IJJJJILkotlin/jvm/internal/DefaultConstructorMarker;)V HSPLcom/datadog/android/rum/configuration/SlowFramesConfiguration;->access$getDEFAULT$cp()Lcom/datadog/android/rum/configuration/SlowFramesConfiguration; HSPLcom/datadog/android/rum/configuration/SlowFramesConfiguration;->getFreezeDurationThresholdNs$dd_sdk_android_rum_release()J +PLcom/datadog/android/rum/configuration/SlowFramesConfiguration;->getMaxSlowFrameThresholdNs$dd_sdk_android_rum_release()J +PLcom/datadog/android/rum/configuration/SlowFramesConfiguration;->getMaxSlowFramesAmount$dd_sdk_android_rum_release()I +PLcom/datadog/android/rum/configuration/SlowFramesConfiguration;->getMinViewLifetimeThresholdNs$dd_sdk_android_rum_release()J Lcom/datadog/android/rum/configuration/SlowFramesConfiguration$Companion; HSPLcom/datadog/android/rum/configuration/SlowFramesConfiguration$Companion;->()V HSPLcom/datadog/android/rum/configuration/SlowFramesConfiguration$Companion;->(Lkotlin/jvm/internal/DefaultConstructorMarker;)V @@ -20486,7 +20558,7 @@ Lcom/datadog/android/rum/configuration/VitalsUpdateFrequency; HSPLcom/datadog/android/rum/configuration/VitalsUpdateFrequency;->$values()[Lcom/datadog/android/rum/configuration/VitalsUpdateFrequency; HSPLcom/datadog/android/rum/configuration/VitalsUpdateFrequency;->()V HSPLcom/datadog/android/rum/configuration/VitalsUpdateFrequency;->(Ljava/lang/String;IJ)V -PLcom/datadog/android/rum/configuration/VitalsUpdateFrequency;->getPeriodInMs$dd_sdk_android_rum_release()J +Lcom/datadog/android/rum/event/ViewEventMapper; Lcom/datadog/android/rum/internal/AnonymousIdentifierDeserializer; HSPLcom/datadog/android/rum/internal/AnonymousIdentifierDeserializer;->()V HSPLcom/datadog/android/rum/internal/AnonymousIdentifierDeserializer;->deserialize(Ljava/lang/Object;)Ljava/lang/Object; @@ -20515,17 +20587,18 @@ HSPLcom/datadog/android/rum/internal/NoOpRumSessionListener;->onSessionStarted(L Lcom/datadog/android/rum/internal/RumAnonymousIdentifierManager; HSPLcom/datadog/android/rum/internal/RumAnonymousIdentifierManager;->()V HSPLcom/datadog/android/rum/internal/RumAnonymousIdentifierManager;->(Lcom/datadog/android/api/storage/datastore/DataStoreHandler;Lcom/datadog/android/api/feature/FeatureSdkCore;)V -HSPLcom/datadog/android/rum/internal/RumAnonymousIdentifierManager;->access$createAndStoreAnonymousId(Lcom/datadog/android/rum/internal/RumAnonymousIdentifierManager;Lcom/datadog/android/api/storage/datastore/DataStoreHandler;Lcom/datadog/android/api/feature/FeatureSdkCore;)V HSPLcom/datadog/android/rum/internal/RumAnonymousIdentifierManager;->createAndStoreAnonymousId(Lcom/datadog/android/api/storage/datastore/DataStoreHandler;Lcom/datadog/android/api/feature/FeatureSdkCore;)V +HSPLcom/datadog/android/rum/internal/RumAnonymousIdentifierManager;->handleAnonymousIdTracking$lambda$0(Lcom/datadog/android/rum/internal/RumAnonymousIdentifierManager;Lcom/datadog/android/api/storage/datastore/DataStoreHandler;Lcom/datadog/android/api/feature/FeatureSdkCore;Ljava/util/UUID;)Lkotlin/Unit; HSPLcom/datadog/android/rum/internal/RumAnonymousIdentifierManager;->handleAnonymousIdTracking(Lcom/datadog/android/api/storage/datastore/DataStoreHandler;Lcom/datadog/android/api/feature/FeatureSdkCore;)V HSPLcom/datadog/android/rum/internal/RumAnonymousIdentifierManager;->manageAnonymousId(Z)V +Lcom/datadog/android/rum/internal/RumAnonymousIdentifierManager$$ExternalSyntheticLambda0; +HSPLcom/datadog/android/rum/internal/RumAnonymousIdentifierManager$$ExternalSyntheticLambda0;->(Lcom/datadog/android/rum/internal/RumAnonymousIdentifierManager;Lcom/datadog/android/api/storage/datastore/DataStoreHandler;Lcom/datadog/android/api/feature/FeatureSdkCore;)V +HSPLcom/datadog/android/rum/internal/RumAnonymousIdentifierManager$$ExternalSyntheticLambda0;->invoke(Ljava/lang/Object;)Ljava/lang/Object; Lcom/datadog/android/rum/internal/RumAnonymousIdentifierManager$Companion; HSPLcom/datadog/android/rum/internal/RumAnonymousIdentifierManager$Companion;->()V HSPLcom/datadog/android/rum/internal/RumAnonymousIdentifierManager$Companion;->(Lkotlin/jvm/internal/DefaultConstructorMarker;)V -Lcom/datadog/android/rum/internal/RumAnonymousIdentifierManager$handleAnonymousIdTracking$1; -HSPLcom/datadog/android/rum/internal/RumAnonymousIdentifierManager$handleAnonymousIdTracking$1;->(Lcom/datadog/android/rum/internal/RumAnonymousIdentifierManager;Lcom/datadog/android/api/storage/datastore/DataStoreHandler;Lcom/datadog/android/api/feature/FeatureSdkCore;)V -HSPLcom/datadog/android/rum/internal/RumAnonymousIdentifierManager$handleAnonymousIdTracking$1;->invoke(Ljava/lang/Object;)Ljava/lang/Object; -HSPLcom/datadog/android/rum/internal/RumAnonymousIdentifierManager$handleAnonymousIdTracking$1;->invoke(Ljava/util/UUID;)V +Lcom/datadog/android/rum/internal/RumConfigurationExtKt; +HSPLcom/datadog/android/rum/internal/RumConfigurationExtKt;->applyRemoteConfiguration(Lcom/datadog/android/rum/RumConfiguration;Lcom/datadog/android/core/internal/remote/model/RemoteConfiguration;)Lcom/datadog/android/rum/RumConfiguration; Lcom/datadog/android/rum/internal/RumErrorSourceType; HSPLcom/datadog/android/rum/internal/RumErrorSourceType;->$values()[Lcom/datadog/android/rum/internal/RumErrorSourceType; HSPLcom/datadog/android/rum/internal/RumErrorSourceType;->()V @@ -20535,11 +20608,11 @@ Lcom/datadog/android/rum/internal/RumFeature; HSPLcom/datadog/android/rum/internal/RumFeature;->()V HSPLcom/datadog/android/rum/internal/RumFeature;->(Lcom/datadog/android/api/feature/FeatureSdkCore;Ljava/lang/String;Lcom/datadog/android/rum/internal/RumFeature$Configuration;Lkotlin/jvm/functions/Function1;Lcom/datadog/android/internal/system/BuildSdkVersionProvider;Landroid/os/Handler;)V HSPLcom/datadog/android/rum/internal/RumFeature;->(Lcom/datadog/android/api/feature/FeatureSdkCore;Ljava/lang/String;Lcom/datadog/android/rum/internal/RumFeature$Configuration;Lkotlin/jvm/functions/Function1;Lcom/datadog/android/internal/system/BuildSdkVersionProvider;Landroid/os/Handler;ILkotlin/jvm/internal/DefaultConstructorMarker;)V +HSPLcom/datadog/android/rum/internal/RumFeature;->DEFAULT_RUM_CONFIG$lambda$25(Lcom/datadog/android/rum/model/ViewEvent;)Lcom/datadog/android/rum/model/ViewEvent; HSPLcom/datadog/android/rum/internal/RumFeature;->access$getDEFAULT_RUM_CONFIG$cp()Lcom/datadog/android/rum/internal/RumFeature$Configuration; -HSPLcom/datadog/android/rum/internal/RumFeature;->access$getSdkCore$p(Lcom/datadog/android/rum/internal/RumFeature;)Lcom/datadog/android/api/feature/FeatureSdkCore; HSPLcom/datadog/android/rum/internal/RumFeature;->consumeLastFatalAnr$dd_sdk_android_rum_release(Ljava/util/concurrent/ExecutorService;)V HSPLcom/datadog/android/rum/internal/RumFeature;->createDataWriter(Lcom/datadog/android/rum/internal/RumFeature$Configuration;Lcom/datadog/android/core/InternalSdkCore;)Lcom/datadog/android/api/storage/DataWriter; -HSPLcom/datadog/android/rum/internal/RumFeature;->getAccessibilitySnapshotManager$dd_sdk_android_rum_release()Lcom/datadog/android/rum/internal/domain/accessibility/AccessibilitySnapshotManager; +HSPLcom/datadog/android/rum/internal/RumFeature;->getAccessibilityReader$dd_sdk_android_rum_release()Lcom/datadog/android/rum/internal/domain/InfoProvider; HSPLcom/datadog/android/rum/internal/RumFeature;->getAppContext$dd_sdk_android_rum_release()Landroid/content/Context; HSPLcom/datadog/android/rum/internal/RumFeature;->getApplicationId$dd_sdk_android_rum_release()Ljava/lang/String; HSPLcom/datadog/android/rum/internal/RumFeature;->getBackgroundEventTracking$dd_sdk_android_rum_release()Z @@ -20573,10 +20646,20 @@ HSPLcom/datadog/android/rum/internal/RumFeature;->initializeSlowFrameListener(Lc HSPLcom/datadog/android/rum/internal/RumFeature;->onInitialize(Landroid/content/Context;)V HSPLcom/datadog/android/rum/internal/RumFeature;->onReceive(Ljava/lang/Object;)V HSPLcom/datadog/android/rum/internal/RumFeature;->registerTrackingStrategies(Landroid/content/Context;)V +HSPLcom/datadog/android/rum/internal/RumFeature;->requestFactory_delegate$lambda$8(Lcom/datadog/android/rum/internal/RumFeature;)Lcom/datadog/android/rum/internal/net/RumRequestFactory; HSPLcom/datadog/android/rum/internal/RumFeature;->setAppContext$dd_sdk_android_rum_release(Landroid/content/Context;)V -Lcom/datadog/android/rum/internal/RumFeature$1; -HSPLcom/datadog/android/rum/internal/RumFeature$1;->()V -HSPLcom/datadog/android/rum/internal/RumFeature$1;->()V +Lcom/datadog/android/rum/internal/RumFeature$$ExternalSyntheticLambda11; +HSPLcom/datadog/android/rum/internal/RumFeature$$ExternalSyntheticLambda11;->()V +HSPLcom/datadog/android/rum/internal/RumFeature$$ExternalSyntheticLambda11;->map(Lcom/datadog/android/rum/model/ViewEvent;)Lcom/datadog/android/rum/model/ViewEvent; +Lcom/datadog/android/rum/internal/RumFeature$$ExternalSyntheticLambda2; +HSPLcom/datadog/android/rum/internal/RumFeature$$ExternalSyntheticLambda2;->(Lcom/datadog/android/rum/internal/RumFeature;)V +Lcom/datadog/android/rum/internal/RumFeature$$ExternalSyntheticLambda3; +HSPLcom/datadog/android/rum/internal/RumFeature$$ExternalSyntheticLambda3;->(Lcom/datadog/android/rum/internal/RumFeature;)V +HSPLcom/datadog/android/rum/internal/RumFeature$$ExternalSyntheticLambda3;->invoke()Ljava/lang/Object; +Lcom/datadog/android/rum/internal/RumFeature$$ExternalSyntheticLambda6; +HSPLcom/datadog/android/rum/internal/RumFeature$$ExternalSyntheticLambda6;->()V +Lcom/datadog/android/rum/internal/RumFeature$$ExternalSyntheticLambda9; +HSPLcom/datadog/android/rum/internal/RumFeature$$ExternalSyntheticLambda9;->()V Lcom/datadog/android/rum/internal/RumFeature$Companion; HSPLcom/datadog/android/rum/internal/RumFeature$Companion;->()V HSPLcom/datadog/android/rum/internal/RumFeature$Companion;->(Lkotlin/jvm/internal/DefaultConstructorMarker;)V @@ -20587,9 +20670,9 @@ HSPLcom/datadog/android/rum/internal/RumFeature$Companion;->isTrackNonFatalAnrsE HSPLcom/datadog/android/rum/internal/RumFeature$Companion;->provideGestureTracker([Lcom/datadog/android/rum/tracking/ViewAttributesProvider;Lcom/datadog/android/rum/tracking/InteractionPredicate;Lcom/datadog/android/rum/tracking/ActionTrackingStrategy;Lcom/datadog/android/api/InternalLogger;)Lcom/datadog/android/rum/internal/instrumentation/gestures/DatadogGesturesTracker; HSPLcom/datadog/android/rum/internal/RumFeature$Companion;->provideUserTrackingStrategy([Lcom/datadog/android/rum/tracking/ViewAttributesProvider;Lcom/datadog/android/rum/tracking/InteractionPredicate;Lcom/datadog/android/rum/tracking/ActionTrackingStrategy;Lcom/datadog/android/internal/system/BuildSdkVersionProvider;Lcom/datadog/android/api/InternalLogger;)Lcom/datadog/android/rum/internal/tracking/UserActionTrackingStrategy; Lcom/datadog/android/rum/internal/RumFeature$Configuration; -HSPLcom/datadog/android/rum/internal/RumFeature$Configuration;->(Ljava/lang/String;FFFZLjava/util/List;Lcom/datadog/android/rum/tracking/InteractionPredicate;Lcom/datadog/android/rum/tracking/ViewTrackingStrategy;Lcom/datadog/android/rum/tracking/TrackingStrategy;Lcom/datadog/android/event/EventMapper;Lcom/datadog/android/event/EventMapper;Lcom/datadog/android/event/EventMapper;Lcom/datadog/android/event/EventMapper;Lcom/datadog/android/event/EventMapper;Lcom/datadog/android/event/EventMapper;Lcom/datadog/android/event/EventMapper;Lcom/datadog/android/event/EventMapper;ZZZLcom/datadog/android/rum/configuration/VitalsUpdateFrequency;Lcom/datadog/android/rum/RumSessionListener;Lcom/datadog/android/rum/metric/networksettled/InitialResourceIdentifier;Lcom/datadog/android/rum/metric/interactiontonextview/LastInteractionIdentifier;Lcom/datadog/android/rum/configuration/SlowFramesConfiguration;Lcom/datadog/android/rum/tracking/ActionTrackingStrategy;Ljava/util/Map;ZLcom/datadog/android/rum/RumSessionType;ZZLcom/datadog/android/rum/internal/instrumentation/insights/InsightsCollector;Lcom/datadog/android/rum/startup/AppStartupActivityPredicate;Lcom/datadog/android/rum/timeseries/TimeseriesConfiguration;)V -HSPLcom/datadog/android/rum/internal/RumFeature$Configuration;->copy$default(Lcom/datadog/android/rum/internal/RumFeature$Configuration;Ljava/lang/String;FFFZLjava/util/List;Lcom/datadog/android/rum/tracking/InteractionPredicate;Lcom/datadog/android/rum/tracking/ViewTrackingStrategy;Lcom/datadog/android/rum/tracking/TrackingStrategy;Lcom/datadog/android/event/EventMapper;Lcom/datadog/android/event/EventMapper;Lcom/datadog/android/event/EventMapper;Lcom/datadog/android/event/EventMapper;Lcom/datadog/android/event/EventMapper;Lcom/datadog/android/event/EventMapper;Lcom/datadog/android/event/EventMapper;Lcom/datadog/android/event/EventMapper;ZZZLcom/datadog/android/rum/configuration/VitalsUpdateFrequency;Lcom/datadog/android/rum/RumSessionListener;Lcom/datadog/android/rum/metric/networksettled/InitialResourceIdentifier;Lcom/datadog/android/rum/metric/interactiontonextview/LastInteractionIdentifier;Lcom/datadog/android/rum/configuration/SlowFramesConfiguration;Lcom/datadog/android/rum/tracking/ActionTrackingStrategy;Ljava/util/Map;ZLcom/datadog/android/rum/RumSessionType;ZZLcom/datadog/android/rum/internal/instrumentation/insights/InsightsCollector;Lcom/datadog/android/rum/startup/AppStartupActivityPredicate;Lcom/datadog/android/rum/timeseries/TimeseriesConfiguration;IILjava/lang/Object;)Lcom/datadog/android/rum/internal/RumFeature$Configuration; -HSPLcom/datadog/android/rum/internal/RumFeature$Configuration;->copy(Ljava/lang/String;FFFZLjava/util/List;Lcom/datadog/android/rum/tracking/InteractionPredicate;Lcom/datadog/android/rum/tracking/ViewTrackingStrategy;Lcom/datadog/android/rum/tracking/TrackingStrategy;Lcom/datadog/android/event/EventMapper;Lcom/datadog/android/event/EventMapper;Lcom/datadog/android/event/EventMapper;Lcom/datadog/android/event/EventMapper;Lcom/datadog/android/event/EventMapper;Lcom/datadog/android/event/EventMapper;Lcom/datadog/android/event/EventMapper;Lcom/datadog/android/event/EventMapper;ZZZLcom/datadog/android/rum/configuration/VitalsUpdateFrequency;Lcom/datadog/android/rum/RumSessionListener;Lcom/datadog/android/rum/metric/networksettled/InitialResourceIdentifier;Lcom/datadog/android/rum/metric/interactiontonextview/LastInteractionIdentifier;Lcom/datadog/android/rum/configuration/SlowFramesConfiguration;Lcom/datadog/android/rum/tracking/ActionTrackingStrategy;Ljava/util/Map;ZLcom/datadog/android/rum/RumSessionType;ZZLcom/datadog/android/rum/internal/instrumentation/insights/InsightsCollector;Lcom/datadog/android/rum/startup/AppStartupActivityPredicate;Lcom/datadog/android/rum/timeseries/TimeseriesConfiguration;)Lcom/datadog/android/rum/internal/RumFeature$Configuration; +HSPLcom/datadog/android/rum/internal/RumFeature$Configuration;->(Ljava/lang/String;FFFZLjava/util/List;Lcom/datadog/android/rum/tracking/InteractionPredicate;Lcom/datadog/android/rum/tracking/ViewTrackingStrategy;Lcom/datadog/android/rum/tracking/TrackingStrategy;Lcom/datadog/android/rum/event/ViewEventMapper;Lcom/datadog/android/event/EventMapper;Lcom/datadog/android/event/EventMapper;Lcom/datadog/android/event/EventMapper;Lcom/datadog/android/event/EventMapper;Lcom/datadog/android/event/EventMapper;Lcom/datadog/android/event/EventMapper;Lcom/datadog/android/event/EventMapper;ZZZLcom/datadog/android/rum/configuration/VitalsUpdateFrequency;Lcom/datadog/android/rum/RumSessionListener;Lcom/datadog/android/rum/metric/networksettled/InitialResourceIdentifier;Lcom/datadog/android/rum/metric/interactiontonextview/LastInteractionIdentifier;Lcom/datadog/android/rum/configuration/SlowFramesConfiguration;Lcom/datadog/android/rum/tracking/ActionTrackingStrategy;Ljava/util/Map;ZLcom/datadog/android/rum/RumSessionType;ZZLcom/datadog/android/rum/internal/instrumentation/insights/InsightsCollector;Lcom/datadog/android/rum/startup/AppStartupActivityPredicate;Lcom/datadog/android/rum/configuration/RumViewEventWriteConfig;Lcom/datadog/android/rum/timeseries/TimeseriesConfiguration;)V +HSPLcom/datadog/android/rum/internal/RumFeature$Configuration;->copy$default(Lcom/datadog/android/rum/internal/RumFeature$Configuration;Ljava/lang/String;FFFZLjava/util/List;Lcom/datadog/android/rum/tracking/InteractionPredicate;Lcom/datadog/android/rum/tracking/ViewTrackingStrategy;Lcom/datadog/android/rum/tracking/TrackingStrategy;Lcom/datadog/android/rum/event/ViewEventMapper;Lcom/datadog/android/event/EventMapper;Lcom/datadog/android/event/EventMapper;Lcom/datadog/android/event/EventMapper;Lcom/datadog/android/event/EventMapper;Lcom/datadog/android/event/EventMapper;Lcom/datadog/android/event/EventMapper;Lcom/datadog/android/event/EventMapper;ZZZLcom/datadog/android/rum/configuration/VitalsUpdateFrequency;Lcom/datadog/android/rum/RumSessionListener;Lcom/datadog/android/rum/metric/networksettled/InitialResourceIdentifier;Lcom/datadog/android/rum/metric/interactiontonextview/LastInteractionIdentifier;Lcom/datadog/android/rum/configuration/SlowFramesConfiguration;Lcom/datadog/android/rum/tracking/ActionTrackingStrategy;Ljava/util/Map;ZLcom/datadog/android/rum/RumSessionType;ZZLcom/datadog/android/rum/internal/instrumentation/insights/InsightsCollector;Lcom/datadog/android/rum/startup/AppStartupActivityPredicate;Lcom/datadog/android/rum/configuration/RumViewEventWriteConfig;Lcom/datadog/android/rum/timeseries/TimeseriesConfiguration;IILjava/lang/Object;)Lcom/datadog/android/rum/internal/RumFeature$Configuration; +HSPLcom/datadog/android/rum/internal/RumFeature$Configuration;->copy(Ljava/lang/String;FFFZLjava/util/List;Lcom/datadog/android/rum/tracking/InteractionPredicate;Lcom/datadog/android/rum/tracking/ViewTrackingStrategy;Lcom/datadog/android/rum/tracking/TrackingStrategy;Lcom/datadog/android/rum/event/ViewEventMapper;Lcom/datadog/android/event/EventMapper;Lcom/datadog/android/event/EventMapper;Lcom/datadog/android/event/EventMapper;Lcom/datadog/android/event/EventMapper;Lcom/datadog/android/event/EventMapper;Lcom/datadog/android/event/EventMapper;Lcom/datadog/android/event/EventMapper;ZZZLcom/datadog/android/rum/configuration/VitalsUpdateFrequency;Lcom/datadog/android/rum/RumSessionListener;Lcom/datadog/android/rum/metric/networksettled/InitialResourceIdentifier;Lcom/datadog/android/rum/metric/interactiontonextview/LastInteractionIdentifier;Lcom/datadog/android/rum/configuration/SlowFramesConfiguration;Lcom/datadog/android/rum/tracking/ActionTrackingStrategy;Ljava/util/Map;ZLcom/datadog/android/rum/RumSessionType;ZZLcom/datadog/android/rum/internal/instrumentation/insights/InsightsCollector;Lcom/datadog/android/rum/startup/AppStartupActivityPredicate;Lcom/datadog/android/rum/configuration/RumViewEventWriteConfig;Lcom/datadog/android/rum/timeseries/TimeseriesConfiguration;)Lcom/datadog/android/rum/internal/RumFeature$Configuration; HSPLcom/datadog/android/rum/internal/RumFeature$Configuration;->getActionEventMapper()Lcom/datadog/android/event/EventMapper; HSPLcom/datadog/android/rum/internal/RumFeature$Configuration;->getAdditionalConfig()Ljava/util/Map; HSPLcom/datadog/android/rum/internal/RumFeature$Configuration;->getAppStartupActivityPredicate()Lcom/datadog/android/rum/startup/AppStartupActivityPredicate; @@ -20607,6 +20690,7 @@ HSPLcom/datadog/android/rum/internal/RumFeature$Configuration;->getLongTaskEvent HSPLcom/datadog/android/rum/internal/RumFeature$Configuration;->getLongTaskTrackingStrategy()Lcom/datadog/android/rum/tracking/TrackingStrategy; HSPLcom/datadog/android/rum/internal/RumFeature$Configuration;->getResourceEventMapper()Lcom/datadog/android/event/EventMapper; HSPLcom/datadog/android/rum/internal/RumFeature$Configuration;->getRumSessionTypeOverride()Lcom/datadog/android/rum/RumSessionType; +HSPLcom/datadog/android/rum/internal/RumFeature$Configuration;->getRumViewEventWriteConfig()Lcom/datadog/android/rum/configuration/RumViewEventWriteConfig; HSPLcom/datadog/android/rum/internal/RumFeature$Configuration;->getSampleRate()F HSPLcom/datadog/android/rum/internal/RumFeature$Configuration;->getSessionListener()Lcom/datadog/android/rum/RumSessionListener; HSPLcom/datadog/android/rum/internal/RumFeature$Configuration;->getSlowFramesConfiguration()Lcom/datadog/android/rum/configuration/SlowFramesConfiguration; @@ -20619,22 +20703,13 @@ HSPLcom/datadog/android/rum/internal/RumFeature$Configuration;->getTrackAnonymou HSPLcom/datadog/android/rum/internal/RumFeature$Configuration;->getTrackFrustrations()Z HSPLcom/datadog/android/rum/internal/RumFeature$Configuration;->getTrackNonFatalAnrs()Z HSPLcom/datadog/android/rum/internal/RumFeature$Configuration;->getUserActionTracking()Z -HSPLcom/datadog/android/rum/internal/RumFeature$Configuration;->getViewEventMapper()Lcom/datadog/android/event/EventMapper; +HSPLcom/datadog/android/rum/internal/RumFeature$Configuration;->getViewEventMapper()Lcom/datadog/android/rum/event/ViewEventMapper; HSPLcom/datadog/android/rum/internal/RumFeature$Configuration;->getViewTrackingStrategy()Lcom/datadog/android/rum/tracking/ViewTrackingStrategy; HSPLcom/datadog/android/rum/internal/RumFeature$Configuration;->getVitalAppLaunchEventMapper()Lcom/datadog/android/event/EventMapper; HSPLcom/datadog/android/rum/internal/RumFeature$Configuration;->getVitalOperationStepEventMapper()Lcom/datadog/android/event/EventMapper; HSPLcom/datadog/android/rum/internal/RumFeature$Configuration;->getVitalsMonitorUpdateFrequency()Lcom/datadog/android/rum/configuration/VitalsUpdateFrequency; Lcom/datadog/android/rum/internal/RumFeature$initRumAppStartupDetector$1; HSPLcom/datadog/android/rum/internal/RumFeature$initRumAppStartupDetector$1;->(Lcom/datadog/android/rum/internal/RumFeature;)V -Lcom/datadog/android/rum/internal/RumFeature$initializeSlowFrameListener$1; -HSPLcom/datadog/android/rum/internal/RumFeature$initializeSlowFrameListener$1;->()V -HSPLcom/datadog/android/rum/internal/RumFeature$initializeSlowFrameListener$1;->()V -Lcom/datadog/android/rum/internal/RumFeature$lateCrashEventHandler$2; -HSPLcom/datadog/android/rum/internal/RumFeature$lateCrashEventHandler$2;->(Lcom/datadog/android/rum/internal/RumFeature;)V -Lcom/datadog/android/rum/internal/RumFeature$requestFactory$2; -HSPLcom/datadog/android/rum/internal/RumFeature$requestFactory$2;->(Lcom/datadog/android/rum/internal/RumFeature;)V -HSPLcom/datadog/android/rum/internal/RumFeature$requestFactory$2;->invoke()Lcom/datadog/android/rum/internal/net/RumRequestFactory; -HSPLcom/datadog/android/rum/internal/RumFeature$requestFactory$2;->invoke()Ljava/lang/Object; Lcom/datadog/android/rum/internal/anr/ANRDetectorRunnable; HSPLcom/datadog/android/rum/internal/anr/ANRDetectorRunnable;->()V HSPLcom/datadog/android/rum/internal/anr/ANRDetectorRunnable;->(Lcom/datadog/android/api/feature/FeatureSdkCore;Landroid/os/Handler;JJ)V @@ -20650,6 +20725,22 @@ Lcom/datadog/android/rum/internal/domain/FrameMetricsData; HSPLcom/datadog/android/rum/internal/domain/FrameMetricsData;->()V HSPLcom/datadog/android/rum/internal/domain/FrameMetricsData;->(IJJJJJJJJJZJJJJD)V HSPLcom/datadog/android/rum/internal/domain/FrameMetricsData;->(IJJJJJJJJJZJJJJDILkotlin/jvm/internal/DefaultConstructorMarker;)V +HSPLcom/datadog/android/rum/internal/domain/FrameMetricsData;->setAnimationDuration(J)V +HSPLcom/datadog/android/rum/internal/domain/FrameMetricsData;->setCommandIssueDuration(J)V +HSPLcom/datadog/android/rum/internal/domain/FrameMetricsData;->setDeadline(J)V +HSPLcom/datadog/android/rum/internal/domain/FrameMetricsData;->setDisplayRefreshRate(D)V +HSPLcom/datadog/android/rum/internal/domain/FrameMetricsData;->setDrawDuration(J)V +HSPLcom/datadog/android/rum/internal/domain/FrameMetricsData;->setDroppedFrames(I)V +HSPLcom/datadog/android/rum/internal/domain/FrameMetricsData;->setFirstDrawFrame(Z)V +HSPLcom/datadog/android/rum/internal/domain/FrameMetricsData;->setGpuDuration(J)V +HSPLcom/datadog/android/rum/internal/domain/FrameMetricsData;->setInputHandlingDuration(J)V +HSPLcom/datadog/android/rum/internal/domain/FrameMetricsData;->setIntendedVsyncTimestamp(J)V +HSPLcom/datadog/android/rum/internal/domain/FrameMetricsData;->setLayoutMeasureDuration(J)V +HSPLcom/datadog/android/rum/internal/domain/FrameMetricsData;->setSwapBuffersDuration(J)V +HSPLcom/datadog/android/rum/internal/domain/FrameMetricsData;->setSyncDuration(J)V +HSPLcom/datadog/android/rum/internal/domain/FrameMetricsData;->setTotalDuration(J)V +HSPLcom/datadog/android/rum/internal/domain/FrameMetricsData;->setUnknownDelayDuration(J)V +HSPLcom/datadog/android/rum/internal/domain/FrameMetricsData;->setVsyncTimestamp(J)V Lcom/datadog/android/rum/internal/domain/FrameMetricsData$Companion; HSPLcom/datadog/android/rum/internal/domain/FrameMetricsData$Companion;->()V HSPLcom/datadog/android/rum/internal/domain/FrameMetricsData$Companion;->(Lkotlin/jvm/internal/DefaultConstructorMarker;)V @@ -20674,14 +20765,28 @@ HSPLcom/datadog/android/rum/internal/domain/RumContext;->toMap()Ljava/util/Map; Lcom/datadog/android/rum/internal/domain/RumContext$Companion; HSPLcom/datadog/android/rum/internal/domain/RumContext$Companion;->()V HSPLcom/datadog/android/rum/internal/domain/RumContext$Companion;->(Lkotlin/jvm/internal/DefaultConstructorMarker;)V -PLcom/datadog/android/rum/internal/domain/RumContext$Companion;->fromFeatureContext(Ljava/util/Map;)Lcom/datadog/android/rum/internal/domain/RumContext; Lcom/datadog/android/rum/internal/domain/RumDataWriter; HSPLcom/datadog/android/rum/internal/domain/RumDataWriter;->()V -HSPLcom/datadog/android/rum/internal/domain/RumDataWriter;->(Lcom/datadog/android/core/persistence/Serializer;Lcom/datadog/android/core/persistence/Serializer;Lcom/datadog/android/core/InternalSdkCore;)V -HSPLcom/datadog/android/rum/internal/domain/RumDataWriter;->onDataWritten$dd_sdk_android_rum_release(Ljava/lang/Object;[B)V +HSPLcom/datadog/android/rum/internal/domain/RumDataWriter;->(Lcom/datadog/android/rum/internal/domain/event/RumEventMapper;Lcom/datadog/android/rum/internal/domain/event/RumEventSerializer;Lcom/datadog/android/core/persistence/Serializer;Lcom/datadog/android/core/InternalSdkCore;)V +HSPLcom/datadog/android/rum/internal/domain/RumDataWriter;->onDataWritten$dd_sdk_android_rum_release(Lcom/datadog/android/rum/model/ViewEvent;[B)V HSPLcom/datadog/android/rum/internal/domain/RumDataWriter;->onViewEventSubmitted$dd_sdk_android_rum_release(Lcom/datadog/android/rum/model/ViewEvent;)V HSPLcom/datadog/android/rum/internal/domain/RumDataWriter;->onViewEventWritten(Lcom/datadog/android/rum/model/ViewEvent;[B)V +HSPLcom/datadog/android/rum/internal/domain/RumDataWriter;->resolveHasAccessibility(Lcom/datadog/android/rum/model/ViewEvent;)Z +HSPLcom/datadog/android/rum/internal/domain/RumDataWriter;->serializeViewEvent(Lcom/datadog/android/rum/model/ViewEvent;)Lkotlin/Pair; +HSPLcom/datadog/android/rum/internal/domain/RumDataWriter;->telemetryContext(Ljava/lang/Object;)Lcom/datadog/android/internal/telemetry/TelemetryContext; HSPLcom/datadog/android/rum/internal/domain/RumDataWriter;->write(Lcom/datadog/android/api/storage/EventBatchWriter;Ljava/lang/Object;Lcom/datadog/android/api/storage/EventType;)Z +HSPLcom/datadog/android/rum/internal/domain/RumDataWriter;->writeBatchEvent$default(Lcom/datadog/android/rum/internal/domain/RumDataWriter;Lcom/datadog/android/api/storage/EventBatchWriter;Lcom/datadog/android/api/storage/RawBatchEvent;Lcom/datadog/android/api/storage/EventType;Lcom/datadog/android/internal/telemetry/TelemetryContext;Lkotlin/jvm/functions/Function0;ILjava/lang/Object;)Z +HSPLcom/datadog/android/rum/internal/domain/RumDataWriter;->writeBatchEvent$lambda$5()Lkotlin/Unit; +HSPLcom/datadog/android/rum/internal/domain/RumDataWriter;->writeBatchEvent(Lcom/datadog/android/api/storage/EventBatchWriter;Lcom/datadog/android/api/storage/RawBatchEvent;Lcom/datadog/android/api/storage/EventType;Lcom/datadog/android/internal/telemetry/TelemetryContext;Lkotlin/jvm/functions/Function0;)Z +HSPLcom/datadog/android/rum/internal/domain/RumDataWriter;->writeMappedViewEvent$lambda$0(Lcom/datadog/android/rum/internal/domain/RumDataWriter;Lcom/datadog/android/rum/model/ViewEvent;[B)Lkotlin/Unit; +HSPLcom/datadog/android/rum/internal/domain/RumDataWriter;->writeMappedViewEvent(Lcom/datadog/android/api/storage/EventBatchWriter;Lcom/datadog/android/rum/model/ViewEvent;Lcom/datadog/android/api/storage/EventType;)Z +HSPLcom/datadog/android/rum/internal/domain/RumDataWriter;->writeOtherEvent(Lcom/datadog/android/api/storage/EventBatchWriter;Ljava/lang/Object;Lcom/datadog/android/api/storage/EventType;)Z +Lcom/datadog/android/rum/internal/domain/RumDataWriter$$ExternalSyntheticLambda0; +HSPLcom/datadog/android/rum/internal/domain/RumDataWriter$$ExternalSyntheticLambda0;->()V +HSPLcom/datadog/android/rum/internal/domain/RumDataWriter$$ExternalSyntheticLambda0;->invoke()Ljava/lang/Object; +Lcom/datadog/android/rum/internal/domain/RumDataWriter$$ExternalSyntheticLambda1; +HSPLcom/datadog/android/rum/internal/domain/RumDataWriter$$ExternalSyntheticLambda1;->(Lcom/datadog/android/rum/internal/domain/RumDataWriter;Lcom/datadog/android/rum/model/ViewEvent;[B)V +HSPLcom/datadog/android/rum/internal/domain/RumDataWriter$$ExternalSyntheticLambda1;->invoke()Ljava/lang/Object; Lcom/datadog/android/rum/internal/domain/RumDataWriter$Companion; HSPLcom/datadog/android/rum/internal/domain/RumDataWriter$Companion;->()V HSPLcom/datadog/android/rum/internal/domain/RumDataWriter$Companion;->(Lkotlin/jvm/internal/DefaultConstructorMarker;)V @@ -20696,12 +20801,20 @@ Lcom/datadog/android/rum/internal/domain/Time$Companion; HSPLcom/datadog/android/rum/internal/domain/Time$Companion;->()V HSPLcom/datadog/android/rum/internal/domain/Time$Companion;->(Lkotlin/jvm/internal/DefaultConstructorMarker;)V HSPLcom/datadog/android/rum/internal/domain/Time$Companion;->now(Lcom/datadog/android/internal/time/TimeProvider;)Lcom/datadog/android/rum/internal/domain/Time; -Lcom/datadog/android/rum/internal/domain/accessibility/AccessibilitySnapshotManager; +Lcom/datadog/android/rum/internal/domain/accessibility/AccessibilityInfo; +HSPLcom/datadog/android/rum/internal/domain/accessibility/AccessibilityInfo;->(Ljava/lang/String;Ljava/lang/Boolean;Ljava/lang/Boolean;Ljava/lang/Boolean;Ljava/lang/Boolean;Ljava/lang/Boolean;Ljava/lang/Boolean;)V +HSPLcom/datadog/android/rum/internal/domain/accessibility/AccessibilityInfo;->(Ljava/lang/String;Ljava/lang/Boolean;Ljava/lang/Boolean;Ljava/lang/Boolean;Ljava/lang/Boolean;Ljava/lang/Boolean;Ljava/lang/Boolean;ILkotlin/jvm/internal/DefaultConstructorMarker;)V +HSPLcom/datadog/android/rum/internal/domain/accessibility/AccessibilityInfo;->getTextSize()Ljava/lang/String; +HSPLcom/datadog/android/rum/internal/domain/accessibility/AccessibilityInfo;->isClosedCaptioningEnabled()Ljava/lang/Boolean; +HSPLcom/datadog/android/rum/internal/domain/accessibility/AccessibilityInfo;->isColorInversionEnabled()Ljava/lang/Boolean; +HSPLcom/datadog/android/rum/internal/domain/accessibility/AccessibilityInfo;->isReducedAnimationsEnabled()Ljava/lang/Boolean; +HSPLcom/datadog/android/rum/internal/domain/accessibility/AccessibilityInfo;->isRtlEnabled()Ljava/lang/Boolean; +HSPLcom/datadog/android/rum/internal/domain/accessibility/AccessibilityInfo;->isScreenPinningEnabled()Ljava/lang/Boolean; +HSPLcom/datadog/android/rum/internal/domain/accessibility/AccessibilityInfo;->isScreenReaderEnabled()Ljava/lang/Boolean; Lcom/datadog/android/rum/internal/domain/accessibility/NoOpAccessibilityReader; HSPLcom/datadog/android/rum/internal/domain/accessibility/NoOpAccessibilityReader;->()V -Lcom/datadog/android/rum/internal/domain/accessibility/NoOpAccessibilitySnapshotManager; -HSPLcom/datadog/android/rum/internal/domain/accessibility/NoOpAccessibilitySnapshotManager;->()V -HSPLcom/datadog/android/rum/internal/domain/accessibility/NoOpAccessibilitySnapshotManager;->getIfChanged()Lcom/datadog/android/rum/internal/domain/accessibility/AccessibilityInfo; +HSPLcom/datadog/android/rum/internal/domain/accessibility/NoOpAccessibilityReader;->getState()Lcom/datadog/android/rum/internal/domain/InfoData; +HSPLcom/datadog/android/rum/internal/domain/accessibility/NoOpAccessibilityReader;->getState()Lcom/datadog/android/rum/internal/domain/accessibility/AccessibilityInfo; Lcom/datadog/android/rum/internal/domain/battery/BatteryInfo; HSPLcom/datadog/android/rum/internal/domain/battery/BatteryInfo;->(Ljava/lang/Float;Ljava/lang/Boolean;)V HSPLcom/datadog/android/rum/internal/domain/battery/BatteryInfo;->getBatteryLevel()Ljava/lang/Float; @@ -20772,6 +20885,7 @@ HSPLcom/datadog/android/rum/internal/domain/event/RumEventMeta$View;->getHasAcce HSPLcom/datadog/android/rum/internal/domain/event/RumEventMeta$View;->getType()Ljava/lang/String; HSPLcom/datadog/android/rum/internal/domain/event/RumEventMeta$View;->getViewId()Ljava/lang/String; HSPLcom/datadog/android/rum/internal/domain/event/RumEventMeta$View;->toJson()Lcom/google/gson/JsonObject; +Lcom/datadog/android/rum/internal/domain/event/RumEventMeta$ViewUpdate; Lcom/datadog/android/rum/internal/domain/event/RumEventMetaDeserializer; HSPLcom/datadog/android/rum/internal/domain/event/RumEventMetaDeserializer;->()V HSPLcom/datadog/android/rum/internal/domain/event/RumEventMetaDeserializer;->(Lcom/datadog/android/api/InternalLogger;)V @@ -20800,15 +20914,20 @@ HSPLcom/datadog/android/rum/internal/domain/event/RumEventSerializer$Companion;- Lcom/datadog/android/rum/internal/domain/event/RumViewEventFilter; HSPLcom/datadog/android/rum/internal/domain/event/RumViewEventFilter;->(Lcom/datadog/android/core/internal/persistence/Deserializer;)V HSPLcom/datadog/android/rum/internal/domain/event/RumViewEventFilter;->filterOutRedundantViewEvents(Ljava/util/List;)Ljava/util/List; +Lcom/datadog/android/rum/internal/domain/scope/DiffThenFullView; +Lcom/datadog/android/rum/internal/domain/scope/MappedViewEvent; +HSPLcom/datadog/android/rum/internal/domain/scope/MappedViewEvent;->(Lcom/datadog/android/rum/model/ViewEvent;)V +HSPLcom/datadog/android/rum/internal/domain/scope/MappedViewEvent;->getViewEvent()Lcom/datadog/android/rum/model/ViewEvent; Lcom/datadog/android/rum/internal/domain/scope/RumActionScope; Lcom/datadog/android/rum/internal/domain/scope/RumApplicationScope; HSPLcom/datadog/android/rum/internal/domain/scope/RumApplicationScope;->()V -HSPLcom/datadog/android/rum/internal/domain/scope/RumApplicationScope;->(Ljava/lang/String;Lcom/datadog/android/core/InternalSdkCore;Lcom/datadog/android/core/sampling/Sampler;ZZLcom/datadog/android/core/internal/net/FirstPartyHostHeaderTypeResolver;Lcom/datadog/android/rum/internal/vitals/VitalMonitor;Lcom/datadog/android/rum/internal/vitals/VitalMonitor;Lcom/datadog/android/rum/internal/vitals/VitalMonitor;Lcom/datadog/android/rum/internal/metric/SessionMetricDispatcher;Lcom/datadog/android/rum/RumSessionListener;Lcom/datadog/android/rum/metric/networksettled/InitialResourceIdentifier;Lcom/datadog/android/rum/metric/interactiontonextview/LastInteractionIdentifier;Lcom/datadog/android/rum/internal/metric/slowframes/SlowFramesListener;Lcom/datadog/android/rum/RumSessionType;Lcom/datadog/android/rum/internal/domain/accessibility/AccessibilitySnapshotManager;Lcom/datadog/android/rum/internal/domain/InfoProvider;Lcom/datadog/android/rum/internal/domain/InfoProvider;Lkotlin/jvm/functions/Function0;Lcom/datadog/android/rum/internal/instrumentation/insights/InsightsCollector;Lcom/datadog/android/internal/heatmaps/HeatmapIdentifierRegistry;Lcom/datadog/android/rum/internal/timeseries/TimeseriesCollector$Factory;)V +HSPLcom/datadog/android/rum/internal/domain/scope/RumApplicationScope;->(Ljava/lang/String;Lcom/datadog/android/core/InternalSdkCore;Lcom/datadog/android/core/sampling/Sampler;ZZLcom/datadog/android/core/internal/net/FirstPartyHostHeaderTypeResolver;Lcom/datadog/android/rum/internal/vitals/VitalMonitor;Lcom/datadog/android/rum/internal/vitals/VitalMonitor;Lcom/datadog/android/rum/internal/vitals/VitalMonitor;Lcom/datadog/android/rum/internal/metric/SessionMetricDispatcher;Lcom/datadog/android/rum/RumSessionListener;Lcom/datadog/android/rum/metric/networksettled/InitialResourceIdentifier;Lcom/datadog/android/rum/metric/interactiontonextview/LastInteractionIdentifier;Lcom/datadog/android/rum/internal/metric/slowframes/SlowFramesListener;Lcom/datadog/android/rum/RumSessionType;Lcom/datadog/android/rum/internal/domain/InfoProvider;Lcom/datadog/android/rum/internal/domain/InfoProvider;Lcom/datadog/android/rum/internal/domain/InfoProvider;Lkotlin/jvm/functions/Function0;Lcom/datadog/android/rum/internal/instrumentation/insights/InsightsCollector;Lcom/datadog/android/rum/event/ViewEventMapper;Lcom/datadog/android/rum/configuration/RumViewEventWriteConfig;Lcom/datadog/android/internal/heatmaps/HeatmapIdentifierRegistry;Lcom/datadog/android/rum/internal/timeseries/TimeseriesCollector$Factory;)V HSPLcom/datadog/android/rum/internal/domain/scope/RumApplicationScope;->delegateToChildren(Lcom/datadog/android/rum/internal/domain/scope/RumRawEvent;Lcom/datadog/android/api/context/DatadogContext;Lkotlin/jvm/functions/Function1;Lcom/datadog/android/api/storage/DataWriter;)V HSPLcom/datadog/android/rum/internal/domain/scope/RumApplicationScope;->getActiveSession()Lcom/datadog/android/rum/internal/domain/scope/RumSessionScope; HSPLcom/datadog/android/rum/internal/domain/scope/RumApplicationScope;->getCustomAttributes()Ljava/util/Map; HSPLcom/datadog/android/rum/internal/domain/scope/RumApplicationScope;->getRumContext()Lcom/datadog/android/rum/internal/domain/RumContext; HSPLcom/datadog/android/rum/internal/domain/scope/RumApplicationScope;->handleEvent(Lcom/datadog/android/rum/internal/domain/scope/RumRawEvent;Lcom/datadog/android/api/context/DatadogContext;Lkotlin/jvm/functions/Function1;Lcom/datadog/android/api/storage/DataWriter;)Lcom/datadog/android/rum/internal/domain/scope/RumScope; +HSPLcom/datadog/android/rum/internal/domain/scope/RumApplicationScope;->onViewChanged(Lcom/datadog/android/rum/internal/domain/scope/RumViewInfo;)V HSPLcom/datadog/android/rum/internal/domain/scope/RumApplicationScope;->sendApplicationStartEvent(Lcom/datadog/android/rum/internal/domain/Time;Lcom/datadog/android/api/context/DatadogContext;Lkotlin/jvm/functions/Function1;Lcom/datadog/android/api/storage/DataWriter;)V Lcom/datadog/android/rum/internal/domain/scope/RumApplicationScope$Companion; HSPLcom/datadog/android/rum/internal/domain/scope/RumApplicationScope$Companion;->()V @@ -20871,6 +20990,10 @@ Lcom/datadog/android/rum/internal/domain/scope/RumRawEvent$StartAction; Lcom/datadog/android/rum/internal/domain/scope/RumRawEvent$StartOperation; Lcom/datadog/android/rum/internal/domain/scope/RumRawEvent$StartResource; Lcom/datadog/android/rum/internal/domain/scope/RumRawEvent$StartView; +HSPLcom/datadog/android/rum/internal/domain/scope/RumRawEvent$StartView;->(Lcom/datadog/android/rum/internal/domain/scope/RumScopeKey;Ljava/util/Map;Lcom/datadog/android/rum/internal/domain/Time;)V +HSPLcom/datadog/android/rum/internal/domain/scope/RumRawEvent$StartView;->getAttributes()Ljava/util/Map; +HSPLcom/datadog/android/rum/internal/domain/scope/RumRawEvent$StartView;->getEventTime()Lcom/datadog/android/rum/internal/domain/Time; +HSPLcom/datadog/android/rum/internal/domain/scope/RumRawEvent$StartView;->getKey()Lcom/datadog/android/rum/internal/domain/scope/RumScopeKey; Lcom/datadog/android/rum/internal/domain/scope/RumRawEvent$StopOperation; Lcom/datadog/android/rum/internal/domain/scope/RumRawEvent$StopSession; Lcom/datadog/android/rum/internal/domain/scope/RumRawEvent$StopView; @@ -20891,10 +21014,13 @@ HSPLcom/datadog/android/rum/internal/domain/scope/RumScopeKey;->getUrl()Ljava/la Lcom/datadog/android/rum/internal/domain/scope/RumScopeKey$Companion; HSPLcom/datadog/android/rum/internal/domain/scope/RumScopeKey$Companion;->()V HSPLcom/datadog/android/rum/internal/domain/scope/RumScopeKey$Companion;->(Lkotlin/jvm/internal/DefaultConstructorMarker;)V +HSPLcom/datadog/android/rum/internal/domain/scope/RumScopeKey$Companion;->from(Ljava/lang/Object;Ljava/lang/String;)Lcom/datadog/android/rum/internal/domain/scope/RumScopeKey; +HSPLcom/datadog/android/rum/internal/domain/scope/RumScopeKey$Companion;->resolveComponent(Ljava/lang/Object;)Landroid/content/ComponentName; +HSPLcom/datadog/android/rum/internal/domain/scope/RumScopeKey$Companion;->resolveComponentUrl(Landroid/content/ComponentName;)Ljava/lang/String; Lcom/datadog/android/rum/internal/domain/scope/RumSessionScope; HSPLcom/datadog/android/rum/internal/domain/scope/RumSessionScope;->()V -HSPLcom/datadog/android/rum/internal/domain/scope/RumSessionScope;->(Lcom/datadog/android/rum/internal/domain/scope/RumScope;Lcom/datadog/android/core/InternalSdkCore;Lcom/datadog/android/rum/internal/metric/SessionMetricDispatcher;Lcom/datadog/android/core/sampling/Sampler;ZZLcom/datadog/android/rum/internal/domain/scope/RumViewChangedListener;Lcom/datadog/android/core/internal/net/FirstPartyHostHeaderTypeResolver;Lcom/datadog/android/rum/internal/vitals/VitalMonitor;Lcom/datadog/android/rum/internal/vitals/VitalMonitor;Lcom/datadog/android/rum/internal/vitals/VitalMonitor;Lcom/datadog/android/rum/RumSessionListener;ZLcom/datadog/android/rum/metric/networksettled/InitialResourceIdentifier;Lcom/datadog/android/rum/metric/interactiontonextview/LastInteractionIdentifier;Lcom/datadog/android/rum/internal/metric/slowframes/SlowFramesListener;Lcom/datadog/android/rum/internal/domain/accessibility/AccessibilitySnapshotManager;Lcom/datadog/android/rum/internal/domain/InfoProvider;Lcom/datadog/android/rum/internal/domain/InfoProvider;JJLcom/datadog/android/rum/RumSessionType;Lkotlin/jvm/functions/Function0;Lcom/datadog/android/rum/internal/instrumentation/insights/InsightsCollector;Lcom/datadog/android/internal/heatmaps/HeatmapIdentifierRegistry;Lcom/datadog/android/rum/internal/timeseries/TimeseriesCollector$Factory;)V -HSPLcom/datadog/android/rum/internal/domain/scope/RumSessionScope;->(Lcom/datadog/android/rum/internal/domain/scope/RumScope;Lcom/datadog/android/core/InternalSdkCore;Lcom/datadog/android/rum/internal/metric/SessionMetricDispatcher;Lcom/datadog/android/core/sampling/Sampler;ZZLcom/datadog/android/rum/internal/domain/scope/RumViewChangedListener;Lcom/datadog/android/core/internal/net/FirstPartyHostHeaderTypeResolver;Lcom/datadog/android/rum/internal/vitals/VitalMonitor;Lcom/datadog/android/rum/internal/vitals/VitalMonitor;Lcom/datadog/android/rum/internal/vitals/VitalMonitor;Lcom/datadog/android/rum/RumSessionListener;ZLcom/datadog/android/rum/metric/networksettled/InitialResourceIdentifier;Lcom/datadog/android/rum/metric/interactiontonextview/LastInteractionIdentifier;Lcom/datadog/android/rum/internal/metric/slowframes/SlowFramesListener;Lcom/datadog/android/rum/internal/domain/accessibility/AccessibilitySnapshotManager;Lcom/datadog/android/rum/internal/domain/InfoProvider;Lcom/datadog/android/rum/internal/domain/InfoProvider;JJLcom/datadog/android/rum/RumSessionType;Lkotlin/jvm/functions/Function0;Lcom/datadog/android/rum/internal/instrumentation/insights/InsightsCollector;Lcom/datadog/android/internal/heatmaps/HeatmapIdentifierRegistry;Lcom/datadog/android/rum/internal/timeseries/TimeseriesCollector$Factory;ILkotlin/jvm/internal/DefaultConstructorMarker;)V +HSPLcom/datadog/android/rum/internal/domain/scope/RumSessionScope;->(Lcom/datadog/android/rum/internal/domain/scope/RumScope;Lcom/datadog/android/core/InternalSdkCore;Lcom/datadog/android/rum/internal/metric/SessionMetricDispatcher;Lcom/datadog/android/core/sampling/Sampler;ZZLcom/datadog/android/rum/internal/domain/scope/RumViewChangedListener;Lcom/datadog/android/core/internal/net/FirstPartyHostHeaderTypeResolver;Lcom/datadog/android/rum/internal/vitals/VitalMonitor;Lcom/datadog/android/rum/internal/vitals/VitalMonitor;Lcom/datadog/android/rum/internal/vitals/VitalMonitor;Lcom/datadog/android/rum/RumSessionListener;ZLcom/datadog/android/rum/metric/networksettled/InitialResourceIdentifier;Lcom/datadog/android/rum/metric/interactiontonextview/LastInteractionIdentifier;Lcom/datadog/android/rum/internal/metric/slowframes/SlowFramesListener;Lcom/datadog/android/rum/internal/domain/InfoProvider;Lcom/datadog/android/rum/internal/domain/InfoProvider;Lcom/datadog/android/rum/internal/domain/InfoProvider;JJLcom/datadog/android/rum/RumSessionType;Lkotlin/jvm/functions/Function0;Lcom/datadog/android/rum/internal/instrumentation/insights/InsightsCollector;Lcom/datadog/android/rum/event/ViewEventMapper;Lcom/datadog/android/rum/configuration/RumViewEventWriteConfig;Lcom/datadog/android/internal/heatmaps/HeatmapIdentifierRegistry;Lcom/datadog/android/rum/internal/timeseries/TimeseriesCollector$Factory;)V +HSPLcom/datadog/android/rum/internal/domain/scope/RumSessionScope;->(Lcom/datadog/android/rum/internal/domain/scope/RumScope;Lcom/datadog/android/core/InternalSdkCore;Lcom/datadog/android/rum/internal/metric/SessionMetricDispatcher;Lcom/datadog/android/core/sampling/Sampler;ZZLcom/datadog/android/rum/internal/domain/scope/RumViewChangedListener;Lcom/datadog/android/core/internal/net/FirstPartyHostHeaderTypeResolver;Lcom/datadog/android/rum/internal/vitals/VitalMonitor;Lcom/datadog/android/rum/internal/vitals/VitalMonitor;Lcom/datadog/android/rum/internal/vitals/VitalMonitor;Lcom/datadog/android/rum/RumSessionListener;ZLcom/datadog/android/rum/metric/networksettled/InitialResourceIdentifier;Lcom/datadog/android/rum/metric/interactiontonextview/LastInteractionIdentifier;Lcom/datadog/android/rum/internal/metric/slowframes/SlowFramesListener;Lcom/datadog/android/rum/internal/domain/InfoProvider;Lcom/datadog/android/rum/internal/domain/InfoProvider;Lcom/datadog/android/rum/internal/domain/InfoProvider;JJLcom/datadog/android/rum/RumSessionType;Lkotlin/jvm/functions/Function0;Lcom/datadog/android/rum/internal/instrumentation/insights/InsightsCollector;Lcom/datadog/android/rum/event/ViewEventMapper;Lcom/datadog/android/rum/configuration/RumViewEventWriteConfig;Lcom/datadog/android/internal/heatmaps/HeatmapIdentifierRegistry;Lcom/datadog/android/rum/internal/timeseries/TimeseriesCollector$Factory;ILkotlin/jvm/internal/DefaultConstructorMarker;)V HSPLcom/datadog/android/rum/internal/domain/scope/RumSessionScope;->getActiveRumContext()Lcom/datadog/android/rum/internal/domain/RumContext; HSPLcom/datadog/android/rum/internal/domain/scope/RumSessionScope;->getActiveView$dd_sdk_android_rum_release()Lcom/datadog/android/rum/internal/domain/scope/RumViewScope; HSPLcom/datadog/android/rum/internal/domain/scope/RumSessionScope;->getCustomAttributes()Ljava/util/Map; @@ -20921,22 +21047,44 @@ HSPLcom/datadog/android/rum/internal/domain/scope/RumSessionScope$StartReason;-> Lcom/datadog/android/rum/internal/domain/scope/RumSessionScope$StartReason$Companion; HSPLcom/datadog/android/rum/internal/domain/scope/RumSessionScope$StartReason$Companion;->()V HSPLcom/datadog/android/rum/internal/domain/scope/RumSessionScope$StartReason$Companion;->(Lkotlin/jvm/internal/DefaultConstructorMarker;)V -PLcom/datadog/android/rum/internal/domain/scope/RumSessionScope$StartReason$Companion;->fromString(Ljava/lang/String;)Lcom/datadog/android/rum/internal/domain/scope/RumSessionScope$StartReason; Lcom/datadog/android/rum/internal/domain/scope/RumSessionScope$State; HSPLcom/datadog/android/rum/internal/domain/scope/RumSessionScope$State;->$values()[Lcom/datadog/android/rum/internal/domain/scope/RumSessionScope$State; HSPLcom/datadog/android/rum/internal/domain/scope/RumSessionScope$State;->()V HSPLcom/datadog/android/rum/internal/domain/scope/RumSessionScope$State;->(Ljava/lang/String;ILjava/lang/String;)V HSPLcom/datadog/android/rum/internal/domain/scope/RumSessionScope$State;->getAsString()Ljava/lang/String; -PLcom/datadog/android/rum/internal/domain/scope/RumSessionScope$State;->values()[Lcom/datadog/android/rum/internal/domain/scope/RumSessionScope$State; Lcom/datadog/android/rum/internal/domain/scope/RumSessionScope$State$Companion; HSPLcom/datadog/android/rum/internal/domain/scope/RumSessionScope$State$Companion;->()V HSPLcom/datadog/android/rum/internal/domain/scope/RumSessionScope$State$Companion;->(Lkotlin/jvm/internal/DefaultConstructorMarker;)V -PLcom/datadog/android/rum/internal/domain/scope/RumSessionScope$State$Companion;->fromString(Ljava/lang/String;)Lcom/datadog/android/rum/internal/domain/scope/RumSessionScope$State; Lcom/datadog/android/rum/internal/domain/scope/RumViewChangedListener; +Lcom/datadog/android/rum/internal/domain/scope/RumViewEventWriter; +HSPLcom/datadog/android/rum/internal/domain/scope/RumViewEventWriter;->()V +Lcom/datadog/android/rum/internal/domain/scope/RumViewEventWriter$Companion; +HSPLcom/datadog/android/rum/internal/domain/scope/RumViewEventWriter$Companion;->()V +HSPLcom/datadog/android/rum/internal/domain/scope/RumViewEventWriter$Companion;->()V +HSPLcom/datadog/android/rum/internal/domain/scope/RumViewEventWriter$Companion;->create(Lcom/datadog/android/rum/configuration/RumViewEventWriteConfig;Lcom/datadog/android/rum/event/ViewEventMapper;Lcom/datadog/android/core/InternalSdkCore;)Lcom/datadog/android/rum/internal/domain/scope/RumViewEventWriter; +Lcom/datadog/android/rum/internal/domain/scope/RumViewEventWriterImpl; +HSPLcom/datadog/android/rum/internal/domain/scope/RumViewEventWriterImpl;->()V +HSPLcom/datadog/android/rum/internal/domain/scope/RumViewEventWriterImpl;->(Lcom/datadog/android/rum/configuration/RumViewEventWriteConfig;Lcom/datadog/android/rum/event/ViewEventMapper;Lcom/datadog/android/core/InternalSdkCore;)V +HSPLcom/datadog/android/rum/internal/domain/scope/RumViewEventWriterImpl;->writeViewEvent$lambda$2(Lcom/datadog/android/rum/internal/domain/scope/RumViewEventWriterImpl;Lcom/datadog/android/rum/model/ViewEvent;Lkotlin/jvm/internal/Ref$ObjectRef;)Ljava/lang/Object; +HSPLcom/datadog/android/rum/internal/domain/scope/RumViewEventWriterImpl;->writeViewEvent$lambda$4(Lkotlin/jvm/internal/Ref$ObjectRef;Lcom/datadog/android/rum/internal/domain/scope/RumViewEventWriterImpl;Lcom/datadog/android/rum/internal/monitor/AdvancedRumMonitor;)Lkotlin/Unit; +HSPLcom/datadog/android/rum/internal/domain/scope/RumViewEventWriterImpl;->writeViewEvent(Lcom/datadog/android/rum/model/ViewEvent;Lcom/datadog/android/api/context/DatadogContext;Lkotlin/jvm/functions/Function1;Lcom/datadog/android/api/storage/DataWriter;Lcom/datadog/android/api/storage/EventType;)V +Lcom/datadog/android/rum/internal/domain/scope/RumViewEventWriterImpl$$ExternalSyntheticLambda0; +HSPLcom/datadog/android/rum/internal/domain/scope/RumViewEventWriterImpl$$ExternalSyntheticLambda0;->(Lcom/datadog/android/rum/internal/domain/scope/RumViewEventWriterImpl;Lcom/datadog/android/rum/model/ViewEvent;Lkotlin/jvm/internal/Ref$ObjectRef;)V +HSPLcom/datadog/android/rum/internal/domain/scope/RumViewEventWriterImpl$$ExternalSyntheticLambda0;->invoke()Ljava/lang/Object; +Lcom/datadog/android/rum/internal/domain/scope/RumViewEventWriterImpl$$ExternalSyntheticLambda1; +HSPLcom/datadog/android/rum/internal/domain/scope/RumViewEventWriterImpl$$ExternalSyntheticLambda1;->(Lkotlin/jvm/internal/Ref$ObjectRef;Lcom/datadog/android/rum/internal/domain/scope/RumViewEventWriterImpl;)V +HSPLcom/datadog/android/rum/internal/domain/scope/RumViewEventWriterImpl$$ExternalSyntheticLambda1;->invoke(Ljava/lang/Object;)Ljava/lang/Object; +Lcom/datadog/android/rum/internal/domain/scope/RumViewEventWriterImpl$Companion; +HSPLcom/datadog/android/rum/internal/domain/scope/RumViewEventWriterImpl$Companion;->()V +HSPLcom/datadog/android/rum/internal/domain/scope/RumViewEventWriterImpl$Companion;->(Lkotlin/jvm/internal/DefaultConstructorMarker;)V +Lcom/datadog/android/rum/internal/domain/scope/RumViewInfo; +HSPLcom/datadog/android/rum/internal/domain/scope/RumViewInfo;->(Lcom/datadog/android/rum/internal/domain/scope/RumScopeKey;Ljava/util/Map;Z)V +HSPLcom/datadog/android/rum/internal/domain/scope/RumViewInfo;->isActive()Z Lcom/datadog/android/rum/internal/domain/scope/RumViewManagerScope; HSPLcom/datadog/android/rum/internal/domain/scope/RumViewManagerScope;->()V -HSPLcom/datadog/android/rum/internal/domain/scope/RumViewManagerScope;->(Lcom/datadog/android/rum/internal/domain/scope/RumScope;Lcom/datadog/android/core/InternalSdkCore;Lcom/datadog/android/rum/internal/metric/SessionMetricDispatcher;ZZLcom/datadog/android/rum/internal/domain/scope/RumViewChangedListener;Lcom/datadog/android/core/internal/net/FirstPartyHostHeaderTypeResolver;Lcom/datadog/android/rum/internal/vitals/VitalMonitor;Lcom/datadog/android/rum/internal/vitals/VitalMonitor;Lcom/datadog/android/rum/internal/vitals/VitalMonitor;ZFLcom/datadog/android/rum/metric/networksettled/InitialResourceIdentifier;Lcom/datadog/android/rum/internal/metric/slowframes/SlowFramesListener;Lcom/datadog/android/rum/metric/interactiontonextview/LastInteractionIdentifier;Lcom/datadog/android/rum/RumSessionType;Lcom/datadog/android/rum/internal/domain/accessibility/AccessibilitySnapshotManager;Lcom/datadog/android/rum/internal/domain/InfoProvider;Lcom/datadog/android/rum/internal/domain/InfoProvider;Lcom/datadog/android/rum/internal/instrumentation/insights/InsightsCollector;Lcom/datadog/android/internal/heatmaps/HeatmapIdentifierRegistry;)V +HSPLcom/datadog/android/rum/internal/domain/scope/RumViewManagerScope;->(Lcom/datadog/android/rum/internal/domain/scope/RumScope;Lcom/datadog/android/core/InternalSdkCore;Lcom/datadog/android/rum/internal/metric/SessionMetricDispatcher;ZZLcom/datadog/android/rum/internal/domain/scope/RumViewChangedListener;Lcom/datadog/android/core/internal/net/FirstPartyHostHeaderTypeResolver;Lcom/datadog/android/rum/internal/vitals/VitalMonitor;Lcom/datadog/android/rum/internal/vitals/VitalMonitor;Lcom/datadog/android/rum/internal/vitals/VitalMonitor;ZFLcom/datadog/android/rum/metric/networksettled/InitialResourceIdentifier;Lcom/datadog/android/rum/internal/metric/slowframes/SlowFramesListener;Lcom/datadog/android/rum/metric/interactiontonextview/LastInteractionIdentifier;Lcom/datadog/android/rum/RumSessionType;Lcom/datadog/android/rum/internal/domain/InfoProvider;Lcom/datadog/android/rum/internal/domain/InfoProvider;Lcom/datadog/android/rum/internal/domain/InfoProvider;Lcom/datadog/android/rum/internal/instrumentation/insights/InsightsCollector;Lcom/datadog/android/rum/event/ViewEventMapper;Lcom/datadog/android/rum/configuration/RumViewEventWriteConfig;Lcom/datadog/android/internal/heatmaps/HeatmapIdentifierRegistry;)V HSPLcom/datadog/android/rum/internal/domain/scope/RumViewManagerScope;->access$getValidBackgroundEventTypes$cp()[Ljava/lang/Class; +HSPLcom/datadog/android/rum/internal/domain/scope/RumViewManagerScope;->createAppLaunchViewScope$lambda$16(Lcom/datadog/android/rum/internal/domain/scope/RumViewManagerScope;)Lcom/datadog/android/rum/internal/domain/scope/RumViewEventWriter; HSPLcom/datadog/android/rum/internal/domain/scope/RumViewManagerScope;->createAppLaunchViewScope(Lcom/datadog/android/rum/internal/domain/Time;)Lcom/datadog/android/rum/internal/domain/scope/RumViewScope; HSPLcom/datadog/android/rum/internal/domain/scope/RumViewManagerScope;->delegateToChildren(Lcom/datadog/android/rum/internal/domain/scope/RumRawEvent;Lcom/datadog/android/api/context/DatadogContext;Lkotlin/jvm/functions/Function1;Lcom/datadog/android/api/storage/DataWriter;)V HSPLcom/datadog/android/rum/internal/domain/scope/RumViewManagerScope;->getActiveView$dd_sdk_android_rum_release()Lcom/datadog/android/rum/internal/domain/scope/RumViewScope; @@ -20947,79 +21095,90 @@ HSPLcom/datadog/android/rum/internal/domain/scope/RumViewManagerScope;->handleEv HSPLcom/datadog/android/rum/internal/domain/scope/RumViewManagerScope;->isActive()Z HSPLcom/datadog/android/rum/internal/domain/scope/RumViewManagerScope;->isViewManagerComplete()Z HSPLcom/datadog/android/rum/internal/domain/scope/RumViewManagerScope;->renewViewScopes$dd_sdk_android_rum_release(Lcom/datadog/android/rum/internal/domain/Time;)V +HSPLcom/datadog/android/rum/internal/domain/scope/RumViewManagerScope;->sendViewUpdateToChildren(Lcom/datadog/android/rum/internal/domain/scope/RumRawEvent;Lcom/datadog/android/api/context/DatadogContext;Lkotlin/jvm/functions/Function1;Lcom/datadog/android/api/storage/DataWriter;)V HSPLcom/datadog/android/rum/internal/domain/scope/RumViewManagerScope;->startApplicationLaunchView(Lcom/datadog/android/rum/internal/domain/scope/RumRawEvent$ApplicationStarted;Lcom/datadog/android/api/context/DatadogContext;Lkotlin/jvm/functions/Function1;Lcom/datadog/android/api/storage/DataWriter;)V +HSPLcom/datadog/android/rum/internal/domain/scope/RumViewManagerScope;->startForegroundView(Lcom/datadog/android/rum/internal/domain/scope/RumRawEvent$StartView;)V +Lcom/datadog/android/rum/internal/domain/scope/RumViewManagerScope$$ExternalSyntheticLambda6; +HSPLcom/datadog/android/rum/internal/domain/scope/RumViewManagerScope$$ExternalSyntheticLambda6;->(Lcom/datadog/android/rum/internal/domain/scope/RumViewManagerScope;)V +HSPLcom/datadog/android/rum/internal/domain/scope/RumViewManagerScope$$ExternalSyntheticLambda6;->invoke()Ljava/lang/Object; Lcom/datadog/android/rum/internal/domain/scope/RumViewManagerScope$Companion; HSPLcom/datadog/android/rum/internal/domain/scope/RumViewManagerScope$Companion;->()V HSPLcom/datadog/android/rum/internal/domain/scope/RumViewManagerScope$Companion;->(Lkotlin/jvm/internal/DefaultConstructorMarker;)V HSPLcom/datadog/android/rum/internal/domain/scope/RumViewManagerScope$Companion;->getValidBackgroundEventTypes$dd_sdk_android_rum_release()[Ljava/lang/Class; Lcom/datadog/android/rum/internal/domain/scope/RumViewScope; HSPLcom/datadog/android/rum/internal/domain/scope/RumViewScope;->()V -HSPLcom/datadog/android/rum/internal/domain/scope/RumViewScope;->(Lcom/datadog/android/rum/internal/domain/scope/RumScope;Lcom/datadog/android/core/InternalSdkCore;Lcom/datadog/android/rum/internal/metric/SessionMetricDispatcher;Lcom/datadog/android/rum/internal/domain/scope/RumScopeKey;Lcom/datadog/android/rum/internal/domain/Time;Ljava/util/Map;Lcom/datadog/android/rum/internal/domain/scope/RumViewChangedListener;Lcom/datadog/android/core/internal/net/FirstPartyHostHeaderTypeResolver;Lcom/datadog/android/rum/internal/vitals/VitalMonitor;Lcom/datadog/android/rum/internal/vitals/VitalMonitor;Lcom/datadog/android/rum/internal/vitals/VitalMonitor;Lcom/datadog/android/rum/internal/FeaturesContextResolver;Lcom/datadog/android/rum/internal/domain/scope/RumViewType;ZFLcom/datadog/android/rum/internal/metric/interactiontonextview/InteractionToNextViewMetricResolver;Lcom/datadog/android/rum/internal/metric/networksettled/NetworkSettledMetricResolver;Lcom/datadog/android/rum/internal/metric/slowframes/SlowFramesListener;Lcom/datadog/android/rum/internal/metric/ViewMetricDispatcher;Lcom/datadog/android/rum/RumSessionType;Lcom/datadog/android/rum/internal/domain/accessibility/AccessibilitySnapshotManager;Lcom/datadog/android/rum/internal/domain/InfoProvider;Lcom/datadog/android/rum/internal/domain/InfoProvider;Lcom/datadog/android/rum/internal/instrumentation/insights/InsightsCollector;Lcom/datadog/android/internal/heatmaps/HeatmapIdentifierRegistry;)V -HSPLcom/datadog/android/rum/internal/domain/scope/RumViewScope;->(Lcom/datadog/android/rum/internal/domain/scope/RumScope;Lcom/datadog/android/core/InternalSdkCore;Lcom/datadog/android/rum/internal/metric/SessionMetricDispatcher;Lcom/datadog/android/rum/internal/domain/scope/RumScopeKey;Lcom/datadog/android/rum/internal/domain/Time;Ljava/util/Map;Lcom/datadog/android/rum/internal/domain/scope/RumViewChangedListener;Lcom/datadog/android/core/internal/net/FirstPartyHostHeaderTypeResolver;Lcom/datadog/android/rum/internal/vitals/VitalMonitor;Lcom/datadog/android/rum/internal/vitals/VitalMonitor;Lcom/datadog/android/rum/internal/vitals/VitalMonitor;Lcom/datadog/android/rum/internal/FeaturesContextResolver;Lcom/datadog/android/rum/internal/domain/scope/RumViewType;ZFLcom/datadog/android/rum/internal/metric/interactiontonextview/InteractionToNextViewMetricResolver;Lcom/datadog/android/rum/internal/metric/networksettled/NetworkSettledMetricResolver;Lcom/datadog/android/rum/internal/metric/slowframes/SlowFramesListener;Lcom/datadog/android/rum/internal/metric/ViewMetricDispatcher;Lcom/datadog/android/rum/RumSessionType;Lcom/datadog/android/rum/internal/domain/accessibility/AccessibilitySnapshotManager;Lcom/datadog/android/rum/internal/domain/InfoProvider;Lcom/datadog/android/rum/internal/domain/InfoProvider;Lcom/datadog/android/rum/internal/instrumentation/insights/InsightsCollector;Lcom/datadog/android/internal/heatmaps/HeatmapIdentifierRegistry;ILkotlin/jvm/internal/DefaultConstructorMarker;)V -HSPLcom/datadog/android/rum/internal/domain/scope/RumViewScope;->access$getONE_SECOND_NS$cp()J -HSPLcom/datadog/android/rum/internal/domain/scope/RumViewScope;->access$getRumSessionTypeOverride$p(Lcom/datadog/android/rum/internal/domain/scope/RumViewScope;)Lcom/datadog/android/rum/RumSessionType; -HSPLcom/datadog/android/rum/internal/domain/scope/RumViewScope;->access$getSdkCore$p(Lcom/datadog/android/rum/internal/domain/scope/RumViewScope;)Lcom/datadog/android/core/InternalSdkCore; -HSPLcom/datadog/android/rum/internal/domain/scope/RumViewScope;->access$getSessionEndedMetricDispatcher$p(Lcom/datadog/android/rum/internal/domain/scope/RumViewScope;)Lcom/datadog/android/rum/internal/metric/SessionMetricDispatcher; -HSPLcom/datadog/android/rum/internal/domain/scope/RumViewScope;->access$getSessionId$p(Lcom/datadog/android/rum/internal/domain/scope/RumViewScope;)Ljava/lang/String; -HSPLcom/datadog/android/rum/internal/domain/scope/RumViewScope;->access$resolveLongTaskProfilingStatus(Lcom/datadog/android/rum/internal/domain/scope/RumViewScope;Lcom/datadog/android/api/context/DatadogContext;)Lcom/datadog/android/rum/model/LongTaskEvent$Profiling; -HSPLcom/datadog/android/rum/internal/domain/scope/RumViewScope;->access$resolveViewProfilingStatus(Lcom/datadog/android/rum/internal/domain/scope/RumViewScope;Lcom/datadog/android/api/context/DatadogContext;)Lcom/datadog/android/rum/model/ViewEvent$Profiling; +HSPLcom/datadog/android/rum/internal/domain/scope/RumViewScope;->(Lcom/datadog/android/rum/internal/domain/scope/RumScope;Lcom/datadog/android/core/InternalSdkCore;Lcom/datadog/android/rum/internal/metric/SessionMetricDispatcher;Lcom/datadog/android/rum/internal/domain/scope/RumScopeKey;Lcom/datadog/android/rum/internal/domain/Time;Ljava/util/Map;Lcom/datadog/android/rum/internal/domain/scope/RumViewChangedListener;Lcom/datadog/android/core/internal/net/FirstPartyHostHeaderTypeResolver;Lcom/datadog/android/rum/internal/vitals/VitalMonitor;Lcom/datadog/android/rum/internal/vitals/VitalMonitor;Lcom/datadog/android/rum/internal/vitals/VitalMonitor;Lcom/datadog/android/rum/internal/FeaturesContextResolver;Lcom/datadog/android/rum/internal/domain/scope/RumViewType;ZFLcom/datadog/android/rum/internal/metric/interactiontonextview/InteractionToNextViewMetricResolver;Lcom/datadog/android/rum/internal/metric/networksettled/NetworkSettledMetricResolver;Lcom/datadog/android/rum/internal/metric/slowframes/SlowFramesListener;Lcom/datadog/android/rum/internal/metric/ViewMetricDispatcher;Lcom/datadog/android/rum/RumSessionType;Lcom/datadog/android/rum/internal/domain/InfoProvider;Lcom/datadog/android/rum/internal/domain/InfoProvider;Lcom/datadog/android/rum/internal/domain/InfoProvider;Lcom/datadog/android/rum/internal/instrumentation/insights/InsightsCollector;Lkotlin/jvm/functions/Function0;Lcom/datadog/android/internal/heatmaps/HeatmapIdentifierRegistry;)V +HSPLcom/datadog/android/rum/internal/domain/scope/RumViewScope;->(Lcom/datadog/android/rum/internal/domain/scope/RumScope;Lcom/datadog/android/core/InternalSdkCore;Lcom/datadog/android/rum/internal/metric/SessionMetricDispatcher;Lcom/datadog/android/rum/internal/domain/scope/RumScopeKey;Lcom/datadog/android/rum/internal/domain/Time;Ljava/util/Map;Lcom/datadog/android/rum/internal/domain/scope/RumViewChangedListener;Lcom/datadog/android/core/internal/net/FirstPartyHostHeaderTypeResolver;Lcom/datadog/android/rum/internal/vitals/VitalMonitor;Lcom/datadog/android/rum/internal/vitals/VitalMonitor;Lcom/datadog/android/rum/internal/vitals/VitalMonitor;Lcom/datadog/android/rum/internal/FeaturesContextResolver;Lcom/datadog/android/rum/internal/domain/scope/RumViewType;ZFLcom/datadog/android/rum/internal/metric/interactiontonextview/InteractionToNextViewMetricResolver;Lcom/datadog/android/rum/internal/metric/networksettled/NetworkSettledMetricResolver;Lcom/datadog/android/rum/internal/metric/slowframes/SlowFramesListener;Lcom/datadog/android/rum/internal/metric/ViewMetricDispatcher;Lcom/datadog/android/rum/RumSessionType;Lcom/datadog/android/rum/internal/domain/InfoProvider;Lcom/datadog/android/rum/internal/domain/InfoProvider;Lcom/datadog/android/rum/internal/domain/InfoProvider;Lcom/datadog/android/rum/internal/instrumentation/insights/InsightsCollector;Lkotlin/jvm/functions/Function0;Lcom/datadog/android/internal/heatmaps/HeatmapIdentifierRegistry;ILkotlin/jvm/internal/DefaultConstructorMarker;)V HSPLcom/datadog/android/rum/internal/domain/scope/RumViewScope;->delegateEventToAction(Lcom/datadog/android/rum/internal/domain/scope/RumRawEvent;Lcom/datadog/android/api/context/DatadogContext;Lkotlin/jvm/functions/Function1;Lcom/datadog/android/api/storage/DataWriter;)V HSPLcom/datadog/android/rum/internal/domain/scope/RumViewScope;->delegateEventToChildren(Lcom/datadog/android/rum/internal/domain/scope/RumRawEvent;Lcom/datadog/android/api/context/DatadogContext;Lkotlin/jvm/functions/Function1;Lcom/datadog/android/api/storage/DataWriter;)V HSPLcom/datadog/android/rum/internal/domain/scope/RumViewScope;->delegateEventToResources(Lcom/datadog/android/rum/internal/domain/scope/RumRawEvent;Lcom/datadog/android/api/context/DatadogContext;Lkotlin/jvm/functions/Function1;Lcom/datadog/android/api/storage/DataWriter;)V HSPLcom/datadog/android/rum/internal/domain/scope/RumViewScope;->getCustomAttributes()Ljava/util/Map; -HSPLcom/datadog/android/rum/internal/domain/scope/RumViewScope;->getEventTimestamp$dd_sdk_android_rum_release()J -HSPLcom/datadog/android/rum/internal/domain/scope/RumViewScope;->getHasReplay$dd_sdk_android_rum_release()Z HSPLcom/datadog/android/rum/internal/domain/scope/RumViewScope;->getParentScope()Lcom/datadog/android/rum/internal/domain/scope/RumScope; HSPLcom/datadog/android/rum/internal/domain/scope/RumViewScope;->getRumContext()Lcom/datadog/android/rum/internal/domain/RumContext; -HSPLcom/datadog/android/rum/internal/domain/scope/RumViewScope;->getSampleRate$dd_sdk_android_rum_release()F -HSPLcom/datadog/android/rum/internal/domain/scope/RumViewScope;->getViewLoadingTime$dd_sdk_android_rum_release()Ljava/lang/Long; HSPLcom/datadog/android/rum/internal/domain/scope/RumViewScope;->handleEvent(Lcom/datadog/android/rum/internal/domain/scope/RumRawEvent;Lcom/datadog/android/api/context/DatadogContext;Lkotlin/jvm/functions/Function1;Lcom/datadog/android/api/storage/DataWriter;)Lcom/datadog/android/rum/internal/domain/scope/RumScope; HSPLcom/datadog/android/rum/internal/domain/scope/RumViewScope;->isActive()Z HSPLcom/datadog/android/rum/internal/domain/scope/RumViewScope;->isViewComplete()Z +HSPLcom/datadog/android/rum/internal/domain/scope/RumViewScope;->onAddLongTask$lambda$36(Lcom/datadog/android/api/context/DatadogContext;Lcom/datadog/android/rum/internal/domain/RumContext;Lcom/datadog/android/rum/internal/domain/scope/RumViewScope;JLcom/datadog/android/rum/internal/domain/scope/RumRawEvent$AddLongTask;Ljava/lang/String;ZLjava/util/Map;)Ljava/lang/Object; +HSPLcom/datadog/android/rum/internal/domain/scope/RumViewScope;->onAddLongTask$lambda$39$lambda$38(Lcom/datadog/android/rum/internal/domain/RumContext;Lcom/datadog/android/rum/internal/monitor/StorageEvent;Lcom/datadog/android/rum/internal/domain/scope/RumViewScope;Ljava/lang/String;JLcom/datadog/android/rum/internal/domain/scope/RumRawEvent$AddLongTask;Lcom/datadog/android/rum/internal/monitor/AdvancedRumMonitor;)Lkotlin/Unit; HSPLcom/datadog/android/rum/internal/domain/scope/RumViewScope;->onAddLongTask(Lcom/datadog/android/rum/internal/domain/scope/RumRawEvent$AddLongTask;Lcom/datadog/android/api/context/DatadogContext;Lkotlin/jvm/functions/Function1;Lcom/datadog/android/api/storage/DataWriter;)V HSPLcom/datadog/android/rum/internal/domain/scope/RumViewScope;->onLongTaskSent(Lcom/datadog/android/rum/internal/domain/scope/RumRawEvent$LongTaskSent;Lcom/datadog/android/api/context/DatadogContext;Lkotlin/jvm/functions/Function1;Lcom/datadog/android/api/storage/DataWriter;)V +HSPLcom/datadog/android/rum/internal/domain/scope/RumViewScope;->onStartView$lambda$13(Lcom/datadog/android/rum/internal/domain/scope/RumViewScope;)Lkotlin/Unit; HSPLcom/datadog/android/rum/internal/domain/scope/RumViewScope;->resolveCustomTimings()Lcom/datadog/android/rum/model/ViewEvent$CustomTimings; HSPLcom/datadog/android/rum/internal/domain/scope/RumViewScope;->resolveLongTaskProfilingStatus(Lcom/datadog/android/api/context/DatadogContext;)Lcom/datadog/android/rum/model/LongTaskEvent$Profiling; HSPLcom/datadog/android/rum/internal/domain/scope/RumViewScope;->resolveRefreshRateInfo(Lcom/datadog/android/rum/internal/vitals/VitalInfo;)Ljava/lang/Boolean; HSPLcom/datadog/android/rum/internal/domain/scope/RumViewScope;->resolveViewDuration(Lcom/datadog/android/rum/internal/domain/scope/RumRawEvent;)V HSPLcom/datadog/android/rum/internal/domain/scope/RumViewScope;->resolveViewProfilingStatus(Lcom/datadog/android/api/context/DatadogContext;)Lcom/datadog/android/rum/model/ViewEvent$Profiling; +HSPLcom/datadog/android/rum/internal/domain/scope/RumViewScope;->sendViewChanged()V HSPLcom/datadog/android/rum/internal/domain/scope/RumViewScope;->sendViewUpdate$dd_sdk_android_rum_release$default(Lcom/datadog/android/rum/internal/domain/scope/RumViewScope;Lcom/datadog/android/rum/internal/domain/scope/RumRawEvent;Lcom/datadog/android/api/context/DatadogContext;Lkotlin/jvm/functions/Function1;Lcom/datadog/android/api/storage/DataWriter;Lcom/datadog/android/api/storage/EventType;ILjava/lang/Object;)V HSPLcom/datadog/android/rum/internal/domain/scope/RumViewScope;->sendViewUpdate$dd_sdk_android_rum_release(Lcom/datadog/android/rum/internal/domain/scope/RumRawEvent;Lcom/datadog/android/api/context/DatadogContext;Lkotlin/jvm/functions/Function1;Lcom/datadog/android/api/storage/DataWriter;Lcom/datadog/android/api/storage/EventType;)V +HSPLcom/datadog/android/rum/internal/domain/scope/RumViewScope;->stopScope(Lcom/datadog/android/rum/internal/domain/scope/RumRawEvent;Lcom/datadog/android/api/context/DatadogContext;Lkotlin/jvm/functions/Function1;Lcom/datadog/android/api/storage/DataWriter;Lkotlin/jvm/functions/Function0;)V +Lcom/datadog/android/rum/internal/domain/scope/RumViewScope$$ExternalSyntheticLambda12; +HSPLcom/datadog/android/rum/internal/domain/scope/RumViewScope$$ExternalSyntheticLambda12;->invoke()Ljava/lang/Object; +Lcom/datadog/android/rum/internal/domain/scope/RumViewScope$$ExternalSyntheticLambda2; +Lcom/datadog/android/rum/internal/domain/scope/RumViewScope$$ExternalSyntheticLambda6; +HSPLcom/datadog/android/rum/internal/domain/scope/RumViewScope$$ExternalSyntheticLambda6;->(Lcom/datadog/android/api/context/DatadogContext;Lcom/datadog/android/rum/internal/domain/RumContext;Lcom/datadog/android/rum/internal/domain/scope/RumViewScope;JLcom/datadog/android/rum/internal/domain/scope/RumRawEvent$AddLongTask;Ljava/lang/String;ZLjava/util/Map;)V +HSPLcom/datadog/android/rum/internal/domain/scope/RumViewScope$$ExternalSyntheticLambda6;->invoke()Ljava/lang/Object; +Lcom/datadog/android/rum/internal/domain/scope/RumViewScope$$ExternalSyntheticLambda7; +HSPLcom/datadog/android/rum/internal/domain/scope/RumViewScope$$ExternalSyntheticLambda7;->(Lcom/datadog/android/rum/internal/domain/RumContext;Lcom/datadog/android/rum/internal/monitor/StorageEvent;)V +Lcom/datadog/android/rum/internal/domain/scope/RumViewScope$$ExternalSyntheticLambda8; +HSPLcom/datadog/android/rum/internal/domain/scope/RumViewScope$$ExternalSyntheticLambda8;->(Lcom/datadog/android/rum/internal/domain/RumContext;Lcom/datadog/android/rum/internal/monitor/StorageEvent;Lcom/datadog/android/rum/internal/domain/scope/RumViewScope;Ljava/lang/String;JLcom/datadog/android/rum/internal/domain/scope/RumRawEvent$AddLongTask;)V +HSPLcom/datadog/android/rum/internal/domain/scope/RumViewScope$$ExternalSyntheticLambda8;->invoke(Ljava/lang/Object;)Ljava/lang/Object; Lcom/datadog/android/rum/internal/domain/scope/RumViewScope$Companion; HSPLcom/datadog/android/rum/internal/domain/scope/RumViewScope$Companion;->()V HSPLcom/datadog/android/rum/internal/domain/scope/RumViewScope$Companion;->(Lkotlin/jvm/internal/DefaultConstructorMarker;)V -HSPLcom/datadog/android/rum/internal/domain/scope/RumViewScope$Companion;->getONE_SECOND_NS$dd_sdk_android_rum_release()J +HSPLcom/datadog/android/rum/internal/domain/scope/RumViewScope$Companion;->fromEvent$dd_sdk_android_rum_release(Lcom/datadog/android/rum/internal/domain/scope/RumScope;Lcom/datadog/android/rum/internal/metric/SessionMetricDispatcher;Lcom/datadog/android/core/InternalSdkCore;Lcom/datadog/android/rum/internal/domain/scope/RumRawEvent$StartView;Lcom/datadog/android/rum/internal/domain/scope/RumViewChangedListener;Lcom/datadog/android/core/internal/net/FirstPartyHostHeaderTypeResolver;Lcom/datadog/android/rum/internal/vitals/VitalMonitor;Lcom/datadog/android/rum/internal/vitals/VitalMonitor;Lcom/datadog/android/rum/internal/vitals/VitalMonitor;ZFLcom/datadog/android/rum/internal/metric/interactiontonextview/InteractionToNextViewMetricResolver;Lcom/datadog/android/rum/metric/networksettled/InitialResourceIdentifier;Lcom/datadog/android/rum/internal/metric/slowframes/SlowFramesListener;Lcom/datadog/android/rum/RumSessionType;Lcom/datadog/android/rum/internal/domain/InfoProvider;Lcom/datadog/android/rum/internal/domain/InfoProvider;Lcom/datadog/android/rum/internal/domain/InfoProvider;Lcom/datadog/android/rum/internal/instrumentation/insights/InsightsCollector;Lcom/datadog/android/rum/event/ViewEventMapper;Lcom/datadog/android/rum/configuration/RumViewEventWriteConfig;Lcom/datadog/android/internal/heatmaps/HeatmapIdentifierRegistry;)Lcom/datadog/android/rum/internal/domain/scope/RumViewScope; +HSPLcom/datadog/android/rum/internal/domain/scope/RumViewScope$Companion;->fromEvent$lambda$0(Lcom/datadog/android/rum/configuration/RumViewEventWriteConfig;Lcom/datadog/android/rum/event/ViewEventMapper;Lcom/datadog/android/core/InternalSdkCore;)Lcom/datadog/android/rum/internal/domain/scope/RumViewEventWriter; +HSPLcom/datadog/android/rum/internal/domain/scope/RumViewScope$Companion;->tryResolveInstrumentationType(Lcom/datadog/android/rum/internal/domain/scope/RumRawEvent$StartView;)Lcom/datadog/android/internal/attributes/ViewScopeInstrumentationType; +Lcom/datadog/android/rum/internal/domain/scope/RumViewScope$Companion$$ExternalSyntheticLambda0; +HSPLcom/datadog/android/rum/internal/domain/scope/RumViewScope$Companion$$ExternalSyntheticLambda0;->(Lcom/datadog/android/rum/configuration/RumViewEventWriteConfig;Lcom/datadog/android/rum/event/ViewEventMapper;Lcom/datadog/android/core/InternalSdkCore;)V +HSPLcom/datadog/android/rum/internal/domain/scope/RumViewScope$Companion$$ExternalSyntheticLambda0;->invoke()Ljava/lang/Object; Lcom/datadog/android/rum/internal/domain/scope/RumViewScope$cpuVitalListener$1; HSPLcom/datadog/android/rum/internal/domain/scope/RumViewScope$cpuVitalListener$1;->(Lcom/datadog/android/rum/internal/domain/scope/RumViewScope;)V Lcom/datadog/android/rum/internal/domain/scope/RumViewScope$frameRateVitalListener$1; HSPLcom/datadog/android/rum/internal/domain/scope/RumViewScope$frameRateVitalListener$1;->(Lcom/datadog/android/rum/internal/domain/scope/RumViewScope;)V Lcom/datadog/android/rum/internal/domain/scope/RumViewScope$memoryVitalListener$1; HSPLcom/datadog/android/rum/internal/domain/scope/RumViewScope$memoryVitalListener$1;->(Lcom/datadog/android/rum/internal/domain/scope/RumViewScope;)V -Lcom/datadog/android/rum/internal/domain/scope/RumViewScope$onAddLongTask$1; -HSPLcom/datadog/android/rum/internal/domain/scope/RumViewScope$onAddLongTask$1;->(Lcom/datadog/android/api/context/DatadogContext;Lcom/datadog/android/rum/internal/domain/RumContext;Lcom/datadog/android/rum/internal/domain/scope/RumViewScope;JLcom/datadog/android/rum/internal/domain/scope/RumRawEvent$AddLongTask;Ljava/lang/String;ZLjava/util/Map;)V -HSPLcom/datadog/android/rum/internal/domain/scope/RumViewScope$onAddLongTask$1;->invoke()Ljava/lang/Object; -Lcom/datadog/android/rum/internal/domain/scope/RumViewScope$onAddLongTask$2$1; -HSPLcom/datadog/android/rum/internal/domain/scope/RumViewScope$onAddLongTask$2$1;->(Lcom/datadog/android/rum/internal/domain/RumContext;Lcom/datadog/android/rum/internal/monitor/StorageEvent;)V -Lcom/datadog/android/rum/internal/domain/scope/RumViewScope$onAddLongTask$2$2; -HSPLcom/datadog/android/rum/internal/domain/scope/RumViewScope$onAddLongTask$2$2;->(Lcom/datadog/android/rum/internal/domain/RumContext;Lcom/datadog/android/rum/internal/monitor/StorageEvent;Lcom/datadog/android/rum/internal/domain/scope/RumViewScope;Ljava/lang/String;JLcom/datadog/android/rum/internal/domain/scope/RumRawEvent$AddLongTask;)V -HSPLcom/datadog/android/rum/internal/domain/scope/RumViewScope$onAddLongTask$2$2;->invoke(Lcom/datadog/android/rum/internal/monitor/AdvancedRumMonitor;)V -HSPLcom/datadog/android/rum/internal/domain/scope/RumViewScope$onAddLongTask$2$2;->invoke(Ljava/lang/Object;)Ljava/lang/Object; -Lcom/datadog/android/rum/internal/domain/scope/RumViewScope$sendViewUpdate$1; -HSPLcom/datadog/android/rum/internal/domain/scope/RumViewScope$sendViewUpdate$1;->(Lcom/datadog/android/api/context/DatadogContext;JLcom/datadog/android/rum/internal/domain/RumContext;Lcom/datadog/android/rum/internal/domain/scope/RumViewScope;Ljava/util/Map;JJJJJJZJLjava/lang/Double;Lcom/datadog/android/rum/internal/vitals/VitalInfo;Lcom/datadog/android/rum/internal/vitals/VitalInfo;ILkotlin/jvm/internal/Ref$ObjectRef;Ljava/lang/String;Ljava/lang/Long;Lcom/datadog/android/rum/model/ViewEvent$CustomTimings;ZLjava/util/List;Ljava/lang/Double;Ljava/lang/Double;Lcom/datadog/android/rum/model/ViewEvent$FlutterBuildTime;Lcom/datadog/android/rum/model/ViewEvent$FlutterBuildTime;Lcom/datadog/android/rum/model/ViewEvent$FlutterBuildTime;Lcom/datadog/android/rum/model/ViewEvent$Performance;Lcom/datadog/android/rum/model/ViewEvent$Accessibility;Lcom/datadog/android/rum/internal/domain/battery/BatteryInfo;Lcom/datadog/android/rum/internal/domain/display/DisplayInfo;Ljava/util/Map;J)V -HSPLcom/datadog/android/rum/internal/domain/scope/RumViewScope$sendViewUpdate$1;->invoke()Ljava/lang/Object; Lcom/datadog/android/rum/internal/domain/scope/RumViewType; HSPLcom/datadog/android/rum/internal/domain/scope/RumViewType;->$values()[Lcom/datadog/android/rum/internal/domain/scope/RumViewType; HSPLcom/datadog/android/rum/internal/domain/scope/RumViewType;->()V HSPLcom/datadog/android/rum/internal/domain/scope/RumViewType;->(Ljava/lang/String;ILjava/lang/String;)V HSPLcom/datadog/android/rum/internal/domain/scope/RumViewType;->getAsString()Ljava/lang/String; -PLcom/datadog/android/rum/internal/domain/scope/RumViewType;->values()[Lcom/datadog/android/rum/internal/domain/scope/RumViewType; +HSPLcom/datadog/android/rum/internal/domain/scope/RumViewType;->values()[Lcom/datadog/android/rum/internal/domain/scope/RumViewType; Lcom/datadog/android/rum/internal/domain/scope/RumViewType$Companion; HSPLcom/datadog/android/rum/internal/domain/scope/RumViewType$Companion;->()V HSPLcom/datadog/android/rum/internal/domain/scope/RumViewType$Companion;->(Lkotlin/jvm/internal/DefaultConstructorMarker;)V -PLcom/datadog/android/rum/internal/domain/scope/RumViewType$Companion;->fromString(Ljava/lang/String;)Lcom/datadog/android/rum/internal/domain/scope/RumViewType; +Lcom/datadog/android/rum/internal/domain/scope/RumViewUpdateData; Lcom/datadog/android/rum/internal/domain/scope/RumVitalAppLaunchEventHelper; HSPLcom/datadog/android/rum/internal/domain/scope/RumVitalAppLaunchEventHelper;->(Lcom/datadog/android/rum/RumSessionType;Lcom/datadog/android/rum/internal/domain/InfoProvider;Lcom/datadog/android/rum/internal/domain/InfoProvider;FLcom/datadog/android/api/InternalLogger;)V Lcom/datadog/android/rum/internal/domain/state/ViewUIPerformanceReport; +PLcom/datadog/android/rum/internal/domain/state/ViewUIPerformanceReport;->()V +PLcom/datadog/android/rum/internal/domain/state/ViewUIPerformanceReport;->(JIJ)V +PLcom/datadog/android/rum/internal/domain/state/ViewUIPerformanceReport;->(JLcom/datadog/android/internal/collections/EvictingQueue;JJJJ)V +PLcom/datadog/android/rum/internal/domain/state/ViewUIPerformanceReport;->(JLcom/datadog/android/internal/collections/EvictingQueue;JJJJILkotlin/jvm/internal/DefaultConstructorMarker;)V +PLcom/datadog/android/rum/internal/domain/state/ViewUIPerformanceReport;->getTotalFramesDurationNs()J +PLcom/datadog/android/rum/internal/domain/state/ViewUIPerformanceReport;->setTotalFramesDurationNs(J)V +PLcom/datadog/android/rum/internal/domain/state/ViewUIPerformanceReport$Companion;->()V +PLcom/datadog/android/rum/internal/domain/state/ViewUIPerformanceReport$Companion;->(Lkotlin/jvm/internal/DefaultConstructorMarker;)V Lcom/datadog/android/rum/internal/instrumentation/MainLooperLongTaskStrategy; HSPLcom/datadog/android/rum/internal/instrumentation/MainLooperLongTaskStrategy;->()V HSPLcom/datadog/android/rum/internal/instrumentation/MainLooperLongTaskStrategy;->(J)V @@ -21060,6 +21219,12 @@ Lcom/datadog/android/rum/internal/metric/NoValueReason$InteractionToNextView; HSPLcom/datadog/android/rum/internal/metric/NoValueReason$InteractionToNextView;->$values()[Lcom/datadog/android/rum/internal/metric/NoValueReason$InteractionToNextView; HSPLcom/datadog/android/rum/internal/metric/NoValueReason$InteractionToNextView;->()V HSPLcom/datadog/android/rum/internal/metric/NoValueReason$InteractionToNextView;->(Ljava/lang/String;I)V +HSPLcom/datadog/android/rum/internal/metric/NoValueReason$InteractionToNextView;->values()[Lcom/datadog/android/rum/internal/metric/NoValueReason$InteractionToNextView; +Lcom/datadog/android/rum/internal/metric/NoValueReason$TimeToNetworkSettle; +HSPLcom/datadog/android/rum/internal/metric/NoValueReason$TimeToNetworkSettle;->$values()[Lcom/datadog/android/rum/internal/metric/NoValueReason$TimeToNetworkSettle; +HSPLcom/datadog/android/rum/internal/metric/NoValueReason$TimeToNetworkSettle;->()V +HSPLcom/datadog/android/rum/internal/metric/NoValueReason$TimeToNetworkSettle;->(Ljava/lang/String;I)V +HSPLcom/datadog/android/rum/internal/metric/NoValueReason$TimeToNetworkSettle;->values()[Lcom/datadog/android/rum/internal/metric/NoValueReason$TimeToNetworkSettle; Lcom/datadog/android/rum/internal/metric/SessionEndedMetric; HSPLcom/datadog/android/rum/internal/metric/SessionEndedMetric;->()V HSPLcom/datadog/android/rum/internal/metric/SessionEndedMetric;->(Ljava/lang/String;Lcom/datadog/android/rum/internal/domain/scope/RumSessionScope$StartReason;JZ)V @@ -21069,6 +21234,8 @@ HSPLcom/datadog/android/rum/internal/metric/SessionEndedMetric$Companion;->(Lkotlin/jvm/internal/DefaultConstructorMarker;)V Lcom/datadog/android/rum/internal/metric/SessionEndedMetric$TrackedView; HSPLcom/datadog/android/rum/internal/metric/SessionEndedMetric$TrackedView;->(Ljava/lang/String;JJZ)V +PLcom/datadog/android/rum/internal/metric/SessionEndedMetric$TrackedView;->getStartMs()J +PLcom/datadog/android/rum/internal/metric/SessionEndedMetric$TrackedView;->getViewUrl()Ljava/lang/String; Lcom/datadog/android/rum/internal/metric/SessionEndedMetricDispatcher; HSPLcom/datadog/android/rum/internal/metric/SessionEndedMetricDispatcher;->(Lcom/datadog/android/api/InternalLogger;F)V HSPLcom/datadog/android/rum/internal/metric/SessionEndedMetricDispatcher;->onViewTracked(Ljava/lang/String;Lcom/datadog/android/rum/model/ViewEvent;)V @@ -21078,16 +21245,31 @@ Lcom/datadog/android/rum/internal/metric/ViewEndedMetricDispatcher; HSPLcom/datadog/android/rum/internal/metric/ViewEndedMetricDispatcher;->()V HSPLcom/datadog/android/rum/internal/metric/ViewEndedMetricDispatcher;->(Lcom/datadog/android/rum/internal/domain/scope/RumViewType;Lcom/datadog/android/api/InternalLogger;Lcom/datadog/android/internal/attributes/ViewScopeInstrumentationType;F)V HSPLcom/datadog/android/rum/internal/metric/ViewEndedMetricDispatcher;->(Lcom/datadog/android/rum/internal/domain/scope/RumViewType;Lcom/datadog/android/api/InternalLogger;Lcom/datadog/android/internal/attributes/ViewScopeInstrumentationType;FILkotlin/jvm/internal/DefaultConstructorMarker;)V +HSPLcom/datadog/android/rum/internal/metric/ViewEndedMetricDispatcher;->buildAttributesMap(Lcom/datadog/android/rum/internal/metric/ViewInitializationMetricsState;Lcom/datadog/android/rum/internal/metric/ViewInitializationMetricsState;)Ljava/util/Map; +HSPLcom/datadog/android/rum/internal/metric/ViewEndedMetricDispatcher;->buildMetricAttributesMap(Lcom/datadog/android/rum/internal/metric/ViewInitializationMetricsState;Lcom/datadog/android/rum/internal/metric/ViewInitializationMetricsState;)Ljava/util/Map; HSPLcom/datadog/android/rum/internal/metric/ViewEndedMetricDispatcher;->onDurationResolved(J)V +HSPLcom/datadog/android/rum/internal/metric/ViewEndedMetricDispatcher;->sendViewEnded(Lcom/datadog/android/rum/internal/metric/ViewInitializationMetricsState;Lcom/datadog/android/rum/internal/metric/ViewInitializationMetricsState;)V +Lcom/datadog/android/rum/internal/metric/ViewEndedMetricDispatcher$$ExternalSyntheticLambda1; +HSPLcom/datadog/android/rum/internal/metric/ViewEndedMetricDispatcher$$ExternalSyntheticLambda1;->()V Lcom/datadog/android/rum/internal/metric/ViewEndedMetricDispatcher$Companion; HSPLcom/datadog/android/rum/internal/metric/ViewEndedMetricDispatcher$Companion;->()V HSPLcom/datadog/android/rum/internal/metric/ViewEndedMetricDispatcher$Companion;->(Lkotlin/jvm/internal/DefaultConstructorMarker;)V +HSPLcom/datadog/android/rum/internal/metric/ViewEndedMetricDispatcher$Companion;->access$putNonNull(Lcom/datadog/android/rum/internal/metric/ViewEndedMetricDispatcher$Companion;Ljava/util/Map;Ljava/lang/Object;Ljava/lang/Object;)V +HSPLcom/datadog/android/rum/internal/metric/ViewEndedMetricDispatcher$Companion;->putNonNull(Ljava/util/Map;Ljava/lang/Object;Ljava/lang/Object;)V +HSPLcom/datadog/android/rum/internal/metric/ViewEndedMetricDispatcher$Companion;->toAttributeValue$dd_sdk_android_rum_release(Lcom/datadog/android/rum/internal/domain/scope/RumViewType;)Ljava/lang/String; +HSPLcom/datadog/android/rum/internal/metric/ViewEndedMetricDispatcher$Companion;->toAttributeValue$dd_sdk_android_rum_release(Lcom/datadog/android/rum/internal/metric/NoValueReason;)Ljava/lang/String; +HSPLcom/datadog/android/rum/internal/metric/ViewEndedMetricDispatcher$Companion;->toAttributeValue$dd_sdk_android_rum_release(Lcom/datadog/android/rum/internal/metric/ViewInitializationMetricsConfig;)Ljava/lang/String; +Lcom/datadog/android/rum/internal/metric/ViewEndedMetricDispatcher$Companion$WhenMappings; +HSPLcom/datadog/android/rum/internal/metric/ViewEndedMetricDispatcher$Companion$WhenMappings;->()V Lcom/datadog/android/rum/internal/metric/ViewInitializationMetricsConfig; HSPLcom/datadog/android/rum/internal/metric/ViewInitializationMetricsConfig;->$values()[Lcom/datadog/android/rum/internal/metric/ViewInitializationMetricsConfig; HSPLcom/datadog/android/rum/internal/metric/ViewInitializationMetricsConfig;->()V HSPLcom/datadog/android/rum/internal/metric/ViewInitializationMetricsConfig;->(Ljava/lang/String;I)V +HSPLcom/datadog/android/rum/internal/metric/ViewInitializationMetricsConfig;->values()[Lcom/datadog/android/rum/internal/metric/ViewInitializationMetricsConfig; Lcom/datadog/android/rum/internal/metric/ViewInitializationMetricsState; HSPLcom/datadog/android/rum/internal/metric/ViewInitializationMetricsState;->(Ljava/lang/Long;Lcom/datadog/android/rum/internal/metric/ViewInitializationMetricsConfig;Lcom/datadog/android/rum/internal/metric/NoValueReason;)V +HSPLcom/datadog/android/rum/internal/metric/ViewInitializationMetricsState;->getConfig()Lcom/datadog/android/rum/internal/metric/ViewInitializationMetricsConfig; +HSPLcom/datadog/android/rum/internal/metric/ViewInitializationMetricsState;->getInitializationTime()Ljava/lang/Long; HSPLcom/datadog/android/rum/internal/metric/ViewInitializationMetricsState;->getNoValueReason()Lcom/datadog/android/rum/internal/metric/NoValueReason; Lcom/datadog/android/rum/internal/metric/ViewMetricDispatcher; Lcom/datadog/android/rum/internal/metric/interactiontonextview/ActionTypeInteractionValidator; @@ -21113,27 +21295,46 @@ HSPLcom/datadog/android/rum/internal/metric/interactiontonextview/InteractionToN HSPLcom/datadog/android/rum/internal/metric/interactiontonextview/InteractionToNextViewMetricResolver$Companion;->(Lkotlin/jvm/internal/DefaultConstructorMarker;)V HSPLcom/datadog/android/rum/internal/metric/interactiontonextview/InteractionToNextViewMetricResolver$Companion;->access$toConfig(Lcom/datadog/android/rum/internal/metric/interactiontonextview/InteractionToNextViewMetricResolver$Companion;Lcom/datadog/android/rum/metric/interactiontonextview/LastInteractionIdentifier;)Lcom/datadog/android/rum/internal/metric/ViewInitializationMetricsConfig; HSPLcom/datadog/android/rum/internal/metric/interactiontonextview/InteractionToNextViewMetricResolver$Companion;->toConfig(Lcom/datadog/android/rum/metric/interactiontonextview/LastInteractionIdentifier;)Lcom/datadog/android/rum/internal/metric/ViewInitializationMetricsConfig; +Lcom/datadog/android/rum/internal/metric/interactiontonextview/InternalInteractionContext; Lcom/datadog/android/rum/internal/metric/networksettled/NetworkSettledMetricResolver; HSPLcom/datadog/android/rum/internal/metric/networksettled/NetworkSettledMetricResolver;->()V HSPLcom/datadog/android/rum/internal/metric/networksettled/NetworkSettledMetricResolver;->(Lcom/datadog/android/rum/metric/networksettled/InitialResourceIdentifier;Lcom/datadog/android/api/InternalLogger;)V HSPLcom/datadog/android/rum/internal/metric/networksettled/NetworkSettledMetricResolver;->computeMetric()Ljava/lang/Long; +HSPLcom/datadog/android/rum/internal/metric/networksettled/NetworkSettledMetricResolver;->getState()Lcom/datadog/android/rum/internal/metric/ViewInitializationMetricsState; HSPLcom/datadog/android/rum/internal/metric/networksettled/NetworkSettledMetricResolver;->resolveMetric()Ljava/lang/Long; HSPLcom/datadog/android/rum/internal/metric/networksettled/NetworkSettledMetricResolver;->viewWasCreated(J)V +HSPLcom/datadog/android/rum/internal/metric/networksettled/NetworkSettledMetricResolver;->viewWasStopped()V Lcom/datadog/android/rum/internal/metric/networksettled/NetworkSettledMetricResolver$Companion; HSPLcom/datadog/android/rum/internal/metric/networksettled/NetworkSettledMetricResolver$Companion;->()V HSPLcom/datadog/android/rum/internal/metric/networksettled/NetworkSettledMetricResolver$Companion;->(Lkotlin/jvm/internal/DefaultConstructorMarker;)V +HSPLcom/datadog/android/rum/internal/metric/networksettled/NetworkSettledMetricResolver$Companion;->access$toConfig(Lcom/datadog/android/rum/internal/metric/networksettled/NetworkSettledMetricResolver$Companion;Lcom/datadog/android/rum/metric/networksettled/InitialResourceIdentifier;)Lcom/datadog/android/rum/internal/metric/ViewInitializationMetricsConfig; +HSPLcom/datadog/android/rum/internal/metric/networksettled/NetworkSettledMetricResolver$Companion;->toConfig(Lcom/datadog/android/rum/metric/networksettled/InitialResourceIdentifier;)Lcom/datadog/android/rum/internal/metric/ViewInitializationMetricsConfig; Lcom/datadog/android/rum/internal/metric/networksettled/NetworkSettledMetricResolver$Companion$Diagnostic; HSPLcom/datadog/android/rum/internal/metric/networksettled/NetworkSettledMetricResolver$Companion$Diagnostic;->(Ljava/util/concurrent/atomic/AtomicInteger;Ljava/util/concurrent/atomic/AtomicInteger;Ljava/util/concurrent/atomic/AtomicInteger;Ljava/util/concurrent/atomic/AtomicInteger;)V HSPLcom/datadog/android/rum/internal/metric/networksettled/NetworkSettledMetricResolver$Companion$Diagnostic;->(Ljava/util/concurrent/atomic/AtomicInteger;Ljava/util/concurrent/atomic/AtomicInteger;Ljava/util/concurrent/atomic/AtomicInteger;Ljava/util/concurrent/atomic/AtomicInteger;ILkotlin/jvm/internal/DefaultConstructorMarker;)V +HSPLcom/datadog/android/rum/internal/metric/networksettled/NetworkSettledMetricResolver$Companion$Diagnostic;->resolveNoValueReason()Lcom/datadog/android/rum/internal/metric/NoValueReason$TimeToNetworkSettle; Lcom/datadog/android/rum/internal/metric/slowframes/DefaultSlowFramesListener; HSPLcom/datadog/android/rum/internal/metric/slowframes/DefaultSlowFramesListener;->(Lcom/datadog/android/rum/configuration/SlowFramesConfiguration;Lcom/datadog/android/rum/internal/metric/slowframes/UISlownessMetricDispatcher;Lcom/datadog/android/rum/internal/instrumentation/insights/InsightsCollector;Lcom/datadog/android/internal/time/TimeProvider;)V +PLcom/datadog/android/rum/internal/metric/slowframes/DefaultSlowFramesListener;->getViewPerformanceReport$lambda$3(Lcom/datadog/android/rum/internal/metric/slowframes/DefaultSlowFramesListener;)Lcom/datadog/android/rum/internal/domain/state/ViewUIPerformanceReport; +PLcom/datadog/android/rum/internal/metric/slowframes/DefaultSlowFramesListener;->getViewPerformanceReport$lambda$4(Lkotlin/jvm/functions/Function0;Ljava/lang/String;)Lcom/datadog/android/rum/internal/domain/state/ViewUIPerformanceReport; +PLcom/datadog/android/rum/internal/metric/slowframes/DefaultSlowFramesListener;->getViewPerformanceReport$lambda$5(Lkotlin/jvm/functions/Function1;Ljava/lang/Object;)Lcom/datadog/android/rum/internal/domain/state/ViewUIPerformanceReport; +PLcom/datadog/android/rum/internal/metric/slowframes/DefaultSlowFramesListener;->getViewPerformanceReport(Ljava/lang/String;)Lcom/datadog/android/rum/internal/domain/state/ViewUIPerformanceReport; HSPLcom/datadog/android/rum/internal/metric/slowframes/DefaultSlowFramesListener;->onAddLongTask(J)V +PLcom/datadog/android/rum/internal/metric/slowframes/DefaultSlowFramesListener;->onFrame(Landroidx/metrics/performance/FrameData;)V +HSPLcom/datadog/android/rum/internal/metric/slowframes/DefaultSlowFramesListener;->onFrameMetricsData(Lcom/datadog/android/rum/internal/domain/FrameMetricsData;)V HSPLcom/datadog/android/rum/internal/metric/slowframes/DefaultSlowFramesListener;->onViewCreated(Ljava/lang/String;J)V HSPLcom/datadog/android/rum/internal/metric/slowframes/DefaultSlowFramesListener;->resolveReport(Ljava/lang/String;ZJ)Lcom/datadog/android/rum/internal/domain/state/ViewUIPerformanceReport$Snapshot; +PLcom/datadog/android/rum/internal/metric/slowframes/DefaultSlowFramesListener$$ExternalSyntheticLambda0;->(Lcom/datadog/android/rum/internal/metric/slowframes/DefaultSlowFramesListener;)V +PLcom/datadog/android/rum/internal/metric/slowframes/DefaultSlowFramesListener$$ExternalSyntheticLambda0;->invoke()Ljava/lang/Object; +PLcom/datadog/android/rum/internal/metric/slowframes/DefaultSlowFramesListener$$ExternalSyntheticLambda1;->(Lkotlin/jvm/functions/Function0;)V +PLcom/datadog/android/rum/internal/metric/slowframes/DefaultSlowFramesListener$$ExternalSyntheticLambda1;->invoke(Ljava/lang/Object;)Ljava/lang/Object; +PLcom/datadog/android/rum/internal/metric/slowframes/DefaultSlowFramesListener$$ExternalSyntheticLambda2;->(Lkotlin/jvm/functions/Function1;)V +PLcom/datadog/android/rum/internal/metric/slowframes/DefaultSlowFramesListener$$ExternalSyntheticLambda2;->apply(Ljava/lang/Object;)Ljava/lang/Object; Lcom/datadog/android/rum/internal/metric/slowframes/DefaultUISlownessMetricDispatcher; HSPLcom/datadog/android/rum/internal/metric/slowframes/DefaultUISlownessMetricDispatcher;->()V HSPLcom/datadog/android/rum/internal/metric/slowframes/DefaultUISlownessMetricDispatcher;->(Lcom/datadog/android/rum/configuration/SlowFramesConfiguration;Lcom/datadog/android/api/InternalLogger;F)V HSPLcom/datadog/android/rum/internal/metric/slowframes/DefaultUISlownessMetricDispatcher;->(Lcom/datadog/android/rum/configuration/SlowFramesConfiguration;Lcom/datadog/android/api/InternalLogger;FILkotlin/jvm/internal/DefaultConstructorMarker;)V +PLcom/datadog/android/rum/internal/metric/slowframes/DefaultUISlownessMetricDispatcher;->incrementIgnoredFrameCount(Ljava/lang/String;)V HSPLcom/datadog/android/rum/internal/metric/slowframes/DefaultUISlownessMetricDispatcher;->onViewCreated(Ljava/lang/String;)V Lcom/datadog/android/rum/internal/metric/slowframes/DefaultUISlownessMetricDispatcher$Companion; HSPLcom/datadog/android/rum/internal/metric/slowframes/DefaultUISlownessMetricDispatcher$Companion;->()V @@ -21141,56 +21342,53 @@ HSPLcom/datadog/android/rum/internal/metric/slowframes/DefaultUISlownessMetricDi Lcom/datadog/android/rum/internal/metric/slowframes/DefaultUISlownessMetricDispatcher$SlowFramesTelemetry; HSPLcom/datadog/android/rum/internal/metric/slowframes/DefaultUISlownessMetricDispatcher$SlowFramesTelemetry;->(Ljava/util/concurrent/atomic/AtomicInteger;Ljava/util/concurrent/atomic/AtomicInteger;Ljava/util/concurrent/atomic/AtomicInteger;)V HSPLcom/datadog/android/rum/internal/metric/slowframes/DefaultUISlownessMetricDispatcher$SlowFramesTelemetry;->(Ljava/util/concurrent/atomic/AtomicInteger;Ljava/util/concurrent/atomic/AtomicInteger;Ljava/util/concurrent/atomic/AtomicInteger;ILkotlin/jvm/internal/DefaultConstructorMarker;)V +PLcom/datadog/android/rum/internal/metric/slowframes/DefaultUISlownessMetricDispatcher$SlowFramesTelemetry;->getIgnoredFramesCount()Ljava/util/concurrent/atomic/AtomicInteger; Lcom/datadog/android/rum/internal/metric/slowframes/SlowFramesListener; Lcom/datadog/android/rum/internal/metric/slowframes/UISlownessMetricDispatcher; Lcom/datadog/android/rum/internal/monitor/AdvancedNetworkRumMonitor; Lcom/datadog/android/rum/internal/monitor/AdvancedRumMonitor; Lcom/datadog/android/rum/internal/monitor/DatadogRumMonitor; HSPLcom/datadog/android/rum/internal/monitor/DatadogRumMonitor;->()V -HSPLcom/datadog/android/rum/internal/monitor/DatadogRumMonitor;->(Ljava/lang/String;Lcom/datadog/android/core/InternalSdkCore;Ljava/lang/String;Lcom/datadog/android/core/sampling/Sampler;ZZLcom/datadog/android/api/storage/DataWriter;Landroid/os/Handler;Lcom/datadog/android/telemetry/internal/TelemetryEventHandler;Lcom/datadog/android/rum/internal/metric/SessionMetricDispatcher;Lcom/datadog/android/core/internal/net/FirstPartyHostHeaderTypeResolver;Lcom/datadog/android/rum/internal/vitals/VitalMonitor;Lcom/datadog/android/rum/internal/vitals/VitalMonitor;Lcom/datadog/android/rum/internal/vitals/VitalMonitor;Lcom/datadog/android/rum/RumSessionListener;Ljava/util/concurrent/ExecutorService;Lcom/datadog/android/rum/metric/networksettled/InitialResourceIdentifier;Lcom/datadog/android/rum/metric/interactiontonextview/LastInteractionIdentifier;Lcom/datadog/android/rum/internal/metric/slowframes/SlowFramesListener;Lcom/datadog/android/rum/RumSessionType;Lcom/datadog/android/rum/internal/domain/accessibility/AccessibilitySnapshotManager;Lcom/datadog/android/rum/internal/domain/InfoProvider;Lcom/datadog/android/rum/internal/domain/InfoProvider;Lkotlin/jvm/functions/Function0;Lcom/datadog/android/rum/internal/instrumentation/insights/InsightsCollector;Lcom/datadog/android/internal/heatmaps/HeatmapIdentifierRegistry;Lcom/datadog/android/rum/internal/timeseries/TimeseriesCollector$Factory;)V -HSPLcom/datadog/android/rum/internal/monitor/DatadogRumMonitor;->access$currentRumContext(Lcom/datadog/android/rum/internal/monitor/DatadogRumMonitor;)Lcom/datadog/android/rum/internal/domain/RumContext; -HSPLcom/datadog/android/rum/internal/monitor/DatadogRumMonitor;->access$getSdkCore$p(Lcom/datadog/android/rum/internal/monitor/DatadogRumMonitor;)Lcom/datadog/android/core/InternalSdkCore; -HSPLcom/datadog/android/rum/internal/monitor/DatadogRumMonitor;->access$getWriter$p(Lcom/datadog/android/rum/internal/monitor/DatadogRumMonitor;)Lcom/datadog/android/api/storage/DataWriter; -HSPLcom/datadog/android/rum/internal/monitor/DatadogRumMonitor;->access$handleEventWithMethodCallPerf(Lcom/datadog/android/rum/internal/monitor/DatadogRumMonitor;Lcom/datadog/android/rum/internal/domain/scope/RumRawEvent;Lcom/datadog/android/api/context/DatadogContext;Lkotlin/jvm/functions/Function1;)V -HSPLcom/datadog/android/rum/internal/monitor/DatadogRumMonitor;->access$updateCachedViewUrl(Lcom/datadog/android/rum/internal/monitor/DatadogRumMonitor;Lcom/datadog/android/rum/internal/domain/RumContext;)V +HSPLcom/datadog/android/rum/internal/monitor/DatadogRumMonitor;->(Ljava/lang/String;Lcom/datadog/android/core/InternalSdkCore;Ljava/lang/String;Lcom/datadog/android/core/sampling/Sampler;ZZLcom/datadog/android/api/storage/DataWriter;Landroid/os/Handler;Lcom/datadog/android/telemetry/internal/TelemetryEventHandler;Lcom/datadog/android/rum/internal/metric/SessionMetricDispatcher;Lcom/datadog/android/core/internal/net/FirstPartyHostHeaderTypeResolver;Lcom/datadog/android/rum/internal/vitals/VitalMonitor;Lcom/datadog/android/rum/internal/vitals/VitalMonitor;Lcom/datadog/android/rum/internal/vitals/VitalMonitor;Lcom/datadog/android/rum/RumSessionListener;Ljava/util/concurrent/ExecutorService;Lcom/datadog/android/rum/metric/networksettled/InitialResourceIdentifier;Lcom/datadog/android/rum/metric/interactiontonextview/LastInteractionIdentifier;Lcom/datadog/android/rum/internal/metric/slowframes/SlowFramesListener;Lcom/datadog/android/rum/RumSessionType;Lcom/datadog/android/rum/internal/domain/InfoProvider;Lcom/datadog/android/rum/internal/domain/InfoProvider;Lcom/datadog/android/rum/internal/domain/InfoProvider;Lkotlin/jvm/functions/Function0;Lcom/datadog/android/rum/internal/instrumentation/insights/InsightsCollector;Lcom/datadog/android/rum/event/ViewEventMapper;Lcom/datadog/android/rum/configuration/RumViewEventWriteConfig;Lcom/datadog/android/internal/heatmaps/HeatmapIdentifierRegistry;Lcom/datadog/android/rum/internal/timeseries/TimeseriesCollector$Factory;)V HSPLcom/datadog/android/rum/internal/monitor/DatadogRumMonitor;->addLongTask(JLjava/lang/String;)V HSPLcom/datadog/android/rum/internal/monitor/DatadogRumMonitor;->currentRumContext()Lcom/datadog/android/rum/internal/domain/RumContext; HSPLcom/datadog/android/rum/internal/monitor/DatadogRumMonitor;->eventSent(Ljava/lang/String;Lcom/datadog/android/rum/internal/monitor/StorageEvent;)V HSPLcom/datadog/android/rum/internal/monitor/DatadogRumMonitor;->getAttributes()Ljava/util/Map; +HSPLcom/datadog/android/rum/internal/monitor/DatadogRumMonitor;->getEventTime(Ljava/util/Map;)Lcom/datadog/android/rum/internal/domain/Time; HSPLcom/datadog/android/rum/internal/monitor/DatadogRumMonitor;->getExecutorService$dd_sdk_android_rum_release()Ljava/util/concurrent/ExecutorService; -HSPLcom/datadog/android/rum/internal/monitor/DatadogRumMonitor;->getRootScope$dd_sdk_android_rum_release()Lcom/datadog/android/rum/internal/domain/scope/RumApplicationScope; HSPLcom/datadog/android/rum/internal/monitor/DatadogRumMonitor;->handleEvent$dd_sdk_android_rum_release(Lcom/datadog/android/rum/internal/domain/scope/RumRawEvent;)V +HSPLcom/datadog/android/rum/internal/monitor/DatadogRumMonitor;->handleEvent$lambda$22$lambda$19(Lcom/datadog/android/rum/internal/monitor/DatadogRumMonitor;Lcom/datadog/android/rum/internal/domain/scope/RumRawEvent;Lcom/datadog/android/api/context/DatadogContext;Lkotlin/jvm/functions/Function1;)Lcom/datadog/android/rum/internal/domain/RumContext; +HSPLcom/datadog/android/rum/internal/monitor/DatadogRumMonitor;->handleEvent$lambda$22$lambda$21(Lcom/datadog/android/rum/internal/domain/RumContext;Ljava/util/Map;)Lkotlin/Unit; +HSPLcom/datadog/android/rum/internal/monitor/DatadogRumMonitor;->handleEvent$lambda$22(Lcom/datadog/android/rum/internal/monitor/DatadogRumMonitor;Lcom/datadog/android/rum/internal/domain/scope/RumRawEvent;Lcom/datadog/android/api/context/DatadogContext;Lkotlin/jvm/functions/Function1;)Lkotlin/Unit; +HSPLcom/datadog/android/rum/internal/monitor/DatadogRumMonitor;->handleEventWithMethodCallPerf$lambda$23(Lcom/datadog/android/rum/internal/monitor/DatadogRumMonitor;Lcom/datadog/android/rum/internal/domain/scope/RumRawEvent;Lcom/datadog/android/api/context/DatadogContext;Lkotlin/jvm/functions/Function1;)Lcom/datadog/android/rum/internal/domain/scope/RumScope; HSPLcom/datadog/android/rum/internal/monitor/DatadogRumMonitor;->handleEventWithMethodCallPerf(Lcom/datadog/android/rum/internal/domain/scope/RumRawEvent;Lcom/datadog/android/api/context/DatadogContext;Lkotlin/jvm/functions/Function1;)V HSPLcom/datadog/android/rum/internal/monitor/DatadogRumMonitor;->notifyDebugListenerWithState$dd_sdk_android_rum_release()V HSPLcom/datadog/android/rum/internal/monitor/DatadogRumMonitor;->notifyInterceptorInstantiated()V HSPLcom/datadog/android/rum/internal/monitor/DatadogRumMonitor;->now()Lcom/datadog/android/rum/internal/domain/Time; +HSPLcom/datadog/android/rum/internal/monitor/DatadogRumMonitor;->reportNetworkingLibraryType$lambda$5(Lcom/datadog/android/internal/telemetry/InternalTelemetryEvent$ApiUsage$NetworkInstrumentation$LibraryType;)Lcom/datadog/android/internal/telemetry/InternalTelemetryEvent$ApiUsage; HSPLcom/datadog/android/rum/internal/monitor/DatadogRumMonitor;->reportNetworkingLibraryType(Lcom/datadog/android/internal/telemetry/InternalTelemetryEvent$ApiUsage$NetworkInstrumentation$LibraryType;)V HSPLcom/datadog/android/rum/internal/monitor/DatadogRumMonitor;->sendTelemetryEvent(Lcom/datadog/android/internal/telemetry/InternalTelemetryEvent;)V HSPLcom/datadog/android/rum/internal/monitor/DatadogRumMonitor;->start()V +HSPLcom/datadog/android/rum/internal/monitor/DatadogRumMonitor;->startView(Ljava/lang/Object;Ljava/lang/String;Ljava/util/Map;)V HSPLcom/datadog/android/rum/internal/monitor/DatadogRumMonitor;->updateCachedViewUrl(Lcom/datadog/android/rum/internal/domain/RumContext;)V +Lcom/datadog/android/rum/internal/monitor/DatadogRumMonitor$$ExternalSyntheticLambda11; +HSPLcom/datadog/android/rum/internal/monitor/DatadogRumMonitor$$ExternalSyntheticLambda11;->(Lcom/datadog/android/rum/internal/monitor/DatadogRumMonitor;Lcom/datadog/android/rum/internal/domain/scope/RumRawEvent;)V +HSPLcom/datadog/android/rum/internal/monitor/DatadogRumMonitor$$ExternalSyntheticLambda11;->invoke(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object; +Lcom/datadog/android/rum/internal/monitor/DatadogRumMonitor$$ExternalSyntheticLambda2; +HSPLcom/datadog/android/rum/internal/monitor/DatadogRumMonitor$$ExternalSyntheticLambda2;->(Lcom/datadog/android/internal/telemetry/InternalTelemetryEvent$ApiUsage$NetworkInstrumentation$LibraryType;)V +HSPLcom/datadog/android/rum/internal/monitor/DatadogRumMonitor$$ExternalSyntheticLambda2;->invoke()Ljava/lang/Object; +Lcom/datadog/android/rum/internal/monitor/DatadogRumMonitor$$ExternalSyntheticLambda5; +HSPLcom/datadog/android/rum/internal/monitor/DatadogRumMonitor$$ExternalSyntheticLambda5;->(Lcom/datadog/android/rum/internal/monitor/DatadogRumMonitor;Lcom/datadog/android/rum/internal/domain/scope/RumRawEvent;Lcom/datadog/android/api/context/DatadogContext;Lkotlin/jvm/functions/Function1;)V +HSPLcom/datadog/android/rum/internal/monitor/DatadogRumMonitor$$ExternalSyntheticLambda5;->call()Ljava/lang/Object; +Lcom/datadog/android/rum/internal/monitor/DatadogRumMonitor$$ExternalSyntheticLambda6; +HSPLcom/datadog/android/rum/internal/monitor/DatadogRumMonitor$$ExternalSyntheticLambda6;->(Lcom/datadog/android/rum/internal/domain/RumContext;)V +HSPLcom/datadog/android/rum/internal/monitor/DatadogRumMonitor$$ExternalSyntheticLambda6;->invoke(Ljava/lang/Object;)Ljava/lang/Object; +Lcom/datadog/android/rum/internal/monitor/DatadogRumMonitor$$ExternalSyntheticLambda8; +HSPLcom/datadog/android/rum/internal/monitor/DatadogRumMonitor$$ExternalSyntheticLambda8;->(Lcom/datadog/android/rum/internal/monitor/DatadogRumMonitor;Lcom/datadog/android/rum/internal/domain/scope/RumRawEvent;Lcom/datadog/android/api/context/DatadogContext;Lkotlin/jvm/functions/Function1;)V +HSPLcom/datadog/android/rum/internal/monitor/DatadogRumMonitor$$ExternalSyntheticLambda8;->invoke()Ljava/lang/Object; Lcom/datadog/android/rum/internal/monitor/DatadogRumMonitor$Companion; HSPLcom/datadog/android/rum/internal/monitor/DatadogRumMonitor$Companion;->()V HSPLcom/datadog/android/rum/internal/monitor/DatadogRumMonitor$Companion;->(Lkotlin/jvm/internal/DefaultConstructorMarker;)V -Lcom/datadog/android/rum/internal/monitor/DatadogRumMonitor$handleEvent$2; -HSPLcom/datadog/android/rum/internal/monitor/DatadogRumMonitor$handleEvent$2;->(Lcom/datadog/android/rum/internal/monitor/DatadogRumMonitor;Lcom/datadog/android/rum/internal/domain/scope/RumRawEvent;)V -HSPLcom/datadog/android/rum/internal/monitor/DatadogRumMonitor$handleEvent$2;->invoke$lambda$1(Lcom/datadog/android/rum/internal/monitor/DatadogRumMonitor;Lcom/datadog/android/rum/internal/domain/scope/RumRawEvent;Lcom/datadog/android/api/context/DatadogContext;Lkotlin/jvm/functions/Function1;)Lcom/datadog/android/rum/internal/domain/RumContext; -HSPLcom/datadog/android/rum/internal/monitor/DatadogRumMonitor$handleEvent$2;->invoke(Lcom/datadog/android/api/context/DatadogContext;Lkotlin/jvm/functions/Function1;)V -HSPLcom/datadog/android/rum/internal/monitor/DatadogRumMonitor$handleEvent$2;->invoke(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object; -Lcom/datadog/android/rum/internal/monitor/DatadogRumMonitor$handleEvent$2$$ExternalSyntheticLambda0; -HSPLcom/datadog/android/rum/internal/monitor/DatadogRumMonitor$handleEvent$2$$ExternalSyntheticLambda0;->(Lcom/datadog/android/rum/internal/monitor/DatadogRumMonitor;Lcom/datadog/android/rum/internal/domain/scope/RumRawEvent;Lcom/datadog/android/api/context/DatadogContext;Lkotlin/jvm/functions/Function1;)V -HSPLcom/datadog/android/rum/internal/monitor/DatadogRumMonitor$handleEvent$2$$ExternalSyntheticLambda0;->call()Ljava/lang/Object; -Lcom/datadog/android/rum/internal/monitor/DatadogRumMonitor$handleEvent$2$1; -HSPLcom/datadog/android/rum/internal/monitor/DatadogRumMonitor$handleEvent$2$1;->(Lcom/datadog/android/rum/internal/domain/RumContext;)V -HSPLcom/datadog/android/rum/internal/monitor/DatadogRumMonitor$handleEvent$2$1;->invoke(Ljava/lang/Object;)Ljava/lang/Object; -HSPLcom/datadog/android/rum/internal/monitor/DatadogRumMonitor$handleEvent$2$1;->invoke(Ljava/util/Map;)V -Lcom/datadog/android/rum/internal/monitor/DatadogRumMonitor$handleEventWithMethodCallPerf$1; -HSPLcom/datadog/android/rum/internal/monitor/DatadogRumMonitor$handleEventWithMethodCallPerf$1;->(Lcom/datadog/android/rum/internal/monitor/DatadogRumMonitor;Lcom/datadog/android/rum/internal/domain/scope/RumRawEvent;Lcom/datadog/android/api/context/DatadogContext;Lkotlin/jvm/functions/Function1;)V -HSPLcom/datadog/android/rum/internal/monitor/DatadogRumMonitor$handleEventWithMethodCallPerf$1;->invoke()Lcom/datadog/android/rum/internal/domain/scope/RumScope; -HSPLcom/datadog/android/rum/internal/monitor/DatadogRumMonitor$handleEventWithMethodCallPerf$1;->invoke()Ljava/lang/Object; -Lcom/datadog/android/rum/internal/monitor/DatadogRumMonitor$reportNetworkingLibraryType$1; -HSPLcom/datadog/android/rum/internal/monitor/DatadogRumMonitor$reportNetworkingLibraryType$1;->(Lcom/datadog/android/internal/telemetry/InternalTelemetryEvent$ApiUsage$NetworkInstrumentation$LibraryType;)V -HSPLcom/datadog/android/rum/internal/monitor/DatadogRumMonitor$reportNetworkingLibraryType$1;->invoke()Lcom/datadog/android/internal/telemetry/InternalTelemetryEvent$ApiUsage; -HSPLcom/datadog/android/rum/internal/monitor/DatadogRumMonitor$reportNetworkingLibraryType$1;->invoke()Ljava/lang/Object; Lcom/datadog/android/rum/internal/monitor/StorageEvent; HSPLcom/datadog/android/rum/internal/monitor/StorageEvent;->()V HSPLcom/datadog/android/rum/internal/monitor/StorageEvent;->(Lkotlin/jvm/internal/DefaultConstructorMarker;)V @@ -21231,14 +21429,16 @@ Lcom/datadog/android/rum/internal/startup/RumAppStartupDetector$Companion; HSPLcom/datadog/android/rum/internal/startup/RumAppStartupDetector$Companion;->()V HSPLcom/datadog/android/rum/internal/startup/RumAppStartupDetector$Companion;->()V HSPLcom/datadog/android/rum/internal/startup/RumAppStartupDetector$Companion;->create(Landroid/app/Application;Lcom/datadog/android/core/InternalSdkCore;Lcom/datadog/android/rum/internal/startup/RumAppStartupDetector$Listener;Lcom/datadog/android/rum/startup/AppStartupActivityPredicate;)Lcom/datadog/android/rum/internal/startup/RumAppStartupDetector; -Lcom/datadog/android/rum/internal/startup/RumAppStartupDetector$Companion$create$1; -HSPLcom/datadog/android/rum/internal/startup/RumAppStartupDetector$Companion$create$1;->(Lcom/datadog/android/core/InternalSdkCore;)V -Lcom/datadog/android/rum/internal/startup/RumAppStartupDetector$Companion$create$2; -HSPLcom/datadog/android/rum/internal/startup/RumAppStartupDetector$Companion$create$2;->(Lcom/datadog/android/core/InternalSdkCore;)V +Lcom/datadog/android/rum/internal/startup/RumAppStartupDetector$Companion$$ExternalSyntheticLambda0; +HSPLcom/datadog/android/rum/internal/startup/RumAppStartupDetector$Companion$$ExternalSyntheticLambda0;->(Lcom/datadog/android/core/InternalSdkCore;)V +Lcom/datadog/android/rum/internal/startup/RumAppStartupDetector$Companion$$ExternalSyntheticLambda1; +HSPLcom/datadog/android/rum/internal/startup/RumAppStartupDetector$Companion$$ExternalSyntheticLambda1;->(Lcom/datadog/android/core/InternalSdkCore;)V Lcom/datadog/android/rum/internal/startup/RumAppStartupDetector$Listener; Lcom/datadog/android/rum/internal/startup/RumAppStartupDetectorImpl; HSPLcom/datadog/android/rum/internal/startup/RumAppStartupDetectorImpl;->()V HSPLcom/datadog/android/rum/internal/startup/RumAppStartupDetectorImpl;->(Landroid/app/Application;Lcom/datadog/android/internal/system/BuildSdkVersionProvider;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function0;Lcom/datadog/android/rum/internal/startup/RumAppStartupDetector$Listener;Lcom/datadog/android/rum/startup/AppStartupActivityPredicate;Lcom/datadog/android/rum/internal/startup/RumFirstDrawTimeReporter;)V +HSPLcom/datadog/android/rum/internal/startup/RumAppStartupDetectorImpl;->onActivityResumed(Landroid/app/Activity;)V +HSPLcom/datadog/android/rum/internal/startup/RumAppStartupDetectorImpl;->onActivityStarted(Landroid/app/Activity;)V Lcom/datadog/android/rum/internal/startup/RumAppStartupDetectorImpl$Companion; HSPLcom/datadog/android/rum/internal/startup/RumAppStartupDetectorImpl$Companion;->()V HSPLcom/datadog/android/rum/internal/startup/RumAppStartupDetectorImpl$Companion;->(Lkotlin/jvm/internal/DefaultConstructorMarker;)V @@ -21260,8 +21460,8 @@ Lcom/datadog/android/rum/internal/startup/RumFirstDrawTimeReporter$Companion; HSPLcom/datadog/android/rum/internal/startup/RumFirstDrawTimeReporter$Companion;->()V HSPLcom/datadog/android/rum/internal/startup/RumFirstDrawTimeReporter$Companion;->()V HSPLcom/datadog/android/rum/internal/startup/RumFirstDrawTimeReporter$Companion;->create(Lcom/datadog/android/core/InternalSdkCore;)Lcom/datadog/android/rum/internal/startup/RumFirstDrawTimeReporter; -Lcom/datadog/android/rum/internal/startup/RumFirstDrawTimeReporter$Companion$create$1; -HSPLcom/datadog/android/rum/internal/startup/RumFirstDrawTimeReporter$Companion$create$1;->(Lcom/datadog/android/core/InternalSdkCore;)V +Lcom/datadog/android/rum/internal/startup/RumFirstDrawTimeReporter$Companion$$ExternalSyntheticLambda0; +HSPLcom/datadog/android/rum/internal/startup/RumFirstDrawTimeReporter$Companion$$ExternalSyntheticLambda0;->(Lcom/datadog/android/core/InternalSdkCore;)V Lcom/datadog/android/rum/internal/startup/RumFirstDrawTimeReporterImpl; HSPLcom/datadog/android/rum/internal/startup/RumFirstDrawTimeReporterImpl;->(Lcom/datadog/android/api/InternalLogger;Lkotlin/jvm/functions/Function0;Lcom/datadog/android/rum/internal/utils/window/RumWindowCallbacksRegistry;Landroid/os/Handler;)V Lcom/datadog/android/rum/internal/startup/RumSessionScopeStartupManager; @@ -21294,35 +21494,28 @@ HSPLcom/datadog/android/rum/internal/tracking/NoOpInteractionPredicate;->( Lcom/datadog/android/rum/internal/tracking/NoOpUserActionTrackingStrategy; HSPLcom/datadog/android/rum/internal/tracking/NoOpUserActionTrackingStrategy;->()V Lcom/datadog/android/rum/internal/tracking/UserActionTrackingStrategy; -PLcom/datadog/android/rum/internal/utils/NumberExtKt;->percent(F)D Lcom/datadog/android/rum/internal/utils/RuntimeUtilsKt; HSPLcom/datadog/android/rum/internal/utils/RuntimeUtilsKt;->hasUserData(Lcom/datadog/android/api/context/UserInfo;)Z Lcom/datadog/android/rum/internal/utils/SdkCoreExtKt; HSPLcom/datadog/android/rum/internal/utils/SdkCoreExtKt;->newRumEventWriteOperation$default(Lcom/datadog/android/api/feature/FeatureSdkCore;Lcom/datadog/android/api/context/DatadogContext;Lkotlin/jvm/functions/Function1;Lcom/datadog/android/api/storage/DataWriter;Lcom/datadog/android/api/storage/EventType;Lkotlin/jvm/functions/Function0;ILjava/lang/Object;)Lcom/datadog/android/rum/internal/utils/WriteOperation; HSPLcom/datadog/android/rum/internal/utils/SdkCoreExtKt;->newRumEventWriteOperation(Lcom/datadog/android/api/feature/FeatureSdkCore;Lcom/datadog/android/api/context/DatadogContext;Lkotlin/jvm/functions/Function1;Lcom/datadog/android/api/storage/DataWriter;Lcom/datadog/android/api/storage/EventType;Lkotlin/jvm/functions/Function0;)Lcom/datadog/android/rum/internal/utils/WriteOperation; +Lcom/datadog/android/rum/internal/utils/ViewUtilsKt; +HSPLcom/datadog/android/rum/internal/utils/ViewUtilsKt;->resolveViewUrl(Ljava/lang/Object;)Ljava/lang/String; Lcom/datadog/android/rum/internal/utils/WriteOperation; HSPLcom/datadog/android/rum/internal/utils/WriteOperation;->()V HSPLcom/datadog/android/rum/internal/utils/WriteOperation;->(Lcom/datadog/android/api/feature/FeatureSdkCore;Lcom/datadog/android/api/context/DatadogContext;Lkotlin/jvm/functions/Function1;Lcom/datadog/android/api/storage/DataWriter;Lcom/datadog/android/api/storage/EventType;Lkotlin/jvm/functions/Function0;)V -HSPLcom/datadog/android/rum/internal/utils/WriteOperation;->access$getAdvancedRumMonitor$p(Lcom/datadog/android/rum/internal/utils/WriteOperation;)Lcom/datadog/android/rum/internal/monitor/AdvancedRumMonitor; -HSPLcom/datadog/android/rum/internal/utils/WriteOperation;->access$getEventSource$p(Lcom/datadog/android/rum/internal/utils/WriteOperation;)Lkotlin/jvm/functions/Function0; -HSPLcom/datadog/android/rum/internal/utils/WriteOperation;->access$getEventType$p(Lcom/datadog/android/rum/internal/utils/WriteOperation;)Lcom/datadog/android/api/storage/EventType; -HSPLcom/datadog/android/rum/internal/utils/WriteOperation;->access$getOnSuccess$p(Lcom/datadog/android/rum/internal/utils/WriteOperation;)Lkotlin/jvm/functions/Function1; -HSPLcom/datadog/android/rum/internal/utils/WriteOperation;->access$getRumDataWriter$p(Lcom/datadog/android/rum/internal/utils/WriteOperation;)Lcom/datadog/android/api/storage/DataWriter; HSPLcom/datadog/android/rum/internal/utils/WriteOperation;->onError(Lkotlin/jvm/functions/Function1;)Lcom/datadog/android/rum/internal/utils/WriteOperation; HSPLcom/datadog/android/rum/internal/utils/WriteOperation;->onSuccess(Lkotlin/jvm/functions/Function1;)Lcom/datadog/android/rum/internal/utils/WriteOperation; +HSPLcom/datadog/android/rum/internal/utils/WriteOperation;->submit$lambda$3(Lcom/datadog/android/rum/internal/utils/WriteOperation;Lcom/datadog/android/api/storage/EventBatchWriter;)Lkotlin/Unit; HSPLcom/datadog/android/rum/internal/utils/WriteOperation;->submit()V +Lcom/datadog/android/rum/internal/utils/WriteOperation$$ExternalSyntheticLambda1; +HSPLcom/datadog/android/rum/internal/utils/WriteOperation$$ExternalSyntheticLambda1;->()V +Lcom/datadog/android/rum/internal/utils/WriteOperation$$ExternalSyntheticLambda2; +HSPLcom/datadog/android/rum/internal/utils/WriteOperation$$ExternalSyntheticLambda2;->(Lcom/datadog/android/rum/internal/utils/WriteOperation;)V +HSPLcom/datadog/android/rum/internal/utils/WriteOperation$$ExternalSyntheticLambda2;->invoke(Ljava/lang/Object;)Ljava/lang/Object; Lcom/datadog/android/rum/internal/utils/WriteOperation$Companion; HSPLcom/datadog/android/rum/internal/utils/WriteOperation$Companion;->()V HSPLcom/datadog/android/rum/internal/utils/WriteOperation$Companion;->(Lkotlin/jvm/internal/DefaultConstructorMarker;)V -Lcom/datadog/android/rum/internal/utils/WriteOperation$Companion$NO_OP_EVENT_OUTCOME_ACTION$1; -HSPLcom/datadog/android/rum/internal/utils/WriteOperation$Companion$NO_OP_EVENT_OUTCOME_ACTION$1;->()V -HSPLcom/datadog/android/rum/internal/utils/WriteOperation$Companion$NO_OP_EVENT_OUTCOME_ACTION$1;->()V -HSPLcom/datadog/android/rum/internal/utils/WriteOperation$Companion$NO_OP_EVENT_OUTCOME_ACTION$1;->invoke(Lcom/datadog/android/rum/internal/monitor/AdvancedRumMonitor;)V -HSPLcom/datadog/android/rum/internal/utils/WriteOperation$Companion$NO_OP_EVENT_OUTCOME_ACTION$1;->invoke(Ljava/lang/Object;)Ljava/lang/Object; -Lcom/datadog/android/rum/internal/utils/WriteOperation$submit$1; -HSPLcom/datadog/android/rum/internal/utils/WriteOperation$submit$1;->(Lcom/datadog/android/rum/internal/utils/WriteOperation;)V -HSPLcom/datadog/android/rum/internal/utils/WriteOperation$submit$1;->invoke(Lcom/datadog/android/api/storage/EventBatchWriter;)V -HSPLcom/datadog/android/rum/internal/utils/WriteOperation$submit$1;->invoke(Ljava/lang/Object;)Ljava/lang/Object; Lcom/datadog/android/rum/internal/utils/window/RumWindowCallbacksRegistry; Lcom/datadog/android/rum/internal/utils/window/RumWindowCallbacksRegistryImpl; HSPLcom/datadog/android/rum/internal/utils/window/RumWindowCallbacksRegistryImpl;->()V @@ -21332,9 +21525,29 @@ Lcom/datadog/android/rum/internal/vitals/FrameStatesAggregator; HSPLcom/datadog/android/rum/internal/vitals/FrameStatesAggregator;->()V HSPLcom/datadog/android/rum/internal/vitals/FrameStatesAggregator;->(Ljava/util/List;Lcom/datadog/android/api/InternalLogger;Lcom/datadog/android/rum/internal/vitals/JankStatsProvider;Lcom/datadog/android/internal/system/BuildSdkVersionProvider;)V HSPLcom/datadog/android/rum/internal/vitals/FrameStatesAggregator;->(Ljava/util/List;Lcom/datadog/android/api/InternalLogger;Lcom/datadog/android/rum/internal/vitals/JankStatsProvider;Lcom/datadog/android/internal/system/BuildSdkVersionProvider;ILkotlin/jvm/internal/DefaultConstructorMarker;)V +HSPLcom/datadog/android/rum/internal/vitals/FrameStatesAggregator;->access$getFrameMetricsData$p(Lcom/datadog/android/rum/internal/vitals/FrameStatesAggregator;)Lcom/datadog/android/rum/internal/domain/FrameMetricsData; +HSPLcom/datadog/android/rum/internal/vitals/FrameStatesAggregator;->access$update(Lcom/datadog/android/rum/internal/vitals/FrameStatesAggregator;Lcom/datadog/android/rum/internal/domain/FrameMetricsData;Landroid/view/FrameMetrics;I)Lcom/datadog/android/rum/internal/domain/FrameMetricsData; +HSPLcom/datadog/android/rum/internal/vitals/FrameStatesAggregator;->getFrameStateListeners$dd_sdk_android_rum_release()Ljava/util/List; +HSPLcom/datadog/android/rum/internal/vitals/FrameStatesAggregator;->onActivityResumed(Landroid/app/Activity;)V +HSPLcom/datadog/android/rum/internal/vitals/FrameStatesAggregator;->onActivityStarted(Landroid/app/Activity;)V +PLcom/datadog/android/rum/internal/vitals/FrameStatesAggregator;->onFrame(Landroidx/metrics/performance/FrameData;)V +HSPLcom/datadog/android/rum/internal/vitals/FrameStatesAggregator;->registerMetricListener$lambda$15(Landroid/view/View;Lcom/datadog/android/rum/internal/vitals/FrameStatesAggregator;Landroid/view/Window;Landroid/os/Handler;)V +HSPLcom/datadog/android/rum/internal/vitals/FrameStatesAggregator;->registerMetricListener(Landroid/view/Window;)V +HSPLcom/datadog/android/rum/internal/vitals/FrameStatesAggregator;->trackActivity(Landroid/view/Window;Landroid/app/Activity;)V +HSPLcom/datadog/android/rum/internal/vitals/FrameStatesAggregator;->trackWindowJankStats(Landroid/view/Window;)V +HSPLcom/datadog/android/rum/internal/vitals/FrameStatesAggregator;->trackWindowMetrics(ZLandroid/view/Window;Landroid/app/Activity;)V +HSPLcom/datadog/android/rum/internal/vitals/FrameStatesAggregator;->update(Lcom/datadog/android/rum/internal/domain/FrameMetricsData;Landroid/view/FrameMetrics;I)Lcom/datadog/android/rum/internal/domain/FrameMetricsData; +Lcom/datadog/android/rum/internal/vitals/FrameStatesAggregator$$ExternalSyntheticLambda5; +HSPLcom/datadog/android/rum/internal/vitals/FrameStatesAggregator$$ExternalSyntheticLambda5;->(Landroid/view/View;Lcom/datadog/android/rum/internal/vitals/FrameStatesAggregator;Landroid/view/Window;Landroid/os/Handler;)V +HSPLcom/datadog/android/rum/internal/vitals/FrameStatesAggregator$$ExternalSyntheticLambda5;->run()V +Lcom/datadog/android/rum/internal/vitals/FrameStatesAggregator$$ExternalSyntheticLambda7; +HSPLcom/datadog/android/rum/internal/vitals/FrameStatesAggregator$$ExternalSyntheticLambda7;->(Landroid/view/Window;)V Lcom/datadog/android/rum/internal/vitals/FrameStatesAggregator$Companion; HSPLcom/datadog/android/rum/internal/vitals/FrameStatesAggregator$Companion;->()V HSPLcom/datadog/android/rum/internal/vitals/FrameStatesAggregator$Companion;->(Lkotlin/jvm/internal/DefaultConstructorMarker;)V +Lcom/datadog/android/rum/internal/vitals/FrameStatesAggregator$DDFrameMetricsListener; +HSPLcom/datadog/android/rum/internal/vitals/FrameStatesAggregator$DDFrameMetricsListener;->(Lcom/datadog/android/rum/internal/vitals/FrameStatesAggregator;)V +HSPLcom/datadog/android/rum/internal/vitals/FrameStatesAggregator$DDFrameMetricsListener;->onFrameMetricsAvailable(Landroid/view/Window;Landroid/view/FrameMetrics;I)V Lcom/datadog/android/rum/internal/vitals/JankStatsProvider; HSPLcom/datadog/android/rum/internal/vitals/JankStatsProvider;->()V Lcom/datadog/android/rum/internal/vitals/JankStatsProvider$Companion; @@ -21343,9 +21556,11 @@ HSPLcom/datadog/android/rum/internal/vitals/JankStatsProvider$Companion;-> HSPLcom/datadog/android/rum/internal/vitals/JankStatsProvider$Companion;->getDEFAULT()Lcom/datadog/android/rum/internal/vitals/JankStatsProvider; Lcom/datadog/android/rum/internal/vitals/JankStatsProvider$Companion$DEFAULT$1; HSPLcom/datadog/android/rum/internal/vitals/JankStatsProvider$Companion$DEFAULT$1;->()V +HSPLcom/datadog/android/rum/internal/vitals/JankStatsProvider$Companion$DEFAULT$1;->createJankStatsAndTrack(Landroid/view/Window;Landroidx/metrics/performance/JankStats$OnFrameListener;Lcom/datadog/android/api/InternalLogger;)Landroidx/metrics/performance/JankStats; Lcom/datadog/android/rum/internal/vitals/NoOpVitalMonitor; HSPLcom/datadog/android/rum/internal/vitals/NoOpVitalMonitor;->()V HSPLcom/datadog/android/rum/internal/vitals/NoOpVitalMonitor;->register(Lcom/datadog/android/rum/internal/vitals/VitalListener;)V +HSPLcom/datadog/android/rum/internal/vitals/NoOpVitalMonitor;->unregister(Lcom/datadog/android/rum/internal/vitals/VitalListener;)V Lcom/datadog/android/rum/internal/vitals/VitalInfo; Lcom/datadog/android/rum/internal/vitals/VitalListener; Lcom/datadog/android/rum/internal/vitals/VitalMonitor; @@ -21358,7 +21573,6 @@ HSPLcom/datadog/android/rum/metric/interactiontonextview/TimeBasedInteractionIde HSPLcom/datadog/android/rum/metric/interactiontonextview/TimeBasedInteractionIdentifier;->(J)V HSPLcom/datadog/android/rum/metric/interactiontonextview/TimeBasedInteractionIdentifier;->(JILkotlin/jvm/internal/DefaultConstructorMarker;)V HSPLcom/datadog/android/rum/metric/interactiontonextview/TimeBasedInteractionIdentifier;->defaultThresholdUsed$dd_sdk_android_rum_release()Z -PLcom/datadog/android/rum/metric/interactiontonextview/TimeBasedInteractionIdentifier;->getTimeThresholdInMilliseconds$dd_sdk_android_rum_release()J Lcom/datadog/android/rum/metric/interactiontonextview/TimeBasedInteractionIdentifier$Companion; HSPLcom/datadog/android/rum/metric/interactiontonextview/TimeBasedInteractionIdentifier$Companion;->()V HSPLcom/datadog/android/rum/metric/interactiontonextview/TimeBasedInteractionIdentifier$Companion;->(Lkotlin/jvm/internal/DefaultConstructorMarker;)V @@ -21369,7 +21583,7 @@ Lcom/datadog/android/rum/metric/networksettled/TimeBasedInitialResourceIdentifie HSPLcom/datadog/android/rum/metric/networksettled/TimeBasedInitialResourceIdentifier;->()V HSPLcom/datadog/android/rum/metric/networksettled/TimeBasedInitialResourceIdentifier;->(J)V HSPLcom/datadog/android/rum/metric/networksettled/TimeBasedInitialResourceIdentifier;->(JILkotlin/jvm/internal/DefaultConstructorMarker;)V -PLcom/datadog/android/rum/metric/networksettled/TimeBasedInitialResourceIdentifier;->getTimeThresholdInMilliseconds$dd_sdk_android_rum_release()J +HSPLcom/datadog/android/rum/metric/networksettled/TimeBasedInitialResourceIdentifier;->defaultThresholdUsed$dd_sdk_android_rum_release()Z Lcom/datadog/android/rum/metric/networksettled/TimeBasedInitialResourceIdentifier$Companion; HSPLcom/datadog/android/rum/metric/networksettled/TimeBasedInitialResourceIdentifier$Companion;->()V HSPLcom/datadog/android/rum/metric/networksettled/TimeBasedInitialResourceIdentifier$Companion;->(Lkotlin/jvm/internal/DefaultConstructorMarker;)V @@ -21513,8 +21727,8 @@ HSPLcom/datadog/android/rum/model/LongTaskEvent$LongTaskEventSource;->$values()[ HSPLcom/datadog/android/rum/model/LongTaskEvent$LongTaskEventSource;->()V HSPLcom/datadog/android/rum/model/LongTaskEvent$LongTaskEventSource;->(Ljava/lang/String;ILjava/lang/String;)V HSPLcom/datadog/android/rum/model/LongTaskEvent$LongTaskEventSource;->access$getJsonValue$p(Lcom/datadog/android/rum/model/LongTaskEvent$LongTaskEventSource;)Ljava/lang/String; +HSPLcom/datadog/android/rum/model/LongTaskEvent$LongTaskEventSource;->getEntries()Lkotlin/enums/EnumEntries; HSPLcom/datadog/android/rum/model/LongTaskEvent$LongTaskEventSource;->toJson()Lcom/google/gson/JsonElement; -HSPLcom/datadog/android/rum/model/LongTaskEvent$LongTaskEventSource;->values()[Lcom/datadog/android/rum/model/LongTaskEvent$LongTaskEventSource; Lcom/datadog/android/rum/model/LongTaskEvent$LongTaskEventSource$Companion; HSPLcom/datadog/android/rum/model/LongTaskEvent$LongTaskEventSource$Companion;->()V HSPLcom/datadog/android/rum/model/LongTaskEvent$LongTaskEventSource$Companion;->(Lkotlin/jvm/internal/DefaultConstructorMarker;)V @@ -21575,6 +21789,14 @@ HSPLcom/datadog/android/rum/model/ViewEvent;->getUsr()Lcom/datadog/android/rum/m HSPLcom/datadog/android/rum/model/ViewEvent;->getView()Lcom/datadog/android/rum/model/ViewEvent$ViewEventView; HSPLcom/datadog/android/rum/model/ViewEvent;->toJson()Lcom/google/gson/JsonElement; Lcom/datadog/android/rum/model/ViewEvent$Accessibility; +HSPLcom/datadog/android/rum/model/ViewEvent$Accessibility;->()V +HSPLcom/datadog/android/rum/model/ViewEvent$Accessibility;->(Ljava/lang/String;Ljava/lang/Boolean;Ljava/lang/Boolean;Ljava/lang/Boolean;Ljava/lang/Boolean;Ljava/lang/Boolean;Ljava/lang/Boolean;Ljava/lang/Boolean;Ljava/lang/Boolean;Ljava/lang/Boolean;Ljava/lang/Boolean;Ljava/lang/Boolean;Ljava/lang/Boolean;Ljava/lang/Boolean;Ljava/lang/Boolean;Ljava/lang/Boolean;Ljava/lang/Boolean;Ljava/lang/Boolean;Ljava/lang/Boolean;Ljava/lang/Boolean;Ljava/lang/Boolean;Ljava/lang/Boolean;)V +HSPLcom/datadog/android/rum/model/ViewEvent$Accessibility;->(Ljava/lang/String;Ljava/lang/Boolean;Ljava/lang/Boolean;Ljava/lang/Boolean;Ljava/lang/Boolean;Ljava/lang/Boolean;Ljava/lang/Boolean;Ljava/lang/Boolean;Ljava/lang/Boolean;Ljava/lang/Boolean;Ljava/lang/Boolean;Ljava/lang/Boolean;Ljava/lang/Boolean;Ljava/lang/Boolean;Ljava/lang/Boolean;Ljava/lang/Boolean;Ljava/lang/Boolean;Ljava/lang/Boolean;Ljava/lang/Boolean;Ljava/lang/Boolean;Ljava/lang/Boolean;Ljava/lang/Boolean;ILkotlin/jvm/internal/DefaultConstructorMarker;)V +HSPLcom/datadog/android/rum/model/ViewEvent$Accessibility;->equals(Ljava/lang/Object;)Z +HSPLcom/datadog/android/rum/model/ViewEvent$Accessibility;->toJson()Lcom/google/gson/JsonElement; +Lcom/datadog/android/rum/model/ViewEvent$Accessibility$Companion; +HSPLcom/datadog/android/rum/model/ViewEvent$Accessibility$Companion;->()V +HSPLcom/datadog/android/rum/model/ViewEvent$Accessibility$Companion;->(Lkotlin/jvm/internal/DefaultConstructorMarker;)V Lcom/datadog/android/rum/model/ViewEvent$Account; Lcom/datadog/android/rum/model/ViewEvent$Action; HSPLcom/datadog/android/rum/model/ViewEvent$Action;->()V @@ -21597,8 +21819,8 @@ HSPLcom/datadog/android/rum/model/ViewEvent$Companion;->()V HSPLcom/datadog/android/rum/model/ViewEvent$Companion;->(Lkotlin/jvm/internal/DefaultConstructorMarker;)V Lcom/datadog/android/rum/model/ViewEvent$Configuration; HSPLcom/datadog/android/rum/model/ViewEvent$Configuration;->()V -HSPLcom/datadog/android/rum/model/ViewEvent$Configuration;->(Ljava/lang/Number;Ljava/lang/Number;Ljava/lang/Number;Ljava/lang/Number;Ljava/util/List;Ljava/lang/Boolean;)V -HSPLcom/datadog/android/rum/model/ViewEvent$Configuration;->(Ljava/lang/Number;Ljava/lang/Number;Ljava/lang/Number;Ljava/lang/Number;Ljava/util/List;Ljava/lang/Boolean;ILkotlin/jvm/internal/DefaultConstructorMarker;)V +HSPLcom/datadog/android/rum/model/ViewEvent$Configuration;->(Ljava/lang/Number;Ljava/lang/Number;Ljava/lang/Number;Ljava/lang/Number;Ljava/util/List;Ljava/lang/Boolean;Ljava/lang/String;)V +HSPLcom/datadog/android/rum/model/ViewEvent$Configuration;->(Ljava/lang/Number;Ljava/lang/Number;Ljava/lang/Number;Ljava/lang/Number;Ljava/util/List;Ljava/lang/Boolean;Ljava/lang/String;ILkotlin/jvm/internal/DefaultConstructorMarker;)V HSPLcom/datadog/android/rum/model/ViewEvent$Configuration;->toJson()Lcom/google/gson/JsonElement; Lcom/datadog/android/rum/model/ViewEvent$Configuration$Companion; HSPLcom/datadog/android/rum/model/ViewEvent$Configuration$Companion;->()V @@ -21782,8 +22004,8 @@ HSPLcom/datadog/android/rum/model/ViewEvent$ViewEventSource;->$values()[Lcom/dat HSPLcom/datadog/android/rum/model/ViewEvent$ViewEventSource;->()V HSPLcom/datadog/android/rum/model/ViewEvent$ViewEventSource;->(Ljava/lang/String;ILjava/lang/String;)V HSPLcom/datadog/android/rum/model/ViewEvent$ViewEventSource;->access$getJsonValue$p(Lcom/datadog/android/rum/model/ViewEvent$ViewEventSource;)Ljava/lang/String; +HSPLcom/datadog/android/rum/model/ViewEvent$ViewEventSource;->getEntries()Lkotlin/enums/EnumEntries; HSPLcom/datadog/android/rum/model/ViewEvent$ViewEventSource;->toJson()Lcom/google/gson/JsonElement; -HSPLcom/datadog/android/rum/model/ViewEvent$ViewEventSource;->values()[Lcom/datadog/android/rum/model/ViewEvent$ViewEventSource; Lcom/datadog/android/rum/model/ViewEvent$ViewEventSource$Companion; HSPLcom/datadog/android/rum/model/ViewEvent$ViewEventSource$Companion;->()V HSPLcom/datadog/android/rum/model/ViewEvent$ViewEventSource$Companion;->(Lkotlin/jvm/internal/DefaultConstructorMarker;)V @@ -21799,10 +22021,12 @@ HSPLcom/datadog/android/rum/model/ViewEvent$ViewEventView;->getCustomTimings()Lc HSPLcom/datadog/android/rum/model/ViewEvent$ViewEventView;->getId()Ljava/lang/String; HSPLcom/datadog/android/rum/model/ViewEvent$ViewEventView;->getTimeSpent()J HSPLcom/datadog/android/rum/model/ViewEvent$ViewEventView;->getUrl()Ljava/lang/String; +HSPLcom/datadog/android/rum/model/ViewEvent$ViewEventView;->isActive()Ljava/lang/Boolean; HSPLcom/datadog/android/rum/model/ViewEvent$ViewEventView;->toJson()Lcom/google/gson/JsonElement; Lcom/datadog/android/rum/model/ViewEvent$ViewEventView$Companion; HSPLcom/datadog/android/rum/model/ViewEvent$ViewEventView$Companion;->()V HSPLcom/datadog/android/rum/model/ViewEvent$ViewEventView$Companion;->(Lkotlin/jvm/internal/DefaultConstructorMarker;)V +Lcom/datadog/android/rum/model/ViewUpdateEvent; Lcom/datadog/android/rum/operations/FailureReason; HSPLcom/datadog/android/rum/operations/FailureReason;->$values()[Lcom/datadog/android/rum/operations/FailureReason; HSPLcom/datadog/android/rum/operations/FailureReason;->()V @@ -21813,12 +22037,21 @@ Lcom/datadog/android/rum/startup/AppStartupActivityPredicate; Lcom/datadog/android/rum/timeseries/TimeseriesConfiguration; Lcom/datadog/android/rum/tracking/AcceptAllActivities; HSPLcom/datadog/android/rum/tracking/AcceptAllActivities;->()V +HSPLcom/datadog/android/rum/tracking/AcceptAllActivities;->accept(Landroid/app/Activity;)Z +HSPLcom/datadog/android/rum/tracking/AcceptAllActivities;->accept(Ljava/lang/Object;)Z +HSPLcom/datadog/android/rum/tracking/AcceptAllActivities;->getViewName(Landroid/app/Activity;)Ljava/lang/String; +HSPLcom/datadog/android/rum/tracking/AcceptAllActivities;->getViewName(Ljava/lang/Object;)Ljava/lang/String; Lcom/datadog/android/rum/tracking/ActionTrackingStrategy; Lcom/datadog/android/rum/tracking/ActivityLifecycleTrackingStrategy; HSPLcom/datadog/android/rum/tracking/ActivityLifecycleTrackingStrategy;->()V HSPLcom/datadog/android/rum/tracking/ActivityLifecycleTrackingStrategy;->()V +HSPLcom/datadog/android/rum/tracking/ActivityLifecycleTrackingStrategy;->getInternalLogger$dd_sdk_android_rum_release()Lcom/datadog/android/api/InternalLogger; +HSPLcom/datadog/android/rum/tracking/ActivityLifecycleTrackingStrategy;->getSdkCore()Lcom/datadog/android/api/feature/FeatureSdkCore; +HSPLcom/datadog/android/rum/tracking/ActivityLifecycleTrackingStrategy;->onActivityResumed(Landroid/app/Activity;)V +HSPLcom/datadog/android/rum/tracking/ActivityLifecycleTrackingStrategy;->onActivityStarted(Landroid/app/Activity;)V HSPLcom/datadog/android/rum/tracking/ActivityLifecycleTrackingStrategy;->register(Lcom/datadog/android/api/SdkCore;Landroid/content/Context;)V HSPLcom/datadog/android/rum/tracking/ActivityLifecycleTrackingStrategy;->setSdkCore(Lcom/datadog/android/api/feature/FeatureSdkCore;)V +HSPLcom/datadog/android/rum/tracking/ActivityLifecycleTrackingStrategy;->withSdkCore(Lkotlin/jvm/functions/Function1;)Ljava/lang/Object; Lcom/datadog/android/rum/tracking/ActivityLifecycleTrackingStrategy$Companion; HSPLcom/datadog/android/rum/tracking/ActivityLifecycleTrackingStrategy$Companion;->()V HSPLcom/datadog/android/rum/tracking/ActivityLifecycleTrackingStrategy$Companion;->(Lkotlin/jvm/internal/DefaultConstructorMarker;)V @@ -21826,11 +22059,17 @@ Lcom/datadog/android/rum/tracking/ActivityViewTrackingStrategy; HSPLcom/datadog/android/rum/tracking/ActivityViewTrackingStrategy;->()V HSPLcom/datadog/android/rum/tracking/ActivityViewTrackingStrategy;->(ZLcom/datadog/android/rum/tracking/ComponentPredicate;)V HSPLcom/datadog/android/rum/tracking/ActivityViewTrackingStrategy;->(ZLcom/datadog/android/rum/tracking/ComponentPredicate;ILkotlin/jvm/internal/DefaultConstructorMarker;)V +HSPLcom/datadog/android/rum/tracking/ActivityViewTrackingStrategy;->getRumMonitor$lambda$4(Lcom/datadog/android/api/feature/FeatureSdkCore;)Lcom/datadog/android/rum/RumMonitor; +HSPLcom/datadog/android/rum/tracking/ActivityViewTrackingStrategy;->getRumMonitor()Lcom/datadog/android/rum/RumMonitor; +HSPLcom/datadog/android/rum/tracking/ActivityViewTrackingStrategy;->onActivityResumed(Landroid/app/Activity;)V +Lcom/datadog/android/rum/tracking/ActivityViewTrackingStrategy$$ExternalSyntheticLambda0; +HSPLcom/datadog/android/rum/tracking/ActivityViewTrackingStrategy$$ExternalSyntheticLambda0;->()V +HSPLcom/datadog/android/rum/tracking/ActivityViewTrackingStrategy$$ExternalSyntheticLambda0;->invoke(Ljava/lang/Object;)Ljava/lang/Object; +Lcom/datadog/android/rum/tracking/ActivityViewTrackingStrategy$$ExternalSyntheticLambda1; +HSPLcom/datadog/android/rum/tracking/ActivityViewTrackingStrategy$$ExternalSyntheticLambda1;->(Lcom/datadog/android/rum/tracking/ActivityViewTrackingStrategy;)V Lcom/datadog/android/rum/tracking/ActivityViewTrackingStrategy$Companion; HSPLcom/datadog/android/rum/tracking/ActivityViewTrackingStrategy$Companion;->()V HSPLcom/datadog/android/rum/tracking/ActivityViewTrackingStrategy$Companion;->(Lkotlin/jvm/internal/DefaultConstructorMarker;)V -Lcom/datadog/android/rum/tracking/ActivityViewTrackingStrategy$executor$2; -HSPLcom/datadog/android/rum/tracking/ActivityViewTrackingStrategy$executor$2;->(Lcom/datadog/android/rum/tracking/ActivityViewTrackingStrategy;)V Lcom/datadog/android/rum/tracking/ComponentPredicate; Lcom/datadog/android/rum/tracking/InteractionPredicate; Lcom/datadog/android/rum/tracking/NoOpActionTrackingStrategy; @@ -21843,35 +22082,19 @@ Lcom/datadog/android/rum/tracking/TrackingStrategy; Lcom/datadog/android/rum/tracking/ViewAttributesProvider; Lcom/datadog/android/rum/tracking/ViewTrackingStrategy; Lcom/datadog/android/security/Encryption; -PLcom/datadog/android/telemetry/internal/TelemetryEventExtKt;->tryFromSource(Lcom/datadog/android/telemetry/model/TelemetryConfigurationEvent$Source$Companion;Ljava/lang/String;Lcom/datadog/android/api/InternalLogger;)Lcom/datadog/android/telemetry/model/TelemetryConfigurationEvent$Source; Lcom/datadog/android/telemetry/internal/TelemetryEventHandler; HSPLcom/datadog/android/telemetry/internal/TelemetryEventHandler;->()V HSPLcom/datadog/android/telemetry/internal/TelemetryEventHandler;->(Lcom/datadog/android/core/InternalSdkCore;Lcom/datadog/android/core/sampling/Sampler;Lcom/datadog/android/core/sampling/Sampler;Lcom/datadog/android/rum/internal/metric/SessionMetricDispatcher;I)V HSPLcom/datadog/android/telemetry/internal/TelemetryEventHandler;->(Lcom/datadog/android/core/InternalSdkCore;Lcom/datadog/android/core/sampling/Sampler;Lcom/datadog/android/core/sampling/Sampler;Lcom/datadog/android/rum/internal/metric/SessionMetricDispatcher;IILkotlin/jvm/internal/DefaultConstructorMarker;)V -HSPLcom/datadog/android/telemetry/internal/TelemetryEventHandler;->access$setTrackNetworkRequests$p(Lcom/datadog/android/telemetry/internal/TelemetryEventHandler;Z)V HSPLcom/datadog/android/telemetry/internal/TelemetryEventHandler;->canWrite(Lcom/datadog/android/internal/telemetry/InternalTelemetryEvent;)Z -PLcom/datadog/android/telemetry/internal/TelemetryEventHandler;->computeEffectiveSampleRate(Ljava/util/Map;Ljava/lang/Float;)F -PLcom/datadog/android/telemetry/internal/TelemetryEventHandler;->createConfigurationEvent(Lcom/datadog/android/api/context/DatadogContext;JLcom/datadog/android/internal/telemetry/InternalTelemetryEvent$Configuration;F)Lcom/datadog/android/telemetry/model/TelemetryConfigurationEvent; -PLcom/datadog/android/telemetry/internal/TelemetryEventHandler;->getRumConfig()Lcom/datadog/android/rum/internal/RumFeature$Configuration; +HSPLcom/datadog/android/telemetry/internal/TelemetryEventHandler;->handleEvent$lambda$1(Lcom/datadog/android/rum/internal/domain/scope/RumRawEvent$TelemetryEventWrapper;Lcom/datadog/android/internal/telemetry/InternalTelemetryEvent;Lcom/datadog/android/telemetry/internal/TelemetryEventHandler;Lcom/datadog/android/api/storage/DataWriter;Lcom/datadog/android/api/context/DatadogContext;Lkotlin/jvm/functions/Function1;)Lkotlin/Unit; HSPLcom/datadog/android/telemetry/internal/TelemetryEventHandler;->handleEvent(Lcom/datadog/android/rum/internal/domain/scope/RumRawEvent$TelemetryEventWrapper;Lcom/datadog/android/api/storage/DataWriter;)V -HSPLcom/datadog/android/telemetry/internal/TelemetryEventHandler;->isLog(Lcom/datadog/android/internal/telemetry/InternalTelemetryEvent;)Z -PLcom/datadog/android/telemetry/internal/TelemetryEventHandler;->isOpenTelemetryRegistered(Ljava/util/Map;)Z HSPLcom/datadog/android/telemetry/internal/TelemetryEventHandler;->onSessionStarted(Ljava/lang/String;Z)V -PLcom/datadog/android/telemetry/internal/TelemetryEventHandler;->resolveOpenTelemetryApiVersion(Lcom/datadog/android/telemetry/internal/TelemetryEventHandler$TracerApi;Ljava/util/Map;)Ljava/lang/String; -PLcom/datadog/android/telemetry/internal/TelemetryEventHandler;->resolveTracerApi(Ljava/util/Map;)Lcom/datadog/android/telemetry/internal/TelemetryEventHandler$TracerApi; -PLcom/datadog/android/telemetry/internal/TelemetryEventHandler;->rumContext(Lcom/datadog/android/api/context/DatadogContext;)Lcom/datadog/android/rum/internal/domain/RumContext; +Lcom/datadog/android/telemetry/internal/TelemetryEventHandler$$ExternalSyntheticLambda0; +HSPLcom/datadog/android/telemetry/internal/TelemetryEventHandler$$ExternalSyntheticLambda0;->invoke(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object; Lcom/datadog/android/telemetry/internal/TelemetryEventHandler$Companion; HSPLcom/datadog/android/telemetry/internal/TelemetryEventHandler$Companion;->()V HSPLcom/datadog/android/telemetry/internal/TelemetryEventHandler$Companion;->(Lkotlin/jvm/internal/DefaultConstructorMarker;)V -PLcom/datadog/android/telemetry/internal/TelemetryEventHandler$TracerApi;->$values()[Lcom/datadog/android/telemetry/internal/TelemetryEventHandler$TracerApi; -PLcom/datadog/android/telemetry/internal/TelemetryEventHandler$TracerApi;->()V -PLcom/datadog/android/telemetry/internal/TelemetryEventHandler$TracerApi;->(Ljava/lang/String;I)V -Lcom/datadog/android/telemetry/internal/TelemetryEventHandler$handleEvent$1; -HSPLcom/datadog/android/telemetry/internal/TelemetryEventHandler$handleEvent$1;->(Lcom/datadog/android/rum/internal/domain/scope/RumRawEvent$TelemetryEventWrapper;Lcom/datadog/android/internal/telemetry/InternalTelemetryEvent;Lcom/datadog/android/telemetry/internal/TelemetryEventHandler;Lcom/datadog/android/api/storage/DataWriter;)V -HSPLcom/datadog/android/telemetry/internal/TelemetryEventHandler$handleEvent$1;->invoke(Lcom/datadog/android/api/context/DatadogContext;Lkotlin/jvm/functions/Function1;)V -HSPLcom/datadog/android/telemetry/internal/TelemetryEventHandler$handleEvent$1;->invoke(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object; -PLcom/datadog/android/telemetry/internal/TelemetryEventHandler$handleEvent$1$1;->invoke(Lcom/datadog/android/api/storage/EventBatchWriter;)V -PLcom/datadog/android/telemetry/internal/TelemetryEventHandler$handleEvent$1$1;->invoke(Ljava/lang/Object;)Ljava/lang/Object; Lcom/datadog/android/telemetry/internal/TelemetryEventId; HSPLcom/datadog/android/telemetry/internal/TelemetryEventId;->(Lcom/datadog/android/telemetry/internal/TelemetryType;Ljava/lang/String;Ljava/lang/String;)V HSPLcom/datadog/android/telemetry/internal/TelemetryEventId;->hashCode()I @@ -21882,77 +22105,6 @@ Lcom/datadog/android/telemetry/internal/TelemetryType; HSPLcom/datadog/android/telemetry/internal/TelemetryType;->$values()[Lcom/datadog/android/telemetry/internal/TelemetryType; HSPLcom/datadog/android/telemetry/internal/TelemetryType;->()V HSPLcom/datadog/android/telemetry/internal/TelemetryType;->(Ljava/lang/String;I)V -PLcom/datadog/android/telemetry/internal/TracingHeaderTypesSetUtilsKt;->toSelectedTracingPropagator(Lcom/datadog/android/internal/telemetry/TracingHeaderType;)Lcom/datadog/android/telemetry/model/TelemetryConfigurationEvent$SelectedTracingPropagator; -PLcom/datadog/android/telemetry/internal/TracingHeaderTypesSetUtilsKt;->toSelectedTracingPropagators(Lcom/datadog/android/internal/telemetry/TracingHeaderTypesSet;)Ljava/util/List; -PLcom/datadog/android/telemetry/internal/TracingHeaderTypesSetUtilsKt$WhenMappings;->()V -PLcom/datadog/android/telemetry/model/TelemetryConfigurationEvent;->()V -PLcom/datadog/android/telemetry/model/TelemetryConfigurationEvent;->(Lcom/datadog/android/telemetry/model/TelemetryConfigurationEvent$Dd;JLjava/lang/String;Lcom/datadog/android/telemetry/model/TelemetryConfigurationEvent$Source;Ljava/lang/String;Lcom/datadog/android/telemetry/model/TelemetryConfigurationEvent$Application;Lcom/datadog/android/telemetry/model/TelemetryConfigurationEvent$Session;Lcom/datadog/android/telemetry/model/TelemetryConfigurationEvent$View;Lcom/datadog/android/telemetry/model/TelemetryConfigurationEvent$Action;Ljava/lang/Number;Ljava/util/List;Lcom/datadog/android/telemetry/model/TelemetryConfigurationEvent$Telemetry;)V -PLcom/datadog/android/telemetry/model/TelemetryConfigurationEvent;->toJson()Lcom/google/gson/JsonElement; -PLcom/datadog/android/telemetry/model/TelemetryConfigurationEvent$Application;->()V -PLcom/datadog/android/telemetry/model/TelemetryConfigurationEvent$Application;->(Ljava/lang/String;)V -PLcom/datadog/android/telemetry/model/TelemetryConfigurationEvent$Application;->toJson()Lcom/google/gson/JsonElement; -PLcom/datadog/android/telemetry/model/TelemetryConfigurationEvent$Application$Companion;->()V -PLcom/datadog/android/telemetry/model/TelemetryConfigurationEvent$Application$Companion;->(Lkotlin/jvm/internal/DefaultConstructorMarker;)V -PLcom/datadog/android/telemetry/model/TelemetryConfigurationEvent$Companion;->()V -PLcom/datadog/android/telemetry/model/TelemetryConfigurationEvent$Companion;->(Lkotlin/jvm/internal/DefaultConstructorMarker;)V -PLcom/datadog/android/telemetry/model/TelemetryConfigurationEvent$Configuration;->()V -PLcom/datadog/android/telemetry/model/TelemetryConfigurationEvent$Configuration;->(Ljava/lang/Long;Ljava/lang/Long;Ljava/lang/Long;Ljava/lang/Long;Ljava/lang/Long;Lcom/datadog/android/telemetry/model/TelemetryConfigurationEvent$TraceContextInjection;Ljava/lang/Long;Ljava/lang/Long;Ljava/lang/Long;Lcom/datadog/android/telemetry/model/TelemetryConfigurationEvent$TrackingConsent;Ljava/lang/Boolean;Ljava/lang/Boolean;Ljava/lang/Boolean;Ljava/lang/Boolean;Ljava/lang/Boolean;Ljava/lang/Boolean;Ljava/lang/Boolean;Ljava/lang/Boolean;Ljava/lang/Boolean;Ljava/lang/Boolean;Ljava/lang/Boolean;Ljava/lang/Boolean;Ljava/lang/Boolean;Ljava/lang/Boolean;Lcom/datadog/android/telemetry/model/TelemetryConfigurationEvent$SessionPersistence;Ljava/lang/Boolean;Ljava/lang/Boolean;Ljava/lang/String;Ljava/lang/Boolean;Ljava/lang/Boolean;Ljava/lang/Boolean;Ljava/lang/Boolean;Ljava/lang/Boolean;Ljava/util/List;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/Boolean;Ljava/lang/Boolean;Ljava/lang/Boolean;Ljava/lang/Boolean;Ljava/lang/Boolean;Ljava/lang/Boolean;Ljava/lang/Boolean;Ljava/lang/Boolean;Ljava/lang/Boolean;Ljava/lang/Long;Ljava/util/List;Ljava/util/List;Ljava/lang/Boolean;Lcom/datadog/android/telemetry/model/TelemetryConfigurationEvent$ViewTrackingStrategy;Ljava/lang/Boolean;Ljava/lang/Boolean;Ljava/lang/Boolean;Ljava/lang/Long;Ljava/lang/Boolean;Ljava/lang/Boolean;Ljava/lang/Boolean;Ljava/lang/Boolean;Ljava/lang/Boolean;Ljava/lang/Boolean;Ljava/lang/Boolean;Ljava/lang/Boolean;Ljava/lang/String;Ljava/lang/Boolean;Ljava/lang/Long;Ljava/lang/Long;Ljava/lang/Long;Ljava/lang/Boolean;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/Long;Ljava/lang/Boolean;Ljava/lang/String;Ljava/lang/String;Ljava/lang/Boolean;Ljava/util/List;Ljava/lang/Boolean;Ljava/lang/Long;Ljava/lang/Long;Ljava/util/List;Ljava/lang/Boolean;Ljava/lang/Boolean;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/Boolean;Ljava/lang/Number;Ljava/lang/Boolean;Lcom/datadog/android/telemetry/model/TelemetryConfigurationEvent$TrackResourceHeaders;Ljava/lang/Boolean;)V -PLcom/datadog/android/telemetry/model/TelemetryConfigurationEvent$Configuration;->(Ljava/lang/Long;Ljava/lang/Long;Ljava/lang/Long;Ljava/lang/Long;Ljava/lang/Long;Lcom/datadog/android/telemetry/model/TelemetryConfigurationEvent$TraceContextInjection;Ljava/lang/Long;Ljava/lang/Long;Ljava/lang/Long;Lcom/datadog/android/telemetry/model/TelemetryConfigurationEvent$TrackingConsent;Ljava/lang/Boolean;Ljava/lang/Boolean;Ljava/lang/Boolean;Ljava/lang/Boolean;Ljava/lang/Boolean;Ljava/lang/Boolean;Ljava/lang/Boolean;Ljava/lang/Boolean;Ljava/lang/Boolean;Ljava/lang/Boolean;Ljava/lang/Boolean;Ljava/lang/Boolean;Ljava/lang/Boolean;Ljava/lang/Boolean;Lcom/datadog/android/telemetry/model/TelemetryConfigurationEvent$SessionPersistence;Ljava/lang/Boolean;Ljava/lang/Boolean;Ljava/lang/String;Ljava/lang/Boolean;Ljava/lang/Boolean;Ljava/lang/Boolean;Ljava/lang/Boolean;Ljava/lang/Boolean;Ljava/util/List;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/Boolean;Ljava/lang/Boolean;Ljava/lang/Boolean;Ljava/lang/Boolean;Ljava/lang/Boolean;Ljava/lang/Boolean;Ljava/lang/Boolean;Ljava/lang/Boolean;Ljava/lang/Boolean;Ljava/lang/Long;Ljava/util/List;Ljava/util/List;Ljava/lang/Boolean;Lcom/datadog/android/telemetry/model/TelemetryConfigurationEvent$ViewTrackingStrategy;Ljava/lang/Boolean;Ljava/lang/Boolean;Ljava/lang/Boolean;Ljava/lang/Long;Ljava/lang/Boolean;Ljava/lang/Boolean;Ljava/lang/Boolean;Ljava/lang/Boolean;Ljava/lang/Boolean;Ljava/lang/Boolean;Ljava/lang/Boolean;Ljava/lang/Boolean;Ljava/lang/String;Ljava/lang/Boolean;Ljava/lang/Long;Ljava/lang/Long;Ljava/lang/Long;Ljava/lang/Boolean;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/Long;Ljava/lang/Boolean;Ljava/lang/String;Ljava/lang/String;Ljava/lang/Boolean;Ljava/util/List;Ljava/lang/Boolean;Ljava/lang/Long;Ljava/lang/Long;Ljava/util/List;Ljava/lang/Boolean;Ljava/lang/Boolean;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/Boolean;Ljava/lang/Number;Ljava/lang/Boolean;Lcom/datadog/android/telemetry/model/TelemetryConfigurationEvent$TrackResourceHeaders;Ljava/lang/Boolean;IIILkotlin/jvm/internal/DefaultConstructorMarker;)V -PLcom/datadog/android/telemetry/model/TelemetryConfigurationEvent$Configuration;->toJson()Lcom/google/gson/JsonElement; -PLcom/datadog/android/telemetry/model/TelemetryConfigurationEvent$Configuration$Companion;->()V -PLcom/datadog/android/telemetry/model/TelemetryConfigurationEvent$Configuration$Companion;->(Lkotlin/jvm/internal/DefaultConstructorMarker;)V -PLcom/datadog/android/telemetry/model/TelemetryConfigurationEvent$Dd;->()V -PLcom/datadog/android/telemetry/model/TelemetryConfigurationEvent$Dd;->()V -PLcom/datadog/android/telemetry/model/TelemetryConfigurationEvent$Dd;->toJson()Lcom/google/gson/JsonElement; -PLcom/datadog/android/telemetry/model/TelemetryConfigurationEvent$Dd$Companion;->()V -PLcom/datadog/android/telemetry/model/TelemetryConfigurationEvent$Dd$Companion;->(Lkotlin/jvm/internal/DefaultConstructorMarker;)V -PLcom/datadog/android/telemetry/model/TelemetryConfigurationEvent$Device;->()V -PLcom/datadog/android/telemetry/model/TelemetryConfigurationEvent$Device;->(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/Number;Ljava/lang/Number;Ljava/lang/Boolean;)V -PLcom/datadog/android/telemetry/model/TelemetryConfigurationEvent$Device;->toJson()Lcom/google/gson/JsonElement; -PLcom/datadog/android/telemetry/model/TelemetryConfigurationEvent$Device$Companion;->()V -PLcom/datadog/android/telemetry/model/TelemetryConfigurationEvent$Device$Companion;->(Lkotlin/jvm/internal/DefaultConstructorMarker;)V -PLcom/datadog/android/telemetry/model/TelemetryConfigurationEvent$Os;->()V -PLcom/datadog/android/telemetry/model/TelemetryConfigurationEvent$Os;->(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)V -PLcom/datadog/android/telemetry/model/TelemetryConfigurationEvent$Os;->toJson()Lcom/google/gson/JsonElement; -PLcom/datadog/android/telemetry/model/TelemetryConfigurationEvent$Os$Companion;->()V -PLcom/datadog/android/telemetry/model/TelemetryConfigurationEvent$Os$Companion;->(Lkotlin/jvm/internal/DefaultConstructorMarker;)V -PLcom/datadog/android/telemetry/model/TelemetryConfigurationEvent$SelectedTracingPropagator;->$values()[Lcom/datadog/android/telemetry/model/TelemetryConfigurationEvent$SelectedTracingPropagator; -PLcom/datadog/android/telemetry/model/TelemetryConfigurationEvent$SelectedTracingPropagator;->()V -PLcom/datadog/android/telemetry/model/TelemetryConfigurationEvent$SelectedTracingPropagator;->(Ljava/lang/String;ILjava/lang/String;)V -PLcom/datadog/android/telemetry/model/TelemetryConfigurationEvent$SelectedTracingPropagator;->toJson()Lcom/google/gson/JsonElement; -PLcom/datadog/android/telemetry/model/TelemetryConfigurationEvent$SelectedTracingPropagator$Companion;->()V -PLcom/datadog/android/telemetry/model/TelemetryConfigurationEvent$SelectedTracingPropagator$Companion;->(Lkotlin/jvm/internal/DefaultConstructorMarker;)V -PLcom/datadog/android/telemetry/model/TelemetryConfigurationEvent$Session;->()V -PLcom/datadog/android/telemetry/model/TelemetryConfigurationEvent$Session;->(Ljava/lang/String;)V -PLcom/datadog/android/telemetry/model/TelemetryConfigurationEvent$Session;->toJson()Lcom/google/gson/JsonElement; -PLcom/datadog/android/telemetry/model/TelemetryConfigurationEvent$Session$Companion;->()V -PLcom/datadog/android/telemetry/model/TelemetryConfigurationEvent$Session$Companion;->(Lkotlin/jvm/internal/DefaultConstructorMarker;)V -PLcom/datadog/android/telemetry/model/TelemetryConfigurationEvent$Source;->$values()[Lcom/datadog/android/telemetry/model/TelemetryConfigurationEvent$Source; -PLcom/datadog/android/telemetry/model/TelemetryConfigurationEvent$Source;->()V -PLcom/datadog/android/telemetry/model/TelemetryConfigurationEvent$Source;->(Ljava/lang/String;ILjava/lang/String;)V -PLcom/datadog/android/telemetry/model/TelemetryConfigurationEvent$Source;->access$getJsonValue$p(Lcom/datadog/android/telemetry/model/TelemetryConfigurationEvent$Source;)Ljava/lang/String; -PLcom/datadog/android/telemetry/model/TelemetryConfigurationEvent$Source;->toJson()Lcom/google/gson/JsonElement; -PLcom/datadog/android/telemetry/model/TelemetryConfigurationEvent$Source;->values()[Lcom/datadog/android/telemetry/model/TelemetryConfigurationEvent$Source; -PLcom/datadog/android/telemetry/model/TelemetryConfigurationEvent$Source$Companion;->()V -PLcom/datadog/android/telemetry/model/TelemetryConfigurationEvent$Source$Companion;->(Lkotlin/jvm/internal/DefaultConstructorMarker;)V -PLcom/datadog/android/telemetry/model/TelemetryConfigurationEvent$Source$Companion;->fromJson(Ljava/lang/String;)Lcom/datadog/android/telemetry/model/TelemetryConfigurationEvent$Source; -PLcom/datadog/android/telemetry/model/TelemetryConfigurationEvent$Telemetry;->()V -PLcom/datadog/android/telemetry/model/TelemetryConfigurationEvent$Telemetry;->(Lcom/datadog/android/telemetry/model/TelemetryConfigurationEvent$Device;Lcom/datadog/android/telemetry/model/TelemetryConfigurationEvent$Os;Lcom/datadog/android/telemetry/model/TelemetryConfigurationEvent$Configuration;Ljava/util/Map;)V -PLcom/datadog/android/telemetry/model/TelemetryConfigurationEvent$Telemetry;->(Lcom/datadog/android/telemetry/model/TelemetryConfigurationEvent$Device;Lcom/datadog/android/telemetry/model/TelemetryConfigurationEvent$Os;Lcom/datadog/android/telemetry/model/TelemetryConfigurationEvent$Configuration;Ljava/util/Map;ILkotlin/jvm/internal/DefaultConstructorMarker;)V -PLcom/datadog/android/telemetry/model/TelemetryConfigurationEvent$Telemetry;->toJson()Lcom/google/gson/JsonElement; -PLcom/datadog/android/telemetry/model/TelemetryConfigurationEvent$Telemetry$Companion;->()V -PLcom/datadog/android/telemetry/model/TelemetryConfigurationEvent$Telemetry$Companion;->(Lkotlin/jvm/internal/DefaultConstructorMarker;)V -PLcom/datadog/android/telemetry/model/TelemetryConfigurationEvent$View;->()V -PLcom/datadog/android/telemetry/model/TelemetryConfigurationEvent$View;->(Ljava/lang/String;)V -PLcom/datadog/android/telemetry/model/TelemetryConfigurationEvent$View;->toJson()Lcom/google/gson/JsonElement; -PLcom/datadog/android/telemetry/model/TelemetryConfigurationEvent$View$Companion;->()V -PLcom/datadog/android/telemetry/model/TelemetryConfigurationEvent$View$Companion;->(Lkotlin/jvm/internal/DefaultConstructorMarker;)V -PLcom/datadog/android/telemetry/model/TelemetryConfigurationEvent$ViewTrackingStrategy;->$values()[Lcom/datadog/android/telemetry/model/TelemetryConfigurationEvent$ViewTrackingStrategy; -PLcom/datadog/android/telemetry/model/TelemetryConfigurationEvent$ViewTrackingStrategy;->()V -PLcom/datadog/android/telemetry/model/TelemetryConfigurationEvent$ViewTrackingStrategy;->(Ljava/lang/String;ILjava/lang/String;)V -PLcom/datadog/android/telemetry/model/TelemetryConfigurationEvent$ViewTrackingStrategy;->toJson()Lcom/google/gson/JsonElement; -PLcom/datadog/android/telemetry/model/TelemetryConfigurationEvent$ViewTrackingStrategy$Companion;->()V -PLcom/datadog/android/telemetry/model/TelemetryConfigurationEvent$ViewTrackingStrategy$Companion;->(Lkotlin/jvm/internal/DefaultConstructorMarker;)V Lcom/datadog/android/trace/DatadogTracing; HSPLcom/datadog/android/trace/DatadogTracing;->()V HSPLcom/datadog/android/trace/DatadogTracing;->()V @@ -21960,12 +22112,12 @@ HSPLcom/datadog/android/trace/DatadogTracing;->newTracerBuilder(Lcom/datadog/and Lcom/datadog/android/trace/DeterministicTraceSampler; HSPLcom/datadog/android/trace/DeterministicTraceSampler;->(F)V HSPLcom/datadog/android/trace/DeterministicTraceSampler;->(Lkotlin/jvm/functions/Function0;)V +HSPLcom/datadog/android/trace/DeterministicTraceSampler;->_init_$lambda$0(F)F +Lcom/datadog/android/trace/DeterministicTraceSampler$$ExternalSyntheticLambda0; +HSPLcom/datadog/android/trace/DeterministicTraceSampler$$ExternalSyntheticLambda0;->(F)V +HSPLcom/datadog/android/trace/DeterministicTraceSampler$$ExternalSyntheticLambda0;->invoke()Ljava/lang/Object; Lcom/datadog/android/trace/DeterministicTraceSampler$1; HSPLcom/datadog/android/trace/DeterministicTraceSampler$1;->(Ljava/lang/Object;)V -Lcom/datadog/android/trace/DeterministicTraceSampler$2; -HSPLcom/datadog/android/trace/DeterministicTraceSampler$2;->(F)V -HSPLcom/datadog/android/trace/DeterministicTraceSampler$2;->invoke()Ljava/lang/Float; -HSPLcom/datadog/android/trace/DeterministicTraceSampler$2;->invoke()Ljava/lang/Object; Lcom/datadog/android/trace/InternalCoreWriterProvider; Lcom/datadog/android/trace/Trace; HSPLcom/datadog/android/trace/Trace;->()V @@ -21973,10 +22125,11 @@ HSPLcom/datadog/android/trace/Trace;->()V HSPLcom/datadog/android/trace/Trace;->enable$default(Lcom/datadog/android/trace/TraceConfiguration;Lcom/datadog/android/api/SdkCore;ILjava/lang/Object;)V HSPLcom/datadog/android/trace/Trace;->enable(Lcom/datadog/android/trace/TraceConfiguration;Lcom/datadog/android/api/SdkCore;)V Lcom/datadog/android/trace/TraceConfiguration; -HSPLcom/datadog/android/trace/TraceConfiguration;->(Ljava/lang/String;Lcom/datadog/android/trace/event/SpanEventMapper;Z)V +HSPLcom/datadog/android/trace/TraceConfiguration;->(Ljava/lang/String;Lcom/datadog/android/trace/event/SpanEventMapper;ZZLjava/lang/String;)V HSPLcom/datadog/android/trace/TraceConfiguration;->getCustomEndpointUrl$dd_sdk_android_trace_release()Ljava/lang/String; HSPLcom/datadog/android/trace/TraceConfiguration;->getEventMapper$dd_sdk_android_trace_release()Lcom/datadog/android/trace/event/SpanEventMapper; HSPLcom/datadog/android/trace/TraceConfiguration;->getNetworkInfoEnabled$dd_sdk_android_trace_release()Z +HSPLcom/datadog/android/trace/TraceConfiguration;->getStatsComputationEnabled$dd_sdk_android_trace_release()Z Lcom/datadog/android/trace/TraceConfiguration$Builder; HSPLcom/datadog/android/trace/TraceConfiguration$Builder;->()V HSPLcom/datadog/android/trace/TraceConfiguration$Builder;->build()Lcom/datadog/android/trace/TraceConfiguration; @@ -22020,13 +22173,14 @@ Lcom/datadog/android/trace/internal/DatadogTracerAdapter; HSPLcom/datadog/android/trace/internal/DatadogTracerAdapter;->(Lcom/datadog/android/api/feature/FeatureSdkCore;Lcom/datadog/trace/bootstrap/instrumentation/api/AgentTracer$TracerAPI;ZLcom/datadog/android/trace/internal/DatadogSpanLogger;Lcom/datadog/android/trace/internal/RumContextPropagator;)V HSPLcom/datadog/android/trace/internal/DatadogTracerAdapter;->(Lcom/datadog/android/api/feature/FeatureSdkCore;Lcom/datadog/trace/bootstrap/instrumentation/api/AgentTracer$TracerAPI;ZLcom/datadog/android/trace/internal/DatadogSpanLogger;Lcom/datadog/android/trace/internal/RumContextPropagator;ILkotlin/jvm/internal/DefaultConstructorMarker;)V HSPLcom/datadog/android/trace/internal/DatadogTracerAdapter;->addScopeListener(Lcom/datadog/android/trace/api/scope/DatadogScopeListener;)V -Lcom/datadog/android/trace/internal/DatadogTracerAdapter$1; -HSPLcom/datadog/android/trace/internal/DatadogTracerAdapter$1;->(Lcom/datadog/android/api/feature/FeatureSdkCore;)V +Lcom/datadog/android/trace/internal/DatadogTracerAdapter$$ExternalSyntheticLambda0; +HSPLcom/datadog/android/trace/internal/DatadogTracerAdapter$$ExternalSyntheticLambda0;->(Lcom/datadog/android/api/feature/FeatureSdkCore;)V Lcom/datadog/android/trace/internal/DatadogTracerBuilderAdapter; HSPLcom/datadog/android/trace/internal/DatadogTracerBuilderAdapter;->()V HSPLcom/datadog/android/trace/internal/DatadogTracerBuilderAdapter;->(Lcom/datadog/android/api/feature/FeatureSdkCore;Ljava/lang/String;Lcom/datadog/trace/core/CoreTracer$CoreTracerBuilder;)V HSPLcom/datadog/android/trace/internal/DatadogTracerBuilderAdapter;->build()Lcom/datadog/android/trace/api/tracer/DatadogTracer; HSPLcom/datadog/android/trace/internal/DatadogTracerBuilderAdapter;->properties$dd_sdk_android_trace_release()Ljava/util/Properties; +HSPLcom/datadog/android/trace/internal/DatadogTracerBuilderAdapter;->properties$lambda$8(Lcom/datadog/android/trace/TracingHeaderType;)Ljava/lang/CharSequence; HSPLcom/datadog/android/trace/internal/DatadogTracerBuilderAdapter;->setCustomIdGenerationStrategy$dd_sdk_android_trace_release(Lcom/datadog/trace/api/IdGenerationStrategy;)Lcom/datadog/android/trace/internal/DatadogTracerBuilderAdapter; HSPLcom/datadog/android/trace/internal/DatadogTracerBuilderAdapter;->setSdkV2Compatible$dd_sdk_android_trace_release()V HSPLcom/datadog/android/trace/internal/DatadogTracerBuilderAdapter;->setTraceId128BitGenerationEnabled$dd_sdk_android_trace_release(Z)Lcom/datadog/android/trace/internal/DatadogTracerBuilderAdapter; @@ -22036,17 +22190,15 @@ HSPLcom/datadog/android/trace/internal/DatadogTracerBuilderAdapter;->withService HSPLcom/datadog/android/trace/internal/DatadogTracerBuilderAdapter;->withServiceName(Ljava/lang/String;)Lcom/datadog/android/trace/internal/DatadogTracerBuilderAdapter; HSPLcom/datadog/android/trace/internal/DatadogTracerBuilderAdapter;->withTracingHeadersTypes(Ljava/util/Set;)Lcom/datadog/android/trace/api/tracer/DatadogTracerBuilder; HSPLcom/datadog/android/trace/internal/DatadogTracerBuilderAdapter;->withTracingHeadersTypes(Ljava/util/Set;)Lcom/datadog/android/trace/internal/DatadogTracerBuilderAdapter; +Lcom/datadog/android/trace/internal/DatadogTracerBuilderAdapter$$ExternalSyntheticLambda0; +HSPLcom/datadog/android/trace/internal/DatadogTracerBuilderAdapter$$ExternalSyntheticLambda0;->()V +HSPLcom/datadog/android/trace/internal/DatadogTracerBuilderAdapter$$ExternalSyntheticLambda0;->invoke(Ljava/lang/Object;)Ljava/lang/Object; Lcom/datadog/android/trace/internal/DatadogTracerBuilderAdapter$Companion; HSPLcom/datadog/android/trace/internal/DatadogTracerBuilderAdapter$Companion;->()V HSPLcom/datadog/android/trace/internal/DatadogTracerBuilderAdapter$Companion;->(Lkotlin/jvm/internal/DefaultConstructorMarker;)V HSPLcom/datadog/android/trace/internal/DatadogTracerBuilderAdapter$Companion;->toPropagationStyle$dd_sdk_android_trace_release(Lcom/datadog/android/trace/TracingHeaderType;)Ljava/lang/String; Lcom/datadog/android/trace/internal/DatadogTracerBuilderAdapter$Companion$WhenMappings; HSPLcom/datadog/android/trace/internal/DatadogTracerBuilderAdapter$Companion$WhenMappings;->()V -Lcom/datadog/android/trace/internal/DatadogTracerBuilderAdapter$properties$propagationStyles$1; -HSPLcom/datadog/android/trace/internal/DatadogTracerBuilderAdapter$properties$propagationStyles$1;->()V -HSPLcom/datadog/android/trace/internal/DatadogTracerBuilderAdapter$properties$propagationStyles$1;->()V -HSPLcom/datadog/android/trace/internal/DatadogTracerBuilderAdapter$properties$propagationStyles$1;->invoke(Lcom/datadog/android/trace/TracingHeaderType;)Ljava/lang/CharSequence; -HSPLcom/datadog/android/trace/internal/DatadogTracerBuilderAdapter$properties$propagationStyles$1;->invoke(Ljava/lang/Object;)Ljava/lang/Object; Lcom/datadog/android/trace/internal/RumContextPropagator; HSPLcom/datadog/android/trace/internal/RumContextPropagator;->()V HSPLcom/datadog/android/trace/internal/RumContextPropagator;->(Lkotlin/jvm/functions/Function0;)V @@ -22056,8 +22208,7 @@ HSPLcom/datadog/android/trace/internal/RumContextPropagator$Companion;->(L Lcom/datadog/android/trace/internal/TracePropagationScopeListener; HSPLcom/datadog/android/trace/internal/TracePropagationScopeListener;->(Lcom/datadog/android/api/feature/FeatureSdkCore;Lcom/datadog/android/trace/api/tracer/DatadogTracer;)V Lcom/datadog/android/trace/internal/TracingFeature; -HSPLcom/datadog/android/trace/internal/TracingFeature;->(Lcom/datadog/android/api/feature/FeatureSdkCore;Ljava/lang/String;Lcom/datadog/android/trace/event/SpanEventMapper;Z)V -HSPLcom/datadog/android/trace/internal/TracingFeature;->access$getSdkCore$p(Lcom/datadog/android/trace/internal/TracingFeature;)Lcom/datadog/android/api/feature/FeatureSdkCore; +HSPLcom/datadog/android/trace/internal/TracingFeature;->(Lcom/datadog/android/api/feature/FeatureSdkCore;Ljava/lang/String;Lcom/datadog/android/trace/event/SpanEventMapper;ZZ)V HSPLcom/datadog/android/trace/internal/TracingFeature;->createDataWriter(Lcom/datadog/android/api/feature/FeatureSdkCore;)Lcom/datadog/trace/common/writer/Writer; HSPLcom/datadog/android/trace/internal/TracingFeature;->getCoreTracerWriter()Lcom/datadog/android/trace/api/span/DatadogSpanWriter; HSPLcom/datadog/android/trace/internal/TracingFeature;->getCoreTracerWriter()Lcom/datadog/android/trace/internal/DatadogSpanWriterWrapper; @@ -22065,10 +22216,10 @@ HSPLcom/datadog/android/trace/internal/TracingFeature;->getName()Ljava/lang/Stri HSPLcom/datadog/android/trace/internal/TracingFeature;->getRequestFactory()Lcom/datadog/android/api/net/RequestFactory; HSPLcom/datadog/android/trace/internal/TracingFeature;->getStorageConfiguration()Lcom/datadog/android/api/storage/FeatureStorageConfiguration; HSPLcom/datadog/android/trace/internal/TracingFeature;->onInitialize(Landroid/content/Context;)V -Lcom/datadog/android/trace/internal/TracingFeature$requestFactory$2; -HSPLcom/datadog/android/trace/internal/TracingFeature$requestFactory$2;->(Ljava/lang/String;Lcom/datadog/android/trace/internal/TracingFeature;)V -HSPLcom/datadog/android/trace/internal/TracingFeature$requestFactory$2;->invoke()Lcom/datadog/android/trace/internal/net/TracesRequestFactory; -HSPLcom/datadog/android/trace/internal/TracingFeature$requestFactory$2;->invoke()Ljava/lang/Object; +HSPLcom/datadog/android/trace/internal/TracingFeature;->requestFactory_delegate$lambda$0(Ljava/lang/String;Lcom/datadog/android/trace/internal/TracingFeature;)Lcom/datadog/android/trace/internal/net/TracesRequestFactory; +Lcom/datadog/android/trace/internal/TracingFeature$$ExternalSyntheticLambda0; +HSPLcom/datadog/android/trace/internal/TracingFeature$$ExternalSyntheticLambda0;->(Ljava/lang/String;Lcom/datadog/android/trace/internal/TracingFeature;)V +HSPLcom/datadog/android/trace/internal/TracingFeature$$ExternalSyntheticLambda0;->invoke()Ljava/lang/Object; Lcom/datadog/android/trace/internal/_TraceInternalProxy; HSPLcom/datadog/android/trace/internal/_TraceInternalProxy;->()V HSPLcom/datadog/android/trace/internal/_TraceInternalProxy;->()V @@ -22083,8 +22234,8 @@ HSPLcom/datadog/android/trace/internal/data/CoreTraceWriter;->()V HSPLcom/datadog/android/trace/internal/data/CoreTraceWriter;->(Lcom/datadog/android/api/feature/FeatureSdkCore;Lcom/datadog/android/trace/internal/domain/event/ContextAwareMapper;Lcom/datadog/android/event/EventMapper;Lcom/datadog/android/trace/internal/storage/ContextAwareSerializer;Lcom/datadog/android/api/InternalLogger;Lcom/datadog/android/trace/internal/RumContextPropagator;)V HSPLcom/datadog/android/trace/internal/data/CoreTraceWriter;->(Lcom/datadog/android/api/feature/FeatureSdkCore;Lcom/datadog/android/trace/internal/domain/event/ContextAwareMapper;Lcom/datadog/android/event/EventMapper;Lcom/datadog/android/trace/internal/storage/ContextAwareSerializer;Lcom/datadog/android/api/InternalLogger;Lcom/datadog/android/trace/internal/RumContextPropagator;ILkotlin/jvm/internal/DefaultConstructorMarker;)V HSPLcom/datadog/android/trace/internal/data/CoreTraceWriter;->start()V -Lcom/datadog/android/trace/internal/data/CoreTraceWriter$1; -HSPLcom/datadog/android/trace/internal/data/CoreTraceWriter$1;->(Lcom/datadog/android/api/feature/FeatureSdkCore;)V +Lcom/datadog/android/trace/internal/data/CoreTraceWriter$$ExternalSyntheticLambda0; +HSPLcom/datadog/android/trace/internal/data/CoreTraceWriter$$ExternalSyntheticLambda0;->(Lcom/datadog/android/api/feature/FeatureSdkCore;)V Lcom/datadog/android/trace/internal/data/CoreTraceWriter$Companion; HSPLcom/datadog/android/trace/internal/data/CoreTraceWriter$Companion;->()V HSPLcom/datadog/android/trace/internal/data/CoreTraceWriter$Companion;->(Lkotlin/jvm/internal/DefaultConstructorMarker;)V @@ -22093,7 +22244,7 @@ HSPLcom/datadog/android/trace/internal/domain/event/BaseSpanEventMapper;-> Lcom/datadog/android/trace/internal/domain/event/ContextAwareMapper; Lcom/datadog/android/trace/internal/domain/event/CoreTracerSpanToSpanEventMapper; HSPLcom/datadog/android/trace/internal/domain/event/CoreTracerSpanToSpanEventMapper;->()V -HSPLcom/datadog/android/trace/internal/domain/event/CoreTracerSpanToSpanEventMapper;->(Z)V +HSPLcom/datadog/android/trace/internal/domain/event/CoreTracerSpanToSpanEventMapper;->(ZZ)V Lcom/datadog/android/trace/internal/domain/event/CoreTracerSpanToSpanEventMapper$Companion; HSPLcom/datadog/android/trace/internal/domain/event/CoreTracerSpanToSpanEventMapper$Companion;->()V HSPLcom/datadog/android/trace/internal/domain/event/CoreTracerSpanToSpanEventMapper$Companion;->(Lkotlin/jvm/internal/DefaultConstructorMarker;)V @@ -22129,33 +22280,29 @@ HSPLcom/datadog/android/trace/opentelemetry/DatadogOpenTelemetry;->(Ljava/ Lcom/datadog/android/trace/opentelemetry/OtelTracerProvider; HSPLcom/datadog/android/trace/opentelemetry/OtelTracerProvider;->()V HSPLcom/datadog/android/trace/opentelemetry/OtelTracerProvider;->(Lcom/datadog/android/trace/api/tracer/DatadogTracer;Lcom/datadog/android/api/InternalLogger;)V +HSPLcom/datadog/android/trace/opentelemetry/OtelTracerProvider;->_init_$lambda$3()Lkotlin/Unit; +Lcom/datadog/android/trace/opentelemetry/OtelTracerProvider$$ExternalSyntheticLambda0; +HSPLcom/datadog/android/trace/opentelemetry/OtelTracerProvider$$ExternalSyntheticLambda0;->()V +HSPLcom/datadog/android/trace/opentelemetry/OtelTracerProvider$$ExternalSyntheticLambda0;->invoke()Ljava/lang/Object; Lcom/datadog/android/trace/opentelemetry/OtelTracerProvider$Builder; HSPLcom/datadog/android/trace/opentelemetry/OtelTracerProvider$Builder;->(Lcom/datadog/android/api/SdkCore;)V HSPLcom/datadog/android/trace/opentelemetry/OtelTracerProvider$Builder;->(Lcom/datadog/android/api/SdkCore;ILkotlin/jvm/internal/DefaultConstructorMarker;)V HSPLcom/datadog/android/trace/opentelemetry/OtelTracerProvider$Builder;->(Lcom/datadog/android/api/feature/FeatureSdkCore;)V -HSPLcom/datadog/android/trace/opentelemetry/OtelTracerProvider$Builder;->access$createDatadogTracer(Lcom/datadog/android/trace/opentelemetry/OtelTracerProvider$Builder;)Lcom/datadog/android/trace/api/tracer/DatadogTracer; -HSPLcom/datadog/android/trace/opentelemetry/OtelTracerProvider$Builder;->access$getSdkCore$p(Lcom/datadog/android/trace/opentelemetry/OtelTracerProvider$Builder;)Lcom/datadog/android/api/feature/FeatureSdkCore; +HSPLcom/datadog/android/trace/opentelemetry/OtelTracerProvider$Builder;->build$lambda$4$lambda$3(Ljava/util/Map;)Lkotlin/Unit; +HSPLcom/datadog/android/trace/opentelemetry/OtelTracerProvider$Builder;->build$lambda$4(Lcom/datadog/android/trace/opentelemetry/OtelTracerProvider$Builder;)Lio/opentelemetry/api/trace/TracerProvider; HSPLcom/datadog/android/trace/opentelemetry/OtelTracerProvider$Builder;->build()Lio/opentelemetry/api/trace/TracerProvider; HSPLcom/datadog/android/trace/opentelemetry/OtelTracerProvider$Builder;->createDatadogTracer()Lcom/datadog/android/trace/api/tracer/DatadogTracer; HSPLcom/datadog/android/trace/opentelemetry/OtelTracerProvider$Builder;->getServiceName()Ljava/lang/String; HSPLcom/datadog/android/trace/opentelemetry/OtelTracerProvider$Builder;->setService(Ljava/lang/String;)Lcom/datadog/android/trace/opentelemetry/OtelTracerProvider$Builder; -Lcom/datadog/android/trace/opentelemetry/OtelTracerProvider$Builder$build$1; -HSPLcom/datadog/android/trace/opentelemetry/OtelTracerProvider$Builder$build$1;->(Lcom/datadog/android/trace/opentelemetry/OtelTracerProvider$Builder;)V -HSPLcom/datadog/android/trace/opentelemetry/OtelTracerProvider$Builder$build$1;->invoke()Lio/opentelemetry/api/trace/TracerProvider; -HSPLcom/datadog/android/trace/opentelemetry/OtelTracerProvider$Builder$build$1;->invoke()Ljava/lang/Object; -Lcom/datadog/android/trace/opentelemetry/OtelTracerProvider$Builder$build$1$1; -HSPLcom/datadog/android/trace/opentelemetry/OtelTracerProvider$Builder$build$1$1;->()V -HSPLcom/datadog/android/trace/opentelemetry/OtelTracerProvider$Builder$build$1$1;->()V -HSPLcom/datadog/android/trace/opentelemetry/OtelTracerProvider$Builder$build$1$1;->invoke(Ljava/lang/Object;)Ljava/lang/Object; -HSPLcom/datadog/android/trace/opentelemetry/OtelTracerProvider$Builder$build$1$1;->invoke(Ljava/util/Map;)V +Lcom/datadog/android/trace/opentelemetry/OtelTracerProvider$Builder$$ExternalSyntheticLambda0; +HSPLcom/datadog/android/trace/opentelemetry/OtelTracerProvider$Builder$$ExternalSyntheticLambda0;->(Lcom/datadog/android/trace/opentelemetry/OtelTracerProvider$Builder;)V +HSPLcom/datadog/android/trace/opentelemetry/OtelTracerProvider$Builder$$ExternalSyntheticLambda0;->invoke()Ljava/lang/Object; +Lcom/datadog/android/trace/opentelemetry/OtelTracerProvider$Builder$$ExternalSyntheticLambda1; +HSPLcom/datadog/android/trace/opentelemetry/OtelTracerProvider$Builder$$ExternalSyntheticLambda1;->()V +HSPLcom/datadog/android/trace/opentelemetry/OtelTracerProvider$Builder$$ExternalSyntheticLambda1;->invoke(Ljava/lang/Object;)Ljava/lang/Object; Lcom/datadog/android/trace/opentelemetry/OtelTracerProvider$Companion; HSPLcom/datadog/android/trace/opentelemetry/OtelTracerProvider$Companion;->()V HSPLcom/datadog/android/trace/opentelemetry/OtelTracerProvider$Companion;->(Lkotlin/jvm/internal/DefaultConstructorMarker;)V -Lcom/datadog/android/trace/opentelemetry/OtelTracerProvider$Companion$1; -HSPLcom/datadog/android/trace/opentelemetry/OtelTracerProvider$Companion$1;->()V -HSPLcom/datadog/android/trace/opentelemetry/OtelTracerProvider$Companion$1;->()V -HSPLcom/datadog/android/trace/opentelemetry/OtelTracerProvider$Companion$1;->invoke()Ljava/lang/Object; -HSPLcom/datadog/android/trace/opentelemetry/OtelTracerProvider$Companion$1;->invoke()V Lcom/datadog/android/trace/opentelemetry/internal/DatadogContextStorageWrapper; HSPLcom/datadog/android/trace/opentelemetry/internal/DatadogContextStorageWrapper;->()V Lcom/datadog/android/trace/opentelemetry/internal/MiscUtilsKt; @@ -22529,8 +22676,8 @@ Lcom/datadog/trace/common/writer/Writer; Lcom/datadog/trace/core/CoreTracer; HSPLcom/datadog/trace/core/CoreTracer;->-$$Nest$sminvertMap(Ljava/util/Map;)Ljava/util/Map; HSPLcom/datadog/trace/core/CoreTracer;->()V -HSPLcom/datadog/trace/core/CoreTracer;->(Lcom/datadog/trace/api/Config;Ljava/lang/String;Lcom/datadog/trace/common/writer/Writer;Lcom/datadog/trace/api/IdGenerationStrategy;Lcom/datadog/trace/common/sampling/Sampler;Lcom/datadog/trace/core/propagation/HttpCodec$Injector;Lcom/datadog/trace/core/propagation/HttpCodec$Extractor;Lcom/datadog/trace/bootstrap/instrumentation/api/AgentScopeManager;Ljava/util/Map;Ljava/util/Map;Ljava/util/Map;Ljava/util/Map;Ljava/util/Map;ILcom/datadog/trace/core/taginterceptor/TagInterceptor;ZLcom/datadog/trace/api/time/TimeSource;Lcom/datadog/trace/bootstrap/instrumentation/api/ProfilingContextIntegration;ZLcom/datadog/android/api/InternalLogger;)V -HSPLcom/datadog/trace/core/CoreTracer;->(Lcom/datadog/trace/api/Config;Ljava/lang/String;Lcom/datadog/trace/common/writer/Writer;Lcom/datadog/trace/api/IdGenerationStrategy;Lcom/datadog/trace/common/sampling/Sampler;Lcom/datadog/trace/core/propagation/HttpCodec$Injector;Lcom/datadog/trace/core/propagation/HttpCodec$Extractor;Lcom/datadog/trace/bootstrap/instrumentation/api/AgentScopeManager;Ljava/util/Map;Ljava/util/Map;Ljava/util/Map;Ljava/util/Map;Ljava/util/Map;ILcom/datadog/trace/core/taginterceptor/TagInterceptor;ZLcom/datadog/trace/api/time/TimeSource;Lcom/datadog/trace/bootstrap/instrumentation/api/ProfilingContextIntegration;ZLcom/datadog/android/api/InternalLogger;Lcom/datadog/trace/core/CoreTracer-IA;)V +HSPLcom/datadog/trace/core/CoreTracer;->(Lcom/datadog/trace/api/Config;Ljava/lang/String;Lcom/datadog/trace/common/writer/Writer;Lcom/datadog/trace/common/metrics/MetricsAggregator;Lcom/datadog/trace/api/IdGenerationStrategy;Lcom/datadog/trace/common/sampling/Sampler;Lcom/datadog/trace/core/propagation/HttpCodec$Injector;Lcom/datadog/trace/core/propagation/HttpCodec$Extractor;Lcom/datadog/trace/bootstrap/instrumentation/api/AgentScopeManager;Ljava/util/Map;Ljava/util/Map;Ljava/util/Map;Ljava/util/Map;Ljava/util/Map;ILcom/datadog/trace/core/taginterceptor/TagInterceptor;ZLcom/datadog/trace/api/time/TimeSource;Lcom/datadog/trace/bootstrap/instrumentation/api/ProfilingContextIntegration;ZLcom/datadog/android/api/InternalLogger;)V +HSPLcom/datadog/trace/core/CoreTracer;->(Lcom/datadog/trace/api/Config;Ljava/lang/String;Lcom/datadog/trace/common/writer/Writer;Lcom/datadog/trace/common/metrics/MetricsAggregator;Lcom/datadog/trace/api/IdGenerationStrategy;Lcom/datadog/trace/common/sampling/Sampler;Lcom/datadog/trace/core/propagation/HttpCodec$Injector;Lcom/datadog/trace/core/propagation/HttpCodec$Extractor;Lcom/datadog/trace/bootstrap/instrumentation/api/AgentScopeManager;Ljava/util/Map;Ljava/util/Map;Ljava/util/Map;Ljava/util/Map;Ljava/util/Map;ILcom/datadog/trace/core/taginterceptor/TagInterceptor;ZLcom/datadog/trace/api/time/TimeSource;Lcom/datadog/trace/bootstrap/instrumentation/api/ProfilingContextIntegration;ZLcom/datadog/android/api/InternalLogger;Lcom/datadog/trace/core/CoreTracer-IA;)V HSPLcom/datadog/trace/core/CoreTracer;->addScopeListener(Lcom/datadog/trace/api/scopemanager/ScopeListener;)V HSPLcom/datadog/trace/core/CoreTracer;->invertMap(Ljava/util/Map;)Ljava/util/Map; HSPLcom/datadog/trace/core/CoreTracer;->lambda$new$0$com-datadog-trace-core-CoreTracer(Lcom/datadog/trace/api/DynamicConfig$Builder;Lcom/datadog/trace/core/CoreTracer$ConfigSnapshot;)Lcom/datadog/trace/core/CoreTracer$ConfigSnapshot; @@ -22551,6 +22698,7 @@ HSPLcom/datadog/trace/core/CoreTracer$CoreTracerBuilder;->idGenerationStrategy(L HSPLcom/datadog/trace/core/CoreTracer$CoreTracerBuilder;->injectBaggageAsTags(Z)Lcom/datadog/trace/core/CoreTracer$CoreTracerBuilder; HSPLcom/datadog/trace/core/CoreTracer$CoreTracerBuilder;->injector(Lcom/datadog/trace/core/propagation/HttpCodec$Injector;)Lcom/datadog/trace/core/CoreTracer$CoreTracerBuilder; HSPLcom/datadog/trace/core/CoreTracer$CoreTracerBuilder;->localRootSpanTags(Ljava/util/Map;)Lcom/datadog/trace/core/CoreTracer$CoreTracerBuilder; +HSPLcom/datadog/trace/core/CoreTracer$CoreTracerBuilder;->metricsAggregator(Lcom/datadog/trace/common/metrics/MetricsAggregator;)Lcom/datadog/trace/core/CoreTracer$CoreTracerBuilder; HSPLcom/datadog/trace/core/CoreTracer$CoreTracerBuilder;->partialFlushMinSpans(I)Lcom/datadog/trace/core/CoreTracer$CoreTracerBuilder; HSPLcom/datadog/trace/core/CoreTracer$CoreTracerBuilder;->sampler(Lcom/datadog/trace/common/sampling/Sampler;)Lcom/datadog/trace/core/CoreTracer$CoreTracerBuilder; HSPLcom/datadog/trace/core/CoreTracer$CoreTracerBuilder;->serviceName(Ljava/lang/String;)Lcom/datadog/trace/core/CoreTracer$CoreTracerBuilder; @@ -22765,27 +22913,32 @@ HSPLcom/datadog/trace/util/PidHelper;->getPidAsLong()J HSPLcom/datadog/trace/util/PidHelper;->parsePid()J Lcom/datadog/trace/util/Strings; HSPLcom/datadog/trace/util/Strings;->trim(Ljava/lang/String;)Ljava/lang/String; -PLcom/google/android/datatransport/AutoValue_Event;->(Ljava/lang/Integer;Ljava/lang/Object;Lcom/google/android/datatransport/Priority;Lcom/google/android/datatransport/ProductData;Lcom/google/android/datatransport/EventContext;)V -PLcom/google/android/datatransport/AutoValue_Event;->getCode()Ljava/lang/Integer; -PLcom/google/android/datatransport/AutoValue_Event;->getEventContext()Lcom/google/android/datatransport/EventContext; -PLcom/google/android/datatransport/AutoValue_Event;->getPayload()Ljava/lang/Object; -PLcom/google/android/datatransport/AutoValue_Event;->getPriority()Lcom/google/android/datatransport/Priority; -PLcom/google/android/datatransport/AutoValue_Event;->getProductData()Lcom/google/android/datatransport/ProductData; +Lcom/google/android/datatransport/AutoValue_Event; +HSPLcom/google/android/datatransport/AutoValue_Event;->(Ljava/lang/Integer;Ljava/lang/Object;Lcom/google/android/datatransport/Priority;Lcom/google/android/datatransport/ProductData;Lcom/google/android/datatransport/EventContext;)V +HSPLcom/google/android/datatransport/AutoValue_Event;->getCode()Ljava/lang/Integer; +HSPLcom/google/android/datatransport/AutoValue_Event;->getEventContext()Lcom/google/android/datatransport/EventContext; +HSPLcom/google/android/datatransport/AutoValue_Event;->getPayload()Ljava/lang/Object; +HSPLcom/google/android/datatransport/AutoValue_Event;->getPriority()Lcom/google/android/datatransport/Priority; +HSPLcom/google/android/datatransport/AutoValue_Event;->getProductData()Lcom/google/android/datatransport/ProductData; Lcom/google/android/datatransport/Encoding; HSPLcom/google/android/datatransport/Encoding;->(Ljava/lang/String;)V HSPLcom/google/android/datatransport/Encoding;->equals(Ljava/lang/Object;)Z -PLcom/google/android/datatransport/Encoding;->getName()Ljava/lang/String; +HSPLcom/google/android/datatransport/Encoding;->getName()Ljava/lang/String; HSPLcom/google/android/datatransport/Encoding;->hashCode()I HSPLcom/google/android/datatransport/Encoding;->of(Ljava/lang/String;)Lcom/google/android/datatransport/Encoding; -PLcom/google/android/datatransport/Event;->()V -PLcom/google/android/datatransport/Event;->ofData(Ljava/lang/Object;)Lcom/google/android/datatransport/Event; +Lcom/google/android/datatransport/Event; +HSPLcom/google/android/datatransport/Event;->()V +HSPLcom/google/android/datatransport/Event;->ofData(Ljava/lang/Object;)Lcom/google/android/datatransport/Event; +Lcom/google/android/datatransport/EventContext; Lcom/google/android/datatransport/Priority; HSPLcom/google/android/datatransport/Priority;->()V HSPLcom/google/android/datatransport/Priority;->(Ljava/lang/String;I)V HSPLcom/google/android/datatransport/Priority;->values()[Lcom/google/android/datatransport/Priority; +Lcom/google/android/datatransport/ProductData; Lcom/google/android/datatransport/Transformer; Lcom/google/android/datatransport/Transport; Lcom/google/android/datatransport/TransportFactory; +Lcom/google/android/datatransport/TransportScheduleCallback; Lcom/google/android/datatransport/cct/CCTDestination; HSPLcom/google/android/datatransport/cct/CCTDestination;->()V HSPLcom/google/android/datatransport/cct/CCTDestination;->(Ljava/lang/String;Ljava/lang/String;)V @@ -22793,86 +22946,124 @@ HSPLcom/google/android/datatransport/cct/CCTDestination;->asByteArray()[B HSPLcom/google/android/datatransport/cct/CCTDestination;->getExtras()[B HSPLcom/google/android/datatransport/cct/CCTDestination;->getName()Ljava/lang/String; HSPLcom/google/android/datatransport/cct/CCTDestination;->getSupportedEncodings()Ljava/util/Set; -PLcom/google/android/datatransport/cct/CctBackendFactory;->()V -PLcom/google/android/datatransport/cct/CctBackendFactory;->create(Lcom/google/android/datatransport/runtime/backends/CreationContext;)Lcom/google/android/datatransport/runtime/backends/TransportBackend; -PLcom/google/android/datatransport/cct/CctTransportBackend;->(Landroid/content/Context;Lcom/google/android/datatransport/runtime/time/Clock;Lcom/google/android/datatransport/runtime/time/Clock;)V -PLcom/google/android/datatransport/cct/CctTransportBackend;->(Landroid/content/Context;Lcom/google/android/datatransport/runtime/time/Clock;Lcom/google/android/datatransport/runtime/time/Clock;I)V -PLcom/google/android/datatransport/cct/CctTransportBackend;->decorate(Lcom/google/android/datatransport/runtime/EventInternal;)Lcom/google/android/datatransport/runtime/EventInternal; -PLcom/google/android/datatransport/cct/CctTransportBackend;->getMccMncOrEmpty(Landroid/content/Context;)Ljava/lang/String; -PLcom/google/android/datatransport/cct/CctTransportBackend;->getNetSubtypeValue(Landroid/net/NetworkInfo;)I -PLcom/google/android/datatransport/cct/CctTransportBackend;->getNetTypeValue(Landroid/net/NetworkInfo;)I -PLcom/google/android/datatransport/cct/CctTransportBackend;->getPackageVersionCode(Landroid/content/Context;)I -PLcom/google/android/datatransport/cct/CctTransportBackend;->getTelephonyManager(Landroid/content/Context;)Landroid/telephony/TelephonyManager; -PLcom/google/android/datatransport/cct/CctTransportBackend;->getTzOffset()J -PLcom/google/android/datatransport/cct/CctTransportBackend;->parseUrlOrThrow(Ljava/lang/String;)Ljava/net/URL; +Lcom/google/android/datatransport/cct/CctBackendFactory; +HSPLcom/google/android/datatransport/cct/CctBackendFactory;->()V +HSPLcom/google/android/datatransport/cct/CctBackendFactory;->create(Lcom/google/android/datatransport/runtime/backends/CreationContext;)Lcom/google/android/datatransport/runtime/backends/TransportBackend; +Lcom/google/android/datatransport/cct/CctTransportBackend; +HSPLcom/google/android/datatransport/cct/CctTransportBackend;->(Landroid/content/Context;Lcom/google/android/datatransport/runtime/time/Clock;Lcom/google/android/datatransport/runtime/time/Clock;)V +HSPLcom/google/android/datatransport/cct/CctTransportBackend;->(Landroid/content/Context;Lcom/google/android/datatransport/runtime/time/Clock;Lcom/google/android/datatransport/runtime/time/Clock;I)V +HSPLcom/google/android/datatransport/cct/CctTransportBackend;->decorate(Lcom/google/android/datatransport/runtime/EventInternal;)Lcom/google/android/datatransport/runtime/EventInternal; +HSPLcom/google/android/datatransport/cct/CctTransportBackend;->getMccMncOrEmpty(Landroid/content/Context;)Ljava/lang/String; +HSPLcom/google/android/datatransport/cct/CctTransportBackend;->getNetSubtypeValue(Landroid/net/NetworkInfo;)I +HSPLcom/google/android/datatransport/cct/CctTransportBackend;->getNetTypeValue(Landroid/net/NetworkInfo;)I +HSPLcom/google/android/datatransport/cct/CctTransportBackend;->getPackageVersionCode(Landroid/content/Context;)I +HSPLcom/google/android/datatransport/cct/CctTransportBackend;->getTelephonyManager(Landroid/content/Context;)Landroid/telephony/TelephonyManager; +HSPLcom/google/android/datatransport/cct/CctTransportBackend;->getTzOffset()J +HSPLcom/google/android/datatransport/cct/CctTransportBackend;->parseUrlOrThrow(Ljava/lang/String;)Ljava/net/URL; Lcom/google/android/datatransport/cct/StringMerger; HSPLcom/google/android/datatransport/cct/StringMerger;->mergeStrings(Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String; -PLcom/google/android/datatransport/cct/internal/AutoBatchedLogRequestEncoder;->()V -PLcom/google/android/datatransport/cct/internal/AutoBatchedLogRequestEncoder;->()V -PLcom/google/android/datatransport/cct/internal/AutoBatchedLogRequestEncoder;->configure(Lcom/google/firebase/encoders/config/EncoderConfig;)V -PLcom/google/android/datatransport/cct/internal/AutoBatchedLogRequestEncoder$AndroidClientInfoEncoder;->()V -PLcom/google/android/datatransport/cct/internal/AutoBatchedLogRequestEncoder$AndroidClientInfoEncoder;->()V -PLcom/google/android/datatransport/cct/internal/AutoBatchedLogRequestEncoder$BatchedLogRequestEncoder;->()V -PLcom/google/android/datatransport/cct/internal/AutoBatchedLogRequestEncoder$BatchedLogRequestEncoder;->()V -PLcom/google/android/datatransport/cct/internal/AutoBatchedLogRequestEncoder$ClientInfoEncoder;->()V -PLcom/google/android/datatransport/cct/internal/AutoBatchedLogRequestEncoder$ClientInfoEncoder;->()V -PLcom/google/android/datatransport/cct/internal/AutoBatchedLogRequestEncoder$ComplianceDataEncoder;->()V -PLcom/google/android/datatransport/cct/internal/AutoBatchedLogRequestEncoder$ComplianceDataEncoder;->()V -PLcom/google/android/datatransport/cct/internal/AutoBatchedLogRequestEncoder$ExperimentIdsEncoder;->()V -PLcom/google/android/datatransport/cct/internal/AutoBatchedLogRequestEncoder$ExperimentIdsEncoder;->()V -PLcom/google/android/datatransport/cct/internal/AutoBatchedLogRequestEncoder$ExternalPRequestContextEncoder;->()V -PLcom/google/android/datatransport/cct/internal/AutoBatchedLogRequestEncoder$ExternalPRequestContextEncoder;->()V -PLcom/google/android/datatransport/cct/internal/AutoBatchedLogRequestEncoder$ExternalPrivacyContextEncoder;->()V -PLcom/google/android/datatransport/cct/internal/AutoBatchedLogRequestEncoder$ExternalPrivacyContextEncoder;->()V -PLcom/google/android/datatransport/cct/internal/AutoBatchedLogRequestEncoder$LogEventEncoder;->()V -PLcom/google/android/datatransport/cct/internal/AutoBatchedLogRequestEncoder$LogEventEncoder;->()V -PLcom/google/android/datatransport/cct/internal/AutoBatchedLogRequestEncoder$LogRequestEncoder;->()V -PLcom/google/android/datatransport/cct/internal/AutoBatchedLogRequestEncoder$LogRequestEncoder;->()V -PLcom/google/android/datatransport/cct/internal/AutoBatchedLogRequestEncoder$NetworkConnectionInfoEncoder;->()V -PLcom/google/android/datatransport/cct/internal/AutoBatchedLogRequestEncoder$NetworkConnectionInfoEncoder;->()V -PLcom/google/android/datatransport/cct/internal/BatchedLogRequest;->createDataEncoder()Lcom/google/firebase/encoders/DataEncoder; -PLcom/google/android/datatransport/cct/internal/NetworkConnectionInfo$MobileSubtype;->()V -PLcom/google/android/datatransport/cct/internal/NetworkConnectionInfo$MobileSubtype;->(Ljava/lang/String;II)V -PLcom/google/android/datatransport/cct/internal/NetworkConnectionInfo$MobileSubtype;->getValue()I -PLcom/google/android/datatransport/runtime/AutoValue_EventInternal;->(Ljava/lang/String;Ljava/lang/Integer;Lcom/google/android/datatransport/runtime/EncodedPayload;JJLjava/util/Map;Ljava/lang/Integer;Ljava/lang/String;[B[B)V -PLcom/google/android/datatransport/runtime/AutoValue_EventInternal;->(Ljava/lang/String;Ljava/lang/Integer;Lcom/google/android/datatransport/runtime/EncodedPayload;JJLjava/util/Map;Ljava/lang/Integer;Ljava/lang/String;[B[BLcom/google/android/datatransport/runtime/AutoValue_EventInternal$1;)V -PLcom/google/android/datatransport/runtime/AutoValue_EventInternal;->getAutoMetadata()Ljava/util/Map; -PLcom/google/android/datatransport/runtime/AutoValue_EventInternal;->getCode()Ljava/lang/Integer; -PLcom/google/android/datatransport/runtime/AutoValue_EventInternal;->getEncodedPayload()Lcom/google/android/datatransport/runtime/EncodedPayload; -PLcom/google/android/datatransport/runtime/AutoValue_EventInternal;->getEventMillis()J -PLcom/google/android/datatransport/runtime/AutoValue_EventInternal;->getExperimentIdsClear()[B -PLcom/google/android/datatransport/runtime/AutoValue_EventInternal;->getExperimentIdsEncrypted()[B -PLcom/google/android/datatransport/runtime/AutoValue_EventInternal;->getProductId()Ljava/lang/Integer; -PLcom/google/android/datatransport/runtime/AutoValue_EventInternal;->getPseudonymousId()Ljava/lang/String; -PLcom/google/android/datatransport/runtime/AutoValue_EventInternal;->getTransportName()Ljava/lang/String; -PLcom/google/android/datatransport/runtime/AutoValue_EventInternal;->getUptimeMillis()J -PLcom/google/android/datatransport/runtime/AutoValue_EventInternal$Builder;->()V -PLcom/google/android/datatransport/runtime/AutoValue_EventInternal$Builder;->build()Lcom/google/android/datatransport/runtime/EventInternal; -PLcom/google/android/datatransport/runtime/AutoValue_EventInternal$Builder;->getAutoMetadata()Ljava/util/Map; -PLcom/google/android/datatransport/runtime/AutoValue_EventInternal$Builder;->setAutoMetadata(Ljava/util/Map;)Lcom/google/android/datatransport/runtime/EventInternal$Builder; -PLcom/google/android/datatransport/runtime/AutoValue_EventInternal$Builder;->setCode(Ljava/lang/Integer;)Lcom/google/android/datatransport/runtime/EventInternal$Builder; -PLcom/google/android/datatransport/runtime/AutoValue_EventInternal$Builder;->setEncodedPayload(Lcom/google/android/datatransport/runtime/EncodedPayload;)Lcom/google/android/datatransport/runtime/EventInternal$Builder; -PLcom/google/android/datatransport/runtime/AutoValue_EventInternal$Builder;->setEventMillis(J)Lcom/google/android/datatransport/runtime/EventInternal$Builder; -PLcom/google/android/datatransport/runtime/AutoValue_EventInternal$Builder;->setExperimentIdsClear([B)Lcom/google/android/datatransport/runtime/EventInternal$Builder; -PLcom/google/android/datatransport/runtime/AutoValue_EventInternal$Builder;->setExperimentIdsEncrypted([B)Lcom/google/android/datatransport/runtime/EventInternal$Builder; -PLcom/google/android/datatransport/runtime/AutoValue_EventInternal$Builder;->setProductId(Ljava/lang/Integer;)Lcom/google/android/datatransport/runtime/EventInternal$Builder; -PLcom/google/android/datatransport/runtime/AutoValue_EventInternal$Builder;->setPseudonymousId(Ljava/lang/String;)Lcom/google/android/datatransport/runtime/EventInternal$Builder; -PLcom/google/android/datatransport/runtime/AutoValue_EventInternal$Builder;->setTransportName(Ljava/lang/String;)Lcom/google/android/datatransport/runtime/EventInternal$Builder; -PLcom/google/android/datatransport/runtime/AutoValue_EventInternal$Builder;->setUptimeMillis(J)Lcom/google/android/datatransport/runtime/EventInternal$Builder; -PLcom/google/android/datatransport/runtime/AutoValue_SendRequest;->(Lcom/google/android/datatransport/runtime/TransportContext;Ljava/lang/String;Lcom/google/android/datatransport/Event;Lcom/google/android/datatransport/Transformer;Lcom/google/android/datatransport/Encoding;)V -PLcom/google/android/datatransport/runtime/AutoValue_SendRequest;->(Lcom/google/android/datatransport/runtime/TransportContext;Ljava/lang/String;Lcom/google/android/datatransport/Event;Lcom/google/android/datatransport/Transformer;Lcom/google/android/datatransport/Encoding;Lcom/google/android/datatransport/runtime/AutoValue_SendRequest$1;)V -PLcom/google/android/datatransport/runtime/AutoValue_SendRequest;->getEncoding()Lcom/google/android/datatransport/Encoding; -PLcom/google/android/datatransport/runtime/AutoValue_SendRequest;->getEvent()Lcom/google/android/datatransport/Event; -PLcom/google/android/datatransport/runtime/AutoValue_SendRequest;->getTransformer()Lcom/google/android/datatransport/Transformer; -PLcom/google/android/datatransport/runtime/AutoValue_SendRequest;->getTransportContext()Lcom/google/android/datatransport/runtime/TransportContext; -PLcom/google/android/datatransport/runtime/AutoValue_SendRequest;->getTransportName()Ljava/lang/String; -PLcom/google/android/datatransport/runtime/AutoValue_SendRequest$Builder;->()V -PLcom/google/android/datatransport/runtime/AutoValue_SendRequest$Builder;->build()Lcom/google/android/datatransport/runtime/SendRequest; -PLcom/google/android/datatransport/runtime/AutoValue_SendRequest$Builder;->setEncoding(Lcom/google/android/datatransport/Encoding;)Lcom/google/android/datatransport/runtime/SendRequest$Builder; -PLcom/google/android/datatransport/runtime/AutoValue_SendRequest$Builder;->setEvent(Lcom/google/android/datatransport/Event;)Lcom/google/android/datatransport/runtime/SendRequest$Builder; -PLcom/google/android/datatransport/runtime/AutoValue_SendRequest$Builder;->setTransformer(Lcom/google/android/datatransport/Transformer;)Lcom/google/android/datatransport/runtime/SendRequest$Builder; -PLcom/google/android/datatransport/runtime/AutoValue_SendRequest$Builder;->setTransportContext(Lcom/google/android/datatransport/runtime/TransportContext;)Lcom/google/android/datatransport/runtime/SendRequest$Builder; -PLcom/google/android/datatransport/runtime/AutoValue_SendRequest$Builder;->setTransportName(Ljava/lang/String;)Lcom/google/android/datatransport/runtime/SendRequest$Builder; +Lcom/google/android/datatransport/cct/internal/AndroidClientInfo; +Lcom/google/android/datatransport/cct/internal/AutoBatchedLogRequestEncoder; +HSPLcom/google/android/datatransport/cct/internal/AutoBatchedLogRequestEncoder;->()V +HSPLcom/google/android/datatransport/cct/internal/AutoBatchedLogRequestEncoder;->()V +HSPLcom/google/android/datatransport/cct/internal/AutoBatchedLogRequestEncoder;->configure(Lcom/google/firebase/encoders/config/EncoderConfig;)V +Lcom/google/android/datatransport/cct/internal/AutoBatchedLogRequestEncoder$AndroidClientInfoEncoder; +HSPLcom/google/android/datatransport/cct/internal/AutoBatchedLogRequestEncoder$AndroidClientInfoEncoder;->()V +HSPLcom/google/android/datatransport/cct/internal/AutoBatchedLogRequestEncoder$AndroidClientInfoEncoder;->()V +Lcom/google/android/datatransport/cct/internal/AutoBatchedLogRequestEncoder$BatchedLogRequestEncoder; +HSPLcom/google/android/datatransport/cct/internal/AutoBatchedLogRequestEncoder$BatchedLogRequestEncoder;->()V +HSPLcom/google/android/datatransport/cct/internal/AutoBatchedLogRequestEncoder$BatchedLogRequestEncoder;->()V +Lcom/google/android/datatransport/cct/internal/AutoBatchedLogRequestEncoder$ClientInfoEncoder; +HSPLcom/google/android/datatransport/cct/internal/AutoBatchedLogRequestEncoder$ClientInfoEncoder;->()V +HSPLcom/google/android/datatransport/cct/internal/AutoBatchedLogRequestEncoder$ClientInfoEncoder;->()V +Lcom/google/android/datatransport/cct/internal/AutoBatchedLogRequestEncoder$ComplianceDataEncoder; +HSPLcom/google/android/datatransport/cct/internal/AutoBatchedLogRequestEncoder$ComplianceDataEncoder;->()V +HSPLcom/google/android/datatransport/cct/internal/AutoBatchedLogRequestEncoder$ComplianceDataEncoder;->()V +Lcom/google/android/datatransport/cct/internal/AutoBatchedLogRequestEncoder$ExperimentIdsEncoder; +HSPLcom/google/android/datatransport/cct/internal/AutoBatchedLogRequestEncoder$ExperimentIdsEncoder;->()V +HSPLcom/google/android/datatransport/cct/internal/AutoBatchedLogRequestEncoder$ExperimentIdsEncoder;->()V +Lcom/google/android/datatransport/cct/internal/AutoBatchedLogRequestEncoder$ExternalPRequestContextEncoder; +HSPLcom/google/android/datatransport/cct/internal/AutoBatchedLogRequestEncoder$ExternalPRequestContextEncoder;->()V +HSPLcom/google/android/datatransport/cct/internal/AutoBatchedLogRequestEncoder$ExternalPRequestContextEncoder;->()V +Lcom/google/android/datatransport/cct/internal/AutoBatchedLogRequestEncoder$ExternalPrivacyContextEncoder; +HSPLcom/google/android/datatransport/cct/internal/AutoBatchedLogRequestEncoder$ExternalPrivacyContextEncoder;->()V +HSPLcom/google/android/datatransport/cct/internal/AutoBatchedLogRequestEncoder$ExternalPrivacyContextEncoder;->()V +Lcom/google/android/datatransport/cct/internal/AutoBatchedLogRequestEncoder$LogEventEncoder; +HSPLcom/google/android/datatransport/cct/internal/AutoBatchedLogRequestEncoder$LogEventEncoder;->()V +HSPLcom/google/android/datatransport/cct/internal/AutoBatchedLogRequestEncoder$LogEventEncoder;->()V +Lcom/google/android/datatransport/cct/internal/AutoBatchedLogRequestEncoder$LogRequestEncoder; +HSPLcom/google/android/datatransport/cct/internal/AutoBatchedLogRequestEncoder$LogRequestEncoder;->()V +HSPLcom/google/android/datatransport/cct/internal/AutoBatchedLogRequestEncoder$LogRequestEncoder;->()V +Lcom/google/android/datatransport/cct/internal/AutoBatchedLogRequestEncoder$NetworkConnectionInfoEncoder; +HSPLcom/google/android/datatransport/cct/internal/AutoBatchedLogRequestEncoder$NetworkConnectionInfoEncoder;->()V +HSPLcom/google/android/datatransport/cct/internal/AutoBatchedLogRequestEncoder$NetworkConnectionInfoEncoder;->()V +Lcom/google/android/datatransport/cct/internal/AutoValue_AndroidClientInfo; +Lcom/google/android/datatransport/cct/internal/AutoValue_BatchedLogRequest; +Lcom/google/android/datatransport/cct/internal/AutoValue_ClientInfo; +Lcom/google/android/datatransport/cct/internal/AutoValue_ComplianceData; +Lcom/google/android/datatransport/cct/internal/AutoValue_ExperimentIds; +Lcom/google/android/datatransport/cct/internal/AutoValue_ExternalPRequestContext; +Lcom/google/android/datatransport/cct/internal/AutoValue_ExternalPrivacyContext; +Lcom/google/android/datatransport/cct/internal/AutoValue_LogEvent; +Lcom/google/android/datatransport/cct/internal/AutoValue_LogRequest; +Lcom/google/android/datatransport/cct/internal/AutoValue_NetworkConnectionInfo; +Lcom/google/android/datatransport/cct/internal/BatchedLogRequest; +HSPLcom/google/android/datatransport/cct/internal/BatchedLogRequest;->createDataEncoder()Lcom/google/firebase/encoders/DataEncoder; +Lcom/google/android/datatransport/cct/internal/ClientInfo; +Lcom/google/android/datatransport/cct/internal/ComplianceData; +Lcom/google/android/datatransport/cct/internal/ExperimentIds; +Lcom/google/android/datatransport/cct/internal/ExternalPRequestContext; +Lcom/google/android/datatransport/cct/internal/ExternalPrivacyContext; +Lcom/google/android/datatransport/cct/internal/LogEvent; +Lcom/google/android/datatransport/cct/internal/LogRequest; +Lcom/google/android/datatransport/cct/internal/NetworkConnectionInfo; +Lcom/google/android/datatransport/cct/internal/NetworkConnectionInfo$MobileSubtype; +HSPLcom/google/android/datatransport/cct/internal/NetworkConnectionInfo$MobileSubtype;->()V +HSPLcom/google/android/datatransport/cct/internal/NetworkConnectionInfo$MobileSubtype;->(Ljava/lang/String;II)V +HSPLcom/google/android/datatransport/cct/internal/NetworkConnectionInfo$MobileSubtype;->getValue()I +Lcom/google/android/datatransport/runtime/AutoValue_EventInternal; +HSPLcom/google/android/datatransport/runtime/AutoValue_EventInternal;->(Ljava/lang/String;Ljava/lang/Integer;Lcom/google/android/datatransport/runtime/EncodedPayload;JJLjava/util/Map;Ljava/lang/Integer;Ljava/lang/String;[B[B)V +HSPLcom/google/android/datatransport/runtime/AutoValue_EventInternal;->(Ljava/lang/String;Ljava/lang/Integer;Lcom/google/android/datatransport/runtime/EncodedPayload;JJLjava/util/Map;Ljava/lang/Integer;Ljava/lang/String;[B[BLcom/google/android/datatransport/runtime/AutoValue_EventInternal$1;)V +HSPLcom/google/android/datatransport/runtime/AutoValue_EventInternal;->getAutoMetadata()Ljava/util/Map; +HSPLcom/google/android/datatransport/runtime/AutoValue_EventInternal;->getCode()Ljava/lang/Integer; +HSPLcom/google/android/datatransport/runtime/AutoValue_EventInternal;->getEncodedPayload()Lcom/google/android/datatransport/runtime/EncodedPayload; +HSPLcom/google/android/datatransport/runtime/AutoValue_EventInternal;->getEventMillis()J +HSPLcom/google/android/datatransport/runtime/AutoValue_EventInternal;->getExperimentIdsClear()[B +HSPLcom/google/android/datatransport/runtime/AutoValue_EventInternal;->getExperimentIdsEncrypted()[B +HSPLcom/google/android/datatransport/runtime/AutoValue_EventInternal;->getProductId()Ljava/lang/Integer; +HSPLcom/google/android/datatransport/runtime/AutoValue_EventInternal;->getPseudonymousId()Ljava/lang/String; +HSPLcom/google/android/datatransport/runtime/AutoValue_EventInternal;->getTransportName()Ljava/lang/String; +HSPLcom/google/android/datatransport/runtime/AutoValue_EventInternal;->getUptimeMillis()J +Lcom/google/android/datatransport/runtime/AutoValue_EventInternal$Builder; +HSPLcom/google/android/datatransport/runtime/AutoValue_EventInternal$Builder;->()V +HSPLcom/google/android/datatransport/runtime/AutoValue_EventInternal$Builder;->build()Lcom/google/android/datatransport/runtime/EventInternal; +HSPLcom/google/android/datatransport/runtime/AutoValue_EventInternal$Builder;->getAutoMetadata()Ljava/util/Map; +HSPLcom/google/android/datatransport/runtime/AutoValue_EventInternal$Builder;->setAutoMetadata(Ljava/util/Map;)Lcom/google/android/datatransport/runtime/EventInternal$Builder; +HSPLcom/google/android/datatransport/runtime/AutoValue_EventInternal$Builder;->setCode(Ljava/lang/Integer;)Lcom/google/android/datatransport/runtime/EventInternal$Builder; +HSPLcom/google/android/datatransport/runtime/AutoValue_EventInternal$Builder;->setEncodedPayload(Lcom/google/android/datatransport/runtime/EncodedPayload;)Lcom/google/android/datatransport/runtime/EventInternal$Builder; +HSPLcom/google/android/datatransport/runtime/AutoValue_EventInternal$Builder;->setEventMillis(J)Lcom/google/android/datatransport/runtime/EventInternal$Builder; +HSPLcom/google/android/datatransport/runtime/AutoValue_EventInternal$Builder;->setExperimentIdsClear([B)Lcom/google/android/datatransport/runtime/EventInternal$Builder; +HSPLcom/google/android/datatransport/runtime/AutoValue_EventInternal$Builder;->setExperimentIdsEncrypted([B)Lcom/google/android/datatransport/runtime/EventInternal$Builder; +HSPLcom/google/android/datatransport/runtime/AutoValue_EventInternal$Builder;->setProductId(Ljava/lang/Integer;)Lcom/google/android/datatransport/runtime/EventInternal$Builder; +HSPLcom/google/android/datatransport/runtime/AutoValue_EventInternal$Builder;->setPseudonymousId(Ljava/lang/String;)Lcom/google/android/datatransport/runtime/EventInternal$Builder; +HSPLcom/google/android/datatransport/runtime/AutoValue_EventInternal$Builder;->setTransportName(Ljava/lang/String;)Lcom/google/android/datatransport/runtime/EventInternal$Builder; +HSPLcom/google/android/datatransport/runtime/AutoValue_EventInternal$Builder;->setUptimeMillis(J)Lcom/google/android/datatransport/runtime/EventInternal$Builder; +Lcom/google/android/datatransport/runtime/AutoValue_SendRequest; +HSPLcom/google/android/datatransport/runtime/AutoValue_SendRequest;->(Lcom/google/android/datatransport/runtime/TransportContext;Ljava/lang/String;Lcom/google/android/datatransport/Event;Lcom/google/android/datatransport/Transformer;Lcom/google/android/datatransport/Encoding;)V +HSPLcom/google/android/datatransport/runtime/AutoValue_SendRequest;->(Lcom/google/android/datatransport/runtime/TransportContext;Ljava/lang/String;Lcom/google/android/datatransport/Event;Lcom/google/android/datatransport/Transformer;Lcom/google/android/datatransport/Encoding;Lcom/google/android/datatransport/runtime/AutoValue_SendRequest$1;)V +HSPLcom/google/android/datatransport/runtime/AutoValue_SendRequest;->getEncoding()Lcom/google/android/datatransport/Encoding; +HSPLcom/google/android/datatransport/runtime/AutoValue_SendRequest;->getEvent()Lcom/google/android/datatransport/Event; +HSPLcom/google/android/datatransport/runtime/AutoValue_SendRequest;->getTransformer()Lcom/google/android/datatransport/Transformer; +HSPLcom/google/android/datatransport/runtime/AutoValue_SendRequest;->getTransportContext()Lcom/google/android/datatransport/runtime/TransportContext; +HSPLcom/google/android/datatransport/runtime/AutoValue_SendRequest;->getTransportName()Ljava/lang/String; +Lcom/google/android/datatransport/runtime/AutoValue_SendRequest$Builder; +HSPLcom/google/android/datatransport/runtime/AutoValue_SendRequest$Builder;->()V +HSPLcom/google/android/datatransport/runtime/AutoValue_SendRequest$Builder;->build()Lcom/google/android/datatransport/runtime/SendRequest; +HSPLcom/google/android/datatransport/runtime/AutoValue_SendRequest$Builder;->setEncoding(Lcom/google/android/datatransport/Encoding;)Lcom/google/android/datatransport/runtime/SendRequest$Builder; +HSPLcom/google/android/datatransport/runtime/AutoValue_SendRequest$Builder;->setEvent(Lcom/google/android/datatransport/Event;)Lcom/google/android/datatransport/runtime/SendRequest$Builder; +HSPLcom/google/android/datatransport/runtime/AutoValue_SendRequest$Builder;->setTransformer(Lcom/google/android/datatransport/Transformer;)Lcom/google/android/datatransport/runtime/SendRequest$Builder; +HSPLcom/google/android/datatransport/runtime/AutoValue_SendRequest$Builder;->setTransportContext(Lcom/google/android/datatransport/runtime/TransportContext;)Lcom/google/android/datatransport/runtime/SendRequest$Builder; +HSPLcom/google/android/datatransport/runtime/AutoValue_SendRequest$Builder;->setTransportName(Ljava/lang/String;)Lcom/google/android/datatransport/runtime/SendRequest$Builder; Lcom/google/android/datatransport/runtime/AutoValue_TransportContext; HSPLcom/google/android/datatransport/runtime/AutoValue_TransportContext;->(Ljava/lang/String;[BLcom/google/android/datatransport/Priority;)V HSPLcom/google/android/datatransport/runtime/AutoValue_TransportContext;->(Ljava/lang/String;[BLcom/google/android/datatransport/Priority;Lcom/google/android/datatransport/runtime/AutoValue_TransportContext$1;)V @@ -22900,17 +23091,20 @@ HSPLcom/google/android/datatransport/runtime/DaggerTransportRuntimeComponent$Tra HSPLcom/google/android/datatransport/runtime/DaggerTransportRuntimeComponent$TransportRuntimeComponentImpl;->initialize(Landroid/content/Context;)V Lcom/google/android/datatransport/runtime/Destination; Lcom/google/android/datatransport/runtime/EncodedDestination; -PLcom/google/android/datatransport/runtime/EncodedPayload;->(Lcom/google/android/datatransport/Encoding;[B)V -PLcom/google/android/datatransport/runtime/EncodedPayload;->getBytes()[B -PLcom/google/android/datatransport/runtime/EncodedPayload;->getEncoding()Lcom/google/android/datatransport/Encoding; -PLcom/google/android/datatransport/runtime/EventInternal;->()V -PLcom/google/android/datatransport/runtime/EventInternal;->builder()Lcom/google/android/datatransport/runtime/EventInternal$Builder; -PLcom/google/android/datatransport/runtime/EventInternal;->getMetadata()Ljava/util/Map; -PLcom/google/android/datatransport/runtime/EventInternal;->toBuilder()Lcom/google/android/datatransport/runtime/EventInternal$Builder; -PLcom/google/android/datatransport/runtime/EventInternal$Builder;->()V -PLcom/google/android/datatransport/runtime/EventInternal$Builder;->addMetadata(Ljava/lang/String;I)Lcom/google/android/datatransport/runtime/EventInternal$Builder; -PLcom/google/android/datatransport/runtime/EventInternal$Builder;->addMetadata(Ljava/lang/String;J)Lcom/google/android/datatransport/runtime/EventInternal$Builder; -PLcom/google/android/datatransport/runtime/EventInternal$Builder;->addMetadata(Ljava/lang/String;Ljava/lang/String;)Lcom/google/android/datatransport/runtime/EventInternal$Builder; +Lcom/google/android/datatransport/runtime/EncodedPayload; +HSPLcom/google/android/datatransport/runtime/EncodedPayload;->(Lcom/google/android/datatransport/Encoding;[B)V +HSPLcom/google/android/datatransport/runtime/EncodedPayload;->getBytes()[B +HSPLcom/google/android/datatransport/runtime/EncodedPayload;->getEncoding()Lcom/google/android/datatransport/Encoding; +Lcom/google/android/datatransport/runtime/EventInternal; +HSPLcom/google/android/datatransport/runtime/EventInternal;->()V +HSPLcom/google/android/datatransport/runtime/EventInternal;->builder()Lcom/google/android/datatransport/runtime/EventInternal$Builder; +HSPLcom/google/android/datatransport/runtime/EventInternal;->getMetadata()Ljava/util/Map; +HSPLcom/google/android/datatransport/runtime/EventInternal;->toBuilder()Lcom/google/android/datatransport/runtime/EventInternal$Builder; +Lcom/google/android/datatransport/runtime/EventInternal$Builder; +HSPLcom/google/android/datatransport/runtime/EventInternal$Builder;->()V +HSPLcom/google/android/datatransport/runtime/EventInternal$Builder;->addMetadata(Ljava/lang/String;I)Lcom/google/android/datatransport/runtime/EventInternal$Builder; +HSPLcom/google/android/datatransport/runtime/EventInternal$Builder;->addMetadata(Ljava/lang/String;J)Lcom/google/android/datatransport/runtime/EventInternal$Builder; +HSPLcom/google/android/datatransport/runtime/EventInternal$Builder;->addMetadata(Ljava/lang/String;Ljava/lang/String;)Lcom/google/android/datatransport/runtime/EventInternal$Builder; Lcom/google/android/datatransport/runtime/ExecutionModule; HSPLcom/google/android/datatransport/runtime/ExecutionModule;->executor()Ljava/util/concurrent/Executor; Lcom/google/android/datatransport/runtime/ExecutionModule_ExecutorFactory; @@ -22928,14 +23122,16 @@ HSPLcom/google/android/datatransport/runtime/SafeLoggingExecutor;->execute(Ljava Lcom/google/android/datatransport/runtime/SafeLoggingExecutor$SafeLoggingRunnable; HSPLcom/google/android/datatransport/runtime/SafeLoggingExecutor$SafeLoggingRunnable;->(Ljava/lang/Runnable;)V HSPLcom/google/android/datatransport/runtime/SafeLoggingExecutor$SafeLoggingRunnable;->run()V -PLcom/google/android/datatransport/runtime/SendRequest;->()V -PLcom/google/android/datatransport/runtime/SendRequest;->builder()Lcom/google/android/datatransport/runtime/SendRequest$Builder; -PLcom/google/android/datatransport/runtime/SendRequest;->getPayload()[B -PLcom/google/android/datatransport/runtime/SendRequest$Builder;->()V +Lcom/google/android/datatransport/runtime/SendRequest; +HSPLcom/google/android/datatransport/runtime/SendRequest;->()V +HSPLcom/google/android/datatransport/runtime/SendRequest;->builder()Lcom/google/android/datatransport/runtime/SendRequest$Builder; +HSPLcom/google/android/datatransport/runtime/SendRequest;->getPayload()[B +Lcom/google/android/datatransport/runtime/SendRequest$Builder; +HSPLcom/google/android/datatransport/runtime/SendRequest$Builder;->()V Lcom/google/android/datatransport/runtime/TransportContext; HSPLcom/google/android/datatransport/runtime/TransportContext;->()V HSPLcom/google/android/datatransport/runtime/TransportContext;->builder()Lcom/google/android/datatransport/runtime/TransportContext$Builder; -PLcom/google/android/datatransport/runtime/TransportContext;->withPriority(Lcom/google/android/datatransport/Priority;)Lcom/google/android/datatransport/runtime/TransportContext; +HSPLcom/google/android/datatransport/runtime/TransportContext;->withPriority(Lcom/google/android/datatransport/Priority;)Lcom/google/android/datatransport/runtime/TransportContext; Lcom/google/android/datatransport/runtime/TransportContext$Builder; HSPLcom/google/android/datatransport/runtime/TransportContext$Builder;->()V Lcom/google/android/datatransport/runtime/TransportFactoryImpl; @@ -22943,21 +23139,22 @@ HSPLcom/google/android/datatransport/runtime/TransportFactoryImpl;->(Ljava HSPLcom/google/android/datatransport/runtime/TransportFactoryImpl;->getTransport(Ljava/lang/String;Ljava/lang/Class;Lcom/google/android/datatransport/Encoding;Lcom/google/android/datatransport/Transformer;)Lcom/google/android/datatransport/Transport; Lcom/google/android/datatransport/runtime/TransportImpl; HSPLcom/google/android/datatransport/runtime/TransportImpl;->(Lcom/google/android/datatransport/runtime/TransportContext;Ljava/lang/String;Lcom/google/android/datatransport/Encoding;Lcom/google/android/datatransport/Transformer;Lcom/google/android/datatransport/runtime/TransportInternal;)V -PLcom/google/android/datatransport/runtime/TransportImpl;->lambda$send$0(Ljava/lang/Exception;)V -PLcom/google/android/datatransport/runtime/TransportImpl;->schedule(Lcom/google/android/datatransport/Event;Lcom/google/android/datatransport/TransportScheduleCallback;)V -PLcom/google/android/datatransport/runtime/TransportImpl;->send(Lcom/google/android/datatransport/Event;)V -PLcom/google/android/datatransport/runtime/TransportImpl$$ExternalSyntheticLambda0;->()V -PLcom/google/android/datatransport/runtime/TransportImpl$$ExternalSyntheticLambda0;->onSchedule(Ljava/lang/Exception;)V +HSPLcom/google/android/datatransport/runtime/TransportImpl;->lambda$send$0(Ljava/lang/Exception;)V +HSPLcom/google/android/datatransport/runtime/TransportImpl;->schedule(Lcom/google/android/datatransport/Event;Lcom/google/android/datatransport/TransportScheduleCallback;)V +HSPLcom/google/android/datatransport/runtime/TransportImpl;->send(Lcom/google/android/datatransport/Event;)V +Lcom/google/android/datatransport/runtime/TransportImpl$$ExternalSyntheticLambda0; +HSPLcom/google/android/datatransport/runtime/TransportImpl$$ExternalSyntheticLambda0;->()V +HSPLcom/google/android/datatransport/runtime/TransportImpl$$ExternalSyntheticLambda0;->onSchedule(Ljava/lang/Exception;)V Lcom/google/android/datatransport/runtime/TransportInternal; Lcom/google/android/datatransport/runtime/TransportRuntime; HSPLcom/google/android/datatransport/runtime/TransportRuntime;->()V HSPLcom/google/android/datatransport/runtime/TransportRuntime;->(Lcom/google/android/datatransport/runtime/time/Clock;Lcom/google/android/datatransport/runtime/time/Clock;Lcom/google/android/datatransport/runtime/scheduling/Scheduler;Lcom/google/android/datatransport/runtime/scheduling/jobscheduling/Uploader;Lcom/google/android/datatransport/runtime/scheduling/jobscheduling/WorkInitializer;)V -PLcom/google/android/datatransport/runtime/TransportRuntime;->convert(Lcom/google/android/datatransport/runtime/SendRequest;)Lcom/google/android/datatransport/runtime/EventInternal; +HSPLcom/google/android/datatransport/runtime/TransportRuntime;->convert(Lcom/google/android/datatransport/runtime/SendRequest;)Lcom/google/android/datatransport/runtime/EventInternal; HSPLcom/google/android/datatransport/runtime/TransportRuntime;->getInstance()Lcom/google/android/datatransport/runtime/TransportRuntime; HSPLcom/google/android/datatransport/runtime/TransportRuntime;->getSupportedEncodings(Lcom/google/android/datatransport/runtime/Destination;)Ljava/util/Set; HSPLcom/google/android/datatransport/runtime/TransportRuntime;->initialize(Landroid/content/Context;)V HSPLcom/google/android/datatransport/runtime/TransportRuntime;->newFactory(Lcom/google/android/datatransport/runtime/Destination;)Lcom/google/android/datatransport/TransportFactory; -PLcom/google/android/datatransport/runtime/TransportRuntime;->send(Lcom/google/android/datatransport/runtime/SendRequest;Lcom/google/android/datatransport/TransportScheduleCallback;)V +HSPLcom/google/android/datatransport/runtime/TransportRuntime;->send(Lcom/google/android/datatransport/runtime/SendRequest;Lcom/google/android/datatransport/TransportScheduleCallback;)V Lcom/google/android/datatransport/runtime/TransportRuntimeComponent; HSPLcom/google/android/datatransport/runtime/TransportRuntimeComponent;->()V Lcom/google/android/datatransport/runtime/TransportRuntimeComponent$Builder; @@ -22967,16 +23164,19 @@ HSPLcom/google/android/datatransport/runtime/TransportRuntime_Factory;->create(L HSPLcom/google/android/datatransport/runtime/TransportRuntime_Factory;->get()Lcom/google/android/datatransport/runtime/TransportRuntime; HSPLcom/google/android/datatransport/runtime/TransportRuntime_Factory;->get()Ljava/lang/Object; HSPLcom/google/android/datatransport/runtime/TransportRuntime_Factory;->newInstance(Lcom/google/android/datatransport/runtime/time/Clock;Lcom/google/android/datatransport/runtime/time/Clock;Lcom/google/android/datatransport/runtime/scheduling/Scheduler;Lcom/google/android/datatransport/runtime/scheduling/jobscheduling/Uploader;Lcom/google/android/datatransport/runtime/scheduling/jobscheduling/WorkInitializer;)Lcom/google/android/datatransport/runtime/TransportRuntime; -PLcom/google/android/datatransport/runtime/backends/AutoValue_CreationContext;->(Landroid/content/Context;Lcom/google/android/datatransport/runtime/time/Clock;Lcom/google/android/datatransport/runtime/time/Clock;Ljava/lang/String;)V -PLcom/google/android/datatransport/runtime/backends/AutoValue_CreationContext;->getApplicationContext()Landroid/content/Context; -PLcom/google/android/datatransport/runtime/backends/AutoValue_CreationContext;->getMonotonicClock()Lcom/google/android/datatransport/runtime/time/Clock; -PLcom/google/android/datatransport/runtime/backends/AutoValue_CreationContext;->getWallClock()Lcom/google/android/datatransport/runtime/time/Clock; +Lcom/google/android/datatransport/runtime/backends/AutoValue_CreationContext; +HSPLcom/google/android/datatransport/runtime/backends/AutoValue_CreationContext;->(Landroid/content/Context;Lcom/google/android/datatransport/runtime/time/Clock;Lcom/google/android/datatransport/runtime/time/Clock;Ljava/lang/String;)V +HSPLcom/google/android/datatransport/runtime/backends/AutoValue_CreationContext;->getApplicationContext()Landroid/content/Context; +HSPLcom/google/android/datatransport/runtime/backends/AutoValue_CreationContext;->getMonotonicClock()Lcom/google/android/datatransport/runtime/time/Clock; +HSPLcom/google/android/datatransport/runtime/backends/AutoValue_CreationContext;->getWallClock()Lcom/google/android/datatransport/runtime/time/Clock; +Lcom/google/android/datatransport/runtime/backends/BackendFactory; Lcom/google/android/datatransport/runtime/backends/BackendRegistry; -PLcom/google/android/datatransport/runtime/backends/CreationContext;->()V -PLcom/google/android/datatransport/runtime/backends/CreationContext;->create(Landroid/content/Context;Lcom/google/android/datatransport/runtime/time/Clock;Lcom/google/android/datatransport/runtime/time/Clock;Ljava/lang/String;)Lcom/google/android/datatransport/runtime/backends/CreationContext; +Lcom/google/android/datatransport/runtime/backends/CreationContext; +HSPLcom/google/android/datatransport/runtime/backends/CreationContext;->()V +HSPLcom/google/android/datatransport/runtime/backends/CreationContext;->create(Landroid/content/Context;Lcom/google/android/datatransport/runtime/time/Clock;Lcom/google/android/datatransport/runtime/time/Clock;Ljava/lang/String;)Lcom/google/android/datatransport/runtime/backends/CreationContext; Lcom/google/android/datatransport/runtime/backends/CreationContextFactory; HSPLcom/google/android/datatransport/runtime/backends/CreationContextFactory;->(Landroid/content/Context;Lcom/google/android/datatransport/runtime/time/Clock;Lcom/google/android/datatransport/runtime/time/Clock;)V -PLcom/google/android/datatransport/runtime/backends/CreationContextFactory;->create(Ljava/lang/String;)Lcom/google/android/datatransport/runtime/backends/CreationContext; +HSPLcom/google/android/datatransport/runtime/backends/CreationContextFactory;->create(Ljava/lang/String;)Lcom/google/android/datatransport/runtime/backends/CreationContext; Lcom/google/android/datatransport/runtime/backends/CreationContextFactory_Factory; HSPLcom/google/android/datatransport/runtime/backends/CreationContextFactory_Factory;->(Ljavax/inject/Provider;Ljavax/inject/Provider;Ljavax/inject/Provider;)V HSPLcom/google/android/datatransport/runtime/backends/CreationContextFactory_Factory;->create(Ljavax/inject/Provider;Ljavax/inject/Provider;Ljavax/inject/Provider;)Lcom/google/android/datatransport/runtime/backends/CreationContextFactory_Factory; @@ -22986,19 +23186,21 @@ HSPLcom/google/android/datatransport/runtime/backends/CreationContextFactory_Fac Lcom/google/android/datatransport/runtime/backends/MetadataBackendRegistry; HSPLcom/google/android/datatransport/runtime/backends/MetadataBackendRegistry;->(Landroid/content/Context;Lcom/google/android/datatransport/runtime/backends/CreationContextFactory;)V HSPLcom/google/android/datatransport/runtime/backends/MetadataBackendRegistry;->(Lcom/google/android/datatransport/runtime/backends/MetadataBackendRegistry$BackendFactoryProvider;Lcom/google/android/datatransport/runtime/backends/CreationContextFactory;)V -PLcom/google/android/datatransport/runtime/backends/MetadataBackendRegistry;->get(Ljava/lang/String;)Lcom/google/android/datatransport/runtime/backends/TransportBackend; +HSPLcom/google/android/datatransport/runtime/backends/MetadataBackendRegistry;->get(Ljava/lang/String;)Lcom/google/android/datatransport/runtime/backends/TransportBackend; Lcom/google/android/datatransport/runtime/backends/MetadataBackendRegistry$BackendFactoryProvider; HSPLcom/google/android/datatransport/runtime/backends/MetadataBackendRegistry$BackendFactoryProvider;->(Landroid/content/Context;)V -PLcom/google/android/datatransport/runtime/backends/MetadataBackendRegistry$BackendFactoryProvider;->discover(Landroid/content/Context;)Ljava/util/Map; -PLcom/google/android/datatransport/runtime/backends/MetadataBackendRegistry$BackendFactoryProvider;->get(Ljava/lang/String;)Lcom/google/android/datatransport/runtime/backends/BackendFactory; -PLcom/google/android/datatransport/runtime/backends/MetadataBackendRegistry$BackendFactoryProvider;->getBackendProviders()Ljava/util/Map; -PLcom/google/android/datatransport/runtime/backends/MetadataBackendRegistry$BackendFactoryProvider;->getMetadata(Landroid/content/Context;)Landroid/os/Bundle; +HSPLcom/google/android/datatransport/runtime/backends/MetadataBackendRegistry$BackendFactoryProvider;->discover(Landroid/content/Context;)Ljava/util/Map; +HSPLcom/google/android/datatransport/runtime/backends/MetadataBackendRegistry$BackendFactoryProvider;->get(Ljava/lang/String;)Lcom/google/android/datatransport/runtime/backends/BackendFactory; +HSPLcom/google/android/datatransport/runtime/backends/MetadataBackendRegistry$BackendFactoryProvider;->getBackendProviders()Ljava/util/Map; +HSPLcom/google/android/datatransport/runtime/backends/MetadataBackendRegistry$BackendFactoryProvider;->getMetadata(Landroid/content/Context;)Landroid/os/Bundle; Lcom/google/android/datatransport/runtime/backends/MetadataBackendRegistry_Factory; HSPLcom/google/android/datatransport/runtime/backends/MetadataBackendRegistry_Factory;->(Ljavax/inject/Provider;Ljavax/inject/Provider;)V HSPLcom/google/android/datatransport/runtime/backends/MetadataBackendRegistry_Factory;->create(Ljavax/inject/Provider;Ljavax/inject/Provider;)Lcom/google/android/datatransport/runtime/backends/MetadataBackendRegistry_Factory; HSPLcom/google/android/datatransport/runtime/backends/MetadataBackendRegistry_Factory;->get()Lcom/google/android/datatransport/runtime/backends/MetadataBackendRegistry; HSPLcom/google/android/datatransport/runtime/backends/MetadataBackendRegistry_Factory;->get()Ljava/lang/Object; HSPLcom/google/android/datatransport/runtime/backends/MetadataBackendRegistry_Factory;->newInstance(Landroid/content/Context;Ljava/lang/Object;)Lcom/google/android/datatransport/runtime/backends/MetadataBackendRegistry; +Lcom/google/android/datatransport/runtime/backends/TransportBackend; +Lcom/google/android/datatransport/runtime/backends/TransportBackendDiscovery; Lcom/google/android/datatransport/runtime/dagger/Lazy; Lcom/google/android/datatransport/runtime/dagger/internal/DoubleCheck; HSPLcom/google/android/datatransport/runtime/dagger/internal/DoubleCheck;->()V @@ -23018,19 +23220,21 @@ HSPLcom/google/android/datatransport/runtime/dagger/internal/Preconditions;->che HSPLcom/google/android/datatransport/runtime/dagger/internal/Preconditions;->checkNotNull(Ljava/lang/Object;Ljava/lang/String;)Ljava/lang/Object; HSPLcom/google/android/datatransport/runtime/dagger/internal/Preconditions;->checkNotNullFromProvides(Ljava/lang/Object;)Ljava/lang/Object; Lcom/google/android/datatransport/runtime/logging/Logging; -PLcom/google/android/datatransport/runtime/logging/Logging;->d(Ljava/lang/String;Ljava/lang/String;Ljava/lang/Object;)V +HSPLcom/google/android/datatransport/runtime/logging/Logging;->d(Ljava/lang/String;Ljava/lang/String;Ljava/lang/Object;)V HSPLcom/google/android/datatransport/runtime/logging/Logging;->d(Ljava/lang/String;Ljava/lang/String;[Ljava/lang/Object;)V HSPLcom/google/android/datatransport/runtime/logging/Logging;->getTag(Ljava/lang/String;)Ljava/lang/String; Lcom/google/android/datatransport/runtime/scheduling/DefaultScheduler; HSPLcom/google/android/datatransport/runtime/scheduling/DefaultScheduler;->()V HSPLcom/google/android/datatransport/runtime/scheduling/DefaultScheduler;->(Ljava/util/concurrent/Executor;Lcom/google/android/datatransport/runtime/backends/BackendRegistry;Lcom/google/android/datatransport/runtime/scheduling/jobscheduling/WorkScheduler;Lcom/google/android/datatransport/runtime/scheduling/persistence/EventStore;Lcom/google/android/datatransport/runtime/synchronization/SynchronizationGuard;)V -PLcom/google/android/datatransport/runtime/scheduling/DefaultScheduler;->lambda$schedule$0$com-google-android-datatransport-runtime-scheduling-DefaultScheduler(Lcom/google/android/datatransport/runtime/TransportContext;Lcom/google/android/datatransport/runtime/EventInternal;)Ljava/lang/Object; -PLcom/google/android/datatransport/runtime/scheduling/DefaultScheduler;->lambda$schedule$1$com-google-android-datatransport-runtime-scheduling-DefaultScheduler(Lcom/google/android/datatransport/runtime/TransportContext;Lcom/google/android/datatransport/TransportScheduleCallback;Lcom/google/android/datatransport/runtime/EventInternal;)V -PLcom/google/android/datatransport/runtime/scheduling/DefaultScheduler;->schedule(Lcom/google/android/datatransport/runtime/TransportContext;Lcom/google/android/datatransport/runtime/EventInternal;Lcom/google/android/datatransport/TransportScheduleCallback;)V -PLcom/google/android/datatransport/runtime/scheduling/DefaultScheduler$$ExternalSyntheticLambda0;->(Lcom/google/android/datatransport/runtime/scheduling/DefaultScheduler;Lcom/google/android/datatransport/runtime/TransportContext;Lcom/google/android/datatransport/TransportScheduleCallback;Lcom/google/android/datatransport/runtime/EventInternal;)V -PLcom/google/android/datatransport/runtime/scheduling/DefaultScheduler$$ExternalSyntheticLambda0;->run()V -PLcom/google/android/datatransport/runtime/scheduling/DefaultScheduler$$ExternalSyntheticLambda1;->(Lcom/google/android/datatransport/runtime/scheduling/DefaultScheduler;Lcom/google/android/datatransport/runtime/TransportContext;Lcom/google/android/datatransport/runtime/EventInternal;)V -PLcom/google/android/datatransport/runtime/scheduling/DefaultScheduler$$ExternalSyntheticLambda1;->execute()Ljava/lang/Object; +HSPLcom/google/android/datatransport/runtime/scheduling/DefaultScheduler;->lambda$schedule$0$com-google-android-datatransport-runtime-scheduling-DefaultScheduler(Lcom/google/android/datatransport/runtime/TransportContext;Lcom/google/android/datatransport/runtime/EventInternal;)Ljava/lang/Object; +HSPLcom/google/android/datatransport/runtime/scheduling/DefaultScheduler;->lambda$schedule$1$com-google-android-datatransport-runtime-scheduling-DefaultScheduler(Lcom/google/android/datatransport/runtime/TransportContext;Lcom/google/android/datatransport/TransportScheduleCallback;Lcom/google/android/datatransport/runtime/EventInternal;)V +HSPLcom/google/android/datatransport/runtime/scheduling/DefaultScheduler;->schedule(Lcom/google/android/datatransport/runtime/TransportContext;Lcom/google/android/datatransport/runtime/EventInternal;Lcom/google/android/datatransport/TransportScheduleCallback;)V +Lcom/google/android/datatransport/runtime/scheduling/DefaultScheduler$$ExternalSyntheticLambda0; +HSPLcom/google/android/datatransport/runtime/scheduling/DefaultScheduler$$ExternalSyntheticLambda0;->(Lcom/google/android/datatransport/runtime/scheduling/DefaultScheduler;Lcom/google/android/datatransport/runtime/TransportContext;Lcom/google/android/datatransport/TransportScheduleCallback;Lcom/google/android/datatransport/runtime/EventInternal;)V +HSPLcom/google/android/datatransport/runtime/scheduling/DefaultScheduler$$ExternalSyntheticLambda0;->run()V +Lcom/google/android/datatransport/runtime/scheduling/DefaultScheduler$$ExternalSyntheticLambda1; +HSPLcom/google/android/datatransport/runtime/scheduling/DefaultScheduler$$ExternalSyntheticLambda1;->(Lcom/google/android/datatransport/runtime/scheduling/DefaultScheduler;Lcom/google/android/datatransport/runtime/TransportContext;Lcom/google/android/datatransport/runtime/EventInternal;)V +HSPLcom/google/android/datatransport/runtime/scheduling/DefaultScheduler$$ExternalSyntheticLambda1;->execute()Ljava/lang/Object; Lcom/google/android/datatransport/runtime/scheduling/DefaultScheduler_Factory; HSPLcom/google/android/datatransport/runtime/scheduling/DefaultScheduler_Factory;->(Ljavax/inject/Provider;Ljavax/inject/Provider;Ljavax/inject/Provider;Ljavax/inject/Provider;Ljavax/inject/Provider;)V HSPLcom/google/android/datatransport/runtime/scheduling/DefaultScheduler_Factory;->create(Ljavax/inject/Provider;Ljavax/inject/Provider;Ljavax/inject/Provider;Ljavax/inject/Provider;Ljavax/inject/Provider;)Lcom/google/android/datatransport/runtime/scheduling/DefaultScheduler_Factory; @@ -23129,8 +23333,8 @@ Lcom/google/android/datatransport/runtime/scheduling/jobscheduling/WorkScheduler Lcom/google/android/datatransport/runtime/scheduling/persistence/AutoValue_EventStoreConfig; HSPLcom/google/android/datatransport/runtime/scheduling/persistence/AutoValue_EventStoreConfig;->(JIIJI)V HSPLcom/google/android/datatransport/runtime/scheduling/persistence/AutoValue_EventStoreConfig;->(JIIJILcom/google/android/datatransport/runtime/scheduling/persistence/AutoValue_EventStoreConfig$1;)V -PLcom/google/android/datatransport/runtime/scheduling/persistence/AutoValue_EventStoreConfig;->getMaxBlobByteSizePerRow()I -PLcom/google/android/datatransport/runtime/scheduling/persistence/AutoValue_EventStoreConfig;->getMaxStorageSizeInBytes()J +HSPLcom/google/android/datatransport/runtime/scheduling/persistence/AutoValue_EventStoreConfig;->getMaxBlobByteSizePerRow()I +HSPLcom/google/android/datatransport/runtime/scheduling/persistence/AutoValue_EventStoreConfig;->getMaxStorageSizeInBytes()J Lcom/google/android/datatransport/runtime/scheduling/persistence/AutoValue_EventStoreConfig$Builder; HSPLcom/google/android/datatransport/runtime/scheduling/persistence/AutoValue_EventStoreConfig$Builder;->()V HSPLcom/google/android/datatransport/runtime/scheduling/persistence/AutoValue_EventStoreConfig$Builder;->build()Lcom/google/android/datatransport/runtime/scheduling/persistence/EventStoreConfig; @@ -23139,7 +23343,8 @@ HSPLcom/google/android/datatransport/runtime/scheduling/persistence/AutoValue_Ev HSPLcom/google/android/datatransport/runtime/scheduling/persistence/AutoValue_EventStoreConfig$Builder;->setLoadBatchSize(I)Lcom/google/android/datatransport/runtime/scheduling/persistence/EventStoreConfig$Builder; HSPLcom/google/android/datatransport/runtime/scheduling/persistence/AutoValue_EventStoreConfig$Builder;->setMaxBlobByteSizePerRow(I)Lcom/google/android/datatransport/runtime/scheduling/persistence/EventStoreConfig$Builder; HSPLcom/google/android/datatransport/runtime/scheduling/persistence/AutoValue_EventStoreConfig$Builder;->setMaxStorageSizeInBytes(J)Lcom/google/android/datatransport/runtime/scheduling/persistence/EventStoreConfig$Builder; -PLcom/google/android/datatransport/runtime/scheduling/persistence/AutoValue_PersistedEvent;->(JLcom/google/android/datatransport/runtime/TransportContext;Lcom/google/android/datatransport/runtime/EventInternal;)V +Lcom/google/android/datatransport/runtime/scheduling/persistence/AutoValue_PersistedEvent; +HSPLcom/google/android/datatransport/runtime/scheduling/persistence/AutoValue_PersistedEvent;->(JLcom/google/android/datatransport/runtime/TransportContext;Lcom/google/android/datatransport/runtime/EventInternal;)V Lcom/google/android/datatransport/runtime/scheduling/persistence/ClientHealthMetricsStore; Lcom/google/android/datatransport/runtime/scheduling/persistence/EventStore; Lcom/google/android/datatransport/runtime/scheduling/persistence/EventStoreConfig; @@ -23182,29 +23387,30 @@ HSPLcom/google/android/datatransport/runtime/scheduling/persistence/EventStoreMo Lcom/google/android/datatransport/runtime/scheduling/persistence/EventStoreModule_StoreConfigFactory$InstanceHolder; HSPLcom/google/android/datatransport/runtime/scheduling/persistence/EventStoreModule_StoreConfigFactory$InstanceHolder;->()V HSPLcom/google/android/datatransport/runtime/scheduling/persistence/EventStoreModule_StoreConfigFactory$InstanceHolder;->access$000()Lcom/google/android/datatransport/runtime/scheduling/persistence/EventStoreModule_StoreConfigFactory; -PLcom/google/android/datatransport/runtime/scheduling/persistence/PersistedEvent;->()V -PLcom/google/android/datatransport/runtime/scheduling/persistence/PersistedEvent;->create(JLcom/google/android/datatransport/runtime/TransportContext;Lcom/google/android/datatransport/runtime/EventInternal;)Lcom/google/android/datatransport/runtime/scheduling/persistence/PersistedEvent; +Lcom/google/android/datatransport/runtime/scheduling/persistence/PersistedEvent; +HSPLcom/google/android/datatransport/runtime/scheduling/persistence/PersistedEvent;->()V +HSPLcom/google/android/datatransport/runtime/scheduling/persistence/PersistedEvent;->create(JLcom/google/android/datatransport/runtime/TransportContext;Lcom/google/android/datatransport/runtime/EventInternal;)Lcom/google/android/datatransport/runtime/scheduling/persistence/PersistedEvent; Lcom/google/android/datatransport/runtime/scheduling/persistence/SQLiteEventStore; HSPLcom/google/android/datatransport/runtime/scheduling/persistence/SQLiteEventStore;->$r8$lambda$r0gdOZu0LN8RFPU64_YFxoLiMZU(Lcom/google/android/datatransport/runtime/scheduling/persistence/SchemaManager;)Landroid/database/sqlite/SQLiteDatabase; HSPLcom/google/android/datatransport/runtime/scheduling/persistence/SQLiteEventStore;->()V HSPLcom/google/android/datatransport/runtime/scheduling/persistence/SQLiteEventStore;->(Lcom/google/android/datatransport/runtime/time/Clock;Lcom/google/android/datatransport/runtime/time/Clock;Lcom/google/android/datatransport/runtime/scheduling/persistence/EventStoreConfig;Lcom/google/android/datatransport/runtime/scheduling/persistence/SchemaManager;Ljavax/inject/Provider;)V HSPLcom/google/android/datatransport/runtime/scheduling/persistence/SQLiteEventStore;->ensureBeginTransaction(Landroid/database/sqlite/SQLiteDatabase;)V -PLcom/google/android/datatransport/runtime/scheduling/persistence/SQLiteEventStore;->ensureTransportContext(Landroid/database/sqlite/SQLiteDatabase;Lcom/google/android/datatransport/runtime/TransportContext;)J +HSPLcom/google/android/datatransport/runtime/scheduling/persistence/SQLiteEventStore;->ensureTransportContext(Landroid/database/sqlite/SQLiteDatabase;Lcom/google/android/datatransport/runtime/TransportContext;)J HSPLcom/google/android/datatransport/runtime/scheduling/persistence/SQLiteEventStore;->getDb()Landroid/database/sqlite/SQLiteDatabase; HSPLcom/google/android/datatransport/runtime/scheduling/persistence/SQLiteEventStore;->getNextCallTime(Lcom/google/android/datatransport/runtime/TransportContext;)J -PLcom/google/android/datatransport/runtime/scheduling/persistence/SQLiteEventStore;->getPageCount()J -PLcom/google/android/datatransport/runtime/scheduling/persistence/SQLiteEventStore;->getPageSize()J -PLcom/google/android/datatransport/runtime/scheduling/persistence/SQLiteEventStore;->getTransportContextId(Landroid/database/sqlite/SQLiteDatabase;Lcom/google/android/datatransport/runtime/TransportContext;)Ljava/lang/Long; +HSPLcom/google/android/datatransport/runtime/scheduling/persistence/SQLiteEventStore;->getPageCount()J +HSPLcom/google/android/datatransport/runtime/scheduling/persistence/SQLiteEventStore;->getPageSize()J +HSPLcom/google/android/datatransport/runtime/scheduling/persistence/SQLiteEventStore;->getTransportContextId(Landroid/database/sqlite/SQLiteDatabase;Lcom/google/android/datatransport/runtime/TransportContext;)Ljava/lang/Long; HSPLcom/google/android/datatransport/runtime/scheduling/persistence/SQLiteEventStore;->inTransaction(Lcom/google/android/datatransport/runtime/scheduling/persistence/SQLiteEventStore$Function;)Ljava/lang/Object; -PLcom/google/android/datatransport/runtime/scheduling/persistence/SQLiteEventStore;->isStorageAtLimit()Z +HSPLcom/google/android/datatransport/runtime/scheduling/persistence/SQLiteEventStore;->isStorageAtLimit()Z HSPLcom/google/android/datatransport/runtime/scheduling/persistence/SQLiteEventStore;->lambda$ensureBeginTransaction$24(Landroid/database/sqlite/SQLiteDatabase;)Ljava/lang/Object; HSPLcom/google/android/datatransport/runtime/scheduling/persistence/SQLiteEventStore;->lambda$getNextCallTime$5(Landroid/database/Cursor;)Ljava/lang/Long; -PLcom/google/android/datatransport/runtime/scheduling/persistence/SQLiteEventStore;->lambda$getTransportContextId$2(Landroid/database/Cursor;)Ljava/lang/Long; +HSPLcom/google/android/datatransport/runtime/scheduling/persistence/SQLiteEventStore;->lambda$getTransportContextId$2(Landroid/database/Cursor;)Ljava/lang/Long; HSPLcom/google/android/datatransport/runtime/scheduling/persistence/SQLiteEventStore;->lambda$loadActiveContexts$10(Landroid/database/sqlite/SQLiteDatabase;)Ljava/util/List; HSPLcom/google/android/datatransport/runtime/scheduling/persistence/SQLiteEventStore;->lambda$loadActiveContexts$9(Landroid/database/Cursor;)Ljava/util/List; -PLcom/google/android/datatransport/runtime/scheduling/persistence/SQLiteEventStore;->lambda$persist$1$com-google-android-datatransport-runtime-scheduling-persistence-SQLiteEventStore(Lcom/google/android/datatransport/runtime/EventInternal;Lcom/google/android/datatransport/runtime/TransportContext;Landroid/database/sqlite/SQLiteDatabase;)Ljava/lang/Long; +HSPLcom/google/android/datatransport/runtime/scheduling/persistence/SQLiteEventStore;->lambda$persist$1$com-google-android-datatransport-runtime-scheduling-persistence-SQLiteEventStore(Lcom/google/android/datatransport/runtime/EventInternal;Lcom/google/android/datatransport/runtime/TransportContext;Landroid/database/sqlite/SQLiteDatabase;)Ljava/lang/Long; HSPLcom/google/android/datatransport/runtime/scheduling/persistence/SQLiteEventStore;->loadActiveContexts()Ljava/lang/Iterable; -PLcom/google/android/datatransport/runtime/scheduling/persistence/SQLiteEventStore;->persist(Lcom/google/android/datatransport/runtime/TransportContext;Lcom/google/android/datatransport/runtime/EventInternal;)Lcom/google/android/datatransport/runtime/scheduling/persistence/PersistedEvent; +HSPLcom/google/android/datatransport/runtime/scheduling/persistence/SQLiteEventStore;->persist(Lcom/google/android/datatransport/runtime/TransportContext;Lcom/google/android/datatransport/runtime/EventInternal;)Lcom/google/android/datatransport/runtime/scheduling/persistence/PersistedEvent; HSPLcom/google/android/datatransport/runtime/scheduling/persistence/SQLiteEventStore;->retryIfDbLocked(Lcom/google/android/datatransport/runtime/scheduling/persistence/SQLiteEventStore$Producer;Lcom/google/android/datatransport/runtime/scheduling/persistence/SQLiteEventStore$Function;)Ljava/lang/Object; HSPLcom/google/android/datatransport/runtime/scheduling/persistence/SQLiteEventStore;->runCriticalSection(Lcom/google/android/datatransport/runtime/synchronization/SynchronizationGuard$CriticalSection;)Ljava/lang/Object; HSPLcom/google/android/datatransport/runtime/scheduling/persistence/SQLiteEventStore;->tryWithCursor(Landroid/database/Cursor;Lcom/google/android/datatransport/runtime/scheduling/persistence/SQLiteEventStore$Function;)Ljava/lang/Object; @@ -23217,8 +23423,9 @@ HSPLcom/google/android/datatransport/runtime/scheduling/persistence/SQLiteEventS Lcom/google/android/datatransport/runtime/scheduling/persistence/SQLiteEventStore$$ExternalSyntheticLambda17; HSPLcom/google/android/datatransport/runtime/scheduling/persistence/SQLiteEventStore$$ExternalSyntheticLambda17;->()V HSPLcom/google/android/datatransport/runtime/scheduling/persistence/SQLiteEventStore$$ExternalSyntheticLambda17;->apply(Ljava/lang/Object;)Ljava/lang/Object; -PLcom/google/android/datatransport/runtime/scheduling/persistence/SQLiteEventStore$$ExternalSyntheticLambda2;->()V -PLcom/google/android/datatransport/runtime/scheduling/persistence/SQLiteEventStore$$ExternalSyntheticLambda2;->apply(Ljava/lang/Object;)Ljava/lang/Object; +Lcom/google/android/datatransport/runtime/scheduling/persistence/SQLiteEventStore$$ExternalSyntheticLambda2; +HSPLcom/google/android/datatransport/runtime/scheduling/persistence/SQLiteEventStore$$ExternalSyntheticLambda2;->()V +HSPLcom/google/android/datatransport/runtime/scheduling/persistence/SQLiteEventStore$$ExternalSyntheticLambda2;->apply(Ljava/lang/Object;)Ljava/lang/Object; Lcom/google/android/datatransport/runtime/scheduling/persistence/SQLiteEventStore$$ExternalSyntheticLambda20; HSPLcom/google/android/datatransport/runtime/scheduling/persistence/SQLiteEventStore$$ExternalSyntheticLambda20;->(Landroid/database/sqlite/SQLiteDatabase;)V HSPLcom/google/android/datatransport/runtime/scheduling/persistence/SQLiteEventStore$$ExternalSyntheticLambda20;->produce()Ljava/lang/Object; @@ -23229,8 +23436,9 @@ HSPLcom/google/android/datatransport/runtime/scheduling/persistence/SQLiteEventS HSPLcom/google/android/datatransport/runtime/scheduling/persistence/SQLiteEventStore$$ExternalSyntheticLambda25;->produce()Ljava/lang/Object; Lcom/google/android/datatransport/runtime/scheduling/persistence/SQLiteEventStore$$ExternalSyntheticLambda26; HSPLcom/google/android/datatransport/runtime/scheduling/persistence/SQLiteEventStore$$ExternalSyntheticLambda26;->()V -PLcom/google/android/datatransport/runtime/scheduling/persistence/SQLiteEventStore$$ExternalSyntheticLambda4;->(Lcom/google/android/datatransport/runtime/scheduling/persistence/SQLiteEventStore;Lcom/google/android/datatransport/runtime/EventInternal;Lcom/google/android/datatransport/runtime/TransportContext;)V -PLcom/google/android/datatransport/runtime/scheduling/persistence/SQLiteEventStore$$ExternalSyntheticLambda4;->apply(Ljava/lang/Object;)Ljava/lang/Object; +Lcom/google/android/datatransport/runtime/scheduling/persistence/SQLiteEventStore$$ExternalSyntheticLambda4; +HSPLcom/google/android/datatransport/runtime/scheduling/persistence/SQLiteEventStore$$ExternalSyntheticLambda4;->(Lcom/google/android/datatransport/runtime/scheduling/persistence/SQLiteEventStore;Lcom/google/android/datatransport/runtime/EventInternal;Lcom/google/android/datatransport/runtime/TransportContext;)V +HSPLcom/google/android/datatransport/runtime/scheduling/persistence/SQLiteEventStore$$ExternalSyntheticLambda4;->apply(Ljava/lang/Object;)Ljava/lang/Object; Lcom/google/android/datatransport/runtime/scheduling/persistence/SQLiteEventStore$Function; Lcom/google/android/datatransport/runtime/scheduling/persistence/SQLiteEventStore$Producer; Lcom/google/android/datatransport/runtime/scheduling/persistence/SQLiteEventStore_Factory; @@ -23572,6 +23780,7 @@ HSPLcom/google/android/gms/common/internal/GmsClientSupervisor;->()V HSPLcom/google/android/gms/common/internal/GmsClientSupervisor;->()V HSPLcom/google/android/gms/common/internal/GmsClientSupervisor;->getInstance(Landroid/content/Context;)Lcom/google/android/gms/common/internal/GmsClientSupervisor; HSPLcom/google/android/gms/common/internal/GmsClientSupervisor;->getOrStartHandlerThread()Landroid/os/HandlerThread; +PLcom/google/android/gms/common/internal/GmsClientSupervisor;->zzb(Ljava/lang/String;Ljava/lang/String;ILandroid/content/ServiceConnection;Ljava/lang/String;Z)V Lcom/google/android/gms/common/internal/GmsLogger; HSPLcom/google/android/gms/common/internal/GmsLogger;->(Ljava/lang/String;Ljava/lang/String;)V Lcom/google/android/gms/common/internal/IAccountAccessor; @@ -24437,7 +24646,7 @@ HSPLcom/google/android/gms/internal/measurement/zzags;->()V HSPLcom/google/android/gms/internal/measurement/zzags;->()V HSPLcom/google/android/gms/internal/measurement/zzags;->zzC()J HSPLcom/google/android/gms/internal/measurement/zzags;->zzF()Ljava/lang/String; -HSPLcom/google/android/gms/internal/measurement/zzags;->zzI()J +PLcom/google/android/gms/internal/measurement/zzags;->zzI()J HSPLcom/google/android/gms/internal/measurement/zzags;->zzaz()Lcom/google/android/gms/internal/measurement/zzagt; HSPLcom/google/android/gms/internal/measurement/zzags;->zzs()J Lcom/google/android/gms/internal/measurement/zzagt; @@ -24446,7 +24655,7 @@ HSPLcom/google/android/gms/internal/measurement/zzagu;->()V HSPLcom/google/android/gms/internal/measurement/zzagu;->()V HSPLcom/google/android/gms/internal/measurement/zzagu;->zzC()J HSPLcom/google/android/gms/internal/measurement/zzagu;->zzF()Ljava/lang/String; -HSPLcom/google/android/gms/internal/measurement/zzagu;->zzI()J +PLcom/google/android/gms/internal/measurement/zzagu;->zzI()J HSPLcom/google/android/gms/internal/measurement/zzagu;->zzs()J Lcom/google/android/gms/internal/measurement/zzagv; HSPLcom/google/android/gms/internal/measurement/zzagv;->()V @@ -24556,7 +24765,7 @@ HSPLcom/google/android/gms/internal/measurement/zzbm;->onTransact(ILandroid/os/P Lcom/google/android/gms/internal/measurement/zzbn; HSPLcom/google/android/gms/internal/measurement/zzbn;->()V HSPLcom/google/android/gms/internal/measurement/zzbn;->zzb(Landroid/os/Parcel;Landroid/os/Parcelable$Creator;)Landroid/os/Parcelable; -HSPLcom/google/android/gms/internal/measurement/zzbn;->zzc(Landroid/os/Parcel;Landroid/os/Parcelable;)V +PLcom/google/android/gms/internal/measurement/zzbn;->zzc(Landroid/os/Parcel;Landroid/os/Parcelable;)V HSPLcom/google/android/gms/internal/measurement/zzbn;->zzd(Landroid/os/Parcel;Landroid/os/IInterface;)V HSPLcom/google/android/gms/internal/measurement/zzbn;->zzf(Landroid/os/Parcel;)V Lcom/google/android/gms/internal/measurement/zzbw; @@ -24624,6 +24833,7 @@ Lcom/google/android/gms/internal/measurement/zzeb; HSPLcom/google/android/gms/internal/measurement/zzeb;->(Lcom/google/android/gms/internal/measurement/zzez;)V HSPLcom/google/android/gms/internal/measurement/zzeb;->newThread(Ljava/lang/Runnable;)Ljava/lang/Thread; Lcom/google/android/gms/internal/measurement/zzel; +HSPLcom/google/android/gms/internal/measurement/zzel;->(Lcom/google/android/gms/internal/measurement/zzez;Lcom/google/android/gms/internal/measurement/zzeq;)V HSPLcom/google/android/gms/internal/measurement/zzel;->zza()V Lcom/google/android/gms/internal/measurement/zzeo; HSPLcom/google/android/gms/internal/measurement/zzeo;->(Lcom/google/android/gms/internal/measurement/zzez;Z)V @@ -25821,7 +26031,7 @@ HSPLcom/google/android/gms/measurement/internal/zzdh;->()V Lcom/google/android/gms/measurement/internal/zzdi; HSPLcom/google/android/gms/measurement/internal/zzdi;->()V HSPLcom/google/android/gms/measurement/internal/zzdi;->()V -HSPLcom/google/android/gms/measurement/internal/zzdi;->zza()Ljava/lang/Object; +PLcom/google/android/gms/measurement/internal/zzdi;->zza()Ljava/lang/Object; Lcom/google/android/gms/measurement/internal/zzdj; HSPLcom/google/android/gms/measurement/internal/zzdj;->()V HSPLcom/google/android/gms/measurement/internal/zzdj;->()V @@ -26334,7 +26544,7 @@ HSPLcom/google/android/gms/measurement/internal/zzmr;->(Lcom/google/androi PLcom/google/android/gms/measurement/internal/zzmr;->run()V Lcom/google/android/gms/measurement/internal/zzms; HSPLcom/google/android/gms/measurement/internal/zzms;->(Lcom/google/android/gms/measurement/internal/zznl;Lcom/google/android/gms/measurement/internal/zzr;)V -HSPLcom/google/android/gms/measurement/internal/zzms;->run()V +PLcom/google/android/gms/measurement/internal/zzms;->run()V PLcom/google/android/gms/measurement/internal/zzna;->(Lcom/google/android/gms/measurement/internal/zznf;Lcom/google/android/gms/measurement/internal/zzgb;)V PLcom/google/android/gms/measurement/internal/zzna;->run()V Lcom/google/android/gms/measurement/internal/zznf; @@ -26342,22 +26552,22 @@ HSPLcom/google/android/gms/measurement/internal/zznf;->(Lcom/google/androi HSPLcom/google/android/gms/measurement/internal/zznf;->onConnected(Landroid/os/Bundle;)V PLcom/google/android/gms/measurement/internal/zznf;->zzb()V HSPLcom/google/android/gms/measurement/internal/zznf;->zzc()V -HSPLcom/google/android/gms/measurement/internal/zznf;->zzd(Z)V +PLcom/google/android/gms/measurement/internal/zznf;->zzd(Z)V Lcom/google/android/gms/measurement/internal/zznl; HSPLcom/google/android/gms/measurement/internal/zznl;->(Lcom/google/android/gms/measurement/internal/zzic;)V HSPLcom/google/android/gms/measurement/internal/zznl;->zzC(Ljava/util/concurrent/atomic/AtomicReference;)V HSPLcom/google/android/gms/measurement/internal/zznl;->zzI()V HSPLcom/google/android/gms/measurement/internal/zznl;->zzK()Z -HSPLcom/google/android/gms/measurement/internal/zznl;->zzL(Lcom/google/android/gms/measurement/internal/zzgb;)V +PLcom/google/android/gms/measurement/internal/zznl;->zzL(Lcom/google/android/gms/measurement/internal/zzgb;)V PLcom/google/android/gms/measurement/internal/zznl;->zzM()V HSPLcom/google/android/gms/measurement/internal/zznl;->zzO()Z HSPLcom/google/android/gms/measurement/internal/zznl;->zzP()Z -HSPLcom/google/android/gms/measurement/internal/zznl;->zzV()V -HSPLcom/google/android/gms/measurement/internal/zznl;->zzZ()Lcom/google/android/gms/measurement/internal/zzgb; +PLcom/google/android/gms/measurement/internal/zznl;->zzV()V +PLcom/google/android/gms/measurement/internal/zznl;->zzZ()Lcom/google/android/gms/measurement/internal/zzgb; PLcom/google/android/gms/measurement/internal/zznl;->zzab()Ljava/util/concurrent/ScheduledExecutorService; -HSPLcom/google/android/gms/measurement/internal/zznl;->zzae()V +PLcom/google/android/gms/measurement/internal/zznl;->zzae()V HSPLcom/google/android/gms/measurement/internal/zznl;->zzaf(Ljava/lang/Runnable;)V -HSPLcom/google/android/gms/measurement/internal/zznl;->zzag()V +PLcom/google/android/gms/measurement/internal/zznl;->zzag()V HSPLcom/google/android/gms/measurement/internal/zznl;->zzah(Z)Lcom/google/android/gms/measurement/internal/zzr; HSPLcom/google/android/gms/measurement/internal/zznl;->zze()Z HSPLcom/google/android/gms/measurement/internal/zznl;->zzh()Z @@ -26399,7 +26609,7 @@ HSPLcom/google/android/gms/measurement/internal/zzof;->zzb(Landroid/content/Shar HSPLcom/google/android/gms/measurement/internal/zzof;->zzd(Lcom/google/common/collect/ImmutableMap;Lcom/google/common/collect/ImmutableMap;Lcom/google/common/collect/ImmutableSet;[CIIIIILjava/lang/String;Ljava/lang/String;Ljava/lang/String;ZZZ)Ljava/util/Map; Lcom/google/android/gms/measurement/internal/zzog; HSPLcom/google/android/gms/measurement/internal/zzog;->(Lcom/google/android/gms/common/util/Clock;)V -HSPLcom/google/android/gms/measurement/internal/zzog;->zza()V +PLcom/google/android/gms/measurement/internal/zzog;->zza()V Lcom/google/android/gms/measurement/internal/zzpo; Lcom/google/android/gms/measurement/internal/zzpp; HSPLcom/google/android/gms/measurement/internal/zzpp;->()V @@ -26441,7 +26651,7 @@ HSPLcom/google/android/gms/measurement/internal/zzq;->(Lcom/google/android Lcom/google/android/gms/measurement/internal/zzr; HSPLcom/google/android/gms/measurement/internal/zzr;->()V HSPLcom/google/android/gms/measurement/internal/zzr;->(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;JLjava/lang/String;JJLjava/lang/String;ZZLjava/lang/String;JIZZLjava/lang/Boolean;JLjava/util/List;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;ZJILjava/lang/String;IJLjava/lang/String;Ljava/lang/String;JIJ)V -HSPLcom/google/android/gms/measurement/internal/zzr;->writeToParcel(Landroid/os/Parcel;I)V +PLcom/google/android/gms/measurement/internal/zzr;->writeToParcel(Landroid/os/Parcel;I)V Lcom/google/android/gms/measurement/internal/zzs; HSPLcom/google/android/gms/measurement/internal/zzs;->()V Lcom/google/android/gms/measurement/internal/zzx; @@ -27001,7 +27211,7 @@ Lcom/google/firebase/FirebaseCommonRegistrar$$ExternalSyntheticLambda3; HSPLcom/google/firebase/FirebaseCommonRegistrar$$ExternalSyntheticLambda3;->()V HSPLcom/google/firebase/FirebaseCommonRegistrar$$ExternalSyntheticLambda3;->extract(Ljava/lang/Object;)Ljava/lang/String; Lcom/google/firebase/FirebaseException; -PLcom/google/firebase/FirebaseException;->()V +HSPLcom/google/firebase/FirebaseException;->()V HSPLcom/google/firebase/FirebaseException;->(Ljava/lang/String;)V Lcom/google/firebase/FirebaseKt; HSPLcom/google/firebase/FirebaseKt;->getApp(Lcom/google/firebase/Firebase;)Lcom/google/firebase/FirebaseApp; @@ -28340,10 +28550,10 @@ Lcom/google/firebase/datatransport/TransportBackend; Lcom/google/firebase/datatransport/TransportRegistrar; HSPLcom/google/firebase/datatransport/TransportRegistrar;->()V HSPLcom/google/firebase/datatransport/TransportRegistrar;->getComponents()Ljava/util/List; -PLcom/google/firebase/datatransport/TransportRegistrar;->lambda$getComponents$0(Lcom/google/firebase/components/ComponentContainer;)Lcom/google/android/datatransport/TransportFactory; +HSPLcom/google/firebase/datatransport/TransportRegistrar;->lambda$getComponents$0(Lcom/google/firebase/components/ComponentContainer;)Lcom/google/android/datatransport/TransportFactory; Lcom/google/firebase/datatransport/TransportRegistrar$$ExternalSyntheticLambda0; HSPLcom/google/firebase/datatransport/TransportRegistrar$$ExternalSyntheticLambda0;->()V -PLcom/google/firebase/datatransport/TransportRegistrar$$ExternalSyntheticLambda0;->create(Lcom/google/firebase/components/ComponentContainer;)Ljava/lang/Object; +HSPLcom/google/firebase/datatransport/TransportRegistrar$$ExternalSyntheticLambda0;->create(Lcom/google/firebase/components/ComponentContainer;)Ljava/lang/Object; Lcom/google/firebase/datatransport/TransportRegistrar$$ExternalSyntheticLambda1; HSPLcom/google/firebase/datatransport/TransportRegistrar$$ExternalSyntheticLambda1;->()V Lcom/google/firebase/datatransport/TransportRegistrar$$ExternalSyntheticLambda2; @@ -28351,6 +28561,7 @@ HSPLcom/google/firebase/datatransport/TransportRegistrar$$ExternalSyntheticLambd Lcom/google/firebase/dynamicloading/ComponentLoader; Lcom/google/firebase/encoders/DataEncoder; Lcom/google/firebase/encoders/Encoder; +Lcom/google/firebase/encoders/EncodingException; Lcom/google/firebase/encoders/FieldDescriptor; HSPLcom/google/firebase/encoders/FieldDescriptor;->(Ljava/lang/String;Ljava/util/Map;)V HSPLcom/google/firebase/encoders/FieldDescriptor;->getName()Ljava/lang/String; @@ -28392,10 +28603,10 @@ HSPLcom/google/firebase/encoders/json/JsonDataEncoderBuilder$TimestampEncoder;-> HSPLcom/google/firebase/encoders/json/JsonDataEncoderBuilder$TimestampEncoder;->(Lcom/google/firebase/encoders/json/JsonDataEncoderBuilder$1;)V Lcom/google/firebase/encoders/json/JsonValueObjectEncoderContext; HSPLcom/google/firebase/encoders/json/JsonValueObjectEncoderContext;->(Ljava/io/Writer;Ljava/util/Map;Ljava/util/Map;Lcom/google/firebase/encoders/ObjectEncoder;Z)V -PLcom/google/firebase/encoders/json/JsonValueObjectEncoderContext;->add(D)Lcom/google/firebase/encoders/json/JsonValueObjectEncoderContext; +HSPLcom/google/firebase/encoders/json/JsonValueObjectEncoderContext;->add(D)Lcom/google/firebase/encoders/json/JsonValueObjectEncoderContext; HSPLcom/google/firebase/encoders/json/JsonValueObjectEncoderContext;->add(I)Lcom/google/firebase/encoders/json/JsonValueObjectEncoderContext; HSPLcom/google/firebase/encoders/json/JsonValueObjectEncoderContext;->add(J)Lcom/google/firebase/encoders/json/JsonValueObjectEncoderContext; -PLcom/google/firebase/encoders/json/JsonValueObjectEncoderContext;->add(Lcom/google/firebase/encoders/FieldDescriptor;D)Lcom/google/firebase/encoders/ObjectEncoderContext; +HSPLcom/google/firebase/encoders/json/JsonValueObjectEncoderContext;->add(Lcom/google/firebase/encoders/FieldDescriptor;D)Lcom/google/firebase/encoders/ObjectEncoderContext; HSPLcom/google/firebase/encoders/json/JsonValueObjectEncoderContext;->add(Lcom/google/firebase/encoders/FieldDescriptor;I)Lcom/google/firebase/encoders/ObjectEncoderContext; HSPLcom/google/firebase/encoders/json/JsonValueObjectEncoderContext;->add(Lcom/google/firebase/encoders/FieldDescriptor;J)Lcom/google/firebase/encoders/ObjectEncoderContext; HSPLcom/google/firebase/encoders/json/JsonValueObjectEncoderContext;->add(Lcom/google/firebase/encoders/FieldDescriptor;Ljava/lang/Object;)Lcom/google/firebase/encoders/ObjectEncoderContext; @@ -28403,7 +28614,7 @@ HSPLcom/google/firebase/encoders/json/JsonValueObjectEncoderContext;->add(Lcom/g HSPLcom/google/firebase/encoders/json/JsonValueObjectEncoderContext;->add(Ljava/lang/Object;Z)Lcom/google/firebase/encoders/json/JsonValueObjectEncoderContext; HSPLcom/google/firebase/encoders/json/JsonValueObjectEncoderContext;->add(Ljava/lang/String;)Lcom/google/firebase/encoders/ValueEncoderContext; HSPLcom/google/firebase/encoders/json/JsonValueObjectEncoderContext;->add(Ljava/lang/String;)Lcom/google/firebase/encoders/json/JsonValueObjectEncoderContext; -PLcom/google/firebase/encoders/json/JsonValueObjectEncoderContext;->add(Ljava/lang/String;D)Lcom/google/firebase/encoders/json/JsonValueObjectEncoderContext; +HSPLcom/google/firebase/encoders/json/JsonValueObjectEncoderContext;->add(Ljava/lang/String;D)Lcom/google/firebase/encoders/json/JsonValueObjectEncoderContext; HSPLcom/google/firebase/encoders/json/JsonValueObjectEncoderContext;->add(Ljava/lang/String;I)Lcom/google/firebase/encoders/json/JsonValueObjectEncoderContext; HSPLcom/google/firebase/encoders/json/JsonValueObjectEncoderContext;->add(Ljava/lang/String;J)Lcom/google/firebase/encoders/json/JsonValueObjectEncoderContext; HSPLcom/google/firebase/encoders/json/JsonValueObjectEncoderContext;->add(Ljava/lang/String;Ljava/lang/Object;)Lcom/google/firebase/encoders/json/JsonValueObjectEncoderContext; @@ -28525,7 +28736,7 @@ Lcom/google/firebase/installations/FirebaseInstallations$3; HSPLcom/google/firebase/installations/FirebaseInstallations$3;->()V Lcom/google/firebase/installations/FirebaseInstallationsApi; Lcom/google/firebase/installations/FirebaseInstallationsException; -PLcom/google/firebase/installations/FirebaseInstallationsException;->(Lcom/google/firebase/installations/FirebaseInstallationsException$Status;)V +HSPLcom/google/firebase/installations/FirebaseInstallationsException;->(Lcom/google/firebase/installations/FirebaseInstallationsException$Status;)V HSPLcom/google/firebase/installations/FirebaseInstallationsException;->(Ljava/lang/String;Lcom/google/firebase/installations/FirebaseInstallationsException$Status;)V Lcom/google/firebase/installations/FirebaseInstallationsException$Status; HSPLcom/google/firebase/installations/FirebaseInstallationsException$Status;->$values()[Lcom/google/firebase/installations/FirebaseInstallationsException$Status; @@ -28619,13 +28830,13 @@ HSPLcom/google/firebase/installations/local/PersistedInstallationEntry;->withUnr Lcom/google/firebase/installations/local/PersistedInstallationEntry$Builder; HSPLcom/google/firebase/installations/local/PersistedInstallationEntry$Builder;->()V Lcom/google/firebase/installations/remote/AutoValue_InstallationResponse; -PLcom/google/firebase/installations/remote/AutoValue_InstallationResponse;->(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Lcom/google/firebase/installations/remote/TokenResult;Lcom/google/firebase/installations/remote/InstallationResponse$ResponseCode;)V -PLcom/google/firebase/installations/remote/AutoValue_InstallationResponse;->(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Lcom/google/firebase/installations/remote/TokenResult;Lcom/google/firebase/installations/remote/InstallationResponse$ResponseCode;Lcom/google/firebase/installations/remote/AutoValue_InstallationResponse$1;)V +HSPLcom/google/firebase/installations/remote/AutoValue_InstallationResponse;->(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Lcom/google/firebase/installations/remote/TokenResult;Lcom/google/firebase/installations/remote/InstallationResponse$ResponseCode;)V +HSPLcom/google/firebase/installations/remote/AutoValue_InstallationResponse;->(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Lcom/google/firebase/installations/remote/TokenResult;Lcom/google/firebase/installations/remote/InstallationResponse$ResponseCode;Lcom/google/firebase/installations/remote/AutoValue_InstallationResponse$1;)V HSPLcom/google/firebase/installations/remote/AutoValue_InstallationResponse;->getResponseCode()Lcom/google/firebase/installations/remote/InstallationResponse$ResponseCode; Lcom/google/firebase/installations/remote/AutoValue_InstallationResponse$Builder; -PLcom/google/firebase/installations/remote/AutoValue_InstallationResponse$Builder;->()V -PLcom/google/firebase/installations/remote/AutoValue_InstallationResponse$Builder;->build()Lcom/google/firebase/installations/remote/InstallationResponse; -PLcom/google/firebase/installations/remote/AutoValue_InstallationResponse$Builder;->setResponseCode(Lcom/google/firebase/installations/remote/InstallationResponse$ResponseCode;)Lcom/google/firebase/installations/remote/InstallationResponse$Builder; +HSPLcom/google/firebase/installations/remote/AutoValue_InstallationResponse$Builder;->()V +HSPLcom/google/firebase/installations/remote/AutoValue_InstallationResponse$Builder;->build()Lcom/google/firebase/installations/remote/InstallationResponse; +HSPLcom/google/firebase/installations/remote/AutoValue_InstallationResponse$Builder;->setResponseCode(Lcom/google/firebase/installations/remote/InstallationResponse$ResponseCode;)Lcom/google/firebase/installations/remote/InstallationResponse$Builder; Lcom/google/firebase/installations/remote/FirebaseInstallationServiceClient; HSPLcom/google/firebase/installations/remote/FirebaseInstallationServiceClient;->()V HSPLcom/google/firebase/installations/remote/FirebaseInstallationServiceClient;->(Landroid/content/Context;Lcom/google/firebase/inject/Provider;)V @@ -28635,18 +28846,18 @@ HSPLcom/google/firebase/installations/remote/FirebaseInstallationServiceClient;- HSPLcom/google/firebase/installations/remote/FirebaseInstallationServiceClient;->getFingerprintHashForPackage()Ljava/lang/String; HSPLcom/google/firebase/installations/remote/FirebaseInstallationServiceClient;->getFullyQualifiedRequestUri(Ljava/lang/String;)Ljava/net/URL; HSPLcom/google/firebase/installations/remote/FirebaseInstallationServiceClient;->getJsonBytes(Lorg/json/JSONObject;)[B -PLcom/google/firebase/installations/remote/FirebaseInstallationServiceClient;->isSuccessfulResponseCode(I)Z -PLcom/google/firebase/installations/remote/FirebaseInstallationServiceClient;->logBadConfigError()V -PLcom/google/firebase/installations/remote/FirebaseInstallationServiceClient;->logFisCommunicationError(Ljava/net/HttpURLConnection;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)V +HSPLcom/google/firebase/installations/remote/FirebaseInstallationServiceClient;->isSuccessfulResponseCode(I)Z +HSPLcom/google/firebase/installations/remote/FirebaseInstallationServiceClient;->logBadConfigError()V +HSPLcom/google/firebase/installations/remote/FirebaseInstallationServiceClient;->logFisCommunicationError(Ljava/net/HttpURLConnection;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)V HSPLcom/google/firebase/installations/remote/FirebaseInstallationServiceClient;->openHttpURLConnection(Ljava/net/URL;Ljava/lang/String;)Ljava/net/HttpURLConnection; HSPLcom/google/firebase/installations/remote/FirebaseInstallationServiceClient;->readErrorResponse(Ljava/net/HttpURLConnection;)Ljava/lang/String; HSPLcom/google/firebase/installations/remote/FirebaseInstallationServiceClient;->writeFIDCreateRequestBodyToOutputStream(Ljava/net/HttpURLConnection;Ljava/lang/String;Ljava/lang/String;)V HSPLcom/google/firebase/installations/remote/FirebaseInstallationServiceClient;->writeRequestBodyToOutputStream(Ljava/net/URLConnection;[B)V Lcom/google/firebase/installations/remote/InstallationResponse; HSPLcom/google/firebase/installations/remote/InstallationResponse;->()V -PLcom/google/firebase/installations/remote/InstallationResponse;->builder()Lcom/google/firebase/installations/remote/InstallationResponse$Builder; +HSPLcom/google/firebase/installations/remote/InstallationResponse;->builder()Lcom/google/firebase/installations/remote/InstallationResponse$Builder; Lcom/google/firebase/installations/remote/InstallationResponse$Builder; -PLcom/google/firebase/installations/remote/InstallationResponse$Builder;->()V +HSPLcom/google/firebase/installations/remote/InstallationResponse$Builder;->()V Lcom/google/firebase/installations/remote/InstallationResponse$ResponseCode; HSPLcom/google/firebase/installations/remote/InstallationResponse$ResponseCode;->$values()[Lcom/google/firebase/installations/remote/InstallationResponse$ResponseCode; HSPLcom/google/firebase/installations/remote/InstallationResponse$ResponseCode;->()V @@ -28660,6 +28871,7 @@ HSPLcom/google/firebase/installations/remote/RequestLimiter;->isRequestAllowed() HSPLcom/google/firebase/installations/remote/RequestLimiter;->isRetryableError(I)Z HSPLcom/google/firebase/installations/remote/RequestLimiter;->isSuccessfulOrRequiresNewFidCreation(I)Z HSPLcom/google/firebase/installations/remote/RequestLimiter;->setNextRequestTime(I)V +Lcom/google/firebase/installations/remote/TokenResult; Lcom/google/firebase/installations/remote/TokenResult$ResponseCode; HSPLcom/google/firebase/installations/remote/TokenResult$ResponseCode;->$values()[Lcom/google/firebase/installations/remote/TokenResult$ResponseCode; HSPLcom/google/firebase/installations/remote/TokenResult$ResponseCode;->()V @@ -28722,18 +28934,18 @@ Lcom/google/firebase/remoteconfig/interop/rollouts/RolloutsStateSubscriber; Lcom/google/firebase/sessions/AndroidApplicationInfo; HSPLcom/google/firebase/sessions/AndroidApplicationInfo;->(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Lcom/google/firebase/sessions/ProcessDetails;Ljava/util/List;)V HSPLcom/google/firebase/sessions/AndroidApplicationInfo;->getAppBuildVersion()Ljava/lang/String; -PLcom/google/firebase/sessions/AndroidApplicationInfo;->getAppProcessDetails()Ljava/util/List; -PLcom/google/firebase/sessions/AndroidApplicationInfo;->getCurrentProcessDetails()Lcom/google/firebase/sessions/ProcessDetails; -PLcom/google/firebase/sessions/AndroidApplicationInfo;->getDeviceManufacturer()Ljava/lang/String; -PLcom/google/firebase/sessions/AndroidApplicationInfo;->getPackageName()Ljava/lang/String; +HSPLcom/google/firebase/sessions/AndroidApplicationInfo;->getAppProcessDetails()Ljava/util/List; +HSPLcom/google/firebase/sessions/AndroidApplicationInfo;->getCurrentProcessDetails()Lcom/google/firebase/sessions/ProcessDetails; +HSPLcom/google/firebase/sessions/AndroidApplicationInfo;->getDeviceManufacturer()Ljava/lang/String; +HSPLcom/google/firebase/sessions/AndroidApplicationInfo;->getPackageName()Ljava/lang/String; HSPLcom/google/firebase/sessions/AndroidApplicationInfo;->getVersionName()Ljava/lang/String; Lcom/google/firebase/sessions/ApplicationInfo; HSPLcom/google/firebase/sessions/ApplicationInfo;->(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Lcom/google/firebase/sessions/LogEnvironment;Lcom/google/firebase/sessions/AndroidApplicationInfo;)V HSPLcom/google/firebase/sessions/ApplicationInfo;->getAndroidAppInfo()Lcom/google/firebase/sessions/AndroidApplicationInfo; HSPLcom/google/firebase/sessions/ApplicationInfo;->getAppId()Ljava/lang/String; -PLcom/google/firebase/sessions/ApplicationInfo;->getDeviceModel()Ljava/lang/String; -PLcom/google/firebase/sessions/ApplicationInfo;->getLogEnvironment()Lcom/google/firebase/sessions/LogEnvironment; -PLcom/google/firebase/sessions/ApplicationInfo;->getOsVersion()Ljava/lang/String; +HSPLcom/google/firebase/sessions/ApplicationInfo;->getDeviceModel()Ljava/lang/String; +HSPLcom/google/firebase/sessions/ApplicationInfo;->getLogEnvironment()Lcom/google/firebase/sessions/LogEnvironment; +HSPLcom/google/firebase/sessions/ApplicationInfo;->getOsVersion()Ljava/lang/String; HSPLcom/google/firebase/sessions/ApplicationInfo;->getSessionSdkVersion()Ljava/lang/String; Lcom/google/firebase/sessions/AutoSessionEventEncoder; HSPLcom/google/firebase/sessions/AutoSessionEventEncoder;->()V @@ -28742,33 +28954,33 @@ HSPLcom/google/firebase/sessions/AutoSessionEventEncoder;->configure(Lcom/google Lcom/google/firebase/sessions/AutoSessionEventEncoder$AndroidApplicationInfoEncoder; HSPLcom/google/firebase/sessions/AutoSessionEventEncoder$AndroidApplicationInfoEncoder;->()V HSPLcom/google/firebase/sessions/AutoSessionEventEncoder$AndroidApplicationInfoEncoder;->()V -PLcom/google/firebase/sessions/AutoSessionEventEncoder$AndroidApplicationInfoEncoder;->encode(Lcom/google/firebase/sessions/AndroidApplicationInfo;Lcom/google/firebase/encoders/ObjectEncoderContext;)V -PLcom/google/firebase/sessions/AutoSessionEventEncoder$AndroidApplicationInfoEncoder;->encode(Ljava/lang/Object;Ljava/lang/Object;)V +HSPLcom/google/firebase/sessions/AutoSessionEventEncoder$AndroidApplicationInfoEncoder;->encode(Lcom/google/firebase/sessions/AndroidApplicationInfo;Lcom/google/firebase/encoders/ObjectEncoderContext;)V +HSPLcom/google/firebase/sessions/AutoSessionEventEncoder$AndroidApplicationInfoEncoder;->encode(Ljava/lang/Object;Ljava/lang/Object;)V Lcom/google/firebase/sessions/AutoSessionEventEncoder$ApplicationInfoEncoder; HSPLcom/google/firebase/sessions/AutoSessionEventEncoder$ApplicationInfoEncoder;->()V HSPLcom/google/firebase/sessions/AutoSessionEventEncoder$ApplicationInfoEncoder;->()V -PLcom/google/firebase/sessions/AutoSessionEventEncoder$ApplicationInfoEncoder;->encode(Lcom/google/firebase/sessions/ApplicationInfo;Lcom/google/firebase/encoders/ObjectEncoderContext;)V -PLcom/google/firebase/sessions/AutoSessionEventEncoder$ApplicationInfoEncoder;->encode(Ljava/lang/Object;Ljava/lang/Object;)V +HSPLcom/google/firebase/sessions/AutoSessionEventEncoder$ApplicationInfoEncoder;->encode(Lcom/google/firebase/sessions/ApplicationInfo;Lcom/google/firebase/encoders/ObjectEncoderContext;)V +HSPLcom/google/firebase/sessions/AutoSessionEventEncoder$ApplicationInfoEncoder;->encode(Ljava/lang/Object;Ljava/lang/Object;)V Lcom/google/firebase/sessions/AutoSessionEventEncoder$DataCollectionStatusEncoder; HSPLcom/google/firebase/sessions/AutoSessionEventEncoder$DataCollectionStatusEncoder;->()V HSPLcom/google/firebase/sessions/AutoSessionEventEncoder$DataCollectionStatusEncoder;->()V -PLcom/google/firebase/sessions/AutoSessionEventEncoder$DataCollectionStatusEncoder;->encode(Lcom/google/firebase/sessions/DataCollectionStatus;Lcom/google/firebase/encoders/ObjectEncoderContext;)V -PLcom/google/firebase/sessions/AutoSessionEventEncoder$DataCollectionStatusEncoder;->encode(Ljava/lang/Object;Ljava/lang/Object;)V +HSPLcom/google/firebase/sessions/AutoSessionEventEncoder$DataCollectionStatusEncoder;->encode(Lcom/google/firebase/sessions/DataCollectionStatus;Lcom/google/firebase/encoders/ObjectEncoderContext;)V +HSPLcom/google/firebase/sessions/AutoSessionEventEncoder$DataCollectionStatusEncoder;->encode(Ljava/lang/Object;Ljava/lang/Object;)V Lcom/google/firebase/sessions/AutoSessionEventEncoder$ProcessDetailsEncoder; HSPLcom/google/firebase/sessions/AutoSessionEventEncoder$ProcessDetailsEncoder;->()V HSPLcom/google/firebase/sessions/AutoSessionEventEncoder$ProcessDetailsEncoder;->()V -PLcom/google/firebase/sessions/AutoSessionEventEncoder$ProcessDetailsEncoder;->encode(Lcom/google/firebase/sessions/ProcessDetails;Lcom/google/firebase/encoders/ObjectEncoderContext;)V -PLcom/google/firebase/sessions/AutoSessionEventEncoder$ProcessDetailsEncoder;->encode(Ljava/lang/Object;Ljava/lang/Object;)V +HSPLcom/google/firebase/sessions/AutoSessionEventEncoder$ProcessDetailsEncoder;->encode(Lcom/google/firebase/sessions/ProcessDetails;Lcom/google/firebase/encoders/ObjectEncoderContext;)V +HSPLcom/google/firebase/sessions/AutoSessionEventEncoder$ProcessDetailsEncoder;->encode(Ljava/lang/Object;Ljava/lang/Object;)V Lcom/google/firebase/sessions/AutoSessionEventEncoder$SessionEventEncoder; HSPLcom/google/firebase/sessions/AutoSessionEventEncoder$SessionEventEncoder;->()V HSPLcom/google/firebase/sessions/AutoSessionEventEncoder$SessionEventEncoder;->()V -PLcom/google/firebase/sessions/AutoSessionEventEncoder$SessionEventEncoder;->encode(Lcom/google/firebase/sessions/SessionEvent;Lcom/google/firebase/encoders/ObjectEncoderContext;)V -PLcom/google/firebase/sessions/AutoSessionEventEncoder$SessionEventEncoder;->encode(Ljava/lang/Object;Ljava/lang/Object;)V +HSPLcom/google/firebase/sessions/AutoSessionEventEncoder$SessionEventEncoder;->encode(Lcom/google/firebase/sessions/SessionEvent;Lcom/google/firebase/encoders/ObjectEncoderContext;)V +HSPLcom/google/firebase/sessions/AutoSessionEventEncoder$SessionEventEncoder;->encode(Ljava/lang/Object;Ljava/lang/Object;)V Lcom/google/firebase/sessions/AutoSessionEventEncoder$SessionInfoEncoder; HSPLcom/google/firebase/sessions/AutoSessionEventEncoder$SessionInfoEncoder;->()V HSPLcom/google/firebase/sessions/AutoSessionEventEncoder$SessionInfoEncoder;->()V -PLcom/google/firebase/sessions/AutoSessionEventEncoder$SessionInfoEncoder;->encode(Lcom/google/firebase/sessions/SessionInfo;Lcom/google/firebase/encoders/ObjectEncoderContext;)V -PLcom/google/firebase/sessions/AutoSessionEventEncoder$SessionInfoEncoder;->encode(Ljava/lang/Object;Ljava/lang/Object;)V +HSPLcom/google/firebase/sessions/AutoSessionEventEncoder$SessionInfoEncoder;->encode(Lcom/google/firebase/sessions/SessionInfo;Lcom/google/firebase/encoders/ObjectEncoderContext;)V +HSPLcom/google/firebase/sessions/AutoSessionEventEncoder$SessionInfoEncoder;->encode(Ljava/lang/Object;Ljava/lang/Object;)V Lcom/google/firebase/sessions/DaggerFirebaseSessionsComponent; HSPLcom/google/firebase/sessions/DaggerFirebaseSessionsComponent;->builder()Lcom/google/firebase/sessions/FirebaseSessionsComponent$Builder; Lcom/google/firebase/sessions/DaggerFirebaseSessionsComponent$Builder; @@ -28791,23 +29003,25 @@ Lcom/google/firebase/sessions/DaggerFirebaseSessionsComponent$FirebaseSessionsCo HSPLcom/google/firebase/sessions/DaggerFirebaseSessionsComponent$FirebaseSessionsComponentImpl;->(Landroid/content/Context;Lkotlin/coroutines/CoroutineContext;Lkotlin/coroutines/CoroutineContext;Lcom/google/firebase/FirebaseApp;Lcom/google/firebase/installations/FirebaseInstallationsApi;Lcom/google/firebase/inject/Provider;)V HSPLcom/google/firebase/sessions/DaggerFirebaseSessionsComponent$FirebaseSessionsComponentImpl;->getFirebaseSessions()Lcom/google/firebase/sessions/FirebaseSessions; HSPLcom/google/firebase/sessions/DaggerFirebaseSessionsComponent$FirebaseSessionsComponentImpl;->initialize(Landroid/content/Context;Lkotlin/coroutines/CoroutineContext;Lkotlin/coroutines/CoroutineContext;Lcom/google/firebase/FirebaseApp;Lcom/google/firebase/installations/FirebaseInstallationsApi;Lcom/google/firebase/inject/Provider;)V -PLcom/google/firebase/sessions/DataCollectionState;->$values()[Lcom/google/firebase/sessions/DataCollectionState; -PLcom/google/firebase/sessions/DataCollectionState;->()V -PLcom/google/firebase/sessions/DataCollectionState;->(Ljava/lang/String;II)V -PLcom/google/firebase/sessions/DataCollectionState;->getNumber()I +Lcom/google/firebase/sessions/DataCollectionState; +HSPLcom/google/firebase/sessions/DataCollectionState;->$values()[Lcom/google/firebase/sessions/DataCollectionState; +HSPLcom/google/firebase/sessions/DataCollectionState;->()V +HSPLcom/google/firebase/sessions/DataCollectionState;->(Ljava/lang/String;II)V +HSPLcom/google/firebase/sessions/DataCollectionState;->getNumber()I Lcom/google/firebase/sessions/DataCollectionStatus; -PLcom/google/firebase/sessions/DataCollectionStatus;->(Lcom/google/firebase/sessions/DataCollectionState;Lcom/google/firebase/sessions/DataCollectionState;D)V -PLcom/google/firebase/sessions/DataCollectionStatus;->getCrashlytics()Lcom/google/firebase/sessions/DataCollectionState; -PLcom/google/firebase/sessions/DataCollectionStatus;->getPerformance()Lcom/google/firebase/sessions/DataCollectionState; -PLcom/google/firebase/sessions/DataCollectionStatus;->getSessionSamplingRate()D +HSPLcom/google/firebase/sessions/DataCollectionStatus;->(Lcom/google/firebase/sessions/DataCollectionState;Lcom/google/firebase/sessions/DataCollectionState;D)V +HSPLcom/google/firebase/sessions/DataCollectionStatus;->getCrashlytics()Lcom/google/firebase/sessions/DataCollectionState; +HSPLcom/google/firebase/sessions/DataCollectionStatus;->getPerformance()Lcom/google/firebase/sessions/DataCollectionState; +HSPLcom/google/firebase/sessions/DataCollectionStatus;->getSessionSamplingRate()D Lcom/google/firebase/sessions/EventGDTLogger; -PLcom/google/firebase/sessions/EventGDTLogger;->$r8$lambda$W13Olas0VV9EajY9qS9yOYVUJPs(Lcom/google/firebase/sessions/EventGDTLogger;Lcom/google/firebase/sessions/SessionEvent;)[B +HSPLcom/google/firebase/sessions/EventGDTLogger;->$r8$lambda$W13Olas0VV9EajY9qS9yOYVUJPs(Lcom/google/firebase/sessions/EventGDTLogger;Lcom/google/firebase/sessions/SessionEvent;)[B HSPLcom/google/firebase/sessions/EventGDTLogger;->()V HSPLcom/google/firebase/sessions/EventGDTLogger;->(Lcom/google/firebase/inject/Provider;)V -PLcom/google/firebase/sessions/EventGDTLogger;->encode(Lcom/google/firebase/sessions/SessionEvent;)[B -PLcom/google/firebase/sessions/EventGDTLogger;->log(Lcom/google/firebase/sessions/SessionEvent;)V -PLcom/google/firebase/sessions/EventGDTLogger$$ExternalSyntheticLambda0;->(Lcom/google/firebase/sessions/EventGDTLogger;)V -PLcom/google/firebase/sessions/EventGDTLogger$$ExternalSyntheticLambda0;->apply(Ljava/lang/Object;)Ljava/lang/Object; +HSPLcom/google/firebase/sessions/EventGDTLogger;->encode(Lcom/google/firebase/sessions/SessionEvent;)[B +HSPLcom/google/firebase/sessions/EventGDTLogger;->log(Lcom/google/firebase/sessions/SessionEvent;)V +Lcom/google/firebase/sessions/EventGDTLogger$$ExternalSyntheticLambda0; +HSPLcom/google/firebase/sessions/EventGDTLogger$$ExternalSyntheticLambda0;->(Lcom/google/firebase/sessions/EventGDTLogger;)V +HSPLcom/google/firebase/sessions/EventGDTLogger$$ExternalSyntheticLambda0;->apply(Ljava/lang/Object;)Ljava/lang/Object; Lcom/google/firebase/sessions/EventGDTLogger$Companion; HSPLcom/google/firebase/sessions/EventGDTLogger$Companion;->()V HSPLcom/google/firebase/sessions/EventGDTLogger$Companion;->(Lkotlin/jvm/internal/DefaultConstructorMarker;)V @@ -28818,10 +29032,11 @@ HSPLcom/google/firebase/sessions/EventGDTLogger_Factory;->create(Lcom/google/fir HSPLcom/google/firebase/sessions/EventGDTLogger_Factory;->get()Lcom/google/firebase/sessions/EventGDTLogger; HSPLcom/google/firebase/sessions/EventGDTLogger_Factory;->get()Ljava/lang/Object; HSPLcom/google/firebase/sessions/EventGDTLogger_Factory;->newInstance(Lcom/google/firebase/inject/Provider;)Lcom/google/firebase/sessions/EventGDTLogger; -PLcom/google/firebase/sessions/EventType;->$values()[Lcom/google/firebase/sessions/EventType; -PLcom/google/firebase/sessions/EventType;->()V -PLcom/google/firebase/sessions/EventType;->(Ljava/lang/String;II)V -PLcom/google/firebase/sessions/EventType;->getNumber()I +Lcom/google/firebase/sessions/EventType; +HSPLcom/google/firebase/sessions/EventType;->$values()[Lcom/google/firebase/sessions/EventType; +HSPLcom/google/firebase/sessions/EventType;->()V +HSPLcom/google/firebase/sessions/EventType;->(Ljava/lang/String;II)V +HSPLcom/google/firebase/sessions/EventType;->getNumber()I Lcom/google/firebase/sessions/FirebaseSessions; HSPLcom/google/firebase/sessions/FirebaseSessions;->()V HSPLcom/google/firebase/sessions/FirebaseSessions;->(Lcom/google/firebase/FirebaseApp;Lcom/google/firebase/sessions/settings/SessionsSettings;Lkotlin/coroutines/CoroutineContext;Lcom/google/firebase/sessions/SessionsActivityLifecycleCallbacks;)V @@ -28932,7 +29147,7 @@ Lcom/google/firebase/sessions/LogEnvironment; HSPLcom/google/firebase/sessions/LogEnvironment;->$values()[Lcom/google/firebase/sessions/LogEnvironment; HSPLcom/google/firebase/sessions/LogEnvironment;->()V HSPLcom/google/firebase/sessions/LogEnvironment;->(Ljava/lang/String;II)V -PLcom/google/firebase/sessions/LogEnvironment;->getNumber()I +HSPLcom/google/firebase/sessions/LogEnvironment;->getNumber()I Lcom/google/firebase/sessions/ProcessData; HSPLcom/google/firebase/sessions/ProcessData;->()V HSPLcom/google/firebase/sessions/ProcessData;->(IILjava/lang/String;Lkotlinx/serialization/internal/SerializationConstructorMarker;)V @@ -28987,10 +29202,10 @@ HSPLcom/google/firebase/sessions/ProcessDataManagerImpl_Factory;->get()Ljava/lan HSPLcom/google/firebase/sessions/ProcessDataManagerImpl_Factory;->newInstance(Landroid/content/Context;Lcom/google/firebase/sessions/UuidGenerator;)Lcom/google/firebase/sessions/ProcessDataManagerImpl; Lcom/google/firebase/sessions/ProcessDetails; HSPLcom/google/firebase/sessions/ProcessDetails;->(Ljava/lang/String;IIZ)V -PLcom/google/firebase/sessions/ProcessDetails;->getImportance()I +HSPLcom/google/firebase/sessions/ProcessDetails;->getImportance()I HSPLcom/google/firebase/sessions/ProcessDetails;->getPid()I HSPLcom/google/firebase/sessions/ProcessDetails;->getProcessName()Ljava/lang/String; -PLcom/google/firebase/sessions/ProcessDetails;->isDefaultProcess()Z +HSPLcom/google/firebase/sessions/ProcessDetails;->isDefaultProcess()Z Lcom/google/firebase/sessions/ProcessDetailsProvider; HSPLcom/google/firebase/sessions/ProcessDetailsProvider;->()V HSPLcom/google/firebase/sessions/ProcessDetailsProvider;->()V @@ -29044,10 +29259,10 @@ HSPLcom/google/firebase/sessions/SessionDetails;->()V HSPLcom/google/firebase/sessions/SessionDetails;->(ILjava/lang/String;Ljava/lang/String;IJLkotlinx/serialization/internal/SerializationConstructorMarker;)V HSPLcom/google/firebase/sessions/SessionDetails;->(Ljava/lang/String;Ljava/lang/String;IJ)V HSPLcom/google/firebase/sessions/SessionDetails;->equals(Ljava/lang/Object;)Z -PLcom/google/firebase/sessions/SessionDetails;->getFirstSessionId()Ljava/lang/String; +HSPLcom/google/firebase/sessions/SessionDetails;->getFirstSessionId()Ljava/lang/String; HSPLcom/google/firebase/sessions/SessionDetails;->getSessionId()Ljava/lang/String; -PLcom/google/firebase/sessions/SessionDetails;->getSessionIndex()I -PLcom/google/firebase/sessions/SessionDetails;->getSessionStartTimestampUs()J +HSPLcom/google/firebase/sessions/SessionDetails;->getSessionIndex()I +HSPLcom/google/firebase/sessions/SessionDetails;->getSessionStartTimestampUs()J HSPLcom/google/firebase/sessions/SessionDetails;->hashCode()I HSPLcom/google/firebase/sessions/SessionDetails;->write$Self$com_google_firebase_firebase_sessions(Lcom/google/firebase/sessions/SessionDetails;Lkotlinx/serialization/encoding/CompositeEncoder;Lkotlinx/serialization/descriptors/SerialDescriptor;)V Lcom/google/firebase/sessions/SessionDetails$$serializer; @@ -29061,17 +29276,17 @@ Lcom/google/firebase/sessions/SessionDetails$Companion; HSPLcom/google/firebase/sessions/SessionDetails$Companion;->()V HSPLcom/google/firebase/sessions/SessionDetails$Companion;->(Lkotlin/jvm/internal/DefaultConstructorMarker;)V Lcom/google/firebase/sessions/SessionEvent; -PLcom/google/firebase/sessions/SessionEvent;->(Lcom/google/firebase/sessions/EventType;Lcom/google/firebase/sessions/SessionInfo;Lcom/google/firebase/sessions/ApplicationInfo;)V -PLcom/google/firebase/sessions/SessionEvent;->getApplicationInfo()Lcom/google/firebase/sessions/ApplicationInfo; -PLcom/google/firebase/sessions/SessionEvent;->getEventType()Lcom/google/firebase/sessions/EventType; -PLcom/google/firebase/sessions/SessionEvent;->getSessionData()Lcom/google/firebase/sessions/SessionInfo; +HSPLcom/google/firebase/sessions/SessionEvent;->(Lcom/google/firebase/sessions/EventType;Lcom/google/firebase/sessions/SessionInfo;Lcom/google/firebase/sessions/ApplicationInfo;)V +HSPLcom/google/firebase/sessions/SessionEvent;->getApplicationInfo()Lcom/google/firebase/sessions/ApplicationInfo; +HSPLcom/google/firebase/sessions/SessionEvent;->getEventType()Lcom/google/firebase/sessions/EventType; +HSPLcom/google/firebase/sessions/SessionEvent;->getSessionData()Lcom/google/firebase/sessions/SessionInfo; Lcom/google/firebase/sessions/SessionEvents; HSPLcom/google/firebase/sessions/SessionEvents;->()V HSPLcom/google/firebase/sessions/SessionEvents;->()V -PLcom/google/firebase/sessions/SessionEvents;->buildSession(Lcom/google/firebase/FirebaseApp;Lcom/google/firebase/sessions/SessionDetails;Lcom/google/firebase/sessions/settings/SessionsSettings;Ljava/util/Map;Ljava/lang/String;Ljava/lang/String;)Lcom/google/firebase/sessions/SessionEvent; +HSPLcom/google/firebase/sessions/SessionEvents;->buildSession(Lcom/google/firebase/FirebaseApp;Lcom/google/firebase/sessions/SessionDetails;Lcom/google/firebase/sessions/settings/SessionsSettings;Ljava/util/Map;Ljava/lang/String;Ljava/lang/String;)Lcom/google/firebase/sessions/SessionEvent; HSPLcom/google/firebase/sessions/SessionEvents;->getApplicationInfo(Lcom/google/firebase/FirebaseApp;)Lcom/google/firebase/sessions/ApplicationInfo; -PLcom/google/firebase/sessions/SessionEvents;->getSESSION_EVENT_ENCODER$com_google_firebase_firebase_sessions()Lcom/google/firebase/encoders/DataEncoder; -PLcom/google/firebase/sessions/SessionEvents;->toDataCollectionState(Lcom/google/firebase/sessions/api/SessionSubscriber;)Lcom/google/firebase/sessions/DataCollectionState; +HSPLcom/google/firebase/sessions/SessionEvents;->getSESSION_EVENT_ENCODER$com_google_firebase_firebase_sessions()Lcom/google/firebase/encoders/DataEncoder; +HSPLcom/google/firebase/sessions/SessionEvents;->toDataCollectionState(Lcom/google/firebase/sessions/api/SessionSubscriber;)Lcom/google/firebase/sessions/DataCollectionState; Lcom/google/firebase/sessions/SessionFirelogPublisher; Lcom/google/firebase/sessions/SessionFirelogPublisherImpl; HSPLcom/google/firebase/sessions/SessionFirelogPublisherImpl;->()V @@ -29088,7 +29303,7 @@ HSPLcom/google/firebase/sessions/SessionFirelogPublisherImpl$mayLogSession$1;->c HSPLcom/google/firebase/sessions/SessionFirelogPublisherImpl$mayLogSession$1;->invokeSuspend(Ljava/lang/Object;)Ljava/lang/Object; Lcom/google/firebase/sessions/SessionFirelogPublisherImpl$shouldLogSession$1; HSPLcom/google/firebase/sessions/SessionFirelogPublisherImpl$shouldLogSession$1;->(Lcom/google/firebase/sessions/SessionFirelogPublisherImpl;Lkotlin/coroutines/Continuation;)V -PLcom/google/firebase/sessions/SessionFirelogPublisherImpl$shouldLogSession$1;->invokeSuspend(Ljava/lang/Object;)Ljava/lang/Object; +HSPLcom/google/firebase/sessions/SessionFirelogPublisherImpl$shouldLogSession$1;->invokeSuspend(Ljava/lang/Object;)Ljava/lang/Object; Lcom/google/firebase/sessions/SessionFirelogPublisherImpl_Factory; HSPLcom/google/firebase/sessions/SessionFirelogPublisherImpl_Factory;->(Lcom/google/firebase/sessions/dagger/internal/Provider;Lcom/google/firebase/sessions/dagger/internal/Provider;Lcom/google/firebase/sessions/dagger/internal/Provider;Lcom/google/firebase/sessions/dagger/internal/Provider;Lcom/google/firebase/sessions/dagger/internal/Provider;)V HSPLcom/google/firebase/sessions/SessionFirelogPublisherImpl_Factory;->create(Lcom/google/firebase/sessions/dagger/internal/Provider;Lcom/google/firebase/sessions/dagger/internal/Provider;Lcom/google/firebase/sessions/dagger/internal/Provider;Lcom/google/firebase/sessions/dagger/internal/Provider;Lcom/google/firebase/sessions/dagger/internal/Provider;)Lcom/google/firebase/sessions/SessionFirelogPublisherImpl_Factory; @@ -29106,14 +29321,14 @@ HSPLcom/google/firebase/sessions/SessionGenerator_Factory;->get()Lcom/google/fir HSPLcom/google/firebase/sessions/SessionGenerator_Factory;->get()Ljava/lang/Object; HSPLcom/google/firebase/sessions/SessionGenerator_Factory;->newInstance(Lcom/google/firebase/sessions/TimeProvider;Lcom/google/firebase/sessions/UuidGenerator;)Lcom/google/firebase/sessions/SessionGenerator; Lcom/google/firebase/sessions/SessionInfo; -PLcom/google/firebase/sessions/SessionInfo;->(Ljava/lang/String;Ljava/lang/String;IJLcom/google/firebase/sessions/DataCollectionStatus;Ljava/lang/String;Ljava/lang/String;)V -PLcom/google/firebase/sessions/SessionInfo;->getDataCollectionStatus()Lcom/google/firebase/sessions/DataCollectionStatus; -PLcom/google/firebase/sessions/SessionInfo;->getEventTimestampUs()J -PLcom/google/firebase/sessions/SessionInfo;->getFirebaseAuthenticationToken()Ljava/lang/String; -PLcom/google/firebase/sessions/SessionInfo;->getFirebaseInstallationId()Ljava/lang/String; -PLcom/google/firebase/sessions/SessionInfo;->getFirstSessionId()Ljava/lang/String; -PLcom/google/firebase/sessions/SessionInfo;->getSessionId()Ljava/lang/String; -PLcom/google/firebase/sessions/SessionInfo;->getSessionIndex()I +HSPLcom/google/firebase/sessions/SessionInfo;->(Ljava/lang/String;Ljava/lang/String;IJLcom/google/firebase/sessions/DataCollectionStatus;Ljava/lang/String;Ljava/lang/String;)V +HSPLcom/google/firebase/sessions/SessionInfo;->getDataCollectionStatus()Lcom/google/firebase/sessions/DataCollectionStatus; +HSPLcom/google/firebase/sessions/SessionInfo;->getEventTimestampUs()J +HSPLcom/google/firebase/sessions/SessionInfo;->getFirebaseAuthenticationToken()Ljava/lang/String; +HSPLcom/google/firebase/sessions/SessionInfo;->getFirebaseInstallationId()Ljava/lang/String; +HSPLcom/google/firebase/sessions/SessionInfo;->getFirstSessionId()Ljava/lang/String; +HSPLcom/google/firebase/sessions/SessionInfo;->getSessionId()Ljava/lang/String; +HSPLcom/google/firebase/sessions/SessionInfo;->getSessionIndex()I Lcom/google/firebase/sessions/SessionsActivityLifecycleCallbacks; HSPLcom/google/firebase/sessions/SessionsActivityLifecycleCallbacks;->(Lcom/google/firebase/sessions/SharedSessionRepository;)V HSPLcom/google/firebase/sessions/SessionsActivityLifecycleCallbacks;->onActivityCreated(Landroid/app/Activity;Landroid/os/Bundle;)V @@ -29250,7 +29465,7 @@ Lcom/google/firebase/sessions/settings/CrashlyticsSettingsFetcher; Lcom/google/firebase/sessions/settings/LocalOverrideSettings; HSPLcom/google/firebase/sessions/settings/LocalOverrideSettings;->()V HSPLcom/google/firebase/sessions/settings/LocalOverrideSettings;->(Landroid/content/Context;)V -PLcom/google/firebase/sessions/settings/LocalOverrideSettings;->getSamplingRate()Ljava/lang/Double; +HSPLcom/google/firebase/sessions/settings/LocalOverrideSettings;->getSamplingRate()Ljava/lang/Double; HSPLcom/google/firebase/sessions/settings/LocalOverrideSettings;->getSessionEnabled()Ljava/lang/Boolean; HSPLcom/google/firebase/sessions/settings/LocalOverrideSettings;->updateSettings(Lkotlin/coroutines/Continuation;)Ljava/lang/Object; Lcom/google/firebase/sessions/settings/LocalOverrideSettings$Companion; @@ -29265,7 +29480,7 @@ HSPLcom/google/firebase/sessions/settings/LocalOverrideSettings_Factory;->newIns Lcom/google/firebase/sessions/settings/RemoteSettings; HSPLcom/google/firebase/sessions/settings/RemoteSettings;->()V HSPLcom/google/firebase/sessions/settings/RemoteSettings;->(Lcom/google/firebase/sessions/TimeProvider;Lcom/google/firebase/installations/FirebaseInstallationsApi;Lcom/google/firebase/sessions/ApplicationInfo;Lcom/google/firebase/sessions/settings/CrashlyticsSettingsFetcher;Lcom/google/firebase/sessions/settings/SettingsCache;)V -PLcom/google/firebase/sessions/settings/RemoteSettings;->getSamplingRate()Ljava/lang/Double; +HSPLcom/google/firebase/sessions/settings/RemoteSettings;->getSamplingRate()Ljava/lang/Double; HSPLcom/google/firebase/sessions/settings/RemoteSettings;->getSessionEnabled()Ljava/lang/Boolean; HSPLcom/google/firebase/sessions/settings/RemoteSettings;->sanitize(Ljava/lang/String;)Ljava/lang/String; HSPLcom/google/firebase/sessions/settings/RemoteSettings;->updateSettings(Lkotlin/coroutines/Continuation;)Ljava/lang/Object; @@ -29314,7 +29529,7 @@ HSPLcom/google/firebase/sessions/settings/SessionConfigs;->(Ljava/lang/Boo HSPLcom/google/firebase/sessions/settings/SessionConfigs;->equals(Ljava/lang/Object;)Z HSPLcom/google/firebase/sessions/settings/SessionConfigs;->getCacheDurationSeconds()Ljava/lang/Integer; HSPLcom/google/firebase/sessions/settings/SessionConfigs;->getCacheUpdatedTimeSeconds()Ljava/lang/Long; -PLcom/google/firebase/sessions/settings/SessionConfigs;->getSessionSamplingRate()Ljava/lang/Double; +HSPLcom/google/firebase/sessions/settings/SessionConfigs;->getSessionSamplingRate()Ljava/lang/Double; HSPLcom/google/firebase/sessions/settings/SessionConfigs;->getSessionsEnabled()Ljava/lang/Boolean; HSPLcom/google/firebase/sessions/settings/SessionConfigs;->hashCode()I Lcom/google/firebase/sessions/settings/SessionConfigs$Companion; @@ -29328,7 +29543,7 @@ HSPLcom/google/firebase/sessions/settings/SessionConfigsSerializer;->getDefaultV Lcom/google/firebase/sessions/settings/SessionsSettings; HSPLcom/google/firebase/sessions/settings/SessionsSettings;->()V HSPLcom/google/firebase/sessions/settings/SessionsSettings;->(Lcom/google/firebase/sessions/settings/SettingsProvider;Lcom/google/firebase/sessions/settings/SettingsProvider;)V -PLcom/google/firebase/sessions/settings/SessionsSettings;->getSamplingRate()D +HSPLcom/google/firebase/sessions/settings/SessionsSettings;->getSamplingRate()D HSPLcom/google/firebase/sessions/settings/SessionsSettings;->updateSettings(Lkotlin/coroutines/Continuation;)Ljava/lang/Object; Lcom/google/firebase/sessions/settings/SessionsSettings$Companion; HSPLcom/google/firebase/sessions/settings/SessionsSettings$Companion;->()V @@ -29349,7 +29564,7 @@ HSPLcom/google/firebase/sessions/settings/SettingsCacheImpl;->access$getSessionC HSPLcom/google/firebase/sessions/settings/SettingsCacheImpl;->access$getSessionConfigsDataStore$p(Lcom/google/firebase/sessions/settings/SettingsCacheImpl;)Landroidx/datastore/core/DataStore; HSPLcom/google/firebase/sessions/settings/SettingsCacheImpl;->getSessionConfigs()Lcom/google/firebase/sessions/settings/SessionConfigs; HSPLcom/google/firebase/sessions/settings/SettingsCacheImpl;->hasCacheExpired()Z -PLcom/google/firebase/sessions/settings/SettingsCacheImpl;->sessionSamplingRate()Ljava/lang/Double; +HSPLcom/google/firebase/sessions/settings/SettingsCacheImpl;->sessionSamplingRate()Ljava/lang/Double; HSPLcom/google/firebase/sessions/settings/SettingsCacheImpl;->sessionsEnabled()Ljava/lang/Boolean; Lcom/google/firebase/sessions/settings/SettingsCacheImpl$1; HSPLcom/google/firebase/sessions/settings/SettingsCacheImpl$1;->(Lcom/google/firebase/sessions/settings/SettingsCacheImpl;Lkotlin/coroutines/Continuation;)V @@ -29917,6 +30132,7 @@ HSPLcom/squareup/wire/FieldEncoding;->(Ljava/lang/String;II)V Lcom/squareup/wire/FieldEncoding$Companion; HSPLcom/squareup/wire/FieldEncoding$Companion;->()V HSPLcom/squareup/wire/FieldEncoding$Companion;->(Lkotlin/jvm/internal/DefaultConstructorMarker;)V +Lcom/squareup/wire/FieldMask; Lcom/squareup/wire/FloatArrayProtoAdapter; HSPLcom/squareup/wire/FloatArrayProtoAdapter;->(Lcom/squareup/wire/ProtoAdapter;)V Lcom/squareup/wire/FloatProtoAdapter; @@ -29929,6 +30145,9 @@ Lcom/squareup/wire/Message; HSPLcom/squareup/wire/Message;->()V HSPLcom/squareup/wire/Message;->(Lcom/squareup/wire/ProtoAdapter;Lokio/ByteString;)V HSPLcom/squareup/wire/Message;->unknownFields()Lokio/ByteString; +Lcom/squareup/wire/Message$Builder; +HSPLcom/squareup/wire/Message$Builder;->()V +HSPLcom/squareup/wire/Message$Builder;->buildUnknownFields()Lokio/ByteString; Lcom/squareup/wire/Message$Companion; HSPLcom/squareup/wire/Message$Companion;->()V HSPLcom/squareup/wire/Message$Companion;->(Lkotlin/jvm/internal/DefaultConstructorMarker;)V @@ -29953,6 +30172,7 @@ HSPLcom/squareup/wire/ProtoAdapterKt;->commonBytes()Lcom/squareup/wire/ProtoAdap HSPLcom/squareup/wire/ProtoAdapterKt;->commonDouble()Lcom/squareup/wire/DoubleProtoAdapter; HSPLcom/squareup/wire/ProtoAdapterKt;->commonDuration()Lcom/squareup/wire/ProtoAdapter; HSPLcom/squareup/wire/ProtoAdapterKt;->commonEmpty()Lcom/squareup/wire/ProtoAdapter; +HSPLcom/squareup/wire/ProtoAdapterKt;->commonFieldMask()Lcom/squareup/wire/ProtoAdapter; HSPLcom/squareup/wire/ProtoAdapterKt;->commonFixed32()Lcom/squareup/wire/ProtoAdapter; HSPLcom/squareup/wire/ProtoAdapterKt;->commonFixed64()Lcom/squareup/wire/ProtoAdapter; HSPLcom/squareup/wire/ProtoAdapterKt;->commonFloat()Lcom/squareup/wire/FloatProtoAdapter; @@ -29979,6 +30199,8 @@ Lcom/squareup/wire/ProtoAdapterKt$commonDuration$1; HSPLcom/squareup/wire/ProtoAdapterKt$commonDuration$1;->(Lcom/squareup/wire/FieldEncoding;Lkotlin/reflect/KClass;Lcom/squareup/wire/Syntax;)V Lcom/squareup/wire/ProtoAdapterKt$commonEmpty$1; HSPLcom/squareup/wire/ProtoAdapterKt$commonEmpty$1;->(Lcom/squareup/wire/FieldEncoding;Lkotlin/reflect/KClass;Lcom/squareup/wire/Syntax;)V +Lcom/squareup/wire/ProtoAdapterKt$commonFieldMask$1; +HSPLcom/squareup/wire/ProtoAdapterKt$commonFieldMask$1;->(Lcom/squareup/wire/FieldEncoding;Lkotlin/reflect/KClass;Lcom/squareup/wire/Syntax;)V Lcom/squareup/wire/ProtoAdapterKt$commonFixed32$1; HSPLcom/squareup/wire/ProtoAdapterKt$commonFixed32$1;->(Lcom/squareup/wire/FieldEncoding;Lkotlin/reflect/KClass;Lcom/squareup/wire/Syntax;)V Lcom/squareup/wire/ProtoAdapterKt$commonFixed64$1; @@ -30019,6 +30241,11 @@ Lcom/squareup/wire/Syntax$Companion; HSPLcom/squareup/wire/Syntax$Companion;->()V HSPLcom/squareup/wire/Syntax$Companion;->(Lkotlin/jvm/internal/DefaultConstructorMarker;)V Lcom/squareup/wire/WireEnum; +Lcom/squareup/wire/internal/Internal; +HSPLcom/squareup/wire/internal/Internal;->immutableCopyOf(Ljava/lang/String;Ljava/util/List;)Ljava/util/List; +Lcom/squareup/wire/internal/Internal__InternalKt; +HSPLcom/squareup/wire/internal/Internal__InternalKt;->immutableCopyOf(Ljava/lang/String;Ljava/util/List;)Ljava/util/List; +Lcom/squareup/wire/internal/MutableOnWriteList; Lio/ktor/client/HttpClient; HSPLio/ktor/client/HttpClient;->()V HSPLio/ktor/client/HttpClient;->(Lio/ktor/client/engine/HttpClientEngine;Lio/ktor/client/HttpClientConfig;)V @@ -30128,8 +30355,8 @@ HSPLio/ktor/client/engine/okhttp/OkHttpEngine;->getConfig()Lio/ktor/client/engin HSPLio/ktor/client/engine/okhttp/OkHttpEngine;->getCoroutineContext()Lkotlin/coroutines/CoroutineContext; Lio/ktor/client/engine/okhttp/OkHttpEngine$$ExternalSyntheticLambda0; HSPLio/ktor/client/engine/okhttp/OkHttpEngine$$ExternalSyntheticLambda0;->()V -Lio/ktor/client/engine/okhttp/OkHttpEngine$$ExternalSyntheticLambda2; -HSPLio/ktor/client/engine/okhttp/OkHttpEngine$$ExternalSyntheticLambda2;->()V +Lio/ktor/client/engine/okhttp/OkHttpEngine$$ExternalSyntheticLambda1; +HSPLio/ktor/client/engine/okhttp/OkHttpEngine$$ExternalSyntheticLambda1;->()V Lio/ktor/client/engine/okhttp/OkHttpEngine$1; HSPLio/ktor/client/engine/okhttp/OkHttpEngine$1;->(Lio/ktor/client/engine/okhttp/OkHttpEngine;Lkotlin/coroutines/Continuation;)V HSPLio/ktor/client/engine/okhttp/OkHttpEngine$1;->create(Ljava/lang/Object;Lkotlin/coroutines/Continuation;)Lkotlin/coroutines/Continuation; @@ -30533,9 +30760,19 @@ HSPLio/ktor/client/plugins/contentnegotiation/ContentNegotiationKt$ContentNegoti HSPLio/ktor/client/plugins/contentnegotiation/ContentNegotiationKt$ContentNegotiation$1;->invoke()Lio/ktor/client/plugins/contentnegotiation/ContentNegotiationConfig; HSPLio/ktor/client/plugins/contentnegotiation/ContentNegotiationKt$ContentNegotiation$1;->invoke()Ljava/lang/Object; Lio/ktor/client/plugins/contentnegotiation/ContentNegotiationKt$ContentNegotiation$2$1; -HSPLio/ktor/client/plugins/contentnegotiation/ContentNegotiationKt$ContentNegotiation$2$1;->(Ljava/util/List;Ljava/util/Set;Lio/ktor/client/plugins/api/ClientPluginBuilder;Lkotlin/coroutines/Continuation;)V +HSPLio/ktor/client/plugins/contentnegotiation/ContentNegotiationKt$ContentNegotiation$2$1;->(Ljava/util/List;Lio/ktor/client/plugins/api/ClientPluginBuilder;Ljava/util/Set;Lkotlin/coroutines/Continuation;)V Lio/ktor/client/plugins/contentnegotiation/ContentNegotiationKt$ContentNegotiation$2$2; HSPLio/ktor/client/plugins/contentnegotiation/ContentNegotiationKt$ContentNegotiation$2$2;->(Ljava/util/Set;Ljava/util/List;Lio/ktor/client/plugins/api/ClientPluginBuilder;Lkotlin/coroutines/Continuation;)V +Lio/ktor/client/plugins/contentnegotiation/ContentTypeMergeStrategy; +HSPLio/ktor/client/plugins/contentnegotiation/ContentTypeMergeStrategy;->()V +Lio/ktor/client/plugins/contentnegotiation/ContentTypeMergeStrategy$Companion; +HSPLio/ktor/client/plugins/contentnegotiation/ContentTypeMergeStrategy$Companion;->()V +HSPLio/ktor/client/plugins/contentnegotiation/ContentTypeMergeStrategy$Companion;->()V +HSPLio/ktor/client/plugins/contentnegotiation/ContentTypeMergeStrategy$Companion;->getDefault()Lio/ktor/client/plugins/contentnegotiation/ContentTypeMergeStrategy; +Lio/ktor/client/plugins/contentnegotiation/ContentTypeMergeStrategy$Companion$$ExternalSyntheticLambda2; +HSPLio/ktor/client/plugins/contentnegotiation/ContentTypeMergeStrategy$Companion$$ExternalSyntheticLambda2;->()V +Lio/ktor/client/plugins/contentnegotiation/ContentTypeMergeStrategy$Companion$$ExternalSyntheticLambda3; +HSPLio/ktor/client/plugins/contentnegotiation/ContentTypeMergeStrategy$Companion$$ExternalSyntheticLambda3;->()V Lio/ktor/client/plugins/contentnegotiation/DefaultIgnoredTypesJvmKt; HSPLio/ktor/client/plugins/contentnegotiation/DefaultIgnoredTypesJvmKt;->()V HSPLio/ktor/client/plugins/contentnegotiation/DefaultIgnoredTypesJvmKt;->getDefaultIgnoredTypes()Ljava/util/Set; @@ -30610,6 +30847,7 @@ Lio/ktor/events/EventDefinition; HSPLio/ktor/events/EventDefinition;->()V Lio/ktor/events/Events; HSPLio/ktor/events/Events;->()V +Lio/ktor/http/BadContentTypeFormatException; Lio/ktor/http/ContentType; HSPLio/ktor/http/ContentType;->()V HSPLio/ktor/http/ContentType;->(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/util/List;)V @@ -30644,6 +30882,19 @@ HSPLio/ktor/http/HttpMethod$Companion;->()V HSPLio/ktor/http/HttpMethod$Companion;->(Lkotlin/jvm/internal/DefaultConstructorMarker;)V Lio/ktor/http/HttpStatusCode; Lio/ktor/http/content/OutgoingContent; +Lio/ktor/openapi/JsonSchemaInference; +Lio/ktor/openapi/JsonSchemaInferenceKt; +HSPLio/ktor/openapi/JsonSchemaInferenceKt;->()V +HSPLio/ktor/openapi/JsonSchemaInferenceKt;->getKotlinxSerializerDefaultFormats()Lkotlin/jvm/functions/Function1; +Lio/ktor/openapi/JsonSchemaInferenceKt$$ExternalSyntheticLambda0; +HSPLio/ktor/openapi/JsonSchemaInferenceKt$$ExternalSyntheticLambda0;->()V +Lio/ktor/openapi/KotlinxSerializerJsonSchemaInference; +HSPLio/ktor/openapi/KotlinxSerializerJsonSchemaInference;->()V +HSPLio/ktor/openapi/KotlinxSerializerJsonSchemaInference;->(Lkotlinx/serialization/modules/SerializersModule;Lkotlin/jvm/functions/Function1;)V +HSPLio/ktor/openapi/KotlinxSerializerJsonSchemaInference;->(Lkotlinx/serialization/modules/SerializersModule;Lkotlin/jvm/functions/Function1;ILkotlin/jvm/internal/DefaultConstructorMarker;)V +Lio/ktor/openapi/KotlinxSerializerJsonSchemaInference$Companion; +HSPLio/ktor/openapi/KotlinxSerializerJsonSchemaInference$Companion;->()V +HSPLio/ktor/openapi/KotlinxSerializerJsonSchemaInference$Companion;->(Lkotlin/jvm/internal/DefaultConstructorMarker;)V Lio/ktor/serialization/Configuration; HSPLio/ktor/serialization/Configuration;->register$default(Lio/ktor/serialization/Configuration;Lio/ktor/http/ContentType;Lio/ktor/serialization/ContentConverter;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)V HSPLio/ktor/serialization/Configuration;->register$lambda$0(Lio/ktor/serialization/ContentConverter;)Lkotlin/Unit; @@ -30722,7 +30973,12 @@ Lio/ktor/util/collections/CopyOnWriteHashMap; HSPLio/ktor/util/collections/CopyOnWriteHashMap;->()V HSPLio/ktor/util/collections/CopyOnWriteHashMap;->()V Lio/ktor/util/logging/KtorSimpleLoggerJvmKt; +HSPLio/ktor/util/logging/KtorSimpleLoggerJvmKt;->()V HSPLio/ktor/util/logging/KtorSimpleLoggerJvmKt;->KtorSimpleLogger(Ljava/lang/String;)Lorg/slf4j/Logger; +Lio/ktor/util/logging/KtorSimpleLoggerJvmKt$KtorSimpleLogger$1; +HSPLio/ktor/util/logging/KtorSimpleLoggerJvmKt$KtorSimpleLogger$1;->(Ljava/lang/String;)V +Lio/ktor/util/logging/KtorSimpleLoggerJvmKt$KtorSimpleLogger$1$$ExternalSyntheticLambda0; +HSPLio/ktor/util/logging/KtorSimpleLoggerJvmKt$KtorSimpleLogger$1$$ExternalSyntheticLambda0;->(Ljava/lang/String;)V Lio/ktor/util/pipeline/PhaseContent; HSPLio/ktor/util/pipeline/PhaseContent;->()V HSPLio/ktor/util/pipeline/PhaseContent;->(Lio/ktor/util/pipeline/PipelinePhase;Lio/ktor/util/pipeline/PipelinePhaseRelation;)V @@ -30760,7 +31016,7 @@ Lio/ktor/util/pipeline/PipelinePhaseRelation$Last; HSPLio/ktor/util/pipeline/PipelinePhaseRelation$Last;->()V HSPLio/ktor/util/pipeline/PipelinePhaseRelation$Last;->()V Lio/ktor/util/reflect/TypeInfo; -HSPLio/ktor/util/reflect/TypeInfo;->(Lkotlin/reflect/KClass;Lkotlin/reflect/KType;)V +HSPLio/ktor/util/reflect/TypeInfo;->(Lkotlin/reflect/KClass;ZLkotlin/reflect/KType;)V HSPLio/ktor/util/reflect/TypeInfo;->hashCode()I Lio/ktor/utils/io/ByteReadChannel; Lio/ktor/utils/io/IODispatcher_jvmKt; @@ -30785,6 +31041,7 @@ HSPLio/opentelemetry/context/ContextStorageWrappers;->()V HSPLio/opentelemetry/context/ContextStorageWrappers;->addWrapper(Ljava/util/function/Function;)V Ljavax/inject/Provider; Lkotlin/Function; +Lkotlin/KotlinNothingValueException; Lkotlin/KotlinVersion; HSPLkotlin/KotlinVersion;->()V HSPLkotlin/KotlinVersion;->(III)V @@ -31362,7 +31619,7 @@ Lkotlin/coroutines/CombinedContext; HSPLkotlin/coroutines/CombinedContext;->(Lkotlin/coroutines/CoroutineContext;Lkotlin/coroutines/CoroutineContext$Element;)V HSPLkotlin/coroutines/CombinedContext;->fold(Ljava/lang/Object;Lkotlin/jvm/functions/Function2;)Ljava/lang/Object; HSPLkotlin/coroutines/CombinedContext;->get(Lkotlin/coroutines/CoroutineContext$Key;)Lkotlin/coroutines/CoroutineContext$Element; -HPLkotlin/coroutines/CombinedContext;->minusKey(Lkotlin/coroutines/CoroutineContext$Key;)Lkotlin/coroutines/CoroutineContext; +HSPLkotlin/coroutines/CombinedContext;->minusKey(Lkotlin/coroutines/CoroutineContext$Key;)Lkotlin/coroutines/CoroutineContext; HSPLkotlin/coroutines/CombinedContext;->plus(Lkotlin/coroutines/CoroutineContext;)Lkotlin/coroutines/CoroutineContext; Lkotlin/coroutines/Continuation; Lkotlin/coroutines/ContinuationInterceptor; @@ -31561,7 +31818,7 @@ Lkotlin/jdk7/AutoCloseableKt; HSPLkotlin/jdk7/AutoCloseableKt;->closeFinally(Ljava/lang/AutoCloseable;Ljava/lang/Throwable;)V Lkotlin/jvm/JvmClassMappingKt; HSPLkotlin/jvm/JvmClassMappingKt;->getJavaClass(Lkotlin/reflect/KClass;)Ljava/lang/Class; -HPLkotlin/jvm/JvmClassMappingKt;->getJavaObjectType(Lkotlin/reflect/KClass;)Ljava/lang/Class; +HSPLkotlin/jvm/JvmClassMappingKt;->getJavaObjectType(Lkotlin/reflect/KClass;)Ljava/lang/Class; HSPLkotlin/jvm/JvmClassMappingKt;->getKotlinClass(Ljava/lang/Class;)Lkotlin/reflect/KClass; Lkotlin/jvm/functions/Function0; Lkotlin/jvm/functions/Function1; @@ -31742,6 +31999,7 @@ HSPLkotlin/jvm/internal/TypeReference;->(Lkotlin/reflect/KClassifier;Ljava HSPLkotlin/jvm/internal/TypeReference;->getArguments()Ljava/util/List; HSPLkotlin/jvm/internal/TypeReference;->getClassifier()Lkotlin/reflect/KClassifier; HSPLkotlin/jvm/internal/TypeReference;->hashCode()I +HSPLkotlin/jvm/internal/TypeReference;->isMarkedNullable()Z Lkotlin/jvm/internal/TypeReference$Companion; HSPLkotlin/jvm/internal/TypeReference$Companion;->()V HSPLkotlin/jvm/internal/TypeReference$Companion;->(Lkotlin/jvm/internal/DefaultConstructorMarker;)V @@ -32108,8 +32366,8 @@ HSPLkotlin/time/Clock$System;->now()Lkotlin/time/Instant; Lkotlin/time/ComparableTimeMark; Lkotlin/time/Duration; HSPLkotlin/time/Duration;->()V -HSPLkotlin/time/Duration;->(J)V -HSPLkotlin/time/Duration;->box-impl(J)Lkotlin/time/Duration; +PLkotlin/time/Duration;->(J)V +PLkotlin/time/Duration;->box-impl(J)Lkotlin/time/Duration; HSPLkotlin/time/Duration;->constructor-impl(J)J HSPLkotlin/time/Duration;->getInWholeMicroseconds-impl(J)J HSPLkotlin/time/Duration;->getInWholeMilliseconds-impl(J)J @@ -32124,7 +32382,7 @@ HSPLkotlin/time/Duration;->isInfinite-impl(J)Z HSPLkotlin/time/Duration;->isPositive-impl(J)Z HSPLkotlin/time/Duration;->plus-LRDsOJo(JJ)J HSPLkotlin/time/Duration;->toLong-impl(JLkotlin/time/DurationUnit;)J -HSPLkotlin/time/Duration;->unbox-impl()J +PLkotlin/time/Duration;->unbox-impl()J Lkotlin/time/Duration$Companion; HSPLkotlin/time/Duration$Companion;->()V HSPLkotlin/time/Duration$Companion;->(Lkotlin/jvm/internal/DefaultConstructorMarker;)V @@ -32351,7 +32609,7 @@ Lkotlinx/coroutines/CancelHandler$UserSupplied; HSPLkotlinx/coroutines/CancelHandler$UserSupplied;->(Lkotlin/jvm/functions/Function1;)V HSPLkotlinx/coroutines/CancelHandler$UserSupplied;->invoke(Ljava/lang/Throwable;)V Lkotlinx/coroutines/CancellableContinuation; -HSPLkotlinx/coroutines/CancellableContinuation;->cancel$default(Lkotlinx/coroutines/CancellableContinuation;Ljava/lang/Throwable;ILjava/lang/Object;)Z +PLkotlinx/coroutines/CancellableContinuation;->cancel$default(Lkotlinx/coroutines/CancellableContinuation;Ljava/lang/Throwable;ILjava/lang/Object;)Z PLkotlinx/coroutines/CancellableContinuation$DefaultImpls;->cancel$default(Lkotlinx/coroutines/CancellableContinuation;Ljava/lang/Throwable;ILjava/lang/Object;)Z Lkotlinx/coroutines/CancellableContinuationImpl; HSPLkotlinx/coroutines/CancellableContinuationImpl;->()V @@ -32359,6 +32617,7 @@ HSPLkotlinx/coroutines/CancellableContinuationImpl;->(Lkotlin/coroutines/C HSPLkotlinx/coroutines/CancellableContinuationImpl;->callCancelHandler(Lkotlinx/coroutines/CancelHandler;Ljava/lang/Throwable;)V PLkotlinx/coroutines/CancellableContinuationImpl;->callSegmentOnCancellation(Lkotlinx/coroutines/internal/Segment;Ljava/lang/Throwable;)V HSPLkotlinx/coroutines/CancellableContinuationImpl;->cancel(Ljava/lang/Throwable;)Z +HSPLkotlinx/coroutines/CancellableContinuationImpl;->cancelCompletedResult$kotlinx_coroutines_core(Ljava/lang/Object;Ljava/lang/Throwable;)V HSPLkotlinx/coroutines/CancellableContinuationImpl;->cancelLater(Ljava/lang/Throwable;)Z HSPLkotlinx/coroutines/CancellableContinuationImpl;->completeResume(Ljava/lang/Object;)V HSPLkotlinx/coroutines/CancellableContinuationImpl;->detachChild$kotlinx_coroutines_core()V @@ -32389,6 +32648,7 @@ HSPLkotlinx/coroutines/CancellableContinuationImpl;->parentCancelled$kotlinx_cor HSPLkotlinx/coroutines/CancellableContinuationImpl;->releaseClaimedReusableContinuation$kotlinx_coroutines_core()V HSPLkotlinx/coroutines/CancellableContinuationImpl;->resetStateReusable()Z HSPLkotlinx/coroutines/CancellableContinuationImpl;->resume(Ljava/lang/Object;Lkotlin/jvm/functions/Function1;)V +HSPLkotlinx/coroutines/CancellableContinuationImpl;->resume(Ljava/lang/Object;Lkotlin/jvm/functions/Function3;)V HSPLkotlinx/coroutines/CancellableContinuationImpl;->resumeImpl$kotlinx_coroutines_core$default(Lkotlinx/coroutines/CancellableContinuationImpl;Ljava/lang/Object;ILkotlin/jvm/functions/Function3;ILjava/lang/Object;)V HSPLkotlinx/coroutines/CancellableContinuationImpl;->resumeImpl$kotlinx_coroutines_core(Ljava/lang/Object;ILkotlin/jvm/functions/Function3;)V PLkotlinx/coroutines/CancellableContinuationImpl;->resumeUndispatched(Lkotlinx/coroutines/CoroutineDispatcher;Ljava/lang/Object;)V @@ -32419,7 +32679,7 @@ Lkotlinx/coroutines/ChildHandle; Lkotlinx/coroutines/ChildHandleNode; HSPLkotlinx/coroutines/ChildHandleNode;->(Lkotlinx/coroutines/ChildJob;)V HSPLkotlinx/coroutines/ChildHandleNode;->childCancelled(Ljava/lang/Throwable;)Z -HSPLkotlinx/coroutines/ChildHandleNode;->getOnCancelling()Z +PLkotlinx/coroutines/ChildHandleNode;->getOnCancelling()Z HSPLkotlinx/coroutines/ChildHandleNode;->getParent()Lkotlinx/coroutines/Job; HSPLkotlinx/coroutines/ChildHandleNode;->invoke(Ljava/lang/Throwable;)V Lkotlinx/coroutines/ChildJob; @@ -32437,6 +32697,10 @@ Lkotlinx/coroutines/CompletableJob; Lkotlinx/coroutines/CompletedContinuation; HSPLkotlinx/coroutines/CompletedContinuation;->(Ljava/lang/Object;Lkotlinx/coroutines/CancelHandler;Lkotlin/jvm/functions/Function3;Ljava/lang/Object;Ljava/lang/Throwable;)V HSPLkotlinx/coroutines/CompletedContinuation;->(Ljava/lang/Object;Lkotlinx/coroutines/CancelHandler;Lkotlin/jvm/functions/Function3;Ljava/lang/Object;Ljava/lang/Throwable;ILkotlin/jvm/internal/DefaultConstructorMarker;)V +HSPLkotlinx/coroutines/CompletedContinuation;->copy$default(Lkotlinx/coroutines/CompletedContinuation;Ljava/lang/Object;Lkotlinx/coroutines/CancelHandler;Lkotlin/jvm/functions/Function3;Ljava/lang/Object;Ljava/lang/Throwable;ILjava/lang/Object;)Lkotlinx/coroutines/CompletedContinuation; +HSPLkotlinx/coroutines/CompletedContinuation;->copy(Ljava/lang/Object;Lkotlinx/coroutines/CancelHandler;Lkotlin/jvm/functions/Function3;Ljava/lang/Object;Ljava/lang/Throwable;)Lkotlinx/coroutines/CompletedContinuation; +HSPLkotlinx/coroutines/CompletedContinuation;->getCancelled()Z +HSPLkotlinx/coroutines/CompletedContinuation;->invokeHandlers(Lkotlinx/coroutines/CancellableContinuationImpl;Ljava/lang/Throwable;)V Lkotlinx/coroutines/CompletedExceptionally; HSPLkotlinx/coroutines/CompletedExceptionally;->()V HSPLkotlinx/coroutines/CompletedExceptionally;->(Ljava/lang/Throwable;Z)V @@ -32580,7 +32844,7 @@ HSPLkotlinx/coroutines/DisposeOnCancel;->(Lkotlinx/coroutines/DisposableHa HSPLkotlinx/coroutines/DisposeOnCancel;->invoke(Ljava/lang/Throwable;)V Lkotlinx/coroutines/DisposeOnCompletion; HSPLkotlinx/coroutines/DisposeOnCompletion;->(Lkotlinx/coroutines/DisposableHandle;)V -HSPLkotlinx/coroutines/DisposeOnCompletion;->getOnCancelling()Z +PLkotlinx/coroutines/DisposeOnCompletion;->getOnCancelling()Z HSPLkotlinx/coroutines/DisposeOnCompletion;->invoke(Ljava/lang/Throwable;)V Lkotlinx/coroutines/Empty; HSPLkotlinx/coroutines/Empty;->(Z)V @@ -32662,7 +32926,7 @@ HSPLkotlinx/coroutines/ExecutorCoroutineDispatcherImpl;->(Ljava/util/concu HSPLkotlinx/coroutines/ExecutorCoroutineDispatcherImpl;->dispatch(Lkotlin/coroutines/CoroutineContext;Ljava/lang/Runnable;)V HSPLkotlinx/coroutines/ExecutorCoroutineDispatcherImpl;->equals(Ljava/lang/Object;)Z HSPLkotlinx/coroutines/ExecutorCoroutineDispatcherImpl;->getExecutor()Ljava/util/concurrent/Executor; -HSPLkotlinx/coroutines/ExecutorCoroutineDispatcherImpl;->toString()Ljava/lang/String; +PLkotlinx/coroutines/ExecutorCoroutineDispatcherImpl;->toString()Ljava/lang/String; Lkotlinx/coroutines/ExecutorsKt; HSPLkotlinx/coroutines/ExecutorsKt;->asExecutor(Lkotlinx/coroutines/CoroutineDispatcher;)Ljava/util/concurrent/Executor; HSPLkotlinx/coroutines/ExecutorsKt;->from(Ljava/util/concurrent/Executor;)Lkotlinx/coroutines/CoroutineDispatcher; @@ -32694,7 +32958,7 @@ Lkotlinx/coroutines/Job; HSPLkotlinx/coroutines/Job;->()V HSPLkotlinx/coroutines/Job;->cancel$default(Lkotlinx/coroutines/Job;Ljava/util/concurrent/CancellationException;ILjava/lang/Object;)V Lkotlinx/coroutines/Job$DefaultImpls; -HSPLkotlinx/coroutines/Job$DefaultImpls;->cancel$default(Lkotlinx/coroutines/Job;Ljava/util/concurrent/CancellationException;ILjava/lang/Object;)V +PLkotlinx/coroutines/Job$DefaultImpls;->cancel$default(Lkotlinx/coroutines/Job;Ljava/util/concurrent/CancellationException;ILjava/lang/Object;)V HSPLkotlinx/coroutines/Job$DefaultImpls;->fold(Lkotlinx/coroutines/Job;Ljava/lang/Object;Lkotlin/jvm/functions/Function2;)Ljava/lang/Object; HSPLkotlinx/coroutines/Job$DefaultImpls;->get(Lkotlinx/coroutines/Job;Lkotlin/coroutines/CoroutineContext$Key;)Lkotlin/coroutines/CoroutineContext$Element; HSPLkotlinx/coroutines/Job$DefaultImpls;->minusKey(Lkotlinx/coroutines/Job;Lkotlin/coroutines/CoroutineContext$Key;)Lkotlin/coroutines/CoroutineContext; @@ -32709,7 +32973,7 @@ HSPLkotlinx/coroutines/JobCancellationException;->fillInStackTrace()Ljava/lang/T Lkotlinx/coroutines/JobImpl; HSPLkotlinx/coroutines/JobImpl;->(Lkotlinx/coroutines/Job;)V HSPLkotlinx/coroutines/JobImpl;->getHandlesException$kotlinx_coroutines_core()Z -HSPLkotlinx/coroutines/JobImpl;->getOnCancelComplete$kotlinx_coroutines_core()Z +PLkotlinx/coroutines/JobImpl;->getOnCancelComplete$kotlinx_coroutines_core()Z HSPLkotlinx/coroutines/JobImpl;->handlesException()Z Lkotlinx/coroutines/JobKt; HSPLkotlinx/coroutines/JobKt;->Job$default(Lkotlinx/coroutines/Job;ILjava/lang/Object;)Lkotlinx/coroutines/CompletableJob; @@ -32740,7 +33004,7 @@ Lkotlinx/coroutines/JobSupport; HSPLkotlinx/coroutines/JobSupport;->()V HSPLkotlinx/coroutines/JobSupport;->(Z)V HSPLkotlinx/coroutines/JobSupport;->access$cancellationExceptionMessage(Lkotlinx/coroutines/JobSupport;)Ljava/lang/String; -HSPLkotlinx/coroutines/JobSupport;->access$continueCompleting(Lkotlinx/coroutines/JobSupport;Lkotlinx/coroutines/JobSupport$Finishing;Lkotlinx/coroutines/ChildHandleNode;Ljava/lang/Object;)V +PLkotlinx/coroutines/JobSupport;->access$continueCompleting(Lkotlinx/coroutines/JobSupport;Lkotlinx/coroutines/JobSupport$Finishing;Lkotlinx/coroutines/ChildHandleNode;Ljava/lang/Object;)V HSPLkotlinx/coroutines/JobSupport;->addSuppressedExceptions(Ljava/lang/Throwable;Ljava/util/List;)V HSPLkotlinx/coroutines/JobSupport;->afterCompletion(Ljava/lang/Object;)V HSPLkotlinx/coroutines/JobSupport;->attachChild(Lkotlinx/coroutines/ChildJob;)Lkotlinx/coroutines/ChildHandle; @@ -32750,18 +33014,18 @@ HSPLkotlinx/coroutines/JobSupport;->cancel(Ljava/util/concurrent/CancellationExc HSPLkotlinx/coroutines/JobSupport;->cancelCoroutine(Ljava/lang/Throwable;)Z HSPLkotlinx/coroutines/JobSupport;->cancelImpl$kotlinx_coroutines_core(Ljava/lang/Object;)Z HSPLkotlinx/coroutines/JobSupport;->cancelInternal(Ljava/lang/Throwable;)V -HSPLkotlinx/coroutines/JobSupport;->cancelMakeCompleting(Ljava/lang/Object;)Ljava/lang/Object; +PLkotlinx/coroutines/JobSupport;->cancelMakeCompleting(Ljava/lang/Object;)Ljava/lang/Object; HSPLkotlinx/coroutines/JobSupport;->cancelParent(Ljava/lang/Throwable;)Z -HSPLkotlinx/coroutines/JobSupport;->cancellationExceptionMessage()Ljava/lang/String; +PLkotlinx/coroutines/JobSupport;->cancellationExceptionMessage()Ljava/lang/String; HSPLkotlinx/coroutines/JobSupport;->childCancelled(Ljava/lang/Throwable;)Z HSPLkotlinx/coroutines/JobSupport;->completeStateFinalization(Lkotlinx/coroutines/Incomplete;Ljava/lang/Object;)V -HSPLkotlinx/coroutines/JobSupport;->continueCompleting(Lkotlinx/coroutines/JobSupport$Finishing;Lkotlinx/coroutines/ChildHandleNode;Ljava/lang/Object;)V +PLkotlinx/coroutines/JobSupport;->continueCompleting(Lkotlinx/coroutines/JobSupport$Finishing;Lkotlinx/coroutines/ChildHandleNode;Ljava/lang/Object;)V HSPLkotlinx/coroutines/JobSupport;->createCauseException(Ljava/lang/Object;)Ljava/lang/Throwable; HSPLkotlinx/coroutines/JobSupport;->finalizeFinishingState(Lkotlinx/coroutines/JobSupport$Finishing;Ljava/lang/Object;)Ljava/lang/Object; HSPLkotlinx/coroutines/JobSupport;->fold(Ljava/lang/Object;Lkotlin/jvm/functions/Function2;)Ljava/lang/Object; HSPLkotlinx/coroutines/JobSupport;->get(Lkotlin/coroutines/CoroutineContext$Key;)Lkotlin/coroutines/CoroutineContext$Element; HSPLkotlinx/coroutines/JobSupport;->getCancellationException()Ljava/util/concurrent/CancellationException; -HSPLkotlinx/coroutines/JobSupport;->getChildJobCancellationCause()Ljava/util/concurrent/CancellationException; +PLkotlinx/coroutines/JobSupport;->getChildJobCancellationCause()Ljava/util/concurrent/CancellationException; HSPLkotlinx/coroutines/JobSupport;->getCompletedInternal$kotlinx_coroutines_core()Ljava/lang/Object; HSPLkotlinx/coroutines/JobSupport;->getCompletionExceptionOrNull()Ljava/lang/Throwable; HSPLkotlinx/coroutines/JobSupport;->getExceptionOrNull(Ljava/lang/Object;)Ljava/lang/Throwable; @@ -32816,7 +33080,7 @@ Lkotlinx/coroutines/JobSupport$AwaitContinuation; HSPLkotlinx/coroutines/JobSupport$AwaitContinuation;->(Lkotlin/coroutines/Continuation;Lkotlinx/coroutines/JobSupport;)V HSPLkotlinx/coroutines/JobSupport$AwaitContinuation;->getContinuationCancellationCause(Lkotlinx/coroutines/Job;)Ljava/lang/Throwable; Lkotlinx/coroutines/JobSupport$ChildCompletion; -PLkotlinx/coroutines/JobSupport$ChildCompletion;->(Lkotlinx/coroutines/JobSupport;Lkotlinx/coroutines/JobSupport$Finishing;Lkotlinx/coroutines/ChildHandleNode;Ljava/lang/Object;)V +HSPLkotlinx/coroutines/JobSupport$ChildCompletion;->(Lkotlinx/coroutines/JobSupport;Lkotlinx/coroutines/JobSupport$Finishing;Lkotlinx/coroutines/ChildHandleNode;Ljava/lang/Object;)V PLkotlinx/coroutines/JobSupport$ChildCompletion;->getOnCancelling()Z PLkotlinx/coroutines/JobSupport$ChildCompletion;->invoke(Ljava/lang/Throwable;)V Lkotlinx/coroutines/JobSupport$Finishing; @@ -32837,7 +33101,7 @@ PLkotlinx/coroutines/JobSupport$Finishing;->isSealed()Z HSPLkotlinx/coroutines/JobSupport$Finishing;->sealLocked(Ljava/lang/Throwable;)Ljava/util/List; HSPLkotlinx/coroutines/JobSupport$Finishing;->setCompleting(Z)V HSPLkotlinx/coroutines/JobSupport$Finishing;->setExceptionsHolder(Ljava/lang/Object;)V -HSPLkotlinx/coroutines/JobSupport$Finishing;->setRootCause(Ljava/lang/Throwable;)V +PLkotlinx/coroutines/JobSupport$Finishing;->setRootCause(Ljava/lang/Throwable;)V Lkotlinx/coroutines/JobSupportKt; HSPLkotlinx/coroutines/JobSupportKt;->()V HSPLkotlinx/coroutines/JobSupportKt;->access$getCOMPLETING_ALREADY$p()Lkotlinx/coroutines/internal/Symbol; @@ -33077,6 +33341,7 @@ HSPLkotlinx/coroutines/channels/BufferedChannel$BufferedChannelIterator;->hasNex HSPLkotlinx/coroutines/channels/BufferedChannel$BufferedChannelIterator;->hasNextOnNoWaiterSuspend(Lkotlinx/coroutines/channels/ChannelSegment;IJLkotlin/coroutines/Continuation;)Ljava/lang/Object; HSPLkotlinx/coroutines/channels/BufferedChannel$BufferedChannelIterator;->invokeOnCancellation(Lkotlinx/coroutines/internal/Segment;I)V HSPLkotlinx/coroutines/channels/BufferedChannel$BufferedChannelIterator;->next()Ljava/lang/Object; +HSPLkotlinx/coroutines/channels/BufferedChannel$BufferedChannelIterator;->onClosedHasNextNoWaiterSuspend()V HSPLkotlinx/coroutines/channels/BufferedChannel$BufferedChannelIterator;->tryResumeHasNext(Ljava/lang/Object;)Z HSPLkotlinx/coroutines/channels/BufferedChannel$BufferedChannelIterator;->tryResumeHasNextOnClosedChannel()V Lkotlinx/coroutines/channels/BufferedChannel$SendBroadcast; @@ -33395,7 +33660,7 @@ HSPLkotlinx/coroutines/flow/FlowKt__EmittersKt$onStart$$inlined$unsafeFlow$1;->< HSPLkotlinx/coroutines/flow/FlowKt__EmittersKt$onStart$$inlined$unsafeFlow$1;->collect(Lkotlinx/coroutines/flow/FlowCollector;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; Lkotlinx/coroutines/flow/FlowKt__EmittersKt$onStart$$inlined$unsafeFlow$1$1; HSPLkotlinx/coroutines/flow/FlowKt__EmittersKt$onStart$$inlined$unsafeFlow$1$1;->(Lkotlinx/coroutines/flow/FlowKt__EmittersKt$onStart$$inlined$unsafeFlow$1;Lkotlin/coroutines/Continuation;)V -PLkotlinx/coroutines/flow/FlowKt__EmittersKt$onStart$$inlined$unsafeFlow$1$1;->invokeSuspend(Ljava/lang/Object;)Ljava/lang/Object; +HSPLkotlinx/coroutines/flow/FlowKt__EmittersKt$onStart$$inlined$unsafeFlow$1$1;->invokeSuspend(Ljava/lang/Object;)Ljava/lang/Object; Lkotlinx/coroutines/flow/FlowKt__ErrorsKt; HSPLkotlinx/coroutines/flow/FlowKt__ErrorsKt;->catch(Lkotlinx/coroutines/flow/Flow;Lkotlin/jvm/functions/Function3;)Lkotlinx/coroutines/flow/Flow; HSPLkotlinx/coroutines/flow/FlowKt__ErrorsKt;->catchImpl(Lkotlinx/coroutines/flow/Flow;Lkotlinx/coroutines/flow/FlowCollector;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; @@ -33591,6 +33856,7 @@ HSPLkotlinx/coroutines/flow/SharedFlowImpl;->getHead()J HSPLkotlinx/coroutines/flow/SharedFlowImpl;->getLastReplayedLocked()Ljava/lang/Object; HSPLkotlinx/coroutines/flow/SharedFlowImpl;->getPeekedValueLockedAt(J)Ljava/lang/Object; HSPLkotlinx/coroutines/flow/SharedFlowImpl;->getQueueEndIndex()J +HSPLkotlinx/coroutines/flow/SharedFlowImpl;->getReplayCache()Ljava/util/List; HSPLkotlinx/coroutines/flow/SharedFlowImpl;->getReplaySize()I HSPLkotlinx/coroutines/flow/SharedFlowImpl;->getTotalSize()I HSPLkotlinx/coroutines/flow/SharedFlowImpl;->growBuffer([Ljava/lang/Object;II)[Ljava/lang/Object; @@ -33878,7 +34144,7 @@ HSPLkotlinx/coroutines/internal/ContextScope;->getCoroutineContext()Lkotlin/coro Lkotlinx/coroutines/internal/DispatchedContinuation; HSPLkotlinx/coroutines/internal/DispatchedContinuation;->()V HSPLkotlinx/coroutines/internal/DispatchedContinuation;->(Lkotlinx/coroutines/CoroutineDispatcher;Lkotlin/coroutines/Continuation;)V -HPLkotlinx/coroutines/internal/DispatchedContinuation;->awaitReusability$kotlinx_coroutines_core()V +HSPLkotlinx/coroutines/internal/DispatchedContinuation;->awaitReusability$kotlinx_coroutines_core()V HSPLkotlinx/coroutines/internal/DispatchedContinuation;->claimReusableCancellableContinuation$kotlinx_coroutines_core()Lkotlinx/coroutines/CancellableContinuationImpl; HSPLkotlinx/coroutines/internal/DispatchedContinuation;->dispatchYield$kotlinx_coroutines_core(Lkotlin/coroutines/CoroutineContext;Ljava/lang/Object;)V HSPLkotlinx/coroutines/internal/DispatchedContinuation;->getContext()Lkotlin/coroutines/CoroutineContext; @@ -33937,7 +34203,7 @@ HSPLkotlinx/coroutines/internal/LockFreeLinkedListNode;->addNext(Lkotlinx/corout HSPLkotlinx/coroutines/internal/LockFreeLinkedListNode;->addOneIfEmpty(Lkotlinx/coroutines/internal/LockFreeLinkedListNode;)Z HSPLkotlinx/coroutines/internal/LockFreeLinkedListNode;->close(I)V HSPLkotlinx/coroutines/internal/LockFreeLinkedListNode;->correctPrev()Lkotlinx/coroutines/internal/LockFreeLinkedListNode; -HSPLkotlinx/coroutines/internal/LockFreeLinkedListNode;->findPrevNonRemoved(Lkotlinx/coroutines/internal/LockFreeLinkedListNode;)Lkotlinx/coroutines/internal/LockFreeLinkedListNode; +PLkotlinx/coroutines/internal/LockFreeLinkedListNode;->findPrevNonRemoved(Lkotlinx/coroutines/internal/LockFreeLinkedListNode;)Lkotlinx/coroutines/internal/LockFreeLinkedListNode; HSPLkotlinx/coroutines/internal/LockFreeLinkedListNode;->finishAdd(Lkotlinx/coroutines/internal/LockFreeLinkedListNode;)V HSPLkotlinx/coroutines/internal/LockFreeLinkedListNode;->getNext()Ljava/lang/Object; HSPLkotlinx/coroutines/internal/LockFreeLinkedListNode;->getNextNode()Lkotlinx/coroutines/internal/LockFreeLinkedListNode; @@ -33995,7 +34261,7 @@ HSPLkotlinx/coroutines/internal/ScopeCoroutine;->(Lkotlin/coroutines/Corou PLkotlinx/coroutines/internal/ScopeCoroutine;->afterCompletion(Ljava/lang/Object;)V HSPLkotlinx/coroutines/internal/ScopeCoroutine;->afterCompletionUndispatched()V HSPLkotlinx/coroutines/internal/ScopeCoroutine;->afterResume(Ljava/lang/Object;)V -HSPLkotlinx/coroutines/internal/ScopeCoroutine;->isScopedCoroutine()Z +PLkotlinx/coroutines/internal/ScopeCoroutine;->isScopedCoroutine()Z Lkotlinx/coroutines/internal/Segment; HSPLkotlinx/coroutines/internal/Segment;->()V HSPLkotlinx/coroutines/internal/Segment;->(JLkotlinx/coroutines/internal/Segment;I)V @@ -34195,7 +34461,7 @@ HSPLkotlinx/coroutines/scheduling/WorkQueue;->pollWithExclusiveMode(Z)Lkotlinx/c HSPLkotlinx/coroutines/scheduling/WorkQueue;->stealWithExclusiveMode(Z)Lkotlinx/coroutines/scheduling/Task; HSPLkotlinx/coroutines/scheduling/WorkQueue;->tryExtractFromTheMiddle(IZ)Lkotlinx/coroutines/scheduling/Task; HSPLkotlinx/coroutines/scheduling/WorkQueue;->trySteal(ILkotlin/jvm/internal/Ref$ObjectRef;)J -HSPLkotlinx/coroutines/scheduling/WorkQueue;->tryStealLastScheduled(ILkotlin/jvm/internal/Ref$ObjectRef;)J +HPLkotlinx/coroutines/scheduling/WorkQueue;->tryStealLastScheduled(ILkotlin/jvm/internal/Ref$ObjectRef;)J Lkotlinx/coroutines/selects/OnTimeout; HSPLkotlinx/coroutines/selects/OnTimeout;->(J)V HSPLkotlinx/coroutines/selects/OnTimeout;->access$register(Lkotlinx/coroutines/selects/OnTimeout;Lkotlinx/coroutines/selects/SelectInstance;Ljava/lang/Object;)V @@ -34305,7 +34571,6 @@ Lkotlinx/coroutines/sync/MutexImpl$CancellableContinuationWithOwner; HSPLkotlinx/coroutines/sync/MutexImpl$CancellableContinuationWithOwner;->(Lkotlinx/coroutines/sync/MutexImpl;Lkotlinx/coroutines/CancellableContinuationImpl;Ljava/lang/Object;)V HSPLkotlinx/coroutines/sync/MutexImpl$CancellableContinuationWithOwner;->completeResume(Ljava/lang/Object;)V HSPLkotlinx/coroutines/sync/MutexImpl$CancellableContinuationWithOwner;->invokeOnCancellation(Lkotlinx/coroutines/internal/Segment;I)V -HSPLkotlinx/coroutines/sync/MutexImpl$CancellableContinuationWithOwner;->resume(Ljava/lang/Object;Lkotlin/jvm/functions/Function3;)V HSPLkotlinx/coroutines/sync/MutexImpl$CancellableContinuationWithOwner;->resume(Lkotlin/Unit;Lkotlin/jvm/functions/Function3;)V HSPLkotlinx/coroutines/sync/MutexImpl$CancellableContinuationWithOwner;->tryResume(Ljava/lang/Object;Ljava/lang/Object;Lkotlin/jvm/functions/Function3;)Ljava/lang/Object; HSPLkotlinx/coroutines/sync/MutexImpl$CancellableContinuationWithOwner;->tryResume(Lkotlin/Unit;Ljava/lang/Object;Lkotlin/jvm/functions/Function3;)Ljava/lang/Object; @@ -35147,7 +35412,7 @@ HSPLokhttp3/Address;->(Ljava/lang/String;ILokhttp3/Dns;Ljavax/net/SocketFa HSPLokhttp3/Address;->certificatePinner()Lokhttp3/CertificatePinner; HSPLokhttp3/Address;->connectionSpecs()Ljava/util/List; HSPLokhttp3/Address;->dns()Lokhttp3/Dns; -PLokhttp3/Address;->equalsNonHost$okhttp(Lokhttp3/Address;)Z +HSPLokhttp3/Address;->equalsNonHost$okhttp(Lokhttp3/Address;)Z HSPLokhttp3/Address;->hashCode()I HSPLokhttp3/Address;->hostnameVerifier()Ljavax/net/ssl/HostnameVerifier; HSPLokhttp3/Address;->protocols()Ljava/util/List; @@ -35195,7 +35460,7 @@ HSPLokhttp3/CertificatePinner;->()V HSPLokhttp3/CertificatePinner;->(Ljava/util/Set;Lokhttp3/internal/tls/CertificateChainCleaner;)V HSPLokhttp3/CertificatePinner;->(Ljava/util/Set;Lokhttp3/internal/tls/CertificateChainCleaner;ILkotlin/jvm/internal/DefaultConstructorMarker;)V HSPLokhttp3/CertificatePinner;->check$okhttp(Ljava/lang/String;Lkotlin/jvm/functions/Function0;)V -PLokhttp3/CertificatePinner;->equals(Ljava/lang/Object;)Z +HSPLokhttp3/CertificatePinner;->equals(Ljava/lang/Object;)Z HSPLokhttp3/CertificatePinner;->findMatchingPins(Ljava/lang/String;)Ljava/util/List; HSPLokhttp3/CertificatePinner;->hashCode()I HSPLokhttp3/CertificatePinner;->withCertificateChainCleaner$okhttp(Lokhttp3/internal/tls/CertificateChainCleaner;)Lokhttp3/CertificatePinner; @@ -35277,12 +35542,17 @@ HSPLokhttp3/Dispatcher$promoteAndExecute$Effects;->getCallsToExecute()Ljava/util HSPLokhttp3/Dispatcher$promoteAndExecute$Effects;->getIdleCallbackToRun()Ljava/lang/Runnable; Lokhttp3/Dns; HSPLokhttp3/Dns;->()V +HSPLokhttp3/Dns;->newCall(Lokhttp3/Dns$Request;)Lokhttp3/Dns$Call; +Lokhttp3/Dns$Call; Lokhttp3/Dns$Companion; HSPLokhttp3/Dns$Companion;->()V HSPLokhttp3/Dns$Companion;->()V -Lokhttp3/Dns$Companion$DnsSystem; -HSPLokhttp3/Dns$Companion$DnsSystem;->()V -HSPLokhttp3/Dns$Companion$DnsSystem;->lookup(Ljava/lang/String;)Ljava/util/List; +Lokhttp3/Dns$Companion$SYSTEM$1; +HSPLokhttp3/Dns$Companion$SYSTEM$1;->()V +HSPLokhttp3/Dns$Companion$SYSTEM$1;->lookup(Ljava/lang/String;)Ljava/util/List; +Lokhttp3/Dns$Request; +HSPLokhttp3/Dns$Request;->(Ljava/lang/String;I)V +HSPLokhttp3/Dns$Request;->hostname()Ljava/lang/String; Lokhttp3/EventListener; HSPLokhttp3/EventListener;->()V HSPLokhttp3/EventListener;->()V @@ -35400,7 +35670,6 @@ HSPLokhttp3/HttpUrl$Builder;->username(Ljava/lang/String;)Lokhttp3/HttpUrl$Build Lokhttp3/HttpUrl$Companion; HSPLokhttp3/HttpUrl$Companion;->()V HSPLokhttp3/HttpUrl$Companion;->(Lkotlin/jvm/internal/DefaultConstructorMarker;)V -HSPLokhttp3/HttpUrl$Companion;->access$toQueryString(Lokhttp3/HttpUrl$Companion;Ljava/util/List;Ljava/lang/StringBuilder;)V HSPLokhttp3/HttpUrl$Companion;->defaultPort(Ljava/lang/String;)I HSPLokhttp3/HttpUrl$Companion;->get(Ljava/lang/String;)Lokhttp3/HttpUrl; HSPLokhttp3/HttpUrl$Companion;->toQueryString(Ljava/util/List;Ljava/lang/StringBuilder;)V @@ -35423,8 +35692,6 @@ HSPLokhttp3/OkHttpClient;->()V HSPLokhttp3/OkHttpClient;->(Lokhttp3/OkHttpClient$Builder;)V HSPLokhttp3/OkHttpClient;->access$getDEFAULT_CONNECTION_SPECS$cp()Ljava/util/List; HSPLokhttp3/OkHttpClient;->access$getDEFAULT_PROTOCOLS$cp()Ljava/util/List; -HSPLokhttp3/OkHttpClient;->access$getSslSocketFactoryOrNull$p(Lokhttp3/OkHttpClient;)Ljavax/net/ssl/SSLSocketFactory; -HSPLokhttp3/OkHttpClient;->address(Lokhttp3/HttpUrl;)Lokhttp3/Address; HSPLokhttp3/OkHttpClient;->authenticator()Lokhttp3/Authenticator; HSPLokhttp3/OkHttpClient;->cache()Lokhttp3/Cache; HSPLokhttp3/OkHttpClient;->callTimeoutMillis()I @@ -35441,6 +35708,7 @@ HSPLokhttp3/OkHttpClient;->fastFallback()Z HSPLokhttp3/OkHttpClient;->followRedirects()Z HSPLokhttp3/OkHttpClient;->followSslRedirects()Z HSPLokhttp3/OkHttpClient;->getRouteDatabase$okhttp()Lokhttp3/internal/connection/RouteDatabase; +HSPLokhttp3/OkHttpClient;->getSslSocketFactoryOrNull$okhttp()Ljavax/net/ssl/SSLSocketFactory; HSPLokhttp3/OkHttpClient;->getTaskRunner$okhttp()Lokhttp3/internal/concurrent/TaskRunner; HSPLokhttp3/OkHttpClient;->hostnameVerifier()Ljavax/net/ssl/HostnameVerifier; HSPLokhttp3/OkHttpClient;->interceptors()Ljava/util/List; @@ -35456,7 +35724,6 @@ HSPLokhttp3/OkHttpClient;->proxySelector()Ljava/net/ProxySelector; HSPLokhttp3/OkHttpClient;->readTimeoutMillis()I HSPLokhttp3/OkHttpClient;->retryOnConnectionFailure()Z HSPLokhttp3/OkHttpClient;->socketFactory()Ljavax/net/SocketFactory; -HSPLokhttp3/OkHttpClient;->sslSocketFactory()Ljavax/net/ssl/SSLSocketFactory; HSPLokhttp3/OkHttpClient;->verifyClientState()V HSPLokhttp3/OkHttpClient;->webSocketCloseTimeout()I HSPLokhttp3/OkHttpClient;->writeTimeoutMillis()I @@ -35620,7 +35887,9 @@ Lokhttp3/ResponseBody$Companion$asResponseBody$1; HSPLokhttp3/ResponseBody$Companion$asResponseBody$1;->(Lokhttp3/MediaType;JLokio/BufferedSource;)V Lokhttp3/Route; HSPLokhttp3/Route;->(Lokhttp3/Address;Ljava/net/Proxy;Ljava/net/InetSocketAddress;)V +HSPLokhttp3/Route;->(Lokhttp3/Address;Ljava/net/Proxy;Ljava/net/InetSocketAddress;Lokio/ByteString;)V HSPLokhttp3/Route;->address()Lokhttp3/Address; +HSPLokhttp3/Route;->echConfigList()Lokio/ByteString; HSPLokhttp3/Route;->hashCode()I HSPLokhttp3/Route;->proxy()Ljava/net/Proxy; HSPLokhttp3/Route;->requiresTunnel()Z @@ -35732,7 +36001,7 @@ HSPLokhttp3/internal/authenticator/JavaNetAuthenticator;->(Lokhttp3/Dns;)V HSPLokhttp3/internal/authenticator/JavaNetAuthenticator;->(Lokhttp3/Dns;ILkotlin/jvm/internal/DefaultConstructorMarker;)V Lokhttp3/internal/cache/CacheInterceptor; HSPLokhttp3/internal/cache/CacheInterceptor;->()V -HSPLokhttp3/internal/cache/CacheInterceptor;->(Lokhttp3/Cache;)V +HSPLokhttp3/internal/cache/CacheInterceptor;->()V HSPLokhttp3/internal/cache/CacheInterceptor;->intercept(Lokhttp3/Interceptor$Chain;)Lokhttp3/Response; Lokhttp3/internal/cache/CacheInterceptor$Companion; HSPLokhttp3/internal/cache/CacheInterceptor$Companion;->()V @@ -35817,19 +36086,22 @@ HSPLokhttp3/internal/connection/ConnectInterceptor;->()V HSPLokhttp3/internal/connection/ConnectInterceptor;->intercept(Lokhttp3/Interceptor$Chain;)Lokhttp3/Response; Lokhttp3/internal/connection/ConnectPlan; HSPLokhttp3/internal/connection/ConnectPlan;->()V -HSPLokhttp3/internal/connection/ConnectPlan;->(Lokhttp3/internal/concurrent/TaskRunner;Lokhttp3/internal/connection/RealConnectionPool;IIIIIZLokhttp3/internal/connection/RealCall;Lokhttp3/internal/connection/RealRoutePlanner;Lokhttp3/Route;Ljava/util/List;ILokhttp3/Request;IZ)V +HSPLokhttp3/internal/connection/ConnectPlan;->(Lokhttp3/internal/concurrent/TaskRunner;Lokhttp3/internal/connection/RealConnectionPool;IIIIIZLokhttp3/internal/connection/RealCall;Lokhttp3/internal/connection/RealRoutePlanner;Lokhttp3/Route;Ljava/util/List;ILokhttp3/Request;IZLokhttp3/internal/ech/EchRetryPlan;)V +HSPLokhttp3/internal/connection/ConnectPlan;->(Lokhttp3/internal/concurrent/TaskRunner;Lokhttp3/internal/connection/RealConnectionPool;IIIIIZLokhttp3/internal/connection/RealCall;Lokhttp3/internal/connection/RealRoutePlanner;Lokhttp3/Route;Ljava/util/List;ILokhttp3/Request;IZLokhttp3/internal/ech/EchRetryPlan;ILkotlin/jvm/internal/DefaultConstructorMarker;)V +PLokhttp3/internal/connection/ConnectPlan;->cancel()V HSPLokhttp3/internal/connection/ConnectPlan;->connectSocket()V HSPLokhttp3/internal/connection/ConnectPlan;->connectTcp()Lokhttp3/internal/connection/RoutePlanner$ConnectResult; HSPLokhttp3/internal/connection/ConnectPlan;->connectTls(Ljavax/net/ssl/SSLSocket;Lokhttp3/ConnectionSpec;)V HSPLokhttp3/internal/connection/ConnectPlan;->connectTlsEtc()Lokhttp3/internal/connection/RoutePlanner$ConnectResult; -HSPLokhttp3/internal/connection/ConnectPlan;->copy$default(Lokhttp3/internal/connection/ConnectPlan;ILokhttp3/Request;IZILjava/lang/Object;)Lokhttp3/internal/connection/ConnectPlan; -HSPLokhttp3/internal/connection/ConnectPlan;->copy(ILokhttp3/Request;IZ)Lokhttp3/internal/connection/ConnectPlan; +HSPLokhttp3/internal/connection/ConnectPlan;->copy$default(Lokhttp3/internal/connection/ConnectPlan;Lokhttp3/Route;ILokhttp3/Request;IZLokhttp3/internal/ech/EchRetryPlan;ILjava/lang/Object;)Lokhttp3/internal/connection/ConnectPlan; +HSPLokhttp3/internal/connection/ConnectPlan;->copy(Lokhttp3/Route;ILokhttp3/Request;IZLokhttp3/internal/ech/EchRetryPlan;)Lokhttp3/internal/connection/ConnectPlan; HSPLokhttp3/internal/connection/ConnectPlan;->getRoute()Lokhttp3/Route; HSPLokhttp3/internal/connection/ConnectPlan;->getRoutes$okhttp()Ljava/util/List; HSPLokhttp3/internal/connection/ConnectPlan;->handleSuccess()Lokhttp3/internal/connection/RealConnection; HSPLokhttp3/internal/connection/ConnectPlan;->isReady()Z -HSPLokhttp3/internal/connection/ConnectPlan;->nextConnectionSpec$okhttp(Ljava/util/List;Ljavax/net/ssl/SSLSocket;)Lokhttp3/internal/connection/ConnectPlan; +HSPLokhttp3/internal/connection/ConnectPlan;->nextCompatibleConnectionSpec(Ljava/util/List;Ljavax/net/ssl/SSLSocket;)Lokhttp3/internal/connection/ConnectPlan; HSPLokhttp3/internal/connection/ConnectPlan;->planWithCurrentOrInitialConnectionSpec$okhttp(Ljava/util/List;Ljavax/net/ssl/SSLSocket;)Lokhttp3/internal/connection/ConnectPlan; +PLokhttp3/internal/connection/ConnectPlan;->retry()Lokhttp3/internal/connection/RoutePlanner$Plan; Lokhttp3/internal/connection/ConnectPlan$$ExternalSyntheticLambda0; HSPLokhttp3/internal/connection/ConnectPlan$$ExternalSyntheticLambda0;->(Lokhttp3/CertificatePinner;Lokhttp3/Handshake;Lokhttp3/Address;)V Lokhttp3/internal/connection/ConnectPlan$$ExternalSyntheticLambda1; @@ -35855,14 +36127,13 @@ HSPLokhttp3/internal/connection/ConnectionListener$Companion;->getNONE()Lokhttp3 Lokhttp3/internal/connection/ConnectionListener$Companion$NONE$1; HSPLokhttp3/internal/connection/ConnectionListener$Companion$NONE$1;->()V Lokhttp3/internal/connection/Exchange; -HSPLokhttp3/internal/connection/Exchange;->(Lokhttp3/internal/connection/RealCall;Lokhttp3/EventListener;Lokhttp3/internal/connection/ExchangeFinder;Lokhttp3/internal/http/ExchangeCodec;)V +HSPLokhttp3/internal/connection/Exchange;->(Lokhttp3/internal/connection/RealCall;Lokhttp3/internal/connection/ExchangeFinder;Lokhttp3/internal/http/ExchangeCodec;)V HSPLokhttp3/internal/connection/Exchange;->bodyComplete$default(Lokhttp3/internal/connection/Exchange;JZZZLjava/io/IOException;ILjava/lang/Object;)Ljava/io/IOException; HSPLokhttp3/internal/connection/Exchange;->bodyComplete(JZZZLjava/io/IOException;)Ljava/io/IOException; HSPLokhttp3/internal/connection/Exchange;->createRequestBody(Lokhttp3/Request;Z)Lokio/Sink; HSPLokhttp3/internal/connection/Exchange;->finishRequest()V HSPLokhttp3/internal/connection/Exchange;->getCall$okhttp()Lokhttp3/internal/connection/RealCall; HSPLokhttp3/internal/connection/Exchange;->getConnection$okhttp()Lokhttp3/internal/connection/RealConnection; -HSPLokhttp3/internal/connection/Exchange;->getEventListener$okhttp()Lokhttp3/EventListener; HSPLokhttp3/internal/connection/Exchange;->getFinder$okhttp()Lokhttp3/internal/connection/ExchangeFinder; HSPLokhttp3/internal/connection/Exchange;->isDuplex$okhttp()Z HSPLokhttp3/internal/connection/Exchange;->openResponseBody(Lokhttp3/Response;)Lokhttp3/ResponseBody; @@ -35896,6 +36167,7 @@ HSPLokhttp3/internal/connection/FastFallbackExchangeFinder$launchTcpConnect$1;-> Lokhttp3/internal/connection/InetAddressOrderKt; HSPLokhttp3/internal/connection/InetAddressOrderKt;->reorderForHappyEyeballs(Ljava/util/List;)Ljava/util/List; Lokhttp3/internal/connection/RealCall; +HSPLokhttp3/internal/connection/RealCall;->()V HSPLokhttp3/internal/connection/RealCall;->(Lokhttp3/OkHttpClient;Lokhttp3/Request;Z)V HSPLokhttp3/internal/connection/RealCall;->acquireConnectionNoEvents(Lokhttp3/internal/connection/RealConnection;)V HSPLokhttp3/internal/connection/RealCall;->callDone(Ljava/io/IOException;)Ljava/io/IOException; @@ -35917,6 +36189,9 @@ HSPLokhttp3/internal/connection/RealCall;->releaseConnectionNoEvents$okhttp()Lja HSPLokhttp3/internal/connection/RealCall;->timeoutExit(Ljava/io/IOException;)Ljava/io/IOException; Lokhttp3/internal/connection/RealCall$CallReference; HSPLokhttp3/internal/connection/RealCall$CallReference;->(Lokhttp3/internal/connection/RealCall;Ljava/lang/Object;)V +Lokhttp3/internal/connection/RealCall$Companion; +HSPLokhttp3/internal/connection/RealCall$Companion;->()V +HSPLokhttp3/internal/connection/RealCall$Companion;->(Lkotlin/jvm/internal/DefaultConstructorMarker;)V Lokhttp3/internal/connection/RealCall$timeout$1; HSPLokhttp3/internal/connection/RealCall$timeout$1;->(Lokhttp3/internal/connection/RealCall;)V Lokhttp3/internal/connection/RealConnection; @@ -35929,8 +36204,8 @@ HSPLokhttp3/internal/connection/RealConnection;->getNoNewExchanges()Z HSPLokhttp3/internal/connection/RealConnection;->getRoute()Lokhttp3/Route; HSPLokhttp3/internal/connection/RealConnection;->handshake()Lokhttp3/Handshake; HSPLokhttp3/internal/connection/RealConnection;->incrementSuccessCount$okhttp()V -PLokhttp3/internal/connection/RealConnection;->isEligible$okhttp(Lokhttp3/Address;Ljava/util/List;)Z -PLokhttp3/internal/connection/RealConnection;->isHealthy(Z)Z +HSPLokhttp3/internal/connection/RealConnection;->isEligible$okhttp(Lokhttp3/Address;Ljava/util/List;)Z +HSPLokhttp3/internal/connection/RealConnection;->isHealthy(Z)Z HSPLokhttp3/internal/connection/RealConnection;->newCodec$okhttp(Lokhttp3/OkHttpClient;Lokhttp3/internal/http/RealInterceptorChain;)Lokhttp3/internal/http/ExchangeCodec; HSPLokhttp3/internal/connection/RealConnection;->onSettings(Lokhttp3/internal/http2/Http2Connection;Lokhttp3/internal/http2/Settings;)V HSPLokhttp3/internal/connection/RealConnection;->route()Lokhttp3/Route; @@ -35970,9 +36245,12 @@ HSPLokhttp3/internal/connection/RealRoutePlanner;->planReuseCallConnection()Lokh HSPLokhttp3/internal/connection/RealRoutePlanner;->planReusePooledConnection$okhttp$default(Lokhttp3/internal/connection/RealRoutePlanner;Lokhttp3/internal/connection/ConnectPlan;Ljava/util/List;ILjava/lang/Object;)Lokhttp3/internal/connection/ReusePlan; HSPLokhttp3/internal/connection/RealRoutePlanner;->planReusePooledConnection$okhttp(Lokhttp3/internal/connection/ConnectPlan;Ljava/util/List;)Lokhttp3/internal/connection/ReusePlan; HSPLokhttp3/internal/connection/RealRoutePlanner;->sameHostAndPort(Lokhttp3/HttpUrl;)Z -PLokhttp3/internal/connection/ReusePlan;->(Lokhttp3/internal/connection/RealConnection;)V -PLokhttp3/internal/connection/ReusePlan;->handleSuccess()Lokhttp3/internal/connection/RealConnection; -PLokhttp3/internal/connection/ReusePlan;->isReady()Z +Lokhttp3/internal/connection/RetryTlsHandshakeKt; +HSPLokhttp3/internal/connection/RetryTlsHandshakeKt;->attemptAnotherConnection(Ljava/io/IOException;)Z +Lokhttp3/internal/connection/ReusePlan; +HSPLokhttp3/internal/connection/ReusePlan;->(Lokhttp3/internal/connection/RealConnection;)V +HSPLokhttp3/internal/connection/ReusePlan;->handleSuccess()Lokhttp3/internal/connection/RealConnection; +HSPLokhttp3/internal/connection/ReusePlan;->isReady()Z Lokhttp3/internal/connection/RouteDatabase; HSPLokhttp3/internal/connection/RouteDatabase;->()V HSPLokhttp3/internal/connection/RouteDatabase;->connected(Lokhttp3/Route;)V @@ -35990,11 +36268,12 @@ Lokhttp3/internal/connection/RoutePlanner$Plan; Lokhttp3/internal/connection/RouteSelector; HSPLokhttp3/internal/connection/RouteSelector;->()V HSPLokhttp3/internal/connection/RouteSelector;->(Lokhttp3/Address;Lokhttp3/internal/connection/RouteDatabase;Lokhttp3/internal/connection/RealCall;Z)V +HSPLokhttp3/internal/connection/RouteSelector;->dnsLookup(Ljava/net/Proxy;Ljava/lang/String;I)Ljava/util/List; HSPLokhttp3/internal/connection/RouteSelector;->hasNext()Z HSPLokhttp3/internal/connection/RouteSelector;->hasNextProxy()Z HSPLokhttp3/internal/connection/RouteSelector;->next()Lokhttp3/internal/connection/RouteSelector$Selection; -HSPLokhttp3/internal/connection/RouteSelector;->nextProxy()Ljava/net/Proxy; -HSPLokhttp3/internal/connection/RouteSelector;->resetNextInetSocketAddress(Ljava/net/Proxy;)V +HSPLokhttp3/internal/connection/RouteSelector;->nextProxy()Ljava/util/List; +HSPLokhttp3/internal/connection/RouteSelector;->nextRoutes(Ljava/net/Proxy;)Ljava/util/List; HSPLokhttp3/internal/connection/RouteSelector;->resetNextProxy$selectProxies(Ljava/net/Proxy;Lokhttp3/HttpUrl;Lokhttp3/internal/connection/RouteSelector;)Ljava/util/List; HSPLokhttp3/internal/connection/RouteSelector;->resetNextProxy(Lokhttp3/HttpUrl;Ljava/net/Proxy;)V Lokhttp3/internal/connection/RouteSelector$Companion; @@ -36005,8 +36284,20 @@ HSPLokhttp3/internal/connection/RouteSelector$Selection;->(Ljava/util/List HSPLokhttp3/internal/connection/RouteSelector$Selection;->getRoutes()Ljava/util/List; HSPLokhttp3/internal/connection/RouteSelector$Selection;->hasNext()Z HSPLokhttp3/internal/connection/RouteSelector$Selection;->next()Lokhttp3/Route; +Lokhttp3/internal/dns/InetAddressDns; +HSPLokhttp3/internal/dns/InetAddressDns;->()V +HSPLokhttp3/internal/dns/InetAddressDns;->()V +HSPLokhttp3/internal/dns/InetAddressDns;->lookup(Ljava/lang/String;)Ljava/util/List; +Lokhttp3/internal/dns/LookupDnsCall; +HSPLokhttp3/internal/dns/LookupDnsCall;->(Lokhttp3/internal/concurrent/TaskRunner;Lokhttp3/Dns;Lokhttp3/Dns$Request;)V +Lokhttp3/internal/dns/LookupDnsCall$State; +Lokhttp3/internal/dns/LookupDnsCall$State$Idle; +HSPLokhttp3/internal/dns/LookupDnsCall$State$Idle;->()V +HSPLokhttp3/internal/dns/LookupDnsCall$State$Idle;->()V +Lokhttp3/internal/ech/EchRetryPlan; +Lokhttp3/internal/ech/EchUntrustedException; Lokhttp3/internal/http/BridgeInterceptor; -HSPLokhttp3/internal/http/BridgeInterceptor;->(Lokhttp3/CookieJar;)V +HSPLokhttp3/internal/http/BridgeInterceptor;->()V HSPLokhttp3/internal/http/BridgeInterceptor;->intercept(Lokhttp3/Interceptor$Chain;)Lokhttp3/Response; Lokhttp3/internal/http/CallServerInterceptor; HSPLokhttp3/internal/http/CallServerInterceptor;->()V @@ -36026,16 +36317,33 @@ HSPLokhttp3/internal/http/HttpMethod;->()V HSPLokhttp3/internal/http/HttpMethod;->()V HSPLokhttp3/internal/http/HttpMethod;->permitsRequestBody(Ljava/lang/String;)Z Lokhttp3/internal/http/RealInterceptorChain; -HSPLokhttp3/internal/http/RealInterceptorChain;->(Lokhttp3/internal/connection/RealCall;Ljava/util/List;ILokhttp3/internal/connection/Exchange;Lokhttp3/Request;III)V +HSPLokhttp3/internal/http/RealInterceptorChain;->(Lokhttp3/internal/connection/RealCall;Ljava/util/List;ILjava/lang/Void;Lokhttp3/Request;Lokhttp3/OkHttpClient;)V +HSPLokhttp3/internal/http/RealInterceptorChain;->(Lokhttp3/internal/connection/RealCall;Ljava/util/List;ILjava/lang/Void;Lokhttp3/Request;Lokhttp3/OkHttpClient;ILkotlin/jvm/internal/DefaultConstructorMarker;)V +HSPLokhttp3/internal/http/RealInterceptorChain;->(Lokhttp3/internal/connection/RealCall;Ljava/util/List;ILokhttp3/internal/connection/Exchange;Lokhttp3/Request;IIILokhttp3/Authenticator;Lokhttp3/Cache;Lokhttp3/CertificatePinner;Lokhttp3/ConnectionPool;Lokhttp3/CookieJar;Lokhttp3/Dns;Ljavax/net/ssl/HostnameVerifier;Ljava/net/Proxy;Lokhttp3/Authenticator;Ljava/net/ProxySelector;ZLjavax/net/SocketFactory;Ljavax/net/ssl/SSLSocketFactory;Ljavax/net/ssl/X509TrustManager;Lokhttp3/internal/tls/CertificateChainCleaner;)V +HSPLokhttp3/internal/http/RealInterceptorChain;->address(Lokhttp3/HttpUrl;)Lokhttp3/Address; HSPLokhttp3/internal/http/RealInterceptorChain;->call()Lokhttp3/Call; -HSPLokhttp3/internal/http/RealInterceptorChain;->copy$okhttp$default(Lokhttp3/internal/http/RealInterceptorChain;ILokhttp3/internal/connection/Exchange;Lokhttp3/Request;IIIILjava/lang/Object;)Lokhttp3/internal/http/RealInterceptorChain; -HSPLokhttp3/internal/http/RealInterceptorChain;->copy$okhttp(ILokhttp3/internal/connection/Exchange;Lokhttp3/Request;III)Lokhttp3/internal/http/RealInterceptorChain; +HSPLokhttp3/internal/http/RealInterceptorChain;->copy$okhttp$default(Lokhttp3/internal/http/RealInterceptorChain;ILokhttp3/internal/connection/Exchange;Lokhttp3/Request;IIILokhttp3/Authenticator;Lokhttp3/Cache;Lokhttp3/CertificatePinner;Lokhttp3/ConnectionPool;Lokhttp3/CookieJar;Lokhttp3/Dns;Ljavax/net/ssl/HostnameVerifier;Ljava/net/Proxy;Lokhttp3/Authenticator;Ljava/net/ProxySelector;ZLjavax/net/SocketFactory;Ljavax/net/ssl/SSLSocketFactory;Ljavax/net/ssl/X509TrustManager;Lokhttp3/internal/tls/CertificateChainCleaner;ILjava/lang/Object;)Lokhttp3/internal/http/RealInterceptorChain; +HSPLokhttp3/internal/http/RealInterceptorChain;->copy$okhttp(ILokhttp3/internal/connection/Exchange;Lokhttp3/Request;IIILokhttp3/Authenticator;Lokhttp3/Cache;Lokhttp3/CertificatePinner;Lokhttp3/ConnectionPool;Lokhttp3/CookieJar;Lokhttp3/Dns;Ljavax/net/ssl/HostnameVerifier;Ljava/net/Proxy;Lokhttp3/Authenticator;Ljava/net/ProxySelector;ZLjavax/net/SocketFactory;Ljavax/net/ssl/SSLSocketFactory;Ljavax/net/ssl/X509TrustManager;Lokhttp3/internal/tls/CertificateChainCleaner;)Lokhttp3/internal/http/RealInterceptorChain; +HSPLokhttp3/internal/http/RealInterceptorChain;->getAuthenticator()Lokhttp3/Authenticator; +HSPLokhttp3/internal/http/RealInterceptorChain;->getCache()Lokhttp3/Cache; HSPLokhttp3/internal/http/RealInterceptorChain;->getCall$okhttp()Lokhttp3/internal/connection/RealCall; +HSPLokhttp3/internal/http/RealInterceptorChain;->getCertificatePinner()Lokhttp3/CertificatePinner; HSPLokhttp3/internal/http/RealInterceptorChain;->getConnectTimeoutMillis$okhttp()I +HSPLokhttp3/internal/http/RealInterceptorChain;->getConnectionPool()Lokhttp3/ConnectionPool; +HSPLokhttp3/internal/http/RealInterceptorChain;->getCookieJar()Lokhttp3/CookieJar; +HSPLokhttp3/internal/http/RealInterceptorChain;->getDns()Lokhttp3/Dns; HSPLokhttp3/internal/http/RealInterceptorChain;->getExchange$okhttp()Lokhttp3/internal/connection/Exchange; +HSPLokhttp3/internal/http/RealInterceptorChain;->getHostnameVerifier()Ljavax/net/ssl/HostnameVerifier; +HSPLokhttp3/internal/http/RealInterceptorChain;->getProxy()Ljava/net/Proxy; +HSPLokhttp3/internal/http/RealInterceptorChain;->getProxyAuthenticator()Lokhttp3/Authenticator; +HSPLokhttp3/internal/http/RealInterceptorChain;->getProxySelector()Ljava/net/ProxySelector; HSPLokhttp3/internal/http/RealInterceptorChain;->getReadTimeoutMillis$okhttp()I HSPLokhttp3/internal/http/RealInterceptorChain;->getRequest$okhttp()Lokhttp3/Request; +HSPLokhttp3/internal/http/RealInterceptorChain;->getRetryOnConnectionFailure()Z +HSPLokhttp3/internal/http/RealInterceptorChain;->getSocketFactory()Ljavax/net/SocketFactory; +HSPLokhttp3/internal/http/RealInterceptorChain;->getSslSocketFactoryOrNull()Ljavax/net/ssl/SSLSocketFactory; HSPLokhttp3/internal/http/RealInterceptorChain;->getWriteTimeoutMillis$okhttp()I +HSPLokhttp3/internal/http/RealInterceptorChain;->getX509TrustManagerOrNull()Ljavax/net/ssl/X509TrustManager; HSPLokhttp3/internal/http/RealInterceptorChain;->proceed(Lokhttp3/Request;)Lokhttp3/Response; HSPLokhttp3/internal/http/RealInterceptorChain;->request()Lokhttp3/Request; Lokhttp3/internal/http/RealResponseBody; @@ -36049,8 +36357,8 @@ HSPLokhttp3/internal/http/RequestLine;->()V HSPLokhttp3/internal/http/RequestLine;->requestPath(Lokhttp3/HttpUrl;)Ljava/lang/String; Lokhttp3/internal/http/RetryAndFollowUpInterceptor; HSPLokhttp3/internal/http/RetryAndFollowUpInterceptor;->()V -HSPLokhttp3/internal/http/RetryAndFollowUpInterceptor;->(Lokhttp3/OkHttpClient;)V -HSPLokhttp3/internal/http/RetryAndFollowUpInterceptor;->followUpRequest(Lokhttp3/Response;Lokhttp3/internal/connection/Exchange;)Lokhttp3/Request; +HSPLokhttp3/internal/http/RetryAndFollowUpInterceptor;->()V +HSPLokhttp3/internal/http/RetryAndFollowUpInterceptor;->followUpRequest(Lokhttp3/Response;Lokhttp3/internal/connection/Exchange;Lokhttp3/Interceptor$Chain;)Lokhttp3/Request; HSPLokhttp3/internal/http/RetryAndFollowUpInterceptor;->intercept(Lokhttp3/Interceptor$Chain;)Lokhttp3/Response; Lokhttp3/internal/http/RetryAndFollowUpInterceptor$Companion; HSPLokhttp3/internal/http/RetryAndFollowUpInterceptor$Companion;->()V @@ -36066,6 +36374,7 @@ Lokhttp3/internal/http2/ErrorCode; HSPLokhttp3/internal/http2/ErrorCode;->$values()[Lokhttp3/internal/http2/ErrorCode; HSPLokhttp3/internal/http2/ErrorCode;->()V HSPLokhttp3/internal/http2/ErrorCode;->(Ljava/lang/String;II)V +PLokhttp3/internal/http2/ErrorCode;->getHttpCode()I Lokhttp3/internal/http2/ErrorCode$Companion; HSPLokhttp3/internal/http2/ErrorCode$Companion;->()V HSPLokhttp3/internal/http2/ErrorCode$Companion;->(Lkotlin/jvm/internal/DefaultConstructorMarker;)V @@ -36095,7 +36404,8 @@ HSPLokhttp3/internal/http2/Hpack;->nameToFirstIndex()Ljava/util/Map; Lokhttp3/internal/http2/Hpack$Reader; HSPLokhttp3/internal/http2/Hpack$Reader;->(Lokio/Source;II)V HSPLokhttp3/internal/http2/Hpack$Reader;->(Lokio/Source;IIILkotlin/jvm/internal/DefaultConstructorMarker;)V -PLokhttp3/internal/http2/Hpack$Reader;->dynamicTableIndex(I)I +HSPLokhttp3/internal/http2/Hpack$Reader;->addHeader(Lokhttp3/internal/http2/Header;)V +HSPLokhttp3/internal/http2/Hpack$Reader;->dynamicTableIndex(I)I HSPLokhttp3/internal/http2/Hpack$Reader;->evictToRecoverBytes(I)I HSPLokhttp3/internal/http2/Hpack$Reader;->getAndResetHeaderList()Ljava/util/List; HSPLokhttp3/internal/http2/Hpack$Reader;->getName(I)Lokio/ByteString; @@ -36104,7 +36414,7 @@ HSPLokhttp3/internal/http2/Hpack$Reader;->isStaticHeader(I)Z HSPLokhttp3/internal/http2/Hpack$Reader;->readByte()I HSPLokhttp3/internal/http2/Hpack$Reader;->readByteString()Lokio/ByteString; HSPLokhttp3/internal/http2/Hpack$Reader;->readHeaders()V -PLokhttp3/internal/http2/Hpack$Reader;->readIndexedHeader(I)V +HSPLokhttp3/internal/http2/Hpack$Reader;->readIndexedHeader(I)V HSPLokhttp3/internal/http2/Hpack$Reader;->readInt(II)I HSPLokhttp3/internal/http2/Hpack$Reader;->readLiteralHeaderWithIncrementalIndexingIndexedName(I)V HSPLokhttp3/internal/http2/Hpack$Reader;->readLiteralHeaderWithIncrementalIndexingNewName()V @@ -36139,7 +36449,7 @@ HSPLokhttp3/internal/http2/Http2Connection;->getStream(I)Lokhttp3/internal/http2 HSPLokhttp3/internal/http2/Http2Connection;->getStreams$okhttp()Ljava/util/Map; HSPLokhttp3/internal/http2/Http2Connection;->getWriteBytesMaximum()J HSPLokhttp3/internal/http2/Http2Connection;->getWriter()Lokhttp3/internal/http2/Http2Writer; -PLokhttp3/internal/http2/Http2Connection;->isHealthy(J)Z +HSPLokhttp3/internal/http2/Http2Connection;->isHealthy(J)Z HSPLokhttp3/internal/http2/Http2Connection;->newStream(ILjava/util/List;Z)Lokhttp3/internal/http2/Http2Stream; HSPLokhttp3/internal/http2/Http2Connection;->newStream(Ljava/util/List;Z)Lokhttp3/internal/http2/Http2Stream; HSPLokhttp3/internal/http2/Http2Connection;->pushedStream$okhttp(I)Z @@ -36149,6 +36459,7 @@ HSPLokhttp3/internal/http2/Http2Connection;->start$default(Lokhttp3/internal/htt HSPLokhttp3/internal/http2/Http2Connection;->start(Z)V HSPLokhttp3/internal/http2/Http2Connection;->updateConnectionFlowControl$okhttp(J)V HSPLokhttp3/internal/http2/Http2Connection;->writeData(IZLokio/Buffer;J)V +PLokhttp3/internal/http2/Http2Connection;->writeSynReset$okhttp(ILokhttp3/internal/http2/ErrorCode;)V Lokhttp3/internal/http2/Http2Connection$Builder; HSPLokhttp3/internal/http2/Http2Connection$Builder;->(ZLokhttp3/internal/concurrent/TaskRunner;)V HSPLokhttp3/internal/http2/Http2Connection$Builder;->build()Lokhttp3/internal/http2/Http2Connection; @@ -36230,11 +36541,9 @@ HSPLokhttp3/internal/http2/Http2Reader$Companion;->(Lkotlin/jvm/internal/D HSPLokhttp3/internal/http2/Http2Reader$Companion;->lengthWithoutPadding(III)I Lokhttp3/internal/http2/Http2Reader$ContinuationSource; HSPLokhttp3/internal/http2/Http2Reader$ContinuationSource;->(Lokio/BufferedSource;)V -HSPLokhttp3/internal/http2/Http2Reader$ContinuationSource;->getLeft()I HSPLokhttp3/internal/http2/Http2Reader$ContinuationSource;->read(Lokio/Buffer;J)J HSPLokhttp3/internal/http2/Http2Reader$ContinuationSource;->setFlags(I)V HSPLokhttp3/internal/http2/Http2Reader$ContinuationSource;->setLeft(I)V -HSPLokhttp3/internal/http2/Http2Reader$ContinuationSource;->setLength(I)V HSPLokhttp3/internal/http2/Http2Reader$ContinuationSource;->setPadding(I)V HSPLokhttp3/internal/http2/Http2Reader$ContinuationSource;->setStreamId(I)V Lokhttp3/internal/http2/Http2Reader$Handler; @@ -36244,6 +36553,8 @@ HSPLokhttp3/internal/http2/Http2Stream;->(ILokhttp3/internal/http2/Http2Co HSPLokhttp3/internal/http2/Http2Stream;->addBytesToWriteWindow(J)V HSPLokhttp3/internal/http2/Http2Stream;->cancelStreamIfNecessary$okhttp()V HSPLokhttp3/internal/http2/Http2Stream;->checkOutNotClosed$okhttp()V +PLokhttp3/internal/http2/Http2Stream;->close(Lokhttp3/internal/http2/ErrorCode;Ljava/io/IOException;)V +PLokhttp3/internal/http2/Http2Stream;->closeInternal(Lokhttp3/internal/http2/ErrorCode;Ljava/io/IOException;)Z HSPLokhttp3/internal/http2/Http2Stream;->doReadTimeout()Z HSPLokhttp3/internal/http2/Http2Stream;->getConnection()Lokhttp3/internal/http2/Http2Connection; HSPLokhttp3/internal/http2/Http2Stream;->getErrorCode$okhttp()Lokhttp3/internal/http2/ErrorCode; @@ -36296,6 +36607,7 @@ HSPLokhttp3/internal/http2/Http2Writer;->flush()V HSPLokhttp3/internal/http2/Http2Writer;->frameHeader(IIII)V HSPLokhttp3/internal/http2/Http2Writer;->headers(ZILjava/util/List;)V HSPLokhttp3/internal/http2/Http2Writer;->maxDataLength()I +PLokhttp3/internal/http2/Http2Writer;->rstStream(ILokhttp3/internal/http2/ErrorCode;)V HSPLokhttp3/internal/http2/Http2Writer;->settings(Lokhttp3/internal/http2/Settings;)V HSPLokhttp3/internal/http2/Http2Writer;->windowUpdate(IJ)V Lokhttp3/internal/http2/Http2Writer$Companion; @@ -36364,7 +36676,7 @@ HSPLokhttp3/internal/platform/Android10Platform;->()V HSPLokhttp3/internal/platform/Android10Platform;->()V HSPLokhttp3/internal/platform/Android10Platform;->access$isSupported$cp()Z HSPLokhttp3/internal/platform/Android10Platform;->buildCertificateChainCleaner(Ljavax/net/ssl/X509TrustManager;)Lokhttp3/internal/tls/CertificateChainCleaner; -HSPLokhttp3/internal/platform/Android10Platform;->configureTlsExtensions(Ljavax/net/ssl/SSLSocket;Ljava/lang/String;Ljava/util/List;)V +HSPLokhttp3/internal/platform/Android10Platform;->configureTlsExtensions(Ljavax/net/ssl/SSLSocket;Ljava/lang/String;Ljava/util/List;Lokio/ByteString;)V HSPLokhttp3/internal/platform/Android10Platform;->getSelectedProtocol(Ljavax/net/ssl/SSLSocket;)Ljava/lang/String; HSPLokhttp3/internal/platform/Android10Platform;->getStackTraceForCloseable(Ljava/lang/String;)Ljava/lang/Object; HSPLokhttp3/internal/platform/Android10Platform;->newSSLContext()Ljavax/net/ssl/SSLContext; @@ -36381,6 +36693,8 @@ HSPLokhttp3/internal/platform/Platform;->()V HSPLokhttp3/internal/platform/Platform;->access$getPlatform$cp()Lokhttp3/internal/platform/Platform; HSPLokhttp3/internal/platform/Platform;->afterHandshake(Ljavax/net/ssl/SSLSocket;)V HSPLokhttp3/internal/platform/Platform;->connectSocket(Ljava/net/Socket;Ljava/net/InetSocketAddress;I)V +HSPLokhttp3/internal/platform/Platform;->getSocketFactory()Ljavax/net/SocketFactory; +HSPLokhttp3/internal/platform/Platform;->getSystemDns()Lokhttp3/Dns; HSPLokhttp3/internal/platform/Platform;->newSSLContext()Ljavax/net/ssl/SSLContext; HSPLokhttp3/internal/platform/Platform;->newSslSocketFactory(Ljavax/net/ssl/X509TrustManager;)Ljavax/net/ssl/SSLSocketFactory; HSPLokhttp3/internal/platform/Platform;->platformTrustManager()Ljavax/net/ssl/X509TrustManager; @@ -36406,7 +36720,7 @@ HSPLokhttp3/internal/platform/PlatformRegistry;->setApplicationContext(Landroid/ Lokhttp3/internal/platform/android/Android10SocketAdapter; HSPLokhttp3/internal/platform/android/Android10SocketAdapter;->()V HSPLokhttp3/internal/platform/android/Android10SocketAdapter;->()V -HSPLokhttp3/internal/platform/android/Android10SocketAdapter;->configureTlsExtensions(Ljavax/net/ssl/SSLSocket;Ljava/lang/String;Ljava/util/List;)V +HSPLokhttp3/internal/platform/android/Android10SocketAdapter;->configureTlsExtensions(Ljavax/net/ssl/SSLSocket;Ljava/lang/String;Ljava/util/List;Lokio/ByteString;)V HSPLokhttp3/internal/platform/android/Android10SocketAdapter;->getSelectedProtocol(Ljavax/net/ssl/SSLSocket;)Ljava/lang/String; HSPLokhttp3/internal/platform/android/Android10SocketAdapter;->isSupported()Z HSPLokhttp3/internal/platform/android/Android10SocketAdapter;->matchesSocket(Ljavax/net/ssl/SSLSocket;)Z @@ -36415,6 +36729,13 @@ HSPLokhttp3/internal/platform/android/Android10SocketAdapter$Companion;->( HSPLokhttp3/internal/platform/android/Android10SocketAdapter$Companion;->(Lkotlin/jvm/internal/DefaultConstructorMarker;)V HSPLokhttp3/internal/platform/android/Android10SocketAdapter$Companion;->buildIfSupported()Lokhttp3/internal/platform/android/SocketAdapter; HSPLokhttp3/internal/platform/android/Android10SocketAdapter$Companion;->isSupported()Z +Lokhttp3/internal/platform/android/Android17SocketAdapter; +HSPLokhttp3/internal/platform/android/Android17SocketAdapter;->()V +Lokhttp3/internal/platform/android/Android17SocketAdapter$Companion; +HSPLokhttp3/internal/platform/android/Android17SocketAdapter$Companion;->()V +HSPLokhttp3/internal/platform/android/Android17SocketAdapter$Companion;->(Lkotlin/jvm/internal/DefaultConstructorMarker;)V +HSPLokhttp3/internal/platform/android/Android17SocketAdapter$Companion;->buildIfSupported()Lokhttp3/internal/platform/android/SocketAdapter; +HSPLokhttp3/internal/platform/android/Android17SocketAdapter$Companion;->isSupported()Z Lokhttp3/internal/platform/android/AndroidCertificateChainCleaner; HSPLokhttp3/internal/platform/android/AndroidCertificateChainCleaner;->()V HSPLokhttp3/internal/platform/android/AndroidCertificateChainCleaner;->(Ljavax/net/ssl/X509TrustManager;Landroid/net/http/X509TrustManagerExtensions;)V @@ -36673,9 +36994,7 @@ HSPLokio/Path;->isAbsolute()Z HSPLokio/Path;->name()Ljava/lang/String; HSPLokio/Path;->nameBytes()Lokio/ByteString; HSPLokio/Path;->normalized()Lokio/Path; -HSPLokio/Path;->resolve$default(Lokio/Path;Ljava/lang/String;ZILjava/lang/Object;)Lokio/Path; HSPLokio/Path;->resolve(Ljava/lang/String;)Lokio/Path; -HSPLokio/Path;->resolve(Ljava/lang/String;Z)Lokio/Path; HSPLokio/Path;->toFile()Ljava/io/File; HSPLokio/Path;->toNioPath()Ljava/nio/file/Path; HSPLokio/Path;->toString()Ljava/lang/String; @@ -37430,17 +37749,17 @@ HSPLorg/koin/viewmodel/factory/KoinViewModelFactory;->create(Lkotlin/reflect/KCl Lorg/meshtastic/app/ComposableSingletons$MainActivityKt; HSPLorg/meshtastic/app/ComposableSingletons$MainActivityKt;->()V HSPLorg/meshtastic/app/ComposableSingletons$MainActivityKt;->()V +HSPLorg/meshtastic/app/ComposableSingletons$MainActivityKt;->getLambda$-1483891377$androidApp()Lkotlin/jvm/functions/Function4; HSPLorg/meshtastic/app/ComposableSingletons$MainActivityKt;->getLambda$-1568620155$androidApp()Lkotlin/jvm/functions/Function4; -HSPLorg/meshtastic/app/ComposableSingletons$MainActivityKt;->getLambda$-1828719539$androidApp()Lkotlin/jvm/functions/Function4; -HSPLorg/meshtastic/app/ComposableSingletons$MainActivityKt;->getLambda$-604181803$androidApp()Lkotlin/jvm/functions/Function6; -HSPLorg/meshtastic/app/ComposableSingletons$MainActivityKt;->getLambda$-945529377$androidApp()Lkotlin/jvm/functions/Function6; -HSPLorg/meshtastic/app/ComposableSingletons$MainActivityKt;->getLambda$14580037$androidApp()Lkotlin/jvm/functions/Function7; -HSPLorg/meshtastic/app/ComposableSingletons$MainActivityKt;->getLambda$1497467864$androidApp()Lkotlin/jvm/functions/Function2; -HSPLorg/meshtastic/app/ComposableSingletons$MainActivityKt;->getLambda$1502840620$androidApp()Lkotlin/jvm/functions/Function4; +HSPLorg/meshtastic/app/ComposableSingletons$MainActivityKt;->getLambda$-1824458015$androidApp()Lkotlin/jvm/functions/Function2; +HSPLorg/meshtastic/app/ComposableSingletons$MainActivityKt;->getLambda$-259353641$androidApp()Lkotlin/jvm/functions/Function6; +HSPLorg/meshtastic/app/ComposableSingletons$MainActivityKt;->getLambda$-600701215$androidApp()Lkotlin/jvm/functions/Function6; +HSPLorg/meshtastic/app/ComposableSingletons$MainActivityKt;->getLambda$1529737142$androidApp()Lkotlin/jvm/functions/Function6; HSPLorg/meshtastic/app/ComposableSingletons$MainActivityKt;->getLambda$1770700875$androidApp()Lkotlin/jvm/functions/Function5; -HSPLorg/meshtastic/app/ComposableSingletons$MainActivityKt;->getLambda$1859180575$androidApp()Lkotlin/jvm/functions/Function6; -HSPLorg/meshtastic/app/ComposableSingletons$MainActivityKt;->getLambda$502894782$androidApp()Lkotlin/jvm/functions/Function6; -HSPLorg/meshtastic/app/ComposableSingletons$MainActivityKt;->lambda_1497467864$lambda$0(Landroidx/compose/runtime/Composer;I)Lkotlin/Unit; +HSPLorg/meshtastic/app/ComposableSingletons$MainActivityKt;->getLambda$1847668782$androidApp()Lkotlin/jvm/functions/Function4; +HSPLorg/meshtastic/app/ComposableSingletons$MainActivityKt;->getLambda$359408199$androidApp()Lkotlin/jvm/functions/Function7; +HSPLorg/meshtastic/app/ComposableSingletons$MainActivityKt;->getLambda$847722944$androidApp()Lkotlin/jvm/functions/Function6; +HSPLorg/meshtastic/app/ComposableSingletons$MainActivityKt;->lambda__1824458015$lambda$0(Landroidx/compose/runtime/Composer;I)Lkotlin/Unit; Lorg/meshtastic/app/ComposableSingletons$MainActivityKt$$ExternalSyntheticLambda1; HSPLorg/meshtastic/app/ComposableSingletons$MainActivityKt$$ExternalSyntheticLambda1;->()V Lorg/meshtastic/app/ComposableSingletons$MainActivityKt$$ExternalSyntheticLambda10; @@ -37503,31 +37822,33 @@ Lorg/meshtastic/app/MainActivity$$ExternalSyntheticLambda0; HSPLorg/meshtastic/app/MainActivity$$ExternalSyntheticLambda0;->(Z)V HSPLorg/meshtastic/app/MainActivity$$ExternalSyntheticLambda0;->invoke(Ljava/lang/Object;)Ljava/lang/Object; Lorg/meshtastic/app/MainActivity$$ExternalSyntheticLambda1; -HSPLorg/meshtastic/app/MainActivity$$ExternalSyntheticLambda1;->(Lorg/meshtastic/app/MainActivity;)V -HSPLorg/meshtastic/app/MainActivity$$ExternalSyntheticLambda1;->invoke(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object; +HSPLorg/meshtastic/app/MainActivity$$ExternalSyntheticLambda1;->(Lorg/meshtastic/app/MainActivity;Lkotlin/jvm/functions/Function2;I)V Lorg/meshtastic/app/MainActivity$$ExternalSyntheticLambda10; -HSPLorg/meshtastic/app/MainActivity$$ExternalSyntheticLambda10;->()V +HSPLorg/meshtastic/app/MainActivity$$ExternalSyntheticLambda10;->(ZZLorg/meshtastic/app/MainActivity;)V +HSPLorg/meshtastic/app/MainActivity$$ExternalSyntheticLambda10;->invoke(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object; Lorg/meshtastic/app/MainActivity$$ExternalSyntheticLambda11; -HSPLorg/meshtastic/app/MainActivity$$ExternalSyntheticLambda11;->(Lorg/meshtastic/app/MainActivity;Lkotlin/jvm/functions/Function2;I)V -Lorg/meshtastic/app/MainActivity$$ExternalSyntheticLambda3; -HSPLorg/meshtastic/app/MainActivity$$ExternalSyntheticLambda3;->(Z)V -HSPLorg/meshtastic/app/MainActivity$$ExternalSyntheticLambda3;->invoke(Ljava/lang/Object;)Ljava/lang/Object; +HSPLorg/meshtastic/app/MainActivity$$ExternalSyntheticLambda11;->()V +Lorg/meshtastic/app/MainActivity$$ExternalSyntheticLambda12; +HSPLorg/meshtastic/app/MainActivity$$ExternalSyntheticLambda12;->(Lorg/meshtastic/app/MainActivity;)V +Lorg/meshtastic/app/MainActivity$$ExternalSyntheticLambda2; +HSPLorg/meshtastic/app/MainActivity$$ExternalSyntheticLambda2;->(Lorg/meshtastic/app/MainActivity;)V +HSPLorg/meshtastic/app/MainActivity$$ExternalSyntheticLambda2;->invoke(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object; Lorg/meshtastic/app/MainActivity$$ExternalSyntheticLambda4; -HSPLorg/meshtastic/app/MainActivity$$ExternalSyntheticLambda4;->(Lorg/meshtastic/app/MainActivity;)V -HSPLorg/meshtastic/app/MainActivity$$ExternalSyntheticLambda4;->invoke(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object; +HSPLorg/meshtastic/app/MainActivity$$ExternalSyntheticLambda4;->(Z)V +HSPLorg/meshtastic/app/MainActivity$$ExternalSyntheticLambda4;->invoke(Ljava/lang/Object;)Ljava/lang/Object; Lorg/meshtastic/app/MainActivity$$ExternalSyntheticLambda5; HSPLorg/meshtastic/app/MainActivity$$ExternalSyntheticLambda5;->(Lorg/meshtastic/app/MainActivity;)V +HSPLorg/meshtastic/app/MainActivity$$ExternalSyntheticLambda5;->invoke(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object; Lorg/meshtastic/app/MainActivity$$ExternalSyntheticLambda6; -HSPLorg/meshtastic/app/MainActivity$$ExternalSyntheticLambda6;->()V -HSPLorg/meshtastic/app/MainActivity$$ExternalSyntheticLambda6;->invoke()Ljava/lang/Object; +HSPLorg/meshtastic/app/MainActivity$$ExternalSyntheticLambda6;->(Lorg/meshtastic/app/MainActivity;)V Lorg/meshtastic/app/MainActivity$$ExternalSyntheticLambda7; -HSPLorg/meshtastic/app/MainActivity$$ExternalSyntheticLambda7;->(Lorg/meshtastic/app/MainActivity;)V +HSPLorg/meshtastic/app/MainActivity$$ExternalSyntheticLambda7;->()V +HSPLorg/meshtastic/app/MainActivity$$ExternalSyntheticLambda7;->invoke()Ljava/lang/Object; Lorg/meshtastic/app/MainActivity$$ExternalSyntheticLambda8; -HSPLorg/meshtastic/app/MainActivity$$ExternalSyntheticLambda8;->(Lorg/meshtastic/app/MainActivity;Z)V -HSPLorg/meshtastic/app/MainActivity$$ExternalSyntheticLambda8;->invoke()Ljava/lang/Object; +HSPLorg/meshtastic/app/MainActivity$$ExternalSyntheticLambda8;->(Lorg/meshtastic/app/MainActivity;)V Lorg/meshtastic/app/MainActivity$$ExternalSyntheticLambda9; -HSPLorg/meshtastic/app/MainActivity$$ExternalSyntheticLambda9;->(ZZLorg/meshtastic/app/MainActivity;)V -HSPLorg/meshtastic/app/MainActivity$$ExternalSyntheticLambda9;->invoke(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object; +HSPLorg/meshtastic/app/MainActivity$$ExternalSyntheticLambda9;->(Lorg/meshtastic/app/MainActivity;Z)V +HSPLorg/meshtastic/app/MainActivity$$ExternalSyntheticLambda9;->invoke()Ljava/lang/Object; Lorg/meshtastic/app/MainActivity$AppCompositionLocals$1$1; HSPLorg/meshtastic/app/MainActivity$AppCompositionLocals$1$1;->(Ljava/lang/Object;)V Lorg/meshtastic/app/MainActivity$Companion; @@ -37599,6 +37920,8 @@ Lorg/meshtastic/app/OrgMeshtasticAppMainKoinModuleModuleKt$$ExternalSyntheticLam HSPLorg/meshtastic/app/OrgMeshtasticAppMainKoinModuleModuleKt$$ExternalSyntheticLambda5;->()V HSPLorg/meshtastic/app/OrgMeshtasticAppMainKoinModuleModuleKt$$ExternalSyntheticLambda5;->invoke(Ljava/lang/Object;)Ljava/lang/Object; Lorg/meshtastic/app/R$array; +Lorg/meshtastic/app/ai/GeminiNanoDocAssistant$isSupported$4$$ExternalSyntheticBackport0; +HSPLorg/meshtastic/app/ai/GeminiNanoDocAssistant$isSupported$4$$ExternalSyntheticBackport0;->m(II)I Lorg/meshtastic/app/ai/appfunctions/AppFunctionStateSync; HSPLorg/meshtastic/app/ai/appfunctions/AppFunctionStateSync;->()V HSPLorg/meshtastic/app/ai/appfunctions/AppFunctionStateSync;->(Landroid/content/Context;Lorg/meshtastic/core/repository/AppFunctionsPrefs;Lorg/meshtastic/core/di/CoroutineDispatchers;)V @@ -38014,12 +38337,12 @@ HSPLorg/meshtastic/core/common/di/ApplicationCoroutineScopeImpl;->()V Lorg/meshtastic/core/common/di/CoreCommonModule; HSPLorg/meshtastic/core/common/di/CoreCommonModule;->()V Lorg/meshtastic/core/common/di/OrgMeshtasticCoreCommonDiCoreCommonModuleModuleKt; -HSPLorg/meshtastic/core/common/di/OrgMeshtasticCoreCommonDiCoreCommonModuleModuleKt;->$r8$lambda$I-DGDXtqd874HfFM-Np3E4PIzKA(Lorg/koin/core/scope/Scope;Lorg/koin/core/parameter/ParametersHolder;)Lorg/meshtastic/core/common/state/HiddenFeaturesUnlock; HSPLorg/meshtastic/core/common/di/OrgMeshtasticCoreCommonDiCoreCommonModuleModuleKt;->$r8$lambda$K7WC1tfKRtoYO3seEKXDs4MV_Yg(Lorg/koin/core/scope/Scope;Lorg/koin/core/parameter/ParametersHolder;)Lorg/meshtastic/core/common/di/ApplicationCoroutineScopeImpl; -HSPLorg/meshtastic/core/common/di/OrgMeshtasticCoreCommonDiCoreCommonModuleModuleKt;->$r8$lambda$nIdr_JMJBZNDHGj2KXiSaUAHV2c(Lorg/koin/core/scope/Scope;Lorg/koin/core/parameter/ParametersHolder;)Lorg/meshtastic/core/common/state/FirmwareMaintenanceLock; +HSPLorg/meshtastic/core/common/di/OrgMeshtasticCoreCommonDiCoreCommonModuleModuleKt;->$r8$lambda$M6QacCv6WohZ21c25lQMM2z6gGk(Lorg/koin/core/scope/Scope;Lorg/koin/core/parameter/ParametersHolder;)Lorg/meshtastic/core/common/state/RadioOperationLock; +HSPLorg/meshtastic/core/common/di/OrgMeshtasticCoreCommonDiCoreCommonModuleModuleKt;->$r8$lambda$qCq5yc3dzvILOC-TT4uNIwDh2jw(Lorg/koin/core/scope/Scope;Lorg/koin/core/parameter/ParametersHolder;)Lorg/meshtastic/core/common/state/HiddenFeaturesUnlock; HSPLorg/meshtastic/core/common/di/OrgMeshtasticCoreCommonDiCoreCommonModuleModuleKt;->module$lambda$0$3(Lorg/koin/core/scope/Scope;Lorg/koin/core/parameter/ParametersHolder;)Lorg/meshtastic/core/common/di/ApplicationCoroutineScopeImpl; -HSPLorg/meshtastic/core/common/di/OrgMeshtasticCoreCommonDiCoreCommonModuleModuleKt;->module$lambda$0$4(Lorg/koin/core/scope/Scope;Lorg/koin/core/parameter/ParametersHolder;)Lorg/meshtastic/core/common/state/FirmwareMaintenanceLock; -HSPLorg/meshtastic/core/common/di/OrgMeshtasticCoreCommonDiCoreCommonModuleModuleKt;->module$lambda$0$5(Lorg/koin/core/scope/Scope;Lorg/koin/core/parameter/ParametersHolder;)Lorg/meshtastic/core/common/state/HiddenFeaturesUnlock; +HSPLorg/meshtastic/core/common/di/OrgMeshtasticCoreCommonDiCoreCommonModuleModuleKt;->module$lambda$0$4(Lorg/koin/core/scope/Scope;Lorg/koin/core/parameter/ParametersHolder;)Lorg/meshtastic/core/common/state/HiddenFeaturesUnlock; +HSPLorg/meshtastic/core/common/di/OrgMeshtasticCoreCommonDiCoreCommonModuleModuleKt;->module$lambda$0$5(Lorg/koin/core/scope/Scope;Lorg/koin/core/parameter/ParametersHolder;)Lorg/meshtastic/core/common/state/RadioOperationLock; HSPLorg/meshtastic/core/common/di/OrgMeshtasticCoreCommonDiCoreCommonModuleModuleKt;->module$lambda$0(Lorg/koin/core/module/Module;)Lkotlin/Unit; HSPLorg/meshtastic/core/common/di/OrgMeshtasticCoreCommonDiCoreCommonModuleModuleKt;->module(Lorg/meshtastic/core/common/di/CoreCommonModule;)Lorg/koin/core/module/Module; Lorg/meshtastic/core/common/di/OrgMeshtasticCoreCommonDiCoreCommonModuleModuleKt$$ExternalSyntheticLambda0; @@ -38050,11 +38373,14 @@ Lorg/meshtastic/core/common/log/ExpectedCondition; Lorg/meshtastic/core/common/log/ExpectedConditionKt; HSPLorg/meshtastic/core/common/log/ExpectedConditionKt;->shouldDowngradeForDatadog(Lco/touchlab/kermit/Severity;Ljava/lang/Throwable;)Z HSPLorg/meshtastic/core/common/log/ExpectedConditionKt;->shouldReportAsException(Lco/touchlab/kermit/Severity;Ljava/lang/Throwable;)Z -Lorg/meshtastic/core/common/state/FirmwareMaintenanceLock; -HSPLorg/meshtastic/core/common/state/FirmwareMaintenanceLock;->()V Lorg/meshtastic/core/common/state/HiddenFeaturesUnlock; HSPLorg/meshtastic/core/common/state/HiddenFeaturesUnlock;->()V HSPLorg/meshtastic/core/common/state/HiddenFeaturesUnlock;->getUnlocked()Lkotlinx/coroutines/flow/StateFlow; +Lorg/meshtastic/core/common/state/RadioOperationLock; +HSPLorg/meshtastic/core/common/state/RadioOperationLock;->()V +HSPLorg/meshtastic/core/common/state/RadioOperationLock;->getActiveOperations()Ljava/util/Set; +HSPLorg/meshtastic/core/common/state/RadioOperationLock;->getHolders()Lkotlinx/coroutines/flow/StateFlow; +PLorg/meshtastic/core/common/state/RadioOperationLock;->isActive()Z Lorg/meshtastic/core/common/util/AddressUtilsKt; HSPLorg/meshtastic/core/common/util/AddressUtilsKt;->isNoDeviceSentinel(Ljava/lang/String;)Z HSPLorg/meshtastic/core/common/util/AddressUtilsKt;->isValidDeviceAddress(Ljava/lang/String;)Z @@ -38605,6 +38931,8 @@ HSPLorg/meshtastic/core/data/manager/MeshMessageProcessorImpl$Companion;-> Lorg/meshtastic/core/data/manager/MessageFilterImpl; Lorg/meshtastic/core/data/manager/MqttManagerImpl; HSPLorg/meshtastic/core/data/manager/MqttManagerImpl;->(Lorg/meshtastic/core/network/repository/MQTTRepository;Lorg/meshtastic/core/repository/PacketHandler;Lorg/meshtastic/core/repository/ServiceStateWriter;Lorg/meshtastic/core/repository/NodeRepository;Lorg/meshtastic/core/common/di/ServiceScope;)V +Lorg/meshtastic/core/data/manager/MqttManagerImpl$$ExternalSyntheticTypeSwitchClass1; +HSPLorg/meshtastic/core/data/manager/MqttManagerImpl$$ExternalSyntheticTypeSwitchClass1;->switchDispatch$1(Ljava/lang/Object;I)I Lorg/meshtastic/core/data/manager/MqttManagerImpl$mqttConnectionState$1; HSPLorg/meshtastic/core/data/manager/MqttManagerImpl$mqttConnectionState$1;->(Lorg/meshtastic/core/data/manager/MqttManagerImpl;Lkotlin/coroutines/Continuation;)V HSPLorg/meshtastic/core/data/manager/MqttManagerImpl$mqttConnectionState$1;->invoke(Ljava/lang/Object;Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object; @@ -38697,9 +39025,9 @@ Lorg/meshtastic/core/data/repository/MaintenanceUf2RepositoryImpl; Lorg/meshtastic/core/data/repository/MeshLogRepositoryImpl; HSPLorg/meshtastic/core/data/repository/MeshLogRepositoryImpl;->()V HSPLorg/meshtastic/core/data/repository/MeshLogRepositoryImpl;->(Lorg/meshtastic/core/database/DatabaseProvider;Lorg/meshtastic/core/di/CoroutineDispatchers;Lorg/meshtastic/core/repository/MeshLogPrefs;Lorg/meshtastic/core/data/datasource/NodeInfoReadDataSource;)V -HSPLorg/meshtastic/core/data/repository/MeshLogRepositoryImpl;->access$getDbManager$p(Lorg/meshtastic/core/data/repository/MeshLogRepositoryImpl;)Lorg/meshtastic/core/database/DatabaseProvider; -HSPLorg/meshtastic/core/data/repository/MeshLogRepositoryImpl;->deleteLogsOlderThan$suspendImpl(Lorg/meshtastic/core/data/repository/MeshLogRepositoryImpl;ILkotlin/coroutines/Continuation;)Ljava/lang/Object; -HSPLorg/meshtastic/core/data/repository/MeshLogRepositoryImpl;->deleteLogsOlderThan(ILkotlin/coroutines/Continuation;)Ljava/lang/Object; +PLorg/meshtastic/core/data/repository/MeshLogRepositoryImpl;->access$getDbManager$p(Lorg/meshtastic/core/data/repository/MeshLogRepositoryImpl;)Lorg/meshtastic/core/database/DatabaseProvider; +PLorg/meshtastic/core/data/repository/MeshLogRepositoryImpl;->deleteLogsOlderThan$suspendImpl(Lorg/meshtastic/core/data/repository/MeshLogRepositoryImpl;ILkotlin/coroutines/Continuation;)Ljava/lang/Object; +PLorg/meshtastic/core/data/repository/MeshLogRepositoryImpl;->deleteLogsOlderThan(ILkotlin/coroutines/Continuation;)Ljava/lang/Object; HSPLorg/meshtastic/core/data/repository/MeshLogRepositoryImpl;->getMyNodeInfo$lambda$0(Lorg/meshtastic/core/database/MeshtasticDatabase;)Lkotlinx/coroutines/flow/Flow; HSPLorg/meshtastic/core/data/repository/MeshLogRepositoryImpl;->getMyNodeInfo()Lkotlinx/coroutines/flow/Flow; Lorg/meshtastic/core/data/repository/MeshLogRepositoryImpl$$ExternalSyntheticLambda2; @@ -38870,7 +39198,6 @@ HSPLorg/meshtastic/core/database/DatabaseManager;->(Lorg/meshtastic/core/d HSPLorg/meshtastic/core/database/DatabaseManager;->access$checkOpen(Lorg/meshtastic/core/database/DatabaseManager;)V HSPLorg/meshtastic/core/database/DatabaseManager;->access$cleanupLegacyDbIfNeeded(Lorg/meshtastic/core/database/DatabaseManager;Ljava/lang/String;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; HSPLorg/meshtastic/core/database/DatabaseManager;->access$cleanupPersistedRetirementsAtStartup(Lorg/meshtastic/core/database/DatabaseManager;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; -HSPLorg/meshtastic/core/database/DatabaseManager;->access$dbFileExists(Lorg/meshtastic/core/database/DatabaseManager;Ljava/lang/String;)Z HSPLorg/meshtastic/core/database/DatabaseManager;->access$deviceDbNamesLocked(Lorg/meshtastic/core/database/DatabaseManager;)Ljava/util/List; HSPLorg/meshtastic/core/database/DatabaseManager;->access$endWrite(Lorg/meshtastic/core/database/DatabaseManager;Lorg/meshtastic/core/database/MeshtasticDatabase;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; HSPLorg/meshtastic/core/database/DatabaseManager;->access$enforceCacheLimit(Lorg/meshtastic/core/database/DatabaseManager;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; @@ -38901,7 +39228,6 @@ HSPLorg/meshtastic/core/database/DatabaseManager;->checkOpen()V HSPLorg/meshtastic/core/database/DatabaseManager;->cleanupLegacyDbIfNeeded(Ljava/lang/String;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; HSPLorg/meshtastic/core/database/DatabaseManager;->cleanupPersistedRetirementsAtStartup(Lkotlin/coroutines/Continuation;)Ljava/lang/Object; HSPLorg/meshtastic/core/database/DatabaseManager;->createPoolLane()Lkotlinx/coroutines/CoroutineDispatcher; -HSPLorg/meshtastic/core/database/DatabaseManager;->dbFileExists(Ljava/lang/String;)Z HSPLorg/meshtastic/core/database/DatabaseManager;->deviceDbNamesLocked()Ljava/util/List; HSPLorg/meshtastic/core/database/DatabaseManager;->endWrite(Lorg/meshtastic/core/database/MeshtasticDatabase;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; HSPLorg/meshtastic/core/database/DatabaseManager;->enforceCacheLimit(Lkotlin/coroutines/Continuation;)Ljava/lang/Object; @@ -38995,12 +39321,6 @@ HSPLorg/meshtastic/core/database/DatabaseManager$cleanupLegacyDbIfNeeded$2;->cre HSPLorg/meshtastic/core/database/DatabaseManager$cleanupLegacyDbIfNeeded$2;->invoke(Ljava/lang/Object;)Ljava/lang/Object; HSPLorg/meshtastic/core/database/DatabaseManager$cleanupLegacyDbIfNeeded$2;->invoke(Lkotlin/coroutines/Continuation;)Ljava/lang/Object; HSPLorg/meshtastic/core/database/DatabaseManager$cleanupLegacyDbIfNeeded$2;->invokeSuspend(Ljava/lang/Object;)Ljava/lang/Object; -Lorg/meshtastic/core/database/DatabaseManager$cleanupLegacyDbIfNeeded$2$1$4; -HSPLorg/meshtastic/core/database/DatabaseManager$cleanupLegacyDbIfNeeded$2$1$4;->(Lorg/meshtastic/core/database/DatabaseManager;Lkotlin/coroutines/Continuation;)V -HSPLorg/meshtastic/core/database/DatabaseManager$cleanupLegacyDbIfNeeded$2$1$4;->create(Ljava/lang/Object;Lkotlin/coroutines/Continuation;)Lkotlin/coroutines/Continuation; -HSPLorg/meshtastic/core/database/DatabaseManager$cleanupLegacyDbIfNeeded$2$1$4;->invoke(Landroidx/datastore/preferences/core/MutablePreferences;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; -HSPLorg/meshtastic/core/database/DatabaseManager$cleanupLegacyDbIfNeeded$2$1$4;->invoke(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object; -HSPLorg/meshtastic/core/database/DatabaseManager$cleanupLegacyDbIfNeeded$2$1$4;->invokeSuspend(Ljava/lang/Object;)Ljava/lang/Object; Lorg/meshtastic/core/database/DatabaseManager$cleanupPersistedRetirementsAtStartup$1; HSPLorg/meshtastic/core/database/DatabaseManager$cleanupPersistedRetirementsAtStartup$1;->(Lorg/meshtastic/core/database/DatabaseManager;Lkotlin/coroutines/Continuation;)V HSPLorg/meshtastic/core/database/DatabaseManager$cleanupPersistedRetirementsAtStartup$1;->invokeSuspend(Ljava/lang/Object;)Ljava/lang/Object; @@ -39282,6 +39602,10 @@ Lorg/meshtastic/core/database/MeshtasticDatabase_AutoMigration_56_57_Impl; HSPLorg/meshtastic/core/database/MeshtasticDatabase_AutoMigration_56_57_Impl;->()V Lorg/meshtastic/core/database/MeshtasticDatabase_AutoMigration_57_58_Impl; HSPLorg/meshtastic/core/database/MeshtasticDatabase_AutoMigration_57_58_Impl;->()V +Lorg/meshtastic/core/database/MeshtasticDatabase_AutoMigration_58_59_Impl; +HSPLorg/meshtastic/core/database/MeshtasticDatabase_AutoMigration_58_59_Impl;->()V +Lorg/meshtastic/core/database/MeshtasticDatabase_AutoMigration_59_60_Impl; +HSPLorg/meshtastic/core/database/MeshtasticDatabase_AutoMigration_59_60_Impl;->()V Lorg/meshtastic/core/database/MeshtasticDatabase_AutoMigration_5_6_Impl; HSPLorg/meshtastic/core/database/MeshtasticDatabase_AutoMigration_5_6_Impl;->()V Lorg/meshtastic/core/database/MeshtasticDatabase_AutoMigration_6_7_Impl; @@ -39437,7 +39761,7 @@ HSPLorg/meshtastic/core/database/dao/MeshLogDao$Companion;->()V Lorg/meshtastic/core/database/dao/MeshLogDao_Impl; HSPLorg/meshtastic/core/database/dao/MeshLogDao_Impl;->()V HSPLorg/meshtastic/core/database/dao/MeshLogDao_Impl;->(Landroidx/room3/RoomDatabase;)V -HSPLorg/meshtastic/core/database/dao/MeshLogDao_Impl;->deleteOlderThan(JLkotlin/coroutines/Continuation;)Ljava/lang/Object; +PLorg/meshtastic/core/database/dao/MeshLogDao_Impl;->deleteOlderThan(JLkotlin/coroutines/Continuation;)Ljava/lang/Object; HSPLorg/meshtastic/core/database/dao/MeshLogDao_Impl;->getLogsFrom(III)Lkotlinx/coroutines/flow/Flow; Lorg/meshtastic/core/database/dao/MeshLogDao_Impl$1; HSPLorg/meshtastic/core/database/dao/MeshLogDao_Impl$1;->(Lorg/meshtastic/core/database/dao/MeshLogDao_Impl;)V @@ -40129,9 +40453,9 @@ Lorg/meshtastic/core/network/repository/AndroidSerialDevicePresence$special$$inl HSPLorg/meshtastic/core/network/repository/AndroidSerialDevicePresence$special$$inlined$map$1;->(Lkotlinx/coroutines/flow/Flow;)V HSPLorg/meshtastic/core/network/repository/AndroidSerialDevicePresence$special$$inlined$map$1;->collect(Lkotlinx/coroutines/flow/FlowCollector;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; Lorg/meshtastic/core/network/repository/AndroidSerialDevicePresence$special$$inlined$map$1$1; -PLorg/meshtastic/core/network/repository/AndroidSerialDevicePresence$special$$inlined$map$1$1;->(Lorg/meshtastic/core/network/repository/AndroidSerialDevicePresence$special$$inlined$map$1;Lkotlin/coroutines/Continuation;)V +HSPLorg/meshtastic/core/network/repository/AndroidSerialDevicePresence$special$$inlined$map$1$1;->(Lorg/meshtastic/core/network/repository/AndroidSerialDevicePresence$special$$inlined$map$1;Lkotlin/coroutines/Continuation;)V Lorg/meshtastic/core/network/repository/AndroidSerialDevicePresence$special$$inlined$map$1$2; -PLorg/meshtastic/core/network/repository/AndroidSerialDevicePresence$special$$inlined$map$1$2;->(Lkotlinx/coroutines/flow/FlowCollector;)V +HSPLorg/meshtastic/core/network/repository/AndroidSerialDevicePresence$special$$inlined$map$1$2;->(Lkotlinx/coroutines/flow/FlowCollector;)V HSPLorg/meshtastic/core/network/repository/AndroidSerialDevicePresence$special$$inlined$map$1$2;->emit(Ljava/lang/Object;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; Lorg/meshtastic/core/network/repository/AndroidSerialDevicePresence$special$$inlined$map$1$2$1; HSPLorg/meshtastic/core/network/repository/AndroidSerialDevicePresence$special$$inlined$map$1$2$1;->(Lorg/meshtastic/core/network/repository/AndroidSerialDevicePresence$special$$inlined$map$1$2;Lkotlin/coroutines/Continuation;)V @@ -40494,7 +40818,7 @@ HSPLorg/meshtastic/core/prefs/meshlog/MeshLogPrefsImpl;->()V HSPLorg/meshtastic/core/prefs/meshlog/MeshLogPrefsImpl;->(Lorg/meshtastic/core/prefs/di/MeshLogDataStore;Lorg/meshtastic/core/di/CoroutineDispatchers;)V HSPLorg/meshtastic/core/prefs/meshlog/MeshLogPrefsImpl;->access$getKEY_LOGGING_ENABLED_PREF$cp()Landroidx/datastore/preferences/core/Preferences$Key; HSPLorg/meshtastic/core/prefs/meshlog/MeshLogPrefsImpl;->access$getKEY_RETENTION_DAYS_PREF$cp()Landroidx/datastore/preferences/core/Preferences$Key; -HSPLorg/meshtastic/core/prefs/meshlog/MeshLogPrefsImpl;->awaitCleanupPolicy(Lkotlin/coroutines/Continuation;)Ljava/lang/Object; +PLorg/meshtastic/core/prefs/meshlog/MeshLogPrefsImpl;->awaitCleanupPolicy(Lkotlin/coroutines/Continuation;)Ljava/lang/Object; Lorg/meshtastic/core/prefs/meshlog/MeshLogPrefsImpl$Companion; HSPLorg/meshtastic/core/prefs/meshlog/MeshLogPrefsImpl$Companion;->()V HSPLorg/meshtastic/core/prefs/meshlog/MeshLogPrefsImpl$Companion;->(Lkotlin/jvm/internal/DefaultConstructorMarker;)V @@ -40660,10 +40984,34 @@ Lorg/meshtastic/core/prefs/ui/UiPrefsImpl$special$$inlined$map$1$2$1; HSPLorg/meshtastic/core/prefs/ui/UiPrefsImpl$special$$inlined$map$1$2$1;->(Lorg/meshtastic/core/prefs/ui/UiPrefsImpl$special$$inlined$map$1$2;Lkotlin/coroutines/Continuation;)V Lorg/meshtastic/core/prefs/ui/UiPrefsImpl$special$$inlined$map$10; HSPLorg/meshtastic/core/prefs/ui/UiPrefsImpl$special$$inlined$map$10;->(Lkotlinx/coroutines/flow/Flow;)V +HSPLorg/meshtastic/core/prefs/ui/UiPrefsImpl$special$$inlined$map$10;->collect(Lkotlinx/coroutines/flow/FlowCollector;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; +Lorg/meshtastic/core/prefs/ui/UiPrefsImpl$special$$inlined$map$10$1; +HSPLorg/meshtastic/core/prefs/ui/UiPrefsImpl$special$$inlined$map$10$1;->(Lorg/meshtastic/core/prefs/ui/UiPrefsImpl$special$$inlined$map$10;Lkotlin/coroutines/Continuation;)V +Lorg/meshtastic/core/prefs/ui/UiPrefsImpl$special$$inlined$map$10$2; +HSPLorg/meshtastic/core/prefs/ui/UiPrefsImpl$special$$inlined$map$10$2;->(Lkotlinx/coroutines/flow/FlowCollector;)V +HSPLorg/meshtastic/core/prefs/ui/UiPrefsImpl$special$$inlined$map$10$2;->emit(Ljava/lang/Object;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; +Lorg/meshtastic/core/prefs/ui/UiPrefsImpl$special$$inlined$map$10$2$1; +HSPLorg/meshtastic/core/prefs/ui/UiPrefsImpl$special$$inlined$map$10$2$1;->(Lorg/meshtastic/core/prefs/ui/UiPrefsImpl$special$$inlined$map$10$2;Lkotlin/coroutines/Continuation;)V Lorg/meshtastic/core/prefs/ui/UiPrefsImpl$special$$inlined$map$11; HSPLorg/meshtastic/core/prefs/ui/UiPrefsImpl$special$$inlined$map$11;->(Lkotlinx/coroutines/flow/Flow;)V +HSPLorg/meshtastic/core/prefs/ui/UiPrefsImpl$special$$inlined$map$11;->collect(Lkotlinx/coroutines/flow/FlowCollector;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; +Lorg/meshtastic/core/prefs/ui/UiPrefsImpl$special$$inlined$map$11$1; +HSPLorg/meshtastic/core/prefs/ui/UiPrefsImpl$special$$inlined$map$11$1;->(Lorg/meshtastic/core/prefs/ui/UiPrefsImpl$special$$inlined$map$11;Lkotlin/coroutines/Continuation;)V +Lorg/meshtastic/core/prefs/ui/UiPrefsImpl$special$$inlined$map$11$2; +HSPLorg/meshtastic/core/prefs/ui/UiPrefsImpl$special$$inlined$map$11$2;->(Lkotlinx/coroutines/flow/FlowCollector;)V +HSPLorg/meshtastic/core/prefs/ui/UiPrefsImpl$special$$inlined$map$11$2;->emit(Ljava/lang/Object;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; +Lorg/meshtastic/core/prefs/ui/UiPrefsImpl$special$$inlined$map$11$2$1; +HSPLorg/meshtastic/core/prefs/ui/UiPrefsImpl$special$$inlined$map$11$2$1;->(Lorg/meshtastic/core/prefs/ui/UiPrefsImpl$special$$inlined$map$11$2;Lkotlin/coroutines/Continuation;)V Lorg/meshtastic/core/prefs/ui/UiPrefsImpl$special$$inlined$map$12; HSPLorg/meshtastic/core/prefs/ui/UiPrefsImpl$special$$inlined$map$12;->(Lkotlinx/coroutines/flow/Flow;)V +HSPLorg/meshtastic/core/prefs/ui/UiPrefsImpl$special$$inlined$map$12;->collect(Lkotlinx/coroutines/flow/FlowCollector;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; +Lorg/meshtastic/core/prefs/ui/UiPrefsImpl$special$$inlined$map$12$1; +HSPLorg/meshtastic/core/prefs/ui/UiPrefsImpl$special$$inlined$map$12$1;->(Lorg/meshtastic/core/prefs/ui/UiPrefsImpl$special$$inlined$map$12;Lkotlin/coroutines/Continuation;)V +Lorg/meshtastic/core/prefs/ui/UiPrefsImpl$special$$inlined$map$12$2; +HSPLorg/meshtastic/core/prefs/ui/UiPrefsImpl$special$$inlined$map$12$2;->(Lkotlinx/coroutines/flow/FlowCollector;)V +HSPLorg/meshtastic/core/prefs/ui/UiPrefsImpl$special$$inlined$map$12$2;->emit(Ljava/lang/Object;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; +Lorg/meshtastic/core/prefs/ui/UiPrefsImpl$special$$inlined$map$12$2$1; +HSPLorg/meshtastic/core/prefs/ui/UiPrefsImpl$special$$inlined$map$12$2$1;->(Lorg/meshtastic/core/prefs/ui/UiPrefsImpl$special$$inlined$map$12$2;Lkotlin/coroutines/Continuation;)V Lorg/meshtastic/core/prefs/ui/UiPrefsImpl$special$$inlined$map$13; HSPLorg/meshtastic/core/prefs/ui/UiPrefsImpl$special$$inlined$map$13;->(Lkotlinx/coroutines/flow/Flow;)V HSPLorg/meshtastic/core/prefs/ui/UiPrefsImpl$special$$inlined$map$13;->collect(Lkotlinx/coroutines/flow/FlowCollector;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; @@ -40794,56 +41142,6 @@ HSPLorg/meshtastic/core/prefs/ui/UiPrefsImpl$special$$inlined$map$24$2;->( HSPLorg/meshtastic/core/prefs/ui/UiPrefsImpl$special$$inlined$map$24$2;->emit(Ljava/lang/Object;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; Lorg/meshtastic/core/prefs/ui/UiPrefsImpl$special$$inlined$map$24$2$1; HSPLorg/meshtastic/core/prefs/ui/UiPrefsImpl$special$$inlined$map$24$2$1;->(Lorg/meshtastic/core/prefs/ui/UiPrefsImpl$special$$inlined$map$24$2;Lkotlin/coroutines/Continuation;)V -Lorg/meshtastic/core/prefs/ui/UiPrefsImpl$special$$inlined$map$25; -HSPLorg/meshtastic/core/prefs/ui/UiPrefsImpl$special$$inlined$map$25;->(Lkotlinx/coroutines/flow/Flow;)V -HSPLorg/meshtastic/core/prefs/ui/UiPrefsImpl$special$$inlined$map$25;->collect(Lkotlinx/coroutines/flow/FlowCollector;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; -Lorg/meshtastic/core/prefs/ui/UiPrefsImpl$special$$inlined$map$25$1; -HSPLorg/meshtastic/core/prefs/ui/UiPrefsImpl$special$$inlined$map$25$1;->(Lorg/meshtastic/core/prefs/ui/UiPrefsImpl$special$$inlined$map$25;Lkotlin/coroutines/Continuation;)V -Lorg/meshtastic/core/prefs/ui/UiPrefsImpl$special$$inlined$map$25$2; -HSPLorg/meshtastic/core/prefs/ui/UiPrefsImpl$special$$inlined$map$25$2;->(Lkotlinx/coroutines/flow/FlowCollector;)V -HSPLorg/meshtastic/core/prefs/ui/UiPrefsImpl$special$$inlined$map$25$2;->emit(Ljava/lang/Object;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; -Lorg/meshtastic/core/prefs/ui/UiPrefsImpl$special$$inlined$map$25$2$1; -HSPLorg/meshtastic/core/prefs/ui/UiPrefsImpl$special$$inlined$map$25$2$1;->(Lorg/meshtastic/core/prefs/ui/UiPrefsImpl$special$$inlined$map$25$2;Lkotlin/coroutines/Continuation;)V -Lorg/meshtastic/core/prefs/ui/UiPrefsImpl$special$$inlined$map$26; -HSPLorg/meshtastic/core/prefs/ui/UiPrefsImpl$special$$inlined$map$26;->(Lkotlinx/coroutines/flow/Flow;)V -HSPLorg/meshtastic/core/prefs/ui/UiPrefsImpl$special$$inlined$map$26;->collect(Lkotlinx/coroutines/flow/FlowCollector;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; -Lorg/meshtastic/core/prefs/ui/UiPrefsImpl$special$$inlined$map$26$1; -HSPLorg/meshtastic/core/prefs/ui/UiPrefsImpl$special$$inlined$map$26$1;->(Lorg/meshtastic/core/prefs/ui/UiPrefsImpl$special$$inlined$map$26;Lkotlin/coroutines/Continuation;)V -Lorg/meshtastic/core/prefs/ui/UiPrefsImpl$special$$inlined$map$26$2; -HSPLorg/meshtastic/core/prefs/ui/UiPrefsImpl$special$$inlined$map$26$2;->(Lkotlinx/coroutines/flow/FlowCollector;)V -HSPLorg/meshtastic/core/prefs/ui/UiPrefsImpl$special$$inlined$map$26$2;->emit(Ljava/lang/Object;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; -Lorg/meshtastic/core/prefs/ui/UiPrefsImpl$special$$inlined$map$26$2$1; -HSPLorg/meshtastic/core/prefs/ui/UiPrefsImpl$special$$inlined$map$26$2$1;->(Lorg/meshtastic/core/prefs/ui/UiPrefsImpl$special$$inlined$map$26$2;Lkotlin/coroutines/Continuation;)V -Lorg/meshtastic/core/prefs/ui/UiPrefsImpl$special$$inlined$map$27; -HSPLorg/meshtastic/core/prefs/ui/UiPrefsImpl$special$$inlined$map$27;->(Lkotlinx/coroutines/flow/Flow;)V -HSPLorg/meshtastic/core/prefs/ui/UiPrefsImpl$special$$inlined$map$27;->collect(Lkotlinx/coroutines/flow/FlowCollector;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; -Lorg/meshtastic/core/prefs/ui/UiPrefsImpl$special$$inlined$map$27$1; -HSPLorg/meshtastic/core/prefs/ui/UiPrefsImpl$special$$inlined$map$27$1;->(Lorg/meshtastic/core/prefs/ui/UiPrefsImpl$special$$inlined$map$27;Lkotlin/coroutines/Continuation;)V -Lorg/meshtastic/core/prefs/ui/UiPrefsImpl$special$$inlined$map$27$2; -HSPLorg/meshtastic/core/prefs/ui/UiPrefsImpl$special$$inlined$map$27$2;->(Lkotlinx/coroutines/flow/FlowCollector;)V -HSPLorg/meshtastic/core/prefs/ui/UiPrefsImpl$special$$inlined$map$27$2;->emit(Ljava/lang/Object;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; -Lorg/meshtastic/core/prefs/ui/UiPrefsImpl$special$$inlined$map$27$2$1; -HSPLorg/meshtastic/core/prefs/ui/UiPrefsImpl$special$$inlined$map$27$2$1;->(Lorg/meshtastic/core/prefs/ui/UiPrefsImpl$special$$inlined$map$27$2;Lkotlin/coroutines/Continuation;)V -Lorg/meshtastic/core/prefs/ui/UiPrefsImpl$special$$inlined$map$28; -HSPLorg/meshtastic/core/prefs/ui/UiPrefsImpl$special$$inlined$map$28;->(Lkotlinx/coroutines/flow/Flow;)V -HSPLorg/meshtastic/core/prefs/ui/UiPrefsImpl$special$$inlined$map$28;->collect(Lkotlinx/coroutines/flow/FlowCollector;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; -Lorg/meshtastic/core/prefs/ui/UiPrefsImpl$special$$inlined$map$28$1; -HSPLorg/meshtastic/core/prefs/ui/UiPrefsImpl$special$$inlined$map$28$1;->(Lorg/meshtastic/core/prefs/ui/UiPrefsImpl$special$$inlined$map$28;Lkotlin/coroutines/Continuation;)V -Lorg/meshtastic/core/prefs/ui/UiPrefsImpl$special$$inlined$map$28$2; -HSPLorg/meshtastic/core/prefs/ui/UiPrefsImpl$special$$inlined$map$28$2;->(Lkotlinx/coroutines/flow/FlowCollector;)V -HSPLorg/meshtastic/core/prefs/ui/UiPrefsImpl$special$$inlined$map$28$2;->emit(Ljava/lang/Object;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; -Lorg/meshtastic/core/prefs/ui/UiPrefsImpl$special$$inlined$map$28$2$1; -HSPLorg/meshtastic/core/prefs/ui/UiPrefsImpl$special$$inlined$map$28$2$1;->(Lorg/meshtastic/core/prefs/ui/UiPrefsImpl$special$$inlined$map$28$2;Lkotlin/coroutines/Continuation;)V -Lorg/meshtastic/core/prefs/ui/UiPrefsImpl$special$$inlined$map$29; -HSPLorg/meshtastic/core/prefs/ui/UiPrefsImpl$special$$inlined$map$29;->(Lkotlinx/coroutines/flow/Flow;)V -HSPLorg/meshtastic/core/prefs/ui/UiPrefsImpl$special$$inlined$map$29;->collect(Lkotlinx/coroutines/flow/FlowCollector;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; -Lorg/meshtastic/core/prefs/ui/UiPrefsImpl$special$$inlined$map$29$1; -HSPLorg/meshtastic/core/prefs/ui/UiPrefsImpl$special$$inlined$map$29$1;->(Lorg/meshtastic/core/prefs/ui/UiPrefsImpl$special$$inlined$map$29;Lkotlin/coroutines/Continuation;)V -Lorg/meshtastic/core/prefs/ui/UiPrefsImpl$special$$inlined$map$29$2; -HSPLorg/meshtastic/core/prefs/ui/UiPrefsImpl$special$$inlined$map$29$2;->(Lkotlinx/coroutines/flow/FlowCollector;)V -HSPLorg/meshtastic/core/prefs/ui/UiPrefsImpl$special$$inlined$map$29$2;->emit(Ljava/lang/Object;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; -Lorg/meshtastic/core/prefs/ui/UiPrefsImpl$special$$inlined$map$29$2$1; -HSPLorg/meshtastic/core/prefs/ui/UiPrefsImpl$special$$inlined$map$29$2$1;->(Lorg/meshtastic/core/prefs/ui/UiPrefsImpl$special$$inlined$map$29$2;Lkotlin/coroutines/Continuation;)V Lorg/meshtastic/core/prefs/ui/UiPrefsImpl$special$$inlined$map$3; HSPLorg/meshtastic/core/prefs/ui/UiPrefsImpl$special$$inlined$map$3;->(Lkotlinx/coroutines/flow/Flow;)V HSPLorg/meshtastic/core/prefs/ui/UiPrefsImpl$special$$inlined$map$3;->collect(Lkotlinx/coroutines/flow/FlowCollector;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; @@ -40854,16 +41152,6 @@ HSPLorg/meshtastic/core/prefs/ui/UiPrefsImpl$special$$inlined$map$3$2;->(L HSPLorg/meshtastic/core/prefs/ui/UiPrefsImpl$special$$inlined$map$3$2;->emit(Ljava/lang/Object;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; Lorg/meshtastic/core/prefs/ui/UiPrefsImpl$special$$inlined$map$3$2$1; HSPLorg/meshtastic/core/prefs/ui/UiPrefsImpl$special$$inlined$map$3$2$1;->(Lorg/meshtastic/core/prefs/ui/UiPrefsImpl$special$$inlined$map$3$2;Lkotlin/coroutines/Continuation;)V -Lorg/meshtastic/core/prefs/ui/UiPrefsImpl$special$$inlined$map$30; -HSPLorg/meshtastic/core/prefs/ui/UiPrefsImpl$special$$inlined$map$30;->(Lkotlinx/coroutines/flow/Flow;)V -HSPLorg/meshtastic/core/prefs/ui/UiPrefsImpl$special$$inlined$map$30;->collect(Lkotlinx/coroutines/flow/FlowCollector;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; -Lorg/meshtastic/core/prefs/ui/UiPrefsImpl$special$$inlined$map$30$1; -HSPLorg/meshtastic/core/prefs/ui/UiPrefsImpl$special$$inlined$map$30$1;->(Lorg/meshtastic/core/prefs/ui/UiPrefsImpl$special$$inlined$map$30;Lkotlin/coroutines/Continuation;)V -Lorg/meshtastic/core/prefs/ui/UiPrefsImpl$special$$inlined$map$30$2; -HSPLorg/meshtastic/core/prefs/ui/UiPrefsImpl$special$$inlined$map$30$2;->(Lkotlinx/coroutines/flow/FlowCollector;)V -HSPLorg/meshtastic/core/prefs/ui/UiPrefsImpl$special$$inlined$map$30$2;->emit(Ljava/lang/Object;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; -Lorg/meshtastic/core/prefs/ui/UiPrefsImpl$special$$inlined$map$30$2$1; -HSPLorg/meshtastic/core/prefs/ui/UiPrefsImpl$special$$inlined$map$30$2$1;->(Lorg/meshtastic/core/prefs/ui/UiPrefsImpl$special$$inlined$map$30$2;Lkotlin/coroutines/Continuation;)V Lorg/meshtastic/core/prefs/ui/UiPrefsImpl$special$$inlined$map$4; HSPLorg/meshtastic/core/prefs/ui/UiPrefsImpl$special$$inlined$map$4;->(Lkotlinx/coroutines/flow/Flow;)V HSPLorg/meshtastic/core/prefs/ui/UiPrefsImpl$special$$inlined$map$4;->collect(Lkotlinx/coroutines/flow/FlowCollector;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; @@ -40877,13 +41165,37 @@ HSPLorg/meshtastic/core/prefs/ui/UiPrefsImpl$special$$inlined$map$4$2$1;-> Lorg/meshtastic/core/prefs/ui/UiPrefsImpl$special$$inlined$map$5; HSPLorg/meshtastic/core/prefs/ui/UiPrefsImpl$special$$inlined$map$5;->(Lkotlinx/coroutines/flow/Flow;)V Lorg/meshtastic/core/prefs/ui/UiPrefsImpl$special$$inlined$map$6; -HSPLorg/meshtastic/core/prefs/ui/UiPrefsImpl$special$$inlined$map$6;->(Lkotlinx/coroutines/flow/Flow;)V +HSPLorg/meshtastic/core/prefs/ui/UiPrefsImpl$special$$inlined$map$6;->(Lkotlinx/coroutines/flow/Flow;Lorg/meshtastic/core/prefs/ui/UiPrefsImpl;)V Lorg/meshtastic/core/prefs/ui/UiPrefsImpl$special$$inlined$map$7; HSPLorg/meshtastic/core/prefs/ui/UiPrefsImpl$special$$inlined$map$7;->(Lkotlinx/coroutines/flow/Flow;)V +HSPLorg/meshtastic/core/prefs/ui/UiPrefsImpl$special$$inlined$map$7;->collect(Lkotlinx/coroutines/flow/FlowCollector;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; +Lorg/meshtastic/core/prefs/ui/UiPrefsImpl$special$$inlined$map$7$1; +HSPLorg/meshtastic/core/prefs/ui/UiPrefsImpl$special$$inlined$map$7$1;->(Lorg/meshtastic/core/prefs/ui/UiPrefsImpl$special$$inlined$map$7;Lkotlin/coroutines/Continuation;)V +Lorg/meshtastic/core/prefs/ui/UiPrefsImpl$special$$inlined$map$7$2; +HSPLorg/meshtastic/core/prefs/ui/UiPrefsImpl$special$$inlined$map$7$2;->(Lkotlinx/coroutines/flow/FlowCollector;)V +HSPLorg/meshtastic/core/prefs/ui/UiPrefsImpl$special$$inlined$map$7$2;->emit(Ljava/lang/Object;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; +Lorg/meshtastic/core/prefs/ui/UiPrefsImpl$special$$inlined$map$7$2$1; +HSPLorg/meshtastic/core/prefs/ui/UiPrefsImpl$special$$inlined$map$7$2$1;->(Lorg/meshtastic/core/prefs/ui/UiPrefsImpl$special$$inlined$map$7$2;Lkotlin/coroutines/Continuation;)V Lorg/meshtastic/core/prefs/ui/UiPrefsImpl$special$$inlined$map$8; HSPLorg/meshtastic/core/prefs/ui/UiPrefsImpl$special$$inlined$map$8;->(Lkotlinx/coroutines/flow/Flow;)V +HSPLorg/meshtastic/core/prefs/ui/UiPrefsImpl$special$$inlined$map$8;->collect(Lkotlinx/coroutines/flow/FlowCollector;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; +Lorg/meshtastic/core/prefs/ui/UiPrefsImpl$special$$inlined$map$8$1; +HSPLorg/meshtastic/core/prefs/ui/UiPrefsImpl$special$$inlined$map$8$1;->(Lorg/meshtastic/core/prefs/ui/UiPrefsImpl$special$$inlined$map$8;Lkotlin/coroutines/Continuation;)V +Lorg/meshtastic/core/prefs/ui/UiPrefsImpl$special$$inlined$map$8$2; +HSPLorg/meshtastic/core/prefs/ui/UiPrefsImpl$special$$inlined$map$8$2;->(Lkotlinx/coroutines/flow/FlowCollector;)V +HSPLorg/meshtastic/core/prefs/ui/UiPrefsImpl$special$$inlined$map$8$2;->emit(Ljava/lang/Object;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; +Lorg/meshtastic/core/prefs/ui/UiPrefsImpl$special$$inlined$map$8$2$1; +HSPLorg/meshtastic/core/prefs/ui/UiPrefsImpl$special$$inlined$map$8$2$1;->(Lorg/meshtastic/core/prefs/ui/UiPrefsImpl$special$$inlined$map$8$2;Lkotlin/coroutines/Continuation;)V Lorg/meshtastic/core/prefs/ui/UiPrefsImpl$special$$inlined$map$9; HSPLorg/meshtastic/core/prefs/ui/UiPrefsImpl$special$$inlined$map$9;->(Lkotlinx/coroutines/flow/Flow;)V +HSPLorg/meshtastic/core/prefs/ui/UiPrefsImpl$special$$inlined$map$9;->collect(Lkotlinx/coroutines/flow/FlowCollector;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; +Lorg/meshtastic/core/prefs/ui/UiPrefsImpl$special$$inlined$map$9$1; +HSPLorg/meshtastic/core/prefs/ui/UiPrefsImpl$special$$inlined$map$9$1;->(Lorg/meshtastic/core/prefs/ui/UiPrefsImpl$special$$inlined$map$9;Lkotlin/coroutines/Continuation;)V +Lorg/meshtastic/core/prefs/ui/UiPrefsImpl$special$$inlined$map$9$2; +HSPLorg/meshtastic/core/prefs/ui/UiPrefsImpl$special$$inlined$map$9$2;->(Lkotlinx/coroutines/flow/FlowCollector;)V +HSPLorg/meshtastic/core/prefs/ui/UiPrefsImpl$special$$inlined$map$9$2;->emit(Ljava/lang/Object;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; +Lorg/meshtastic/core/prefs/ui/UiPrefsImpl$special$$inlined$map$9$2$1; +HSPLorg/meshtastic/core/prefs/ui/UiPrefsImpl$special$$inlined$map$9$2$1;->(Lorg/meshtastic/core/prefs/ui/UiPrefsImpl$special$$inlined$map$9$2;Lkotlin/coroutines/Continuation;)V Lorg/meshtastic/core/prefs/ui/UiPrefsUnitsOverrideSource; Lorg/meshtastic/core/repository/ActiveConversationTracker; Lorg/meshtastic/core/repository/AdminController; @@ -40958,6 +41270,9 @@ Lorg/meshtastic/core/repository/MqttManager; Lorg/meshtastic/core/repository/NeighborInfoHandler; Lorg/meshtastic/core/repository/NeighborInfoResponseProvider; Lorg/meshtastic/core/repository/NodeController; +Lorg/meshtastic/core/repository/NodeFilterPrefs; +HSPLorg/meshtastic/core/repository/NodeFilterPrefs;->(ZZZZZZZZZ)V +HSPLorg/meshtastic/core/repository/NodeFilterPrefs;->(ZZZZZZZZZILkotlin/jvm/internal/DefaultConstructorMarker;)V Lorg/meshtastic/core/repository/NodeManager; Lorg/meshtastic/core/repository/NodeRepository; Lorg/meshtastic/core/repository/NodeRestartTracker; @@ -41807,33 +42122,26 @@ Lorg/meshtastic/core/resources/String10_commonMainKt$$ExternalSyntheticLambda66; HSPLorg/meshtastic/core/resources/String10_commonMainKt$$ExternalSyntheticLambda66;->()V Lorg/meshtastic/core/resources/String10_commonMainKt$$ExternalSyntheticLambda67; HSPLorg/meshtastic/core/resources/String10_commonMainKt$$ExternalSyntheticLambda67;->()V -HSPLorg/meshtastic/core/resources/String10_commonMainKt$$ExternalSyntheticLambda67;->invoke()Ljava/lang/Object; Lorg/meshtastic/core/resources/String10_commonMainKt$$ExternalSyntheticLambda68; HSPLorg/meshtastic/core/resources/String10_commonMainKt$$ExternalSyntheticLambda68;->()V -HSPLorg/meshtastic/core/resources/String10_commonMainKt$$ExternalSyntheticLambda68;->invoke()Ljava/lang/Object; Lorg/meshtastic/core/resources/String10_commonMainKt$$ExternalSyntheticLambda69; HSPLorg/meshtastic/core/resources/String10_commonMainKt$$ExternalSyntheticLambda69;->()V Lorg/meshtastic/core/resources/String10_commonMainKt$$ExternalSyntheticLambda7; HSPLorg/meshtastic/core/resources/String10_commonMainKt$$ExternalSyntheticLambda7;->()V Lorg/meshtastic/core/resources/String10_commonMainKt$$ExternalSyntheticLambda70; HSPLorg/meshtastic/core/resources/String10_commonMainKt$$ExternalSyntheticLambda70;->()V -HSPLorg/meshtastic/core/resources/String10_commonMainKt$$ExternalSyntheticLambda70;->invoke()Ljava/lang/Object; Lorg/meshtastic/core/resources/String10_commonMainKt$$ExternalSyntheticLambda71; HSPLorg/meshtastic/core/resources/String10_commonMainKt$$ExternalSyntheticLambda71;->()V -HSPLorg/meshtastic/core/resources/String10_commonMainKt$$ExternalSyntheticLambda71;->invoke()Ljava/lang/Object; Lorg/meshtastic/core/resources/String10_commonMainKt$$ExternalSyntheticLambda72; HSPLorg/meshtastic/core/resources/String10_commonMainKt$$ExternalSyntheticLambda72;->()V -HSPLorg/meshtastic/core/resources/String10_commonMainKt$$ExternalSyntheticLambda72;->invoke()Ljava/lang/Object; Lorg/meshtastic/core/resources/String10_commonMainKt$$ExternalSyntheticLambda73; HSPLorg/meshtastic/core/resources/String10_commonMainKt$$ExternalSyntheticLambda73;->()V Lorg/meshtastic/core/resources/String10_commonMainKt$$ExternalSyntheticLambda74; HSPLorg/meshtastic/core/resources/String10_commonMainKt$$ExternalSyntheticLambda74;->()V Lorg/meshtastic/core/resources/String10_commonMainKt$$ExternalSyntheticLambda75; HSPLorg/meshtastic/core/resources/String10_commonMainKt$$ExternalSyntheticLambda75;->()V -HSPLorg/meshtastic/core/resources/String10_commonMainKt$$ExternalSyntheticLambda75;->invoke()Ljava/lang/Object; Lorg/meshtastic/core/resources/String10_commonMainKt$$ExternalSyntheticLambda76; HSPLorg/meshtastic/core/resources/String10_commonMainKt$$ExternalSyntheticLambda76;->()V -HSPLorg/meshtastic/core/resources/String10_commonMainKt$$ExternalSyntheticLambda76;->invoke()Ljava/lang/Object; Lorg/meshtastic/core/resources/String10_commonMainKt$$ExternalSyntheticLambda77; HSPLorg/meshtastic/core/resources/String10_commonMainKt$$ExternalSyntheticLambda77;->()V Lorg/meshtastic/core/resources/String10_commonMainKt$$ExternalSyntheticLambda78; @@ -41848,26 +42156,33 @@ Lorg/meshtastic/core/resources/String10_commonMainKt$$ExternalSyntheticLambda80; HSPLorg/meshtastic/core/resources/String10_commonMainKt$$ExternalSyntheticLambda80;->()V Lorg/meshtastic/core/resources/String10_commonMainKt$$ExternalSyntheticLambda81; HSPLorg/meshtastic/core/resources/String10_commonMainKt$$ExternalSyntheticLambda81;->()V +HSPLorg/meshtastic/core/resources/String10_commonMainKt$$ExternalSyntheticLambda81;->invoke()Ljava/lang/Object; Lorg/meshtastic/core/resources/String10_commonMainKt$$ExternalSyntheticLambda82; HSPLorg/meshtastic/core/resources/String10_commonMainKt$$ExternalSyntheticLambda82;->()V +HSPLorg/meshtastic/core/resources/String10_commonMainKt$$ExternalSyntheticLambda82;->invoke()Ljava/lang/Object; Lorg/meshtastic/core/resources/String10_commonMainKt$$ExternalSyntheticLambda83; HSPLorg/meshtastic/core/resources/String10_commonMainKt$$ExternalSyntheticLambda83;->()V +HSPLorg/meshtastic/core/resources/String10_commonMainKt$$ExternalSyntheticLambda83;->invoke()Ljava/lang/Object; Lorg/meshtastic/core/resources/String10_commonMainKt$$ExternalSyntheticLambda84; HSPLorg/meshtastic/core/resources/String10_commonMainKt$$ExternalSyntheticLambda84;->()V Lorg/meshtastic/core/resources/String10_commonMainKt$$ExternalSyntheticLambda85; HSPLorg/meshtastic/core/resources/String10_commonMainKt$$ExternalSyntheticLambda85;->()V +HSPLorg/meshtastic/core/resources/String10_commonMainKt$$ExternalSyntheticLambda85;->invoke()Ljava/lang/Object; Lorg/meshtastic/core/resources/String10_commonMainKt$$ExternalSyntheticLambda86; HSPLorg/meshtastic/core/resources/String10_commonMainKt$$ExternalSyntheticLambda86;->()V Lorg/meshtastic/core/resources/String10_commonMainKt$$ExternalSyntheticLambda87; HSPLorg/meshtastic/core/resources/String10_commonMainKt$$ExternalSyntheticLambda87;->()V +HSPLorg/meshtastic/core/resources/String10_commonMainKt$$ExternalSyntheticLambda87;->invoke()Ljava/lang/Object; Lorg/meshtastic/core/resources/String10_commonMainKt$$ExternalSyntheticLambda88; HSPLorg/meshtastic/core/resources/String10_commonMainKt$$ExternalSyntheticLambda88;->()V Lorg/meshtastic/core/resources/String10_commonMainKt$$ExternalSyntheticLambda89; HSPLorg/meshtastic/core/resources/String10_commonMainKt$$ExternalSyntheticLambda89;->()V +HSPLorg/meshtastic/core/resources/String10_commonMainKt$$ExternalSyntheticLambda89;->invoke()Ljava/lang/Object; Lorg/meshtastic/core/resources/String10_commonMainKt$$ExternalSyntheticLambda9; HSPLorg/meshtastic/core/resources/String10_commonMainKt$$ExternalSyntheticLambda9;->()V Lorg/meshtastic/core/resources/String10_commonMainKt$$ExternalSyntheticLambda90; HSPLorg/meshtastic/core/resources/String10_commonMainKt$$ExternalSyntheticLambda90;->()V +HSPLorg/meshtastic/core/resources/String10_commonMainKt$$ExternalSyntheticLambda90;->invoke()Ljava/lang/Object; Lorg/meshtastic/core/resources/String10_commonMainKt$$ExternalSyntheticLambda91; HSPLorg/meshtastic/core/resources/String10_commonMainKt$$ExternalSyntheticLambda91;->()V Lorg/meshtastic/core/resources/String10_commonMainKt$$ExternalSyntheticLambda92; @@ -42010,7 +42325,6 @@ Lorg/meshtastic/core/resources/String12_commonMainKt$$ExternalSyntheticLambda61; HSPLorg/meshtastic/core/resources/String12_commonMainKt$$ExternalSyntheticLambda61;->()V Lorg/meshtastic/core/resources/String12_commonMainKt$$ExternalSyntheticLambda62; HSPLorg/meshtastic/core/resources/String12_commonMainKt$$ExternalSyntheticLambda62;->()V -HSPLorg/meshtastic/core/resources/String12_commonMainKt$$ExternalSyntheticLambda62;->invoke()Ljava/lang/Object; Lorg/meshtastic/core/resources/String12_commonMainKt$$ExternalSyntheticLambda63; HSPLorg/meshtastic/core/resources/String12_commonMainKt$$ExternalSyntheticLambda63;->()V Lorg/meshtastic/core/resources/String12_commonMainKt$$ExternalSyntheticLambda64; @@ -42019,6 +42333,7 @@ Lorg/meshtastic/core/resources/String12_commonMainKt$$ExternalSyntheticLambda65; HSPLorg/meshtastic/core/resources/String12_commonMainKt$$ExternalSyntheticLambda65;->()V Lorg/meshtastic/core/resources/String12_commonMainKt$$ExternalSyntheticLambda66; HSPLorg/meshtastic/core/resources/String12_commonMainKt$$ExternalSyntheticLambda66;->()V +HSPLorg/meshtastic/core/resources/String12_commonMainKt$$ExternalSyntheticLambda66;->invoke()Ljava/lang/Object; Lorg/meshtastic/core/resources/String12_commonMainKt$$ExternalSyntheticLambda67; HSPLorg/meshtastic/core/resources/String12_commonMainKt$$ExternalSyntheticLambda67;->()V Lorg/meshtastic/core/resources/String12_commonMainKt$$ExternalSyntheticLambda68; @@ -42125,6 +42440,7 @@ Lorg/meshtastic/core/resources/String14_commonMainKt$$ExternalSyntheticLambda20; HSPLorg/meshtastic/core/resources/String14_commonMainKt$$ExternalSyntheticLambda20;->()V Lorg/meshtastic/core/resources/String14_commonMainKt$$ExternalSyntheticLambda21; HSPLorg/meshtastic/core/resources/String14_commonMainKt$$ExternalSyntheticLambda21;->()V +HSPLorg/meshtastic/core/resources/String14_commonMainKt$$ExternalSyntheticLambda21;->invoke()Ljava/lang/Object; Lorg/meshtastic/core/resources/String14_commonMainKt$$ExternalSyntheticLambda22; HSPLorg/meshtastic/core/resources/String14_commonMainKt$$ExternalSyntheticLambda22;->()V Lorg/meshtastic/core/resources/String14_commonMainKt$$ExternalSyntheticLambda23; @@ -42253,7 +42569,6 @@ Lorg/meshtastic/core/resources/String14_commonMainKt$$ExternalSyntheticLambda79; HSPLorg/meshtastic/core/resources/String14_commonMainKt$$ExternalSyntheticLambda79;->()V Lorg/meshtastic/core/resources/String14_commonMainKt$$ExternalSyntheticLambda8; HSPLorg/meshtastic/core/resources/String14_commonMainKt$$ExternalSyntheticLambda8;->()V -HSPLorg/meshtastic/core/resources/String14_commonMainKt$$ExternalSyntheticLambda8;->invoke()Ljava/lang/Object; Lorg/meshtastic/core/resources/String14_commonMainKt$$ExternalSyntheticLambda80; HSPLorg/meshtastic/core/resources/String14_commonMainKt$$ExternalSyntheticLambda80;->()V Lorg/meshtastic/core/resources/String14_commonMainKt$$ExternalSyntheticLambda81; @@ -42299,9 +42614,7 @@ HSPLorg/meshtastic/core/resources/String14_commonMainKt$$ExternalSyntheticLambda Lorg/meshtastic/core/resources/String16_commonMainKt; HSPLorg/meshtastic/core/resources/String16_commonMainKt;->()V HSPLorg/meshtastic/core/resources/String16_commonMainKt;->getShare_your_location_in_real_time(Lorg/meshtastic/core/resources/Res$string;)Lorg/jetbrains/compose/resources/StringResource; -HSPLorg/meshtastic/core/resources/String16_commonMainKt;->getStay_connected_anywhere(Lorg/meshtastic/core/resources/Res$string;)Lorg/jetbrains/compose/resources/StringResource; HSPLorg/meshtastic/core/resources/String16_commonMainKt;->share_your_location_in_real_time_delegate$lambda$0()Lorg/jetbrains/compose/resources/StringResource; -HSPLorg/meshtastic/core/resources/String16_commonMainKt;->stay_connected_anywhere_delegate$lambda$0()Lorg/jetbrains/compose/resources/StringResource; Lorg/meshtastic/core/resources/String16_commonMainKt$$ExternalSyntheticLambda0; HSPLorg/meshtastic/core/resources/String16_commonMainKt$$ExternalSyntheticLambda0;->()V Lorg/meshtastic/core/resources/String16_commonMainKt$$ExternalSyntheticLambda1; @@ -42332,7 +42645,6 @@ Lorg/meshtastic/core/resources/String16_commonMainKt$$ExternalSyntheticLambda20; HSPLorg/meshtastic/core/resources/String16_commonMainKt$$ExternalSyntheticLambda20;->()V Lorg/meshtastic/core/resources/String16_commonMainKt$$ExternalSyntheticLambda21; HSPLorg/meshtastic/core/resources/String16_commonMainKt$$ExternalSyntheticLambda21;->()V -HSPLorg/meshtastic/core/resources/String16_commonMainKt$$ExternalSyntheticLambda21;->invoke()Ljava/lang/Object; Lorg/meshtastic/core/resources/String16_commonMainKt$$ExternalSyntheticLambda22; HSPLorg/meshtastic/core/resources/String16_commonMainKt$$ExternalSyntheticLambda22;->()V Lorg/meshtastic/core/resources/String16_commonMainKt$$ExternalSyntheticLambda23; @@ -42393,6 +42705,7 @@ Lorg/meshtastic/core/resources/String16_commonMainKt$$ExternalSyntheticLambda48; HSPLorg/meshtastic/core/resources/String16_commonMainKt$$ExternalSyntheticLambda48;->()V Lorg/meshtastic/core/resources/String16_commonMainKt$$ExternalSyntheticLambda49; HSPLorg/meshtastic/core/resources/String16_commonMainKt$$ExternalSyntheticLambda49;->()V +HSPLorg/meshtastic/core/resources/String16_commonMainKt$$ExternalSyntheticLambda49;->invoke()Ljava/lang/Object; Lorg/meshtastic/core/resources/String16_commonMainKt$$ExternalSyntheticLambda5; HSPLorg/meshtastic/core/resources/String16_commonMainKt$$ExternalSyntheticLambda5;->()V Lorg/meshtastic/core/resources/String16_commonMainKt$$ExternalSyntheticLambda50; @@ -42499,11 +42812,215 @@ Lorg/meshtastic/core/resources/String16_commonMainKt$$ExternalSyntheticLambda96; HSPLorg/meshtastic/core/resources/String16_commonMainKt$$ExternalSyntheticLambda96;->()V Lorg/meshtastic/core/resources/String16_commonMainKt$$ExternalSyntheticLambda97; HSPLorg/meshtastic/core/resources/String16_commonMainKt$$ExternalSyntheticLambda97;->()V -HSPLorg/meshtastic/core/resources/String16_commonMainKt$$ExternalSyntheticLambda97;->invoke()Ljava/lang/Object; Lorg/meshtastic/core/resources/String16_commonMainKt$$ExternalSyntheticLambda98; HSPLorg/meshtastic/core/resources/String16_commonMainKt$$ExternalSyntheticLambda98;->()V Lorg/meshtastic/core/resources/String16_commonMainKt$$ExternalSyntheticLambda99; HSPLorg/meshtastic/core/resources/String16_commonMainKt$$ExternalSyntheticLambda99;->()V +Lorg/meshtastic/core/resources/String17_commonMainKt; +HSPLorg/meshtastic/core/resources/String17_commonMainKt;->()V +HSPLorg/meshtastic/core/resources/String17_commonMainKt;->getStay_connected_anywhere(Lorg/meshtastic/core/resources/Res$string;)Lorg/jetbrains/compose/resources/StringResource; +HSPLorg/meshtastic/core/resources/String17_commonMainKt;->stay_connected_anywhere_delegate$lambda$0()Lorg/jetbrains/compose/resources/StringResource; +Lorg/meshtastic/core/resources/String17_commonMainKt$$ExternalSyntheticLambda0; +HSPLorg/meshtastic/core/resources/String17_commonMainKt$$ExternalSyntheticLambda0;->()V +Lorg/meshtastic/core/resources/String17_commonMainKt$$ExternalSyntheticLambda1; +HSPLorg/meshtastic/core/resources/String17_commonMainKt$$ExternalSyntheticLambda1;->()V +Lorg/meshtastic/core/resources/String17_commonMainKt$$ExternalSyntheticLambda10; +HSPLorg/meshtastic/core/resources/String17_commonMainKt$$ExternalSyntheticLambda10;->()V +Lorg/meshtastic/core/resources/String17_commonMainKt$$ExternalSyntheticLambda11; +HSPLorg/meshtastic/core/resources/String17_commonMainKt$$ExternalSyntheticLambda11;->()V +Lorg/meshtastic/core/resources/String17_commonMainKt$$ExternalSyntheticLambda12; +HSPLorg/meshtastic/core/resources/String17_commonMainKt$$ExternalSyntheticLambda12;->()V +Lorg/meshtastic/core/resources/String17_commonMainKt$$ExternalSyntheticLambda13; +HSPLorg/meshtastic/core/resources/String17_commonMainKt$$ExternalSyntheticLambda13;->()V +Lorg/meshtastic/core/resources/String17_commonMainKt$$ExternalSyntheticLambda14; +HSPLorg/meshtastic/core/resources/String17_commonMainKt$$ExternalSyntheticLambda14;->()V +Lorg/meshtastic/core/resources/String17_commonMainKt$$ExternalSyntheticLambda15; +HSPLorg/meshtastic/core/resources/String17_commonMainKt$$ExternalSyntheticLambda15;->()V +Lorg/meshtastic/core/resources/String17_commonMainKt$$ExternalSyntheticLambda16; +HSPLorg/meshtastic/core/resources/String17_commonMainKt$$ExternalSyntheticLambda16;->()V +Lorg/meshtastic/core/resources/String17_commonMainKt$$ExternalSyntheticLambda17; +HSPLorg/meshtastic/core/resources/String17_commonMainKt$$ExternalSyntheticLambda17;->()V +Lorg/meshtastic/core/resources/String17_commonMainKt$$ExternalSyntheticLambda18; +HSPLorg/meshtastic/core/resources/String17_commonMainKt$$ExternalSyntheticLambda18;->()V +Lorg/meshtastic/core/resources/String17_commonMainKt$$ExternalSyntheticLambda19; +HSPLorg/meshtastic/core/resources/String17_commonMainKt$$ExternalSyntheticLambda19;->()V +Lorg/meshtastic/core/resources/String17_commonMainKt$$ExternalSyntheticLambda2; +HSPLorg/meshtastic/core/resources/String17_commonMainKt$$ExternalSyntheticLambda2;->()V +Lorg/meshtastic/core/resources/String17_commonMainKt$$ExternalSyntheticLambda20; +HSPLorg/meshtastic/core/resources/String17_commonMainKt$$ExternalSyntheticLambda20;->()V +Lorg/meshtastic/core/resources/String17_commonMainKt$$ExternalSyntheticLambda21; +HSPLorg/meshtastic/core/resources/String17_commonMainKt$$ExternalSyntheticLambda21;->()V +Lorg/meshtastic/core/resources/String17_commonMainKt$$ExternalSyntheticLambda22; +HSPLorg/meshtastic/core/resources/String17_commonMainKt$$ExternalSyntheticLambda22;->()V +Lorg/meshtastic/core/resources/String17_commonMainKt$$ExternalSyntheticLambda23; +HSPLorg/meshtastic/core/resources/String17_commonMainKt$$ExternalSyntheticLambda23;->()V +Lorg/meshtastic/core/resources/String17_commonMainKt$$ExternalSyntheticLambda24; +HSPLorg/meshtastic/core/resources/String17_commonMainKt$$ExternalSyntheticLambda24;->()V +Lorg/meshtastic/core/resources/String17_commonMainKt$$ExternalSyntheticLambda25; +HSPLorg/meshtastic/core/resources/String17_commonMainKt$$ExternalSyntheticLambda25;->()V +Lorg/meshtastic/core/resources/String17_commonMainKt$$ExternalSyntheticLambda26; +HSPLorg/meshtastic/core/resources/String17_commonMainKt$$ExternalSyntheticLambda26;->()V +Lorg/meshtastic/core/resources/String17_commonMainKt$$ExternalSyntheticLambda27; +HSPLorg/meshtastic/core/resources/String17_commonMainKt$$ExternalSyntheticLambda27;->()V +Lorg/meshtastic/core/resources/String17_commonMainKt$$ExternalSyntheticLambda28; +HSPLorg/meshtastic/core/resources/String17_commonMainKt$$ExternalSyntheticLambda28;->()V +Lorg/meshtastic/core/resources/String17_commonMainKt$$ExternalSyntheticLambda29; +HSPLorg/meshtastic/core/resources/String17_commonMainKt$$ExternalSyntheticLambda29;->()V +Lorg/meshtastic/core/resources/String17_commonMainKt$$ExternalSyntheticLambda3; +HSPLorg/meshtastic/core/resources/String17_commonMainKt$$ExternalSyntheticLambda3;->()V +Lorg/meshtastic/core/resources/String17_commonMainKt$$ExternalSyntheticLambda30; +HSPLorg/meshtastic/core/resources/String17_commonMainKt$$ExternalSyntheticLambda30;->()V +Lorg/meshtastic/core/resources/String17_commonMainKt$$ExternalSyntheticLambda31; +HSPLorg/meshtastic/core/resources/String17_commonMainKt$$ExternalSyntheticLambda31;->()V +Lorg/meshtastic/core/resources/String17_commonMainKt$$ExternalSyntheticLambda32; +HSPLorg/meshtastic/core/resources/String17_commonMainKt$$ExternalSyntheticLambda32;->()V +Lorg/meshtastic/core/resources/String17_commonMainKt$$ExternalSyntheticLambda33; +HSPLorg/meshtastic/core/resources/String17_commonMainKt$$ExternalSyntheticLambda33;->()V +Lorg/meshtastic/core/resources/String17_commonMainKt$$ExternalSyntheticLambda34; +HSPLorg/meshtastic/core/resources/String17_commonMainKt$$ExternalSyntheticLambda34;->()V +Lorg/meshtastic/core/resources/String17_commonMainKt$$ExternalSyntheticLambda35; +HSPLorg/meshtastic/core/resources/String17_commonMainKt$$ExternalSyntheticLambda35;->()V +Lorg/meshtastic/core/resources/String17_commonMainKt$$ExternalSyntheticLambda36; +HSPLorg/meshtastic/core/resources/String17_commonMainKt$$ExternalSyntheticLambda36;->()V +Lorg/meshtastic/core/resources/String17_commonMainKt$$ExternalSyntheticLambda37; +HSPLorg/meshtastic/core/resources/String17_commonMainKt$$ExternalSyntheticLambda37;->()V +Lorg/meshtastic/core/resources/String17_commonMainKt$$ExternalSyntheticLambda38; +HSPLorg/meshtastic/core/resources/String17_commonMainKt$$ExternalSyntheticLambda38;->()V +Lorg/meshtastic/core/resources/String17_commonMainKt$$ExternalSyntheticLambda39; +HSPLorg/meshtastic/core/resources/String17_commonMainKt$$ExternalSyntheticLambda39;->()V +Lorg/meshtastic/core/resources/String17_commonMainKt$$ExternalSyntheticLambda4; +HSPLorg/meshtastic/core/resources/String17_commonMainKt$$ExternalSyntheticLambda4;->()V +Lorg/meshtastic/core/resources/String17_commonMainKt$$ExternalSyntheticLambda40; +HSPLorg/meshtastic/core/resources/String17_commonMainKt$$ExternalSyntheticLambda40;->()V +Lorg/meshtastic/core/resources/String17_commonMainKt$$ExternalSyntheticLambda41; +HSPLorg/meshtastic/core/resources/String17_commonMainKt$$ExternalSyntheticLambda41;->()V +Lorg/meshtastic/core/resources/String17_commonMainKt$$ExternalSyntheticLambda42; +HSPLorg/meshtastic/core/resources/String17_commonMainKt$$ExternalSyntheticLambda42;->()V +Lorg/meshtastic/core/resources/String17_commonMainKt$$ExternalSyntheticLambda43; +HSPLorg/meshtastic/core/resources/String17_commonMainKt$$ExternalSyntheticLambda43;->()V +Lorg/meshtastic/core/resources/String17_commonMainKt$$ExternalSyntheticLambda44; +HSPLorg/meshtastic/core/resources/String17_commonMainKt$$ExternalSyntheticLambda44;->()V +Lorg/meshtastic/core/resources/String17_commonMainKt$$ExternalSyntheticLambda45; +HSPLorg/meshtastic/core/resources/String17_commonMainKt$$ExternalSyntheticLambda45;->()V +Lorg/meshtastic/core/resources/String17_commonMainKt$$ExternalSyntheticLambda46; +HSPLorg/meshtastic/core/resources/String17_commonMainKt$$ExternalSyntheticLambda46;->()V +Lorg/meshtastic/core/resources/String17_commonMainKt$$ExternalSyntheticLambda47; +HSPLorg/meshtastic/core/resources/String17_commonMainKt$$ExternalSyntheticLambda47;->()V +Lorg/meshtastic/core/resources/String17_commonMainKt$$ExternalSyntheticLambda48; +HSPLorg/meshtastic/core/resources/String17_commonMainKt$$ExternalSyntheticLambda48;->()V +Lorg/meshtastic/core/resources/String17_commonMainKt$$ExternalSyntheticLambda49; +HSPLorg/meshtastic/core/resources/String17_commonMainKt$$ExternalSyntheticLambda49;->()V +Lorg/meshtastic/core/resources/String17_commonMainKt$$ExternalSyntheticLambda5; +HSPLorg/meshtastic/core/resources/String17_commonMainKt$$ExternalSyntheticLambda5;->()V +HSPLorg/meshtastic/core/resources/String17_commonMainKt$$ExternalSyntheticLambda5;->invoke()Ljava/lang/Object; +Lorg/meshtastic/core/resources/String17_commonMainKt$$ExternalSyntheticLambda50; +HSPLorg/meshtastic/core/resources/String17_commonMainKt$$ExternalSyntheticLambda50;->()V +Lorg/meshtastic/core/resources/String17_commonMainKt$$ExternalSyntheticLambda51; +HSPLorg/meshtastic/core/resources/String17_commonMainKt$$ExternalSyntheticLambda51;->()V +Lorg/meshtastic/core/resources/String17_commonMainKt$$ExternalSyntheticLambda52; +HSPLorg/meshtastic/core/resources/String17_commonMainKt$$ExternalSyntheticLambda52;->()V +Lorg/meshtastic/core/resources/String17_commonMainKt$$ExternalSyntheticLambda53; +HSPLorg/meshtastic/core/resources/String17_commonMainKt$$ExternalSyntheticLambda53;->()V +Lorg/meshtastic/core/resources/String17_commonMainKt$$ExternalSyntheticLambda54; +HSPLorg/meshtastic/core/resources/String17_commonMainKt$$ExternalSyntheticLambda54;->()V +Lorg/meshtastic/core/resources/String17_commonMainKt$$ExternalSyntheticLambda55; +HSPLorg/meshtastic/core/resources/String17_commonMainKt$$ExternalSyntheticLambda55;->()V +Lorg/meshtastic/core/resources/String17_commonMainKt$$ExternalSyntheticLambda56; +HSPLorg/meshtastic/core/resources/String17_commonMainKt$$ExternalSyntheticLambda56;->()V +Lorg/meshtastic/core/resources/String17_commonMainKt$$ExternalSyntheticLambda57; +HSPLorg/meshtastic/core/resources/String17_commonMainKt$$ExternalSyntheticLambda57;->()V +Lorg/meshtastic/core/resources/String17_commonMainKt$$ExternalSyntheticLambda58; +HSPLorg/meshtastic/core/resources/String17_commonMainKt$$ExternalSyntheticLambda58;->()V +Lorg/meshtastic/core/resources/String17_commonMainKt$$ExternalSyntheticLambda59; +HSPLorg/meshtastic/core/resources/String17_commonMainKt$$ExternalSyntheticLambda59;->()V +Lorg/meshtastic/core/resources/String17_commonMainKt$$ExternalSyntheticLambda6; +HSPLorg/meshtastic/core/resources/String17_commonMainKt$$ExternalSyntheticLambda6;->()V +Lorg/meshtastic/core/resources/String17_commonMainKt$$ExternalSyntheticLambda60; +HSPLorg/meshtastic/core/resources/String17_commonMainKt$$ExternalSyntheticLambda60;->()V +Lorg/meshtastic/core/resources/String17_commonMainKt$$ExternalSyntheticLambda61; +HSPLorg/meshtastic/core/resources/String17_commonMainKt$$ExternalSyntheticLambda61;->()V +Lorg/meshtastic/core/resources/String17_commonMainKt$$ExternalSyntheticLambda62; +HSPLorg/meshtastic/core/resources/String17_commonMainKt$$ExternalSyntheticLambda62;->()V +Lorg/meshtastic/core/resources/String17_commonMainKt$$ExternalSyntheticLambda63; +HSPLorg/meshtastic/core/resources/String17_commonMainKt$$ExternalSyntheticLambda63;->()V +Lorg/meshtastic/core/resources/String17_commonMainKt$$ExternalSyntheticLambda64; +HSPLorg/meshtastic/core/resources/String17_commonMainKt$$ExternalSyntheticLambda64;->()V +Lorg/meshtastic/core/resources/String17_commonMainKt$$ExternalSyntheticLambda65; +HSPLorg/meshtastic/core/resources/String17_commonMainKt$$ExternalSyntheticLambda65;->()V +Lorg/meshtastic/core/resources/String17_commonMainKt$$ExternalSyntheticLambda66; +HSPLorg/meshtastic/core/resources/String17_commonMainKt$$ExternalSyntheticLambda66;->()V +Lorg/meshtastic/core/resources/String17_commonMainKt$$ExternalSyntheticLambda67; +HSPLorg/meshtastic/core/resources/String17_commonMainKt$$ExternalSyntheticLambda67;->()V +Lorg/meshtastic/core/resources/String17_commonMainKt$$ExternalSyntheticLambda68; +HSPLorg/meshtastic/core/resources/String17_commonMainKt$$ExternalSyntheticLambda68;->()V +Lorg/meshtastic/core/resources/String17_commonMainKt$$ExternalSyntheticLambda69; +HSPLorg/meshtastic/core/resources/String17_commonMainKt$$ExternalSyntheticLambda69;->()V +Lorg/meshtastic/core/resources/String17_commonMainKt$$ExternalSyntheticLambda7; +HSPLorg/meshtastic/core/resources/String17_commonMainKt$$ExternalSyntheticLambda7;->()V +Lorg/meshtastic/core/resources/String17_commonMainKt$$ExternalSyntheticLambda70; +HSPLorg/meshtastic/core/resources/String17_commonMainKt$$ExternalSyntheticLambda70;->()V +Lorg/meshtastic/core/resources/String17_commonMainKt$$ExternalSyntheticLambda71; +HSPLorg/meshtastic/core/resources/String17_commonMainKt$$ExternalSyntheticLambda71;->()V +Lorg/meshtastic/core/resources/String17_commonMainKt$$ExternalSyntheticLambda72; +HSPLorg/meshtastic/core/resources/String17_commonMainKt$$ExternalSyntheticLambda72;->()V +Lorg/meshtastic/core/resources/String17_commonMainKt$$ExternalSyntheticLambda73; +HSPLorg/meshtastic/core/resources/String17_commonMainKt$$ExternalSyntheticLambda73;->()V +Lorg/meshtastic/core/resources/String17_commonMainKt$$ExternalSyntheticLambda74; +HSPLorg/meshtastic/core/resources/String17_commonMainKt$$ExternalSyntheticLambda74;->()V +Lorg/meshtastic/core/resources/String17_commonMainKt$$ExternalSyntheticLambda75; +HSPLorg/meshtastic/core/resources/String17_commonMainKt$$ExternalSyntheticLambda75;->()V +Lorg/meshtastic/core/resources/String17_commonMainKt$$ExternalSyntheticLambda76; +HSPLorg/meshtastic/core/resources/String17_commonMainKt$$ExternalSyntheticLambda76;->()V +Lorg/meshtastic/core/resources/String17_commonMainKt$$ExternalSyntheticLambda77; +HSPLorg/meshtastic/core/resources/String17_commonMainKt$$ExternalSyntheticLambda77;->()V +Lorg/meshtastic/core/resources/String17_commonMainKt$$ExternalSyntheticLambda78; +HSPLorg/meshtastic/core/resources/String17_commonMainKt$$ExternalSyntheticLambda78;->()V +Lorg/meshtastic/core/resources/String17_commonMainKt$$ExternalSyntheticLambda79; +HSPLorg/meshtastic/core/resources/String17_commonMainKt$$ExternalSyntheticLambda79;->()V +Lorg/meshtastic/core/resources/String17_commonMainKt$$ExternalSyntheticLambda8; +HSPLorg/meshtastic/core/resources/String17_commonMainKt$$ExternalSyntheticLambda8;->()V +Lorg/meshtastic/core/resources/String17_commonMainKt$$ExternalSyntheticLambda80; +HSPLorg/meshtastic/core/resources/String17_commonMainKt$$ExternalSyntheticLambda80;->()V +Lorg/meshtastic/core/resources/String17_commonMainKt$$ExternalSyntheticLambda81; +HSPLorg/meshtastic/core/resources/String17_commonMainKt$$ExternalSyntheticLambda81;->()V +Lorg/meshtastic/core/resources/String17_commonMainKt$$ExternalSyntheticLambda82; +HSPLorg/meshtastic/core/resources/String17_commonMainKt$$ExternalSyntheticLambda82;->()V +Lorg/meshtastic/core/resources/String17_commonMainKt$$ExternalSyntheticLambda83; +HSPLorg/meshtastic/core/resources/String17_commonMainKt$$ExternalSyntheticLambda83;->()V +Lorg/meshtastic/core/resources/String17_commonMainKt$$ExternalSyntheticLambda84; +HSPLorg/meshtastic/core/resources/String17_commonMainKt$$ExternalSyntheticLambda84;->()V +Lorg/meshtastic/core/resources/String17_commonMainKt$$ExternalSyntheticLambda85; +HSPLorg/meshtastic/core/resources/String17_commonMainKt$$ExternalSyntheticLambda85;->()V +Lorg/meshtastic/core/resources/String17_commonMainKt$$ExternalSyntheticLambda86; +HSPLorg/meshtastic/core/resources/String17_commonMainKt$$ExternalSyntheticLambda86;->()V +Lorg/meshtastic/core/resources/String17_commonMainKt$$ExternalSyntheticLambda87; +HSPLorg/meshtastic/core/resources/String17_commonMainKt$$ExternalSyntheticLambda87;->()V +Lorg/meshtastic/core/resources/String17_commonMainKt$$ExternalSyntheticLambda88; +HSPLorg/meshtastic/core/resources/String17_commonMainKt$$ExternalSyntheticLambda88;->()V +Lorg/meshtastic/core/resources/String17_commonMainKt$$ExternalSyntheticLambda89; +HSPLorg/meshtastic/core/resources/String17_commonMainKt$$ExternalSyntheticLambda89;->()V +Lorg/meshtastic/core/resources/String17_commonMainKt$$ExternalSyntheticLambda9; +HSPLorg/meshtastic/core/resources/String17_commonMainKt$$ExternalSyntheticLambda9;->()V +Lorg/meshtastic/core/resources/String17_commonMainKt$$ExternalSyntheticLambda90; +HSPLorg/meshtastic/core/resources/String17_commonMainKt$$ExternalSyntheticLambda90;->()V +Lorg/meshtastic/core/resources/String17_commonMainKt$$ExternalSyntheticLambda91; +HSPLorg/meshtastic/core/resources/String17_commonMainKt$$ExternalSyntheticLambda91;->()V +Lorg/meshtastic/core/resources/String17_commonMainKt$$ExternalSyntheticLambda92; +HSPLorg/meshtastic/core/resources/String17_commonMainKt$$ExternalSyntheticLambda92;->()V +Lorg/meshtastic/core/resources/String17_commonMainKt$$ExternalSyntheticLambda93; +HSPLorg/meshtastic/core/resources/String17_commonMainKt$$ExternalSyntheticLambda93;->()V +Lorg/meshtastic/core/resources/String17_commonMainKt$$ExternalSyntheticLambda94; +HSPLorg/meshtastic/core/resources/String17_commonMainKt$$ExternalSyntheticLambda94;->()V +Lorg/meshtastic/core/resources/String17_commonMainKt$$ExternalSyntheticLambda95; +HSPLorg/meshtastic/core/resources/String17_commonMainKt$$ExternalSyntheticLambda95;->()V +Lorg/meshtastic/core/resources/String17_commonMainKt$$ExternalSyntheticLambda96; +HSPLorg/meshtastic/core/resources/String17_commonMainKt$$ExternalSyntheticLambda96;->()V +Lorg/meshtastic/core/resources/String17_commonMainKt$$ExternalSyntheticLambda97; +HSPLorg/meshtastic/core/resources/String17_commonMainKt$$ExternalSyntheticLambda97;->()V +Lorg/meshtastic/core/resources/String17_commonMainKt$$ExternalSyntheticLambda98; +HSPLorg/meshtastic/core/resources/String17_commonMainKt$$ExternalSyntheticLambda98;->()V +Lorg/meshtastic/core/resources/String17_commonMainKt$$ExternalSyntheticLambda99; +HSPLorg/meshtastic/core/resources/String17_commonMainKt$$ExternalSyntheticLambda99;->()V Lorg/meshtastic/core/resources/String18_commonMainKt; HSPLorg/meshtastic/core/resources/String18_commonMainKt;->()V HSPLorg/meshtastic/core/resources/String18_commonMainKt;->getTrack_and_share_locations(Lorg/meshtastic/core/resources/Res$string;)Lorg/jetbrains/compose/resources/StringResource; @@ -42522,7 +43039,6 @@ Lorg/meshtastic/core/resources/String18_commonMainKt$$ExternalSyntheticLambda13; HSPLorg/meshtastic/core/resources/String18_commonMainKt$$ExternalSyntheticLambda13;->()V Lorg/meshtastic/core/resources/String18_commonMainKt$$ExternalSyntheticLambda14; HSPLorg/meshtastic/core/resources/String18_commonMainKt$$ExternalSyntheticLambda14;->()V -HSPLorg/meshtastic/core/resources/String18_commonMainKt$$ExternalSyntheticLambda14;->invoke()Ljava/lang/Object; Lorg/meshtastic/core/resources/String18_commonMainKt$$ExternalSyntheticLambda15; HSPLorg/meshtastic/core/resources/String18_commonMainKt$$ExternalSyntheticLambda15;->()V Lorg/meshtastic/core/resources/String18_commonMainKt$$ExternalSyntheticLambda16; @@ -42667,6 +43183,7 @@ Lorg/meshtastic/core/resources/String18_commonMainKt$$ExternalSyntheticLambda79; HSPLorg/meshtastic/core/resources/String18_commonMainKt$$ExternalSyntheticLambda79;->()V Lorg/meshtastic/core/resources/String18_commonMainKt$$ExternalSyntheticLambda8; HSPLorg/meshtastic/core/resources/String18_commonMainKt$$ExternalSyntheticLambda8;->()V +HSPLorg/meshtastic/core/resources/String18_commonMainKt$$ExternalSyntheticLambda8;->invoke()Ljava/lang/Object; Lorg/meshtastic/core/resources/String18_commonMainKt$$ExternalSyntheticLambda80; HSPLorg/meshtastic/core/resources/String18_commonMainKt$$ExternalSyntheticLambda80;->()V Lorg/meshtastic/core/resources/String18_commonMainKt$$ExternalSyntheticLambda81; @@ -42719,19 +43236,20 @@ Lorg/meshtastic/core/resources/String2_commonMainKt$$ExternalSyntheticLambda0; HSPLorg/meshtastic/core/resources/String2_commonMainKt$$ExternalSyntheticLambda0;->()V Lorg/meshtastic/core/resources/String2_commonMainKt$$ExternalSyntheticLambda1; HSPLorg/meshtastic/core/resources/String2_commonMainKt$$ExternalSyntheticLambda1;->()V +HSPLorg/meshtastic/core/resources/String2_commonMainKt$$ExternalSyntheticLambda1;->invoke()Ljava/lang/Object; Lorg/meshtastic/core/resources/String2_commonMainKt$$ExternalSyntheticLambda10; HSPLorg/meshtastic/core/resources/String2_commonMainKt$$ExternalSyntheticLambda10;->()V Lorg/meshtastic/core/resources/String2_commonMainKt$$ExternalSyntheticLambda11; HSPLorg/meshtastic/core/resources/String2_commonMainKt$$ExternalSyntheticLambda11;->()V Lorg/meshtastic/core/resources/String2_commonMainKt$$ExternalSyntheticLambda12; HSPLorg/meshtastic/core/resources/String2_commonMainKt$$ExternalSyntheticLambda12;->()V -HSPLorg/meshtastic/core/resources/String2_commonMainKt$$ExternalSyntheticLambda12;->invoke()Ljava/lang/Object; Lorg/meshtastic/core/resources/String2_commonMainKt$$ExternalSyntheticLambda13; HSPLorg/meshtastic/core/resources/String2_commonMainKt$$ExternalSyntheticLambda13;->()V Lorg/meshtastic/core/resources/String2_commonMainKt$$ExternalSyntheticLambda14; HSPLorg/meshtastic/core/resources/String2_commonMainKt$$ExternalSyntheticLambda14;->()V Lorg/meshtastic/core/resources/String2_commonMainKt$$ExternalSyntheticLambda15; HSPLorg/meshtastic/core/resources/String2_commonMainKt$$ExternalSyntheticLambda15;->()V +HSPLorg/meshtastic/core/resources/String2_commonMainKt$$ExternalSyntheticLambda15;->invoke()Ljava/lang/Object; Lorg/meshtastic/core/resources/String2_commonMainKt$$ExternalSyntheticLambda16; HSPLorg/meshtastic/core/resources/String2_commonMainKt$$ExternalSyntheticLambda16;->()V Lorg/meshtastic/core/resources/String2_commonMainKt$$ExternalSyntheticLambda17; @@ -42892,7 +43410,6 @@ Lorg/meshtastic/core/resources/String2_commonMainKt$$ExternalSyntheticLambda87; HSPLorg/meshtastic/core/resources/String2_commonMainKt$$ExternalSyntheticLambda87;->()V Lorg/meshtastic/core/resources/String2_commonMainKt$$ExternalSyntheticLambda88; HSPLorg/meshtastic/core/resources/String2_commonMainKt$$ExternalSyntheticLambda88;->()V -HSPLorg/meshtastic/core/resources/String2_commonMainKt$$ExternalSyntheticLambda88;->invoke()Ljava/lang/Object; Lorg/meshtastic/core/resources/String2_commonMainKt$$ExternalSyntheticLambda89; HSPLorg/meshtastic/core/resources/String2_commonMainKt$$ExternalSyntheticLambda89;->()V Lorg/meshtastic/core/resources/String2_commonMainKt$$ExternalSyntheticLambda9; @@ -42921,10 +43438,8 @@ Lorg/meshtastic/core/resources/String3_commonMainKt; HSPLorg/meshtastic/core/resources/String3_commonMainKt;->()V HSPLorg/meshtastic/core/resources/String3_commonMainKt;->create_your_own_networks_delegate$lambda$0()Lorg/jetbrains/compose/resources/StringResource; HSPLorg/meshtastic/core/resources/String3_commonMainKt;->datadog_link_delegate$lambda$0()Lorg/jetbrains/compose/resources/StringResource; -HSPLorg/meshtastic/core/resources/String3_commonMainKt;->disconnected_delegate$lambda$0()Lorg/jetbrains/compose/resources/StringResource; HSPLorg/meshtastic/core/resources/String3_commonMainKt;->getCreate_your_own_networks(Lorg/meshtastic/core/resources/Res$string;)Lorg/jetbrains/compose/resources/StringResource; HSPLorg/meshtastic/core/resources/String3_commonMainKt;->getDatadog_link(Lorg/meshtastic/core/resources/Res$string;)Lorg/jetbrains/compose/resources/StringResource; -HSPLorg/meshtastic/core/resources/String3_commonMainKt;->getDisconnected(Lorg/meshtastic/core/resources/Res$string;)Lorg/jetbrains/compose/resources/StringResource; Lorg/meshtastic/core/resources/String3_commonMainKt$$ExternalSyntheticLambda0; HSPLorg/meshtastic/core/resources/String3_commonMainKt$$ExternalSyntheticLambda0;->()V Lorg/meshtastic/core/resources/String3_commonMainKt$$ExternalSyntheticLambda1; @@ -42933,13 +43448,13 @@ Lorg/meshtastic/core/resources/String3_commonMainKt$$ExternalSyntheticLambda10; HSPLorg/meshtastic/core/resources/String3_commonMainKt$$ExternalSyntheticLambda10;->()V Lorg/meshtastic/core/resources/String3_commonMainKt$$ExternalSyntheticLambda11; HSPLorg/meshtastic/core/resources/String3_commonMainKt$$ExternalSyntheticLambda11;->()V +HSPLorg/meshtastic/core/resources/String3_commonMainKt$$ExternalSyntheticLambda11;->invoke()Ljava/lang/Object; Lorg/meshtastic/core/resources/String3_commonMainKt$$ExternalSyntheticLambda12; HSPLorg/meshtastic/core/resources/String3_commonMainKt$$ExternalSyntheticLambda12;->()V Lorg/meshtastic/core/resources/String3_commonMainKt$$ExternalSyntheticLambda13; HSPLorg/meshtastic/core/resources/String3_commonMainKt$$ExternalSyntheticLambda13;->()V Lorg/meshtastic/core/resources/String3_commonMainKt$$ExternalSyntheticLambda14; HSPLorg/meshtastic/core/resources/String3_commonMainKt$$ExternalSyntheticLambda14;->()V -HSPLorg/meshtastic/core/resources/String3_commonMainKt$$ExternalSyntheticLambda14;->invoke()Ljava/lang/Object; Lorg/meshtastic/core/resources/String3_commonMainKt$$ExternalSyntheticLambda15; HSPLorg/meshtastic/core/resources/String3_commonMainKt$$ExternalSyntheticLambda15;->()V Lorg/meshtastic/core/resources/String3_commonMainKt$$ExternalSyntheticLambda16; @@ -42992,6 +43507,7 @@ Lorg/meshtastic/core/resources/String3_commonMainKt$$ExternalSyntheticLambda37; HSPLorg/meshtastic/core/resources/String3_commonMainKt$$ExternalSyntheticLambda37;->()V Lorg/meshtastic/core/resources/String3_commonMainKt$$ExternalSyntheticLambda38; HSPLorg/meshtastic/core/resources/String3_commonMainKt$$ExternalSyntheticLambda38;->()V +HSPLorg/meshtastic/core/resources/String3_commonMainKt$$ExternalSyntheticLambda38;->invoke()Ljava/lang/Object; Lorg/meshtastic/core/resources/String3_commonMainKt$$ExternalSyntheticLambda39; HSPLorg/meshtastic/core/resources/String3_commonMainKt$$ExternalSyntheticLambda39;->()V Lorg/meshtastic/core/resources/String3_commonMainKt$$ExternalSyntheticLambda4; @@ -43098,7 +43614,6 @@ Lorg/meshtastic/core/resources/String3_commonMainKt$$ExternalSyntheticLambda85; HSPLorg/meshtastic/core/resources/String3_commonMainKt$$ExternalSyntheticLambda85;->()V Lorg/meshtastic/core/resources/String3_commonMainKt$$ExternalSyntheticLambda86; HSPLorg/meshtastic/core/resources/String3_commonMainKt$$ExternalSyntheticLambda86;->()V -HSPLorg/meshtastic/core/resources/String3_commonMainKt$$ExternalSyntheticLambda86;->invoke()Ljava/lang/Object; Lorg/meshtastic/core/resources/String3_commonMainKt$$ExternalSyntheticLambda87; HSPLorg/meshtastic/core/resources/String3_commonMainKt$$ExternalSyntheticLambda87;->()V Lorg/meshtastic/core/resources/String3_commonMainKt$$ExternalSyntheticLambda88; @@ -43123,11 +43638,215 @@ Lorg/meshtastic/core/resources/String3_commonMainKt$$ExternalSyntheticLambda96; HSPLorg/meshtastic/core/resources/String3_commonMainKt$$ExternalSyntheticLambda96;->()V Lorg/meshtastic/core/resources/String3_commonMainKt$$ExternalSyntheticLambda97; HSPLorg/meshtastic/core/resources/String3_commonMainKt$$ExternalSyntheticLambda97;->()V -HSPLorg/meshtastic/core/resources/String3_commonMainKt$$ExternalSyntheticLambda97;->invoke()Ljava/lang/Object; Lorg/meshtastic/core/resources/String3_commonMainKt$$ExternalSyntheticLambda98; HSPLorg/meshtastic/core/resources/String3_commonMainKt$$ExternalSyntheticLambda98;->()V Lorg/meshtastic/core/resources/String3_commonMainKt$$ExternalSyntheticLambda99; HSPLorg/meshtastic/core/resources/String3_commonMainKt$$ExternalSyntheticLambda99;->()V +Lorg/meshtastic/core/resources/String4_commonMainKt; +HSPLorg/meshtastic/core/resources/String4_commonMainKt;->()V +HSPLorg/meshtastic/core/resources/String4_commonMainKt;->disconnected_delegate$lambda$0()Lorg/jetbrains/compose/resources/StringResource; +HSPLorg/meshtastic/core/resources/String4_commonMainKt;->getDisconnected(Lorg/meshtastic/core/resources/Res$string;)Lorg/jetbrains/compose/resources/StringResource; +Lorg/meshtastic/core/resources/String4_commonMainKt$$ExternalSyntheticLambda0; +HSPLorg/meshtastic/core/resources/String4_commonMainKt$$ExternalSyntheticLambda0;->()V +Lorg/meshtastic/core/resources/String4_commonMainKt$$ExternalSyntheticLambda1; +HSPLorg/meshtastic/core/resources/String4_commonMainKt$$ExternalSyntheticLambda1;->()V +Lorg/meshtastic/core/resources/String4_commonMainKt$$ExternalSyntheticLambda10; +HSPLorg/meshtastic/core/resources/String4_commonMainKt$$ExternalSyntheticLambda10;->()V +Lorg/meshtastic/core/resources/String4_commonMainKt$$ExternalSyntheticLambda11; +HSPLorg/meshtastic/core/resources/String4_commonMainKt$$ExternalSyntheticLambda11;->()V +Lorg/meshtastic/core/resources/String4_commonMainKt$$ExternalSyntheticLambda12; +HSPLorg/meshtastic/core/resources/String4_commonMainKt$$ExternalSyntheticLambda12;->()V +Lorg/meshtastic/core/resources/String4_commonMainKt$$ExternalSyntheticLambda13; +HSPLorg/meshtastic/core/resources/String4_commonMainKt$$ExternalSyntheticLambda13;->()V +Lorg/meshtastic/core/resources/String4_commonMainKt$$ExternalSyntheticLambda14; +HSPLorg/meshtastic/core/resources/String4_commonMainKt$$ExternalSyntheticLambda14;->()V +Lorg/meshtastic/core/resources/String4_commonMainKt$$ExternalSyntheticLambda15; +HSPLorg/meshtastic/core/resources/String4_commonMainKt$$ExternalSyntheticLambda15;->()V +Lorg/meshtastic/core/resources/String4_commonMainKt$$ExternalSyntheticLambda16; +HSPLorg/meshtastic/core/resources/String4_commonMainKt$$ExternalSyntheticLambda16;->()V +Lorg/meshtastic/core/resources/String4_commonMainKt$$ExternalSyntheticLambda17; +HSPLorg/meshtastic/core/resources/String4_commonMainKt$$ExternalSyntheticLambda17;->()V +Lorg/meshtastic/core/resources/String4_commonMainKt$$ExternalSyntheticLambda18; +HSPLorg/meshtastic/core/resources/String4_commonMainKt$$ExternalSyntheticLambda18;->()V +Lorg/meshtastic/core/resources/String4_commonMainKt$$ExternalSyntheticLambda19; +HSPLorg/meshtastic/core/resources/String4_commonMainKt$$ExternalSyntheticLambda19;->()V +Lorg/meshtastic/core/resources/String4_commonMainKt$$ExternalSyntheticLambda2; +HSPLorg/meshtastic/core/resources/String4_commonMainKt$$ExternalSyntheticLambda2;->()V +HSPLorg/meshtastic/core/resources/String4_commonMainKt$$ExternalSyntheticLambda2;->invoke()Ljava/lang/Object; +Lorg/meshtastic/core/resources/String4_commonMainKt$$ExternalSyntheticLambda20; +HSPLorg/meshtastic/core/resources/String4_commonMainKt$$ExternalSyntheticLambda20;->()V +Lorg/meshtastic/core/resources/String4_commonMainKt$$ExternalSyntheticLambda21; +HSPLorg/meshtastic/core/resources/String4_commonMainKt$$ExternalSyntheticLambda21;->()V +Lorg/meshtastic/core/resources/String4_commonMainKt$$ExternalSyntheticLambda22; +HSPLorg/meshtastic/core/resources/String4_commonMainKt$$ExternalSyntheticLambda22;->()V +Lorg/meshtastic/core/resources/String4_commonMainKt$$ExternalSyntheticLambda23; +HSPLorg/meshtastic/core/resources/String4_commonMainKt$$ExternalSyntheticLambda23;->()V +Lorg/meshtastic/core/resources/String4_commonMainKt$$ExternalSyntheticLambda24; +HSPLorg/meshtastic/core/resources/String4_commonMainKt$$ExternalSyntheticLambda24;->()V +Lorg/meshtastic/core/resources/String4_commonMainKt$$ExternalSyntheticLambda25; +HSPLorg/meshtastic/core/resources/String4_commonMainKt$$ExternalSyntheticLambda25;->()V +Lorg/meshtastic/core/resources/String4_commonMainKt$$ExternalSyntheticLambda26; +HSPLorg/meshtastic/core/resources/String4_commonMainKt$$ExternalSyntheticLambda26;->()V +Lorg/meshtastic/core/resources/String4_commonMainKt$$ExternalSyntheticLambda27; +HSPLorg/meshtastic/core/resources/String4_commonMainKt$$ExternalSyntheticLambda27;->()V +Lorg/meshtastic/core/resources/String4_commonMainKt$$ExternalSyntheticLambda28; +HSPLorg/meshtastic/core/resources/String4_commonMainKt$$ExternalSyntheticLambda28;->()V +Lorg/meshtastic/core/resources/String4_commonMainKt$$ExternalSyntheticLambda29; +HSPLorg/meshtastic/core/resources/String4_commonMainKt$$ExternalSyntheticLambda29;->()V +Lorg/meshtastic/core/resources/String4_commonMainKt$$ExternalSyntheticLambda3; +HSPLorg/meshtastic/core/resources/String4_commonMainKt$$ExternalSyntheticLambda3;->()V +Lorg/meshtastic/core/resources/String4_commonMainKt$$ExternalSyntheticLambda30; +HSPLorg/meshtastic/core/resources/String4_commonMainKt$$ExternalSyntheticLambda30;->()V +Lorg/meshtastic/core/resources/String4_commonMainKt$$ExternalSyntheticLambda31; +HSPLorg/meshtastic/core/resources/String4_commonMainKt$$ExternalSyntheticLambda31;->()V +Lorg/meshtastic/core/resources/String4_commonMainKt$$ExternalSyntheticLambda32; +HSPLorg/meshtastic/core/resources/String4_commonMainKt$$ExternalSyntheticLambda32;->()V +Lorg/meshtastic/core/resources/String4_commonMainKt$$ExternalSyntheticLambda33; +HSPLorg/meshtastic/core/resources/String4_commonMainKt$$ExternalSyntheticLambda33;->()V +Lorg/meshtastic/core/resources/String4_commonMainKt$$ExternalSyntheticLambda34; +HSPLorg/meshtastic/core/resources/String4_commonMainKt$$ExternalSyntheticLambda34;->()V +Lorg/meshtastic/core/resources/String4_commonMainKt$$ExternalSyntheticLambda35; +HSPLorg/meshtastic/core/resources/String4_commonMainKt$$ExternalSyntheticLambda35;->()V +Lorg/meshtastic/core/resources/String4_commonMainKt$$ExternalSyntheticLambda36; +HSPLorg/meshtastic/core/resources/String4_commonMainKt$$ExternalSyntheticLambda36;->()V +Lorg/meshtastic/core/resources/String4_commonMainKt$$ExternalSyntheticLambda37; +HSPLorg/meshtastic/core/resources/String4_commonMainKt$$ExternalSyntheticLambda37;->()V +Lorg/meshtastic/core/resources/String4_commonMainKt$$ExternalSyntheticLambda38; +HSPLorg/meshtastic/core/resources/String4_commonMainKt$$ExternalSyntheticLambda38;->()V +Lorg/meshtastic/core/resources/String4_commonMainKt$$ExternalSyntheticLambda39; +HSPLorg/meshtastic/core/resources/String4_commonMainKt$$ExternalSyntheticLambda39;->()V +Lorg/meshtastic/core/resources/String4_commonMainKt$$ExternalSyntheticLambda4; +HSPLorg/meshtastic/core/resources/String4_commonMainKt$$ExternalSyntheticLambda4;->()V +Lorg/meshtastic/core/resources/String4_commonMainKt$$ExternalSyntheticLambda40; +HSPLorg/meshtastic/core/resources/String4_commonMainKt$$ExternalSyntheticLambda40;->()V +Lorg/meshtastic/core/resources/String4_commonMainKt$$ExternalSyntheticLambda41; +HSPLorg/meshtastic/core/resources/String4_commonMainKt$$ExternalSyntheticLambda41;->()V +Lorg/meshtastic/core/resources/String4_commonMainKt$$ExternalSyntheticLambda42; +HSPLorg/meshtastic/core/resources/String4_commonMainKt$$ExternalSyntheticLambda42;->()V +Lorg/meshtastic/core/resources/String4_commonMainKt$$ExternalSyntheticLambda43; +HSPLorg/meshtastic/core/resources/String4_commonMainKt$$ExternalSyntheticLambda43;->()V +Lorg/meshtastic/core/resources/String4_commonMainKt$$ExternalSyntheticLambda44; +HSPLorg/meshtastic/core/resources/String4_commonMainKt$$ExternalSyntheticLambda44;->()V +Lorg/meshtastic/core/resources/String4_commonMainKt$$ExternalSyntheticLambda45; +HSPLorg/meshtastic/core/resources/String4_commonMainKt$$ExternalSyntheticLambda45;->()V +Lorg/meshtastic/core/resources/String4_commonMainKt$$ExternalSyntheticLambda46; +HSPLorg/meshtastic/core/resources/String4_commonMainKt$$ExternalSyntheticLambda46;->()V +Lorg/meshtastic/core/resources/String4_commonMainKt$$ExternalSyntheticLambda47; +HSPLorg/meshtastic/core/resources/String4_commonMainKt$$ExternalSyntheticLambda47;->()V +Lorg/meshtastic/core/resources/String4_commonMainKt$$ExternalSyntheticLambda48; +HSPLorg/meshtastic/core/resources/String4_commonMainKt$$ExternalSyntheticLambda48;->()V +Lorg/meshtastic/core/resources/String4_commonMainKt$$ExternalSyntheticLambda49; +HSPLorg/meshtastic/core/resources/String4_commonMainKt$$ExternalSyntheticLambda49;->()V +Lorg/meshtastic/core/resources/String4_commonMainKt$$ExternalSyntheticLambda5; +HSPLorg/meshtastic/core/resources/String4_commonMainKt$$ExternalSyntheticLambda5;->()V +Lorg/meshtastic/core/resources/String4_commonMainKt$$ExternalSyntheticLambda50; +HSPLorg/meshtastic/core/resources/String4_commonMainKt$$ExternalSyntheticLambda50;->()V +Lorg/meshtastic/core/resources/String4_commonMainKt$$ExternalSyntheticLambda51; +HSPLorg/meshtastic/core/resources/String4_commonMainKt$$ExternalSyntheticLambda51;->()V +Lorg/meshtastic/core/resources/String4_commonMainKt$$ExternalSyntheticLambda52; +HSPLorg/meshtastic/core/resources/String4_commonMainKt$$ExternalSyntheticLambda52;->()V +Lorg/meshtastic/core/resources/String4_commonMainKt$$ExternalSyntheticLambda53; +HSPLorg/meshtastic/core/resources/String4_commonMainKt$$ExternalSyntheticLambda53;->()V +Lorg/meshtastic/core/resources/String4_commonMainKt$$ExternalSyntheticLambda54; +HSPLorg/meshtastic/core/resources/String4_commonMainKt$$ExternalSyntheticLambda54;->()V +Lorg/meshtastic/core/resources/String4_commonMainKt$$ExternalSyntheticLambda55; +HSPLorg/meshtastic/core/resources/String4_commonMainKt$$ExternalSyntheticLambda55;->()V +Lorg/meshtastic/core/resources/String4_commonMainKt$$ExternalSyntheticLambda56; +HSPLorg/meshtastic/core/resources/String4_commonMainKt$$ExternalSyntheticLambda56;->()V +Lorg/meshtastic/core/resources/String4_commonMainKt$$ExternalSyntheticLambda57; +HSPLorg/meshtastic/core/resources/String4_commonMainKt$$ExternalSyntheticLambda57;->()V +Lorg/meshtastic/core/resources/String4_commonMainKt$$ExternalSyntheticLambda58; +HSPLorg/meshtastic/core/resources/String4_commonMainKt$$ExternalSyntheticLambda58;->()V +Lorg/meshtastic/core/resources/String4_commonMainKt$$ExternalSyntheticLambda59; +HSPLorg/meshtastic/core/resources/String4_commonMainKt$$ExternalSyntheticLambda59;->()V +Lorg/meshtastic/core/resources/String4_commonMainKt$$ExternalSyntheticLambda6; +HSPLorg/meshtastic/core/resources/String4_commonMainKt$$ExternalSyntheticLambda6;->()V +Lorg/meshtastic/core/resources/String4_commonMainKt$$ExternalSyntheticLambda60; +HSPLorg/meshtastic/core/resources/String4_commonMainKt$$ExternalSyntheticLambda60;->()V +Lorg/meshtastic/core/resources/String4_commonMainKt$$ExternalSyntheticLambda61; +HSPLorg/meshtastic/core/resources/String4_commonMainKt$$ExternalSyntheticLambda61;->()V +Lorg/meshtastic/core/resources/String4_commonMainKt$$ExternalSyntheticLambda62; +HSPLorg/meshtastic/core/resources/String4_commonMainKt$$ExternalSyntheticLambda62;->()V +Lorg/meshtastic/core/resources/String4_commonMainKt$$ExternalSyntheticLambda63; +HSPLorg/meshtastic/core/resources/String4_commonMainKt$$ExternalSyntheticLambda63;->()V +Lorg/meshtastic/core/resources/String4_commonMainKt$$ExternalSyntheticLambda64; +HSPLorg/meshtastic/core/resources/String4_commonMainKt$$ExternalSyntheticLambda64;->()V +Lorg/meshtastic/core/resources/String4_commonMainKt$$ExternalSyntheticLambda65; +HSPLorg/meshtastic/core/resources/String4_commonMainKt$$ExternalSyntheticLambda65;->()V +Lorg/meshtastic/core/resources/String4_commonMainKt$$ExternalSyntheticLambda66; +HSPLorg/meshtastic/core/resources/String4_commonMainKt$$ExternalSyntheticLambda66;->()V +Lorg/meshtastic/core/resources/String4_commonMainKt$$ExternalSyntheticLambda67; +HSPLorg/meshtastic/core/resources/String4_commonMainKt$$ExternalSyntheticLambda67;->()V +Lorg/meshtastic/core/resources/String4_commonMainKt$$ExternalSyntheticLambda68; +HSPLorg/meshtastic/core/resources/String4_commonMainKt$$ExternalSyntheticLambda68;->()V +Lorg/meshtastic/core/resources/String4_commonMainKt$$ExternalSyntheticLambda69; +HSPLorg/meshtastic/core/resources/String4_commonMainKt$$ExternalSyntheticLambda69;->()V +Lorg/meshtastic/core/resources/String4_commonMainKt$$ExternalSyntheticLambda7; +HSPLorg/meshtastic/core/resources/String4_commonMainKt$$ExternalSyntheticLambda7;->()V +Lorg/meshtastic/core/resources/String4_commonMainKt$$ExternalSyntheticLambda70; +HSPLorg/meshtastic/core/resources/String4_commonMainKt$$ExternalSyntheticLambda70;->()V +Lorg/meshtastic/core/resources/String4_commonMainKt$$ExternalSyntheticLambda71; +HSPLorg/meshtastic/core/resources/String4_commonMainKt$$ExternalSyntheticLambda71;->()V +Lorg/meshtastic/core/resources/String4_commonMainKt$$ExternalSyntheticLambda72; +HSPLorg/meshtastic/core/resources/String4_commonMainKt$$ExternalSyntheticLambda72;->()V +Lorg/meshtastic/core/resources/String4_commonMainKt$$ExternalSyntheticLambda73; +HSPLorg/meshtastic/core/resources/String4_commonMainKt$$ExternalSyntheticLambda73;->()V +Lorg/meshtastic/core/resources/String4_commonMainKt$$ExternalSyntheticLambda74; +HSPLorg/meshtastic/core/resources/String4_commonMainKt$$ExternalSyntheticLambda74;->()V +Lorg/meshtastic/core/resources/String4_commonMainKt$$ExternalSyntheticLambda75; +HSPLorg/meshtastic/core/resources/String4_commonMainKt$$ExternalSyntheticLambda75;->()V +Lorg/meshtastic/core/resources/String4_commonMainKt$$ExternalSyntheticLambda76; +HSPLorg/meshtastic/core/resources/String4_commonMainKt$$ExternalSyntheticLambda76;->()V +Lorg/meshtastic/core/resources/String4_commonMainKt$$ExternalSyntheticLambda77; +HSPLorg/meshtastic/core/resources/String4_commonMainKt$$ExternalSyntheticLambda77;->()V +Lorg/meshtastic/core/resources/String4_commonMainKt$$ExternalSyntheticLambda78; +HSPLorg/meshtastic/core/resources/String4_commonMainKt$$ExternalSyntheticLambda78;->()V +Lorg/meshtastic/core/resources/String4_commonMainKt$$ExternalSyntheticLambda79; +HSPLorg/meshtastic/core/resources/String4_commonMainKt$$ExternalSyntheticLambda79;->()V +Lorg/meshtastic/core/resources/String4_commonMainKt$$ExternalSyntheticLambda8; +HSPLorg/meshtastic/core/resources/String4_commonMainKt$$ExternalSyntheticLambda8;->()V +Lorg/meshtastic/core/resources/String4_commonMainKt$$ExternalSyntheticLambda80; +HSPLorg/meshtastic/core/resources/String4_commonMainKt$$ExternalSyntheticLambda80;->()V +Lorg/meshtastic/core/resources/String4_commonMainKt$$ExternalSyntheticLambda81; +HSPLorg/meshtastic/core/resources/String4_commonMainKt$$ExternalSyntheticLambda81;->()V +Lorg/meshtastic/core/resources/String4_commonMainKt$$ExternalSyntheticLambda82; +HSPLorg/meshtastic/core/resources/String4_commonMainKt$$ExternalSyntheticLambda82;->()V +Lorg/meshtastic/core/resources/String4_commonMainKt$$ExternalSyntheticLambda83; +HSPLorg/meshtastic/core/resources/String4_commonMainKt$$ExternalSyntheticLambda83;->()V +Lorg/meshtastic/core/resources/String4_commonMainKt$$ExternalSyntheticLambda84; +HSPLorg/meshtastic/core/resources/String4_commonMainKt$$ExternalSyntheticLambda84;->()V +Lorg/meshtastic/core/resources/String4_commonMainKt$$ExternalSyntheticLambda85; +HSPLorg/meshtastic/core/resources/String4_commonMainKt$$ExternalSyntheticLambda85;->()V +Lorg/meshtastic/core/resources/String4_commonMainKt$$ExternalSyntheticLambda86; +HSPLorg/meshtastic/core/resources/String4_commonMainKt$$ExternalSyntheticLambda86;->()V +Lorg/meshtastic/core/resources/String4_commonMainKt$$ExternalSyntheticLambda87; +HSPLorg/meshtastic/core/resources/String4_commonMainKt$$ExternalSyntheticLambda87;->()V +Lorg/meshtastic/core/resources/String4_commonMainKt$$ExternalSyntheticLambda88; +HSPLorg/meshtastic/core/resources/String4_commonMainKt$$ExternalSyntheticLambda88;->()V +Lorg/meshtastic/core/resources/String4_commonMainKt$$ExternalSyntheticLambda89; +HSPLorg/meshtastic/core/resources/String4_commonMainKt$$ExternalSyntheticLambda89;->()V +Lorg/meshtastic/core/resources/String4_commonMainKt$$ExternalSyntheticLambda9; +HSPLorg/meshtastic/core/resources/String4_commonMainKt$$ExternalSyntheticLambda9;->()V +Lorg/meshtastic/core/resources/String4_commonMainKt$$ExternalSyntheticLambda90; +HSPLorg/meshtastic/core/resources/String4_commonMainKt$$ExternalSyntheticLambda90;->()V +Lorg/meshtastic/core/resources/String4_commonMainKt$$ExternalSyntheticLambda91; +HSPLorg/meshtastic/core/resources/String4_commonMainKt$$ExternalSyntheticLambda91;->()V +Lorg/meshtastic/core/resources/String4_commonMainKt$$ExternalSyntheticLambda92; +HSPLorg/meshtastic/core/resources/String4_commonMainKt$$ExternalSyntheticLambda92;->()V +Lorg/meshtastic/core/resources/String4_commonMainKt$$ExternalSyntheticLambda93; +HSPLorg/meshtastic/core/resources/String4_commonMainKt$$ExternalSyntheticLambda93;->()V +Lorg/meshtastic/core/resources/String4_commonMainKt$$ExternalSyntheticLambda94; +HSPLorg/meshtastic/core/resources/String4_commonMainKt$$ExternalSyntheticLambda94;->()V +Lorg/meshtastic/core/resources/String4_commonMainKt$$ExternalSyntheticLambda95; +HSPLorg/meshtastic/core/resources/String4_commonMainKt$$ExternalSyntheticLambda95;->()V +Lorg/meshtastic/core/resources/String4_commonMainKt$$ExternalSyntheticLambda96; +HSPLorg/meshtastic/core/resources/String4_commonMainKt$$ExternalSyntheticLambda96;->()V +Lorg/meshtastic/core/resources/String4_commonMainKt$$ExternalSyntheticLambda97; +HSPLorg/meshtastic/core/resources/String4_commonMainKt$$ExternalSyntheticLambda97;->()V +Lorg/meshtastic/core/resources/String4_commonMainKt$$ExternalSyntheticLambda98; +HSPLorg/meshtastic/core/resources/String4_commonMainKt$$ExternalSyntheticLambda98;->()V +Lorg/meshtastic/core/resources/String4_commonMainKt$$ExternalSyntheticLambda99; +HSPLorg/meshtastic/core/resources/String4_commonMainKt$$ExternalSyntheticLambda99;->()V Lorg/meshtastic/core/resources/String5_commonMainKt; HSPLorg/meshtastic/core/resources/String5_commonMainKt;->()V HSPLorg/meshtastic/core/resources/String5_commonMainKt;->easily_set_up_private_mesh_networks_delegate$lambda$0()Lorg/jetbrains/compose/resources/StringResource; @@ -43184,7 +43903,6 @@ Lorg/meshtastic/core/resources/String5_commonMainKt$$ExternalSyntheticLambda30; HSPLorg/meshtastic/core/resources/String5_commonMainKt$$ExternalSyntheticLambda30;->()V Lorg/meshtastic/core/resources/String5_commonMainKt$$ExternalSyntheticLambda31; HSPLorg/meshtastic/core/resources/String5_commonMainKt$$ExternalSyntheticLambda31;->()V -HSPLorg/meshtastic/core/resources/String5_commonMainKt$$ExternalSyntheticLambda31;->invoke()Ljava/lang/Object; Lorg/meshtastic/core/resources/String5_commonMainKt$$ExternalSyntheticLambda32; HSPLorg/meshtastic/core/resources/String5_commonMainKt$$ExternalSyntheticLambda32;->()V Lorg/meshtastic/core/resources/String5_commonMainKt$$ExternalSyntheticLambda33; @@ -43201,6 +43919,7 @@ Lorg/meshtastic/core/resources/String5_commonMainKt$$ExternalSyntheticLambda38; HSPLorg/meshtastic/core/resources/String5_commonMainKt$$ExternalSyntheticLambda38;->()V Lorg/meshtastic/core/resources/String5_commonMainKt$$ExternalSyntheticLambda39; HSPLorg/meshtastic/core/resources/String5_commonMainKt$$ExternalSyntheticLambda39;->()V +HSPLorg/meshtastic/core/resources/String5_commonMainKt$$ExternalSyntheticLambda39;->invoke()Ljava/lang/Object; Lorg/meshtastic/core/resources/String5_commonMainKt$$ExternalSyntheticLambda4; HSPLorg/meshtastic/core/resources/String5_commonMainKt$$ExternalSyntheticLambda4;->()V Lorg/meshtastic/core/resources/String5_commonMainKt$$ExternalSyntheticLambda40; @@ -43385,7 +44104,6 @@ Lorg/meshtastic/core/resources/String6_commonMainKt$$ExternalSyntheticLambda29; HSPLorg/meshtastic/core/resources/String6_commonMainKt$$ExternalSyntheticLambda29;->()V Lorg/meshtastic/core/resources/String6_commonMainKt$$ExternalSyntheticLambda3; HSPLorg/meshtastic/core/resources/String6_commonMainKt$$ExternalSyntheticLambda3;->()V -HSPLorg/meshtastic/core/resources/String6_commonMainKt$$ExternalSyntheticLambda3;->invoke()Ljava/lang/Object; Lorg/meshtastic/core/resources/String6_commonMainKt$$ExternalSyntheticLambda30; HSPLorg/meshtastic/core/resources/String6_commonMainKt$$ExternalSyntheticLambda30;->()V Lorg/meshtastic/core/resources/String6_commonMainKt$$ExternalSyntheticLambda31; @@ -43518,6 +44236,7 @@ Lorg/meshtastic/core/resources/String6_commonMainKt$$ExternalSyntheticLambda89; HSPLorg/meshtastic/core/resources/String6_commonMainKt$$ExternalSyntheticLambda89;->()V Lorg/meshtastic/core/resources/String6_commonMainKt$$ExternalSyntheticLambda9; HSPLorg/meshtastic/core/resources/String6_commonMainKt$$ExternalSyntheticLambda9;->()V +HSPLorg/meshtastic/core/resources/String6_commonMainKt$$ExternalSyntheticLambda9;->invoke()Ljava/lang/Object; Lorg/meshtastic/core/resources/String6_commonMainKt$$ExternalSyntheticLambda90; HSPLorg/meshtastic/core/resources/String6_commonMainKt$$ExternalSyntheticLambda90;->()V Lorg/meshtastic/core/resources/String6_commonMainKt$$ExternalSyntheticLambda91; @@ -43588,7 +44307,6 @@ Lorg/meshtastic/core/resources/String7_commonMainKt$$ExternalSyntheticLambda27; HSPLorg/meshtastic/core/resources/String7_commonMainKt$$ExternalSyntheticLambda27;->()V Lorg/meshtastic/core/resources/String7_commonMainKt$$ExternalSyntheticLambda28; HSPLorg/meshtastic/core/resources/String7_commonMainKt$$ExternalSyntheticLambda28;->()V -HSPLorg/meshtastic/core/resources/String7_commonMainKt$$ExternalSyntheticLambda28;->invoke()Ljava/lang/Object; Lorg/meshtastic/core/resources/String7_commonMainKt$$ExternalSyntheticLambda29; HSPLorg/meshtastic/core/resources/String7_commonMainKt$$ExternalSyntheticLambda29;->()V Lorg/meshtastic/core/resources/String7_commonMainKt$$ExternalSyntheticLambda3; @@ -43607,6 +44325,7 @@ Lorg/meshtastic/core/resources/String7_commonMainKt$$ExternalSyntheticLambda35; HSPLorg/meshtastic/core/resources/String7_commonMainKt$$ExternalSyntheticLambda35;->()V Lorg/meshtastic/core/resources/String7_commonMainKt$$ExternalSyntheticLambda36; HSPLorg/meshtastic/core/resources/String7_commonMainKt$$ExternalSyntheticLambda36;->()V +HSPLorg/meshtastic/core/resources/String7_commonMainKt$$ExternalSyntheticLambda36;->invoke()Ljava/lang/Object; Lorg/meshtastic/core/resources/String7_commonMainKt$$ExternalSyntheticLambda37; HSPLorg/meshtastic/core/resources/String7_commonMainKt$$ExternalSyntheticLambda37;->()V Lorg/meshtastic/core/resources/String7_commonMainKt$$ExternalSyntheticLambda38; @@ -43687,7 +44406,6 @@ Lorg/meshtastic/core/resources/String7_commonMainKt$$ExternalSyntheticLambda71; HSPLorg/meshtastic/core/resources/String7_commonMainKt$$ExternalSyntheticLambda71;->()V Lorg/meshtastic/core/resources/String7_commonMainKt$$ExternalSyntheticLambda72; HSPLorg/meshtastic/core/resources/String7_commonMainKt$$ExternalSyntheticLambda72;->()V -HSPLorg/meshtastic/core/resources/String7_commonMainKt$$ExternalSyntheticLambda72;->invoke()Ljava/lang/Object; Lorg/meshtastic/core/resources/String7_commonMainKt$$ExternalSyntheticLambda73; HSPLorg/meshtastic/core/resources/String7_commonMainKt$$ExternalSyntheticLambda73;->()V Lorg/meshtastic/core/resources/String7_commonMainKt$$ExternalSyntheticLambda74; @@ -43708,6 +44426,7 @@ Lorg/meshtastic/core/resources/String7_commonMainKt$$ExternalSyntheticLambda80; HSPLorg/meshtastic/core/resources/String7_commonMainKt$$ExternalSyntheticLambda80;->()V Lorg/meshtastic/core/resources/String7_commonMainKt$$ExternalSyntheticLambda81; HSPLorg/meshtastic/core/resources/String7_commonMainKt$$ExternalSyntheticLambda81;->()V +HSPLorg/meshtastic/core/resources/String7_commonMainKt$$ExternalSyntheticLambda81;->invoke()Ljava/lang/Object; Lorg/meshtastic/core/resources/String7_commonMainKt$$ExternalSyntheticLambda82; HSPLorg/meshtastic/core/resources/String7_commonMainKt$$ExternalSyntheticLambda82;->()V Lorg/meshtastic/core/resources/String7_commonMainKt$$ExternalSyntheticLambda83; @@ -43808,7 +44527,6 @@ Lorg/meshtastic/core/resources/String8_commonMainKt$$ExternalSyntheticLambda33; HSPLorg/meshtastic/core/resources/String8_commonMainKt$$ExternalSyntheticLambda33;->()V Lorg/meshtastic/core/resources/String8_commonMainKt$$ExternalSyntheticLambda34; HSPLorg/meshtastic/core/resources/String8_commonMainKt$$ExternalSyntheticLambda34;->()V -HSPLorg/meshtastic/core/resources/String8_commonMainKt$$ExternalSyntheticLambda34;->invoke()Ljava/lang/Object; Lorg/meshtastic/core/resources/String8_commonMainKt$$ExternalSyntheticLambda35; HSPLorg/meshtastic/core/resources/String8_commonMainKt$$ExternalSyntheticLambda35;->()V Lorg/meshtastic/core/resources/String8_commonMainKt$$ExternalSyntheticLambda36; @@ -43827,6 +44545,7 @@ Lorg/meshtastic/core/resources/String8_commonMainKt$$ExternalSyntheticLambda41; HSPLorg/meshtastic/core/resources/String8_commonMainKt$$ExternalSyntheticLambda41;->()V Lorg/meshtastic/core/resources/String8_commonMainKt$$ExternalSyntheticLambda42; HSPLorg/meshtastic/core/resources/String8_commonMainKt$$ExternalSyntheticLambda42;->()V +HSPLorg/meshtastic/core/resources/String8_commonMainKt$$ExternalSyntheticLambda42;->invoke()Ljava/lang/Object; Lorg/meshtastic/core/resources/String8_commonMainKt$$ExternalSyntheticLambda43; HSPLorg/meshtastic/core/resources/String8_commonMainKt$$ExternalSyntheticLambda43;->()V Lorg/meshtastic/core/resources/String8_commonMainKt$$ExternalSyntheticLambda44; @@ -44025,7 +44744,9 @@ HSPLorg/meshtastic/core/service/LockdownPassphraseStoreImpl$Companion;->() HSPLorg/meshtastic/core/service/LockdownPassphraseStoreImpl$Companion;->(Lkotlin/jvm/internal/DefaultConstructorMarker;)V Lorg/meshtastic/core/service/MeshNotificationManagerImpl; HSPLorg/meshtastic/core/service/MeshNotificationManagerImpl;->()V -HSPLorg/meshtastic/core/service/MeshNotificationManagerImpl;->(Landroid/content/Context;Lkotlin/Lazy;Lkotlin/Lazy;Lkotlin/Lazy;Lkotlin/Lazy;Lorg/meshtastic/core/common/di/ServiceScope;)V +HSPLorg/meshtastic/core/service/MeshNotificationManagerImpl;->(Landroid/content/Context;Lkotlin/Lazy;Lkotlin/Lazy;Lkotlin/Lazy;Lkotlin/Lazy;Lorg/meshtastic/core/common/state/RadioOperationLock;Lorg/meshtastic/core/common/di/ServiceScope;)V +HSPLorg/meshtastic/core/service/MeshNotificationManagerImpl;->access$getRadioOperationLock$p(Lorg/meshtastic/core/service/MeshNotificationManagerImpl;)Lorg/meshtastic/core/common/state/RadioOperationLock; +HSPLorg/meshtastic/core/service/MeshNotificationManagerImpl;->access$getServiceNotificationLock$p(Lorg/meshtastic/core/service/MeshNotificationManagerImpl;)Ljava/lang/Object; HSPLorg/meshtastic/core/service/MeshNotificationManagerImpl;->access$getServiceNotificationSnapshots$p(Lorg/meshtastic/core/service/MeshNotificationManagerImpl;)Lkotlinx/coroutines/flow/MutableSharedFlow; HSPLorg/meshtastic/core/service/MeshNotificationManagerImpl;->access$renderAndPostServiceNotification(Lorg/meshtastic/core/service/MeshNotificationManagerImpl;Lorg/meshtastic/core/service/MeshNotificationManagerImpl$ServiceNotificationSnapshot;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; HSPLorg/meshtastic/core/service/MeshNotificationManagerImpl;->access$renderServiceNotification(Lorg/meshtastic/core/service/MeshNotificationManagerImpl;Lorg/meshtastic/core/service/MeshNotificationManagerImpl$ServiceNotificationSnapshot;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; @@ -44049,6 +44770,8 @@ HSPLorg/meshtastic/core/service/MeshNotificationManagerImpl$$ExternalSyntheticLa Lorg/meshtastic/core/service/MeshNotificationManagerImpl$$ExternalSyntheticLambda1; HSPLorg/meshtastic/core/service/MeshNotificationManagerImpl$$ExternalSyntheticLambda1;->(Lorg/meshtastic/core/service/MeshNotificationManagerImpl;)V HSPLorg/meshtastic/core/service/MeshNotificationManagerImpl$$ExternalSyntheticLambda1;->invoke()Ljava/lang/Object; +Lorg/meshtastic/core/service/MeshNotificationManagerImpl$$ExternalSyntheticTypeSwitchClass2; +HSPLorg/meshtastic/core/service/MeshNotificationManagerImpl$$ExternalSyntheticTypeSwitchClass2;->switchDispatch$1(Ljava/lang/Object;I)I Lorg/meshtastic/core/service/MeshNotificationManagerImpl$1; HSPLorg/meshtastic/core/service/MeshNotificationManagerImpl$1;->(Lorg/meshtastic/core/service/MeshNotificationManagerImpl;Lkotlin/coroutines/Continuation;)V HSPLorg/meshtastic/core/service/MeshNotificationManagerImpl$1;->create(Ljava/lang/Object;Lkotlin/coroutines/Continuation;)Lkotlin/coroutines/Continuation; @@ -44057,6 +44780,14 @@ Lorg/meshtastic/core/service/MeshNotificationManagerImpl$1$1; HSPLorg/meshtastic/core/service/MeshNotificationManagerImpl$1$1;->(Ljava/lang/Object;)V HSPLorg/meshtastic/core/service/MeshNotificationManagerImpl$1$1;->invoke(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object; HSPLorg/meshtastic/core/service/MeshNotificationManagerImpl$1$1;->invoke(Lorg/meshtastic/core/service/MeshNotificationManagerImpl$ServiceNotificationSnapshot;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; +Lorg/meshtastic/core/service/MeshNotificationManagerImpl$2; +HSPLorg/meshtastic/core/service/MeshNotificationManagerImpl$2;->(Lorg/meshtastic/core/service/MeshNotificationManagerImpl;Lkotlin/coroutines/Continuation;)V +HSPLorg/meshtastic/core/service/MeshNotificationManagerImpl$2;->create(Ljava/lang/Object;Lkotlin/coroutines/Continuation;)Lkotlin/coroutines/Continuation; +HSPLorg/meshtastic/core/service/MeshNotificationManagerImpl$2;->invokeSuspend(Ljava/lang/Object;)Ljava/lang/Object; +Lorg/meshtastic/core/service/MeshNotificationManagerImpl$2$1; +HSPLorg/meshtastic/core/service/MeshNotificationManagerImpl$2$1;->(Lorg/meshtastic/core/service/MeshNotificationManagerImpl;)V +HSPLorg/meshtastic/core/service/MeshNotificationManagerImpl$2$1;->emit(Ljava/lang/Object;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; +HSPLorg/meshtastic/core/service/MeshNotificationManagerImpl$2$1;->emit(Ljava/util/Map;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; Lorg/meshtastic/core/service/MeshNotificationManagerImpl$Companion; HSPLorg/meshtastic/core/service/MeshNotificationManagerImpl$Companion;->()V HSPLorg/meshtastic/core/service/MeshNotificationManagerImpl$Companion;->(Lkotlin/jvm/internal/DefaultConstructorMarker;)V @@ -44103,7 +44834,8 @@ HSPLorg/meshtastic/core/service/MeshNotificationManagerImpl$RenderedServiceNotif HSPLorg/meshtastic/core/service/MeshNotificationManagerImpl$RenderedServiceNotification;->getMessage()Ljava/lang/String; HSPLorg/meshtastic/core/service/MeshNotificationManagerImpl$RenderedServiceNotification;->getNotification()Landroid/app/Notification; Lorg/meshtastic/core/service/MeshNotificationManagerImpl$ServiceNotificationSnapshot; -HSPLorg/meshtastic/core/service/MeshNotificationManagerImpl$ServiceNotificationSnapshot;->(Lorg/meshtastic/core/model/ConnectionState;Lorg/meshtastic/proto/LocalStats;Lorg/meshtastic/proto/DeviceMetrics;Ljava/lang/String;J)V +HSPLorg/meshtastic/core/service/MeshNotificationManagerImpl$ServiceNotificationSnapshot;->(Lorg/meshtastic/core/model/ConnectionState;Lorg/meshtastic/proto/LocalStats;Lorg/meshtastic/proto/DeviceMetrics;Ljava/lang/String;JLjava/util/Set;)V +HSPLorg/meshtastic/core/service/MeshNotificationManagerImpl$ServiceNotificationSnapshot;->getActiveOperations()Ljava/util/Set; HSPLorg/meshtastic/core/service/MeshNotificationManagerImpl$ServiceNotificationSnapshot;->getDeviceMetrics()Lorg/meshtastic/proto/DeviceMetrics; HSPLorg/meshtastic/core/service/MeshNotificationManagerImpl$ServiceNotificationSnapshot;->getLocalStats()Lorg/meshtastic/proto/LocalStats; HSPLorg/meshtastic/core/service/MeshNotificationManagerImpl$ServiceNotificationSnapshot;->getNextUpdateAt()J @@ -44115,31 +44847,50 @@ HSPLorg/meshtastic/core/service/MeshNotificationManagerImpl$renderAndPostService Lorg/meshtastic/core/service/MeshNotificationManagerImpl$renderServiceNotification$1; HSPLorg/meshtastic/core/service/MeshNotificationManagerImpl$renderServiceNotification$1;->(Lorg/meshtastic/core/service/MeshNotificationManagerImpl;Lkotlin/coroutines/Continuation;)V HSPLorg/meshtastic/core/service/MeshNotificationManagerImpl$renderServiceNotification$1;->invokeSuspend(Ljava/lang/Object;)Ljava/lang/Object; +Lorg/meshtastic/core/service/MeshNotificationManagerImplKt; +HSPLorg/meshtastic/core/service/MeshNotificationManagerImplKt;->notificationTitleResource(Ljava/util/Set;)Lorg/jetbrains/compose/resources/StringResource; Lorg/meshtastic/core/service/MeshService; HSPLorg/meshtastic/core/service/MeshService;->()V HSPLorg/meshtastic/core/service/MeshService;->()V +HSPLorg/meshtastic/core/service/MeshService;->access$getOperationWakeLock(Lorg/meshtastic/core/service/MeshService;)Lorg/meshtastic/core/service/OperationWakeLock; HSPLorg/meshtastic/core/service/MeshService;->access$getRadioInterfaceService(Lorg/meshtastic/core/service/MeshService;)Lorg/meshtastic/core/repository/RadioInterfaceService; +HSPLorg/meshtastic/core/service/MeshService;->access$getRadioOperationLock(Lorg/meshtastic/core/service/MeshService;)Lorg/meshtastic/core/common/state/RadioOperationLock; PLorg/meshtastic/core/service/MeshService;->access$stopServiceCleanly(Lorg/meshtastic/core/service/MeshService;)V HSPLorg/meshtastic/core/service/MeshService;->getAndroidNotifications()Lorg/meshtastic/core/service/MeshNotificationManagerImpl; HSPLorg/meshtastic/core/service/MeshService;->getConnectionManager()Lorg/meshtastic/core/repository/MeshConnectionManager; HSPLorg/meshtastic/core/service/MeshService;->getConversationShortcutPublisher()Lorg/meshtastic/core/service/ConversationShortcutPublisher; HSPLorg/meshtastic/core/service/MeshService;->getNotifications()Lorg/meshtastic/core/repository/MeshNotificationManager; +HSPLorg/meshtastic/core/service/MeshService;->getOperationWakeLock()Lorg/meshtastic/core/service/OperationWakeLock; HSPLorg/meshtastic/core/service/MeshService;->getOrchestrator()Lorg/meshtastic/core/service/MeshServiceOrchestrator; HSPLorg/meshtastic/core/service/MeshService;->getRadioInterfaceService()Lorg/meshtastic/core/repository/RadioInterfaceService; +HSPLorg/meshtastic/core/service/MeshService;->getRadioOperationLock()Lorg/meshtastic/core/common/state/RadioOperationLock; HSPLorg/meshtastic/core/service/MeshService;->onCreate()V PLorg/meshtastic/core/service/MeshService;->onDestroy()V HSPLorg/meshtastic/core/service/MeshService;->onStartCommand(Landroid/content/Intent;II)I +HSPLorg/meshtastic/core/service/MeshService;->operationWakeLock_delegate$lambda$0(Lorg/meshtastic/core/service/MeshService;)Lorg/meshtastic/core/service/OperationWakeLock; PLorg/meshtastic/core/service/MeshService;->releaseWakeLock()V HSPLorg/meshtastic/core/service/MeshService;->scheduleDeviceAddressResolution()V PLorg/meshtastic/core/service/MeshService;->stopServiceCleanly()V +Lorg/meshtastic/core/service/MeshService$$ExternalSyntheticLambda0; +HSPLorg/meshtastic/core/service/MeshService$$ExternalSyntheticLambda0;->(Lorg/meshtastic/core/service/MeshService;)V +HSPLorg/meshtastic/core/service/MeshService$$ExternalSyntheticLambda0;->invoke()Ljava/lang/Object; Lorg/meshtastic/core/service/MeshService$Companion; HSPLorg/meshtastic/core/service/MeshService$Companion;->()V HSPLorg/meshtastic/core/service/MeshService$Companion;->(Lkotlin/jvm/internal/DefaultConstructorMarker;)V HSPLorg/meshtastic/core/service/MeshService$Companion;->createIntent(Landroid/content/Context;)Landroid/content/Intent; +Lorg/meshtastic/core/service/MeshService$onCreate$3; +HSPLorg/meshtastic/core/service/MeshService$onCreate$3;->(Lorg/meshtastic/core/service/MeshService;Lkotlin/coroutines/Continuation;)V +HSPLorg/meshtastic/core/service/MeshService$onCreate$3;->create(Ljava/lang/Object;Lkotlin/coroutines/Continuation;)Lkotlin/coroutines/Continuation; +HSPLorg/meshtastic/core/service/MeshService$onCreate$3;->invokeSuspend(Ljava/lang/Object;)Ljava/lang/Object; +Lorg/meshtastic/core/service/MeshService$onCreate$3$1; +HSPLorg/meshtastic/core/service/MeshService$onCreate$3$1;->(Lorg/meshtastic/core/service/MeshService;)V +HSPLorg/meshtastic/core/service/MeshService$onCreate$3$1;->emit(Ljava/lang/Object;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; +HSPLorg/meshtastic/core/service/MeshService$onCreate$3$1;->emit(Ljava/util/Map;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; Lorg/meshtastic/core/service/MeshService$scheduleDeviceAddressResolution$1; HSPLorg/meshtastic/core/service/MeshService$scheduleDeviceAddressResolution$1;->(Lorg/meshtastic/core/service/MeshService;Lkotlin/coroutines/Continuation;)V HSPLorg/meshtastic/core/service/MeshService$scheduleDeviceAddressResolution$1;->create(Ljava/lang/Object;Lkotlin/coroutines/Continuation;)Lkotlin/coroutines/Continuation; HSPLorg/meshtastic/core/service/MeshService$scheduleDeviceAddressResolution$1;->invokeSuspend(Ljava/lang/Object;)Ljava/lang/Object; +PLorg/meshtastic/core/service/MeshService$scheduleDeviceAddressResolution$1$WhenMappings;->()V Lorg/meshtastic/core/service/MeshService$scheduleDeviceAddressResolution$1$resolved$1; HSPLorg/meshtastic/core/service/MeshService$scheduleDeviceAddressResolution$1$resolved$1;->(Lorg/meshtastic/core/service/MeshService;Lkotlin/coroutines/Continuation;)V HSPLorg/meshtastic/core/service/MeshService$scheduleDeviceAddressResolution$1$resolved$1;->create(Ljava/lang/Object;Lkotlin/coroutines/Continuation;)Lkotlin/coroutines/Continuation; @@ -44166,6 +44917,9 @@ HSPLorg/meshtastic/core/service/MeshService$special$$inlined$inject$default$4;-> Lorg/meshtastic/core/service/MeshService$special$$inlined$inject$default$5; HSPLorg/meshtastic/core/service/MeshService$special$$inlined$inject$default$5;->(Landroid/content/ComponentCallbacks;Lorg/koin/core/qualifier/Qualifier;Lkotlin/jvm/functions/Function0;)V HSPLorg/meshtastic/core/service/MeshService$special$$inlined$inject$default$5;->invoke()Ljava/lang/Object; +Lorg/meshtastic/core/service/MeshService$special$$inlined$inject$default$6; +HSPLorg/meshtastic/core/service/MeshService$special$$inlined$inject$default$6;->(Landroid/content/ComponentCallbacks;Lorg/koin/core/qualifier/Qualifier;Lkotlin/jvm/functions/Function0;)V +HSPLorg/meshtastic/core/service/MeshService$special$$inlined$inject$default$6;->invoke()Ljava/lang/Object; Lorg/meshtastic/core/service/MeshServiceOrchestrator; HSPLorg/meshtastic/core/service/MeshServiceOrchestrator;->(Lorg/meshtastic/core/repository/RadioInterfaceService;Lorg/meshtastic/core/repository/ServiceStateWriter;Lorg/meshtastic/core/repository/NodeManager;Lorg/meshtastic/core/repository/MeshMessageProcessor;Lorg/meshtastic/core/repository/MeshNotificationManager;Lorg/meshtastic/core/takserver/TAKServerManager;Lorg/meshtastic/core/takserver/TAKMeshIntegration;Lorg/meshtastic/core/repository/TakPrefs;Lorg/meshtastic/core/common/database/DatabaseManager;Lorg/meshtastic/core/repository/MeshConnectionManager;Lorg/meshtastic/core/di/CoroutineDispatchers;Lorg/meshtastic/core/service/LocalNetworkAccess;)V HSPLorg/meshtastic/core/service/MeshServiceOrchestrator;->access$coldStartConnect(Lorg/meshtastic/core/service/MeshServiceOrchestrator;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; @@ -44222,6 +44976,14 @@ Lorg/meshtastic/core/service/NotificationChannels; HSPLorg/meshtastic/core/service/NotificationChannels;->()V HSPLorg/meshtastic/core/service/NotificationChannels;->()V HSPLorg/meshtastic/core/service/NotificationChannels;->getLEGACY_CATEGORY_IDS()Ljava/util/List; +Lorg/meshtastic/core/service/OperationWakeLock; +HSPLorg/meshtastic/core/service/OperationWakeLock;->()V +HSPLorg/meshtastic/core/service/OperationWakeLock;->(Landroid/content/Context;Lkotlinx/coroutines/CoroutineScope;)V +HSPLorg/meshtastic/core/service/OperationWakeLock;->release()V +HSPLorg/meshtastic/core/service/OperationWakeLock;->stop()V +Lorg/meshtastic/core/service/OperationWakeLock$Companion; +HSPLorg/meshtastic/core/service/OperationWakeLock$Companion;->()V +HSPLorg/meshtastic/core/service/OperationWakeLock$Companion;->(Lkotlin/jvm/internal/DefaultConstructorMarker;)V Lorg/meshtastic/core/service/QueryControllerImpl; HSPLorg/meshtastic/core/service/QueryControllerImpl;->(Lorg/meshtastic/core/repository/CommandSender;Lorg/meshtastic/core/repository/NodeManager;Lorg/meshtastic/core/repository/UiPrefs;)V Lorg/meshtastic/core/service/RadioControllerImpl; @@ -44266,9 +45028,14 @@ Lorg/meshtastic/core/service/ServiceStartTrigger; HSPLorg/meshtastic/core/service/ServiceStartTrigger;->$values()[Lorg/meshtastic/core/service/ServiceStartTrigger; HSPLorg/meshtastic/core/service/ServiceStartTrigger;->()V HSPLorg/meshtastic/core/service/ServiceStartTrigger;->(Ljava/lang/String;I)V +PLorg/meshtastic/core/service/ServiceStayAliveDecision;->$values()[Lorg/meshtastic/core/service/ServiceStayAliveDecision; +PLorg/meshtastic/core/service/ServiceStayAliveDecision;->()V +PLorg/meshtastic/core/service/ServiceStayAliveDecision;->(Ljava/lang/String;I)V +PLorg/meshtastic/core/service/ServiceStayAliveDecision;->values()[Lorg/meshtastic/core/service/ServiceStayAliveDecision; +PLorg/meshtastic/core/service/ServiceStayAliveDecisionKt;->serviceStayAliveDecision(Ljava/lang/String;Z)Lorg/meshtastic/core/service/ServiceStayAliveDecision; Lorg/meshtastic/core/service/SharedRadioInterfaceService; HSPLorg/meshtastic/core/service/SharedRadioInterfaceService;->()V -HSPLorg/meshtastic/core/service/SharedRadioInterfaceService;->(Lorg/meshtastic/core/di/CoroutineDispatchers;Lorg/meshtastic/core/ble/BluetoothRepository;Lorg/meshtastic/core/network/repository/NetworkRepository;Lorg/meshtastic/core/network/repository/SerialDevicePresence;Landroidx/lifecycle/Lifecycle;Lorg/meshtastic/core/repository/RadioPrefs;Lorg/meshtastic/core/repository/RadioTransportFactory;Lorg/meshtastic/core/repository/PlatformAnalytics;Lorg/meshtastic/core/common/state/FirmwareMaintenanceLock;)V +HSPLorg/meshtastic/core/service/SharedRadioInterfaceService;->(Lorg/meshtastic/core/di/CoroutineDispatchers;Lorg/meshtastic/core/ble/BluetoothRepository;Lorg/meshtastic/core/network/repository/NetworkRepository;Lorg/meshtastic/core/network/repository/SerialDevicePresence;Landroidx/lifecycle/Lifecycle;Lorg/meshtastic/core/repository/RadioPrefs;Lorg/meshtastic/core/repository/RadioTransportFactory;Lorg/meshtastic/core/repository/PlatformAnalytics;Lorg/meshtastic/core/common/state/RadioOperationLock;)V PLorg/meshtastic/core/service/SharedRadioInterfaceService;->access$finishTransportTeardown(Lorg/meshtastic/core/service/SharedRadioInterfaceService;ZZLkotlin/coroutines/Continuation;)Ljava/lang/Object; HSPLorg/meshtastic/core/service/SharedRadioInterfaceService;->access$get_currentDeviceAddressFlow$p(Lorg/meshtastic/core/service/SharedRadioInterfaceService;)Lkotlinx/coroutines/flow/MutableStateFlow; PLorg/meshtastic/core/service/SharedRadioInterfaceService;->disconnect(Lkotlin/coroutines/Continuation;)Ljava/lang/Object; @@ -44452,7 +45219,7 @@ HSPLorg/meshtastic/core/service/di/OrgMeshtasticCoreServiceDiCoreServiceModuleMo Lorg/meshtastic/core/service/worker/MeshLogCleanupWorker; HSPLorg/meshtastic/core/service/worker/MeshLogCleanupWorker;->()V HSPLorg/meshtastic/core/service/worker/MeshLogCleanupWorker;->(Landroid/content/Context;Landroidx/work/WorkerParameters;Lorg/meshtastic/core/repository/MeshLogRepository;Lorg/meshtastic/core/repository/MeshLogPrefs;)V -HSPLorg/meshtastic/core/service/worker/MeshLogCleanupWorker;->doWork(Lkotlin/coroutines/Continuation;)Ljava/lang/Object; +PLorg/meshtastic/core/service/worker/MeshLogCleanupWorker;->doWork(Lkotlin/coroutines/Continuation;)Ljava/lang/Object; Lorg/meshtastic/core/service/worker/MeshLogCleanupWorker$Companion; HSPLorg/meshtastic/core/service/worker/MeshLogCleanupWorker$Companion;->()V HSPLorg/meshtastic/core/service/worker/MeshLogCleanupWorker$Companion;->(Lkotlin/jvm/internal/DefaultConstructorMarker;)V @@ -44576,23 +45343,23 @@ HSPLorg/meshtastic/core/ui/component/MentionSubstitution;->(Ljava/lang/Str HSPLorg/meshtastic/core/ui/component/MentionSubstitution;->getDisplay()Ljava/lang/String; HSPLorg/meshtastic/core/ui/component/MentionSubstitution;->getMentions()Ljava/util/List; Lorg/meshtastic/core/ui/component/MeshtasticNavDisplayKt; -HSPLorg/meshtastic/core/ui/component/MeshtasticNavDisplayKt;->MeshtasticNavDisplay$lambda$8(Lkotlin/jvm/functions/Function0;Landroidx/navigation3/runtime/NavBackStack;Landroidx/compose/material3/adaptive/navigation3/ListDetailSceneStrategy;Landroidx/compose/material3/adaptive/navigation3/SupportingPaneSceneStrategy;Landroidx/compose/ui/Modifier;Ljava/util/List;Lkotlin/jvm/functions/Function1;Landroidx/compose/animation/SharedTransitionScope;Landroidx/compose/runtime/Composer;I)Lkotlin/Unit; +HSPLorg/meshtastic/core/ui/component/MeshtasticNavDisplayKt;->MeshtasticNavDisplay$lambda$8(Lkotlin/jvm/functions/Function0;Landroidx/navigation3/runtime/NavBackStack;Landroidx/compose/material3/adaptive/navigation3/ListDetailSceneStrategy;Landroidx/compose/material3/adaptive/navigation3/SupportingPaneSceneStrategy;Landroidx/compose/animation/core/FiniteAnimationSpec;Landroidx/compose/ui/Modifier;Ljava/util/List;Lkotlin/jvm/functions/Function1;Landroidx/compose/animation/SharedTransitionScope;Landroidx/compose/runtime/Composer;I)Lkotlin/Unit; HSPLorg/meshtastic/core/ui/component/MeshtasticNavDisplayKt;->MeshtasticNavDisplay(Landroidx/navigation3/runtime/NavBackStack;Lkotlin/jvm/functions/Function1;Landroidx/compose/ui/Modifier;Lkotlin/jvm/functions/Function0;Lorg/meshtastic/core/repository/PlatformAnalytics;Landroidx/compose/runtime/Composer;II)V -HSPLorg/meshtastic/core/ui/component/MeshtasticNavDisplayKt;->meshtasticPredictivePopTransitionSpec()Lkotlin/jvm/functions/Function2; -HSPLorg/meshtastic/core/ui/component/MeshtasticNavDisplayKt;->meshtasticTransitionSpec$lambda$0(Landroidx/compose/animation/AnimatedContentTransitionScope;)Landroidx/compose/animation/ContentTransform; -HSPLorg/meshtastic/core/ui/component/MeshtasticNavDisplayKt;->meshtasticTransitionSpec()Lkotlin/jvm/functions/Function1; -Lorg/meshtastic/core/ui/component/MeshtasticNavDisplayKt$$ExternalSyntheticLambda0; -HSPLorg/meshtastic/core/ui/component/MeshtasticNavDisplayKt$$ExternalSyntheticLambda0;->()V +HSPLorg/meshtastic/core/ui/component/MeshtasticNavDisplayKt;->meshtasticPredictivePopTransitionSpec(Landroidx/compose/animation/core/FiniteAnimationSpec;)Lkotlin/jvm/functions/Function2; +HSPLorg/meshtastic/core/ui/component/MeshtasticNavDisplayKt;->meshtasticTransitionSpec$lambda$0(Landroidx/compose/animation/core/FiniteAnimationSpec;Landroidx/compose/animation/AnimatedContentTransitionScope;)Landroidx/compose/animation/ContentTransform; +HSPLorg/meshtastic/core/ui/component/MeshtasticNavDisplayKt;->meshtasticTransitionSpec(Landroidx/compose/animation/core/FiniteAnimationSpec;)Lkotlin/jvm/functions/Function1; +Lorg/meshtastic/core/ui/component/MeshtasticNavDisplayKt$$ExternalSyntheticLambda1; +HSPLorg/meshtastic/core/ui/component/MeshtasticNavDisplayKt$$ExternalSyntheticLambda1;->(Lkotlin/jvm/functions/Function0;Landroidx/navigation3/runtime/NavBackStack;Landroidx/compose/material3/adaptive/navigation3/ListDetailSceneStrategy;Landroidx/compose/material3/adaptive/navigation3/SupportingPaneSceneStrategy;Landroidx/compose/animation/core/FiniteAnimationSpec;Landroidx/compose/ui/Modifier;Ljava/util/List;Lkotlin/jvm/functions/Function1;)V +HSPLorg/meshtastic/core/ui/component/MeshtasticNavDisplayKt$$ExternalSyntheticLambda1;->invoke(Ljava/lang/Object;Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object; Lorg/meshtastic/core/ui/component/MeshtasticNavDisplayKt$$ExternalSyntheticLambda2; -HSPLorg/meshtastic/core/ui/component/MeshtasticNavDisplayKt$$ExternalSyntheticLambda2;->(Lkotlin/jvm/functions/Function0;Landroidx/navigation3/runtime/NavBackStack;Landroidx/compose/material3/adaptive/navigation3/ListDetailSceneStrategy;Landroidx/compose/material3/adaptive/navigation3/SupportingPaneSceneStrategy;Landroidx/compose/ui/Modifier;Ljava/util/List;Lkotlin/jvm/functions/Function1;)V -HSPLorg/meshtastic/core/ui/component/MeshtasticNavDisplayKt$$ExternalSyntheticLambda2;->invoke(Ljava/lang/Object;Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object; -Lorg/meshtastic/core/ui/component/MeshtasticNavDisplayKt$$ExternalSyntheticLambda3; -HSPLorg/meshtastic/core/ui/component/MeshtasticNavDisplayKt$$ExternalSyntheticLambda3;->(Landroidx/navigation3/runtime/NavBackStack;Lkotlin/jvm/functions/Function1;Landroidx/compose/ui/Modifier;Lkotlin/jvm/functions/Function0;Lorg/meshtastic/core/repository/PlatformAnalytics;II)V +HSPLorg/meshtastic/core/ui/component/MeshtasticNavDisplayKt$$ExternalSyntheticLambda2;->(Landroidx/navigation3/runtime/NavBackStack;Lkotlin/jvm/functions/Function1;Landroidx/compose/ui/Modifier;Lkotlin/jvm/functions/Function0;Lorg/meshtastic/core/repository/PlatformAnalytics;II)V Lorg/meshtastic/core/ui/component/MeshtasticNavDisplayKt$$ExternalSyntheticLambda4; -HSPLorg/meshtastic/core/ui/component/MeshtasticNavDisplayKt$$ExternalSyntheticLambda4;->()V +HSPLorg/meshtastic/core/ui/component/MeshtasticNavDisplayKt$$ExternalSyntheticLambda4;->(Landroidx/compose/animation/core/FiniteAnimationSpec;)V HSPLorg/meshtastic/core/ui/component/MeshtasticNavDisplayKt$$ExternalSyntheticLambda4;->invoke(Ljava/lang/Object;)Ljava/lang/Object; -Lorg/meshtastic/core/ui/component/MeshtasticNavDisplayKt$$ExternalSyntheticLambda7; -HSPLorg/meshtastic/core/ui/component/MeshtasticNavDisplayKt$$ExternalSyntheticLambda7;->(Landroidx/navigation3/runtime/NavBackStack;)V +Lorg/meshtastic/core/ui/component/MeshtasticNavDisplayKt$$ExternalSyntheticLambda5; +HSPLorg/meshtastic/core/ui/component/MeshtasticNavDisplayKt$$ExternalSyntheticLambda5;->(Landroidx/compose/animation/core/FiniteAnimationSpec;)V +Lorg/meshtastic/core/ui/component/MeshtasticNavDisplayKt$$ExternalSyntheticLambda8; +HSPLorg/meshtastic/core/ui/component/MeshtasticNavDisplayKt$$ExternalSyntheticLambda8;->(Landroidx/navigation3/runtime/NavBackStack;)V Lorg/meshtastic/core/ui/di/CoreUiModule; HSPLorg/meshtastic/core/ui/di/CoreUiModule;->()V HSPLorg/meshtastic/core/ui/di/CoreUiModule;->()V @@ -44787,6 +45554,8 @@ Lorg/meshtastic/core/ui/util/LocalMapMainScreenProviderKt$$ExternalSyntheticLamb HSPLorg/meshtastic/core/ui/util/LocalMapMainScreenProviderKt$$ExternalSyntheticLambda0;->()V Lorg/meshtastic/core/ui/util/LocalNfcScannerProviderKt; HSPLorg/meshtastic/core/ui/util/LocalNfcScannerProviderKt;->()V +HSPLorg/meshtastic/core/ui/util/LocalNfcScannerProviderKt;->getLocalNfcEmulationSupported()Landroidx/compose/runtime/ProvidableCompositionLocal; +HSPLorg/meshtastic/core/ui/util/LocalNfcScannerProviderKt;->getLocalNfcEmulatorProvider()Landroidx/compose/runtime/ProvidableCompositionLocal; HSPLorg/meshtastic/core/ui/util/LocalNfcScannerProviderKt;->getLocalNfcScannerProvider()Landroidx/compose/runtime/ProvidableCompositionLocal; HSPLorg/meshtastic/core/ui/util/LocalNfcScannerProviderKt;->getLocalNfcScannerSupported()Landroidx/compose/runtime/ProvidableCompositionLocal; HSPLorg/meshtastic/core/ui/util/LocalNfcScannerProviderKt;->getLocalNfcWriterProvider()Landroidx/compose/runtime/ProvidableCompositionLocal; @@ -44796,6 +45565,10 @@ Lorg/meshtastic/core/ui/util/LocalNfcScannerProviderKt$$ExternalSyntheticLambda1 HSPLorg/meshtastic/core/ui/util/LocalNfcScannerProviderKt$$ExternalSyntheticLambda1;->()V Lorg/meshtastic/core/ui/util/LocalNfcScannerProviderKt$$ExternalSyntheticLambda2; HSPLorg/meshtastic/core/ui/util/LocalNfcScannerProviderKt$$ExternalSyntheticLambda2;->()V +Lorg/meshtastic/core/ui/util/LocalNfcScannerProviderKt$$ExternalSyntheticLambda3; +HSPLorg/meshtastic/core/ui/util/LocalNfcScannerProviderKt$$ExternalSyntheticLambda3;->()V +Lorg/meshtastic/core/ui/util/LocalNfcScannerProviderKt$$ExternalSyntheticLambda4; +HSPLorg/meshtastic/core/ui/util/LocalNfcScannerProviderKt$$ExternalSyntheticLambda4;->()V Lorg/meshtastic/core/ui/util/LocalNodeMapScreenProviderKt; HSPLorg/meshtastic/core/ui/util/LocalNodeMapScreenProviderKt;->()V HSPLorg/meshtastic/core/ui/util/LocalNodeMapScreenProviderKt;->getLocalNodeMapScreenProvider()Landroidx/compose/runtime/ProvidableCompositionLocal; @@ -44865,10 +45638,10 @@ HSPLorg/meshtastic/core/ui/util/PlatformUtilsKt$$ExternalSyntheticLambda0;->(Landroidx/compose/runtime/MutableState;[Ljava/lang/String;ZLandroid/app/Activity;Ljava/lang/String;Lorg/meshtastic/core/ui/util/PermissionRequestTracker;Landroid/content/Context;)V HSPLorg/meshtastic/core/ui/util/PlatformUtilsKt$$ExternalSyntheticLambda11;->invoke()Ljava/lang/Object; -Lorg/meshtastic/core/ui/util/PlatformUtilsKt$$ExternalSyntheticLambda21; -HSPLorg/meshtastic/core/ui/util/PlatformUtilsKt$$ExternalSyntheticLambda21;->(Landroidx/activity/compose/ManagedActivityResultLauncher;[Ljava/lang/String;)V -Lorg/meshtastic/core/ui/util/PlatformUtilsKt$$ExternalSyntheticLambda23; -HSPLorg/meshtastic/core/ui/util/PlatformUtilsKt$$ExternalSyntheticLambda23;->(Landroid/content/Context;)V +Lorg/meshtastic/core/ui/util/PlatformUtilsKt$$ExternalSyntheticLambda22; +HSPLorg/meshtastic/core/ui/util/PlatformUtilsKt$$ExternalSyntheticLambda22;->(Landroidx/activity/compose/ManagedActivityResultLauncher;[Ljava/lang/String;)V +Lorg/meshtastic/core/ui/util/PlatformUtilsKt$$ExternalSyntheticLambda24; +HSPLorg/meshtastic/core/ui/util/PlatformUtilsKt$$ExternalSyntheticLambda24;->(Landroid/content/Context;)V Lorg/meshtastic/core/ui/util/SnackbarManager; HSPLorg/meshtastic/core/ui/util/SnackbarManager;->()V HSPLorg/meshtastic/core/ui/util/SnackbarManager;->()V @@ -44893,14 +45666,14 @@ HSPLorg/meshtastic/core/ui/viewmodel/UIViewModel$Companion;->()V HSPLorg/meshtastic/core/ui/viewmodel/UIViewModel$Companion;->(Lkotlin/jvm/internal/DefaultConstructorMarker;)V Lorg/meshtastic/core/ui/viewmodel/UIViewModel$eventEdition$1; HSPLorg/meshtastic/core/ui/viewmodel/UIViewModel$eventEdition$1;->(Lkotlin/coroutines/Continuation;)V -PLorg/meshtastic/core/ui/viewmodel/UIViewModel$eventEdition$1;->invoke(Ljava/lang/Object;Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object; -PLorg/meshtastic/core/ui/viewmodel/UIViewModel$eventEdition$1;->invoke(Lorg/meshtastic/proto/FirmwareEdition;Lorg/meshtastic/core/model/ConnectionState;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; -PLorg/meshtastic/core/ui/viewmodel/UIViewModel$eventEdition$1;->invokeSuspend(Ljava/lang/Object;)Ljava/lang/Object; +HSPLorg/meshtastic/core/ui/viewmodel/UIViewModel$eventEdition$1;->invoke(Ljava/lang/Object;Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object; +HSPLorg/meshtastic/core/ui/viewmodel/UIViewModel$eventEdition$1;->invoke(Lorg/meshtastic/proto/FirmwareEdition;Lorg/meshtastic/core/model/ConnectionState;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; +HSPLorg/meshtastic/core/ui/viewmodel/UIViewModel$eventEdition$1;->invokeSuspend(Ljava/lang/Object;)Ljava/lang/Object; Lorg/meshtastic/core/ui/viewmodel/UIViewModel$special$$inlined$flatMapLatest$1; HSPLorg/meshtastic/core/ui/viewmodel/UIViewModel$special$$inlined$flatMapLatest$1;->(Lkotlin/coroutines/Continuation;Lorg/meshtastic/core/ui/viewmodel/UIViewModel;)V -PLorg/meshtastic/core/ui/viewmodel/UIViewModel$special$$inlined$flatMapLatest$1;->invoke(Ljava/lang/Object;Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object; -PLorg/meshtastic/core/ui/viewmodel/UIViewModel$special$$inlined$flatMapLatest$1;->invoke(Lkotlinx/coroutines/flow/FlowCollector;Ljava/lang/Object;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; -PLorg/meshtastic/core/ui/viewmodel/UIViewModel$special$$inlined$flatMapLatest$1;->invokeSuspend(Ljava/lang/Object;)Ljava/lang/Object; +HSPLorg/meshtastic/core/ui/viewmodel/UIViewModel$special$$inlined$flatMapLatest$1;->invoke(Ljava/lang/Object;Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object; +HSPLorg/meshtastic/core/ui/viewmodel/UIViewModel$special$$inlined$flatMapLatest$1;->invoke(Lkotlinx/coroutines/flow/FlowCollector;Ljava/lang/Object;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; +HSPLorg/meshtastic/core/ui/viewmodel/UIViewModel$special$$inlined$flatMapLatest$1;->invokeSuspend(Ljava/lang/Object;)Ljava/lang/Object; Lorg/meshtastic/core/ui/viewmodel/UIViewModel$special$$inlined$map$1; HSPLorg/meshtastic/core/ui/viewmodel/UIViewModel$special$$inlined$map$1;->(Lkotlinx/coroutines/flow/Flow;)V HSPLorg/meshtastic/core/ui/viewmodel/UIViewModel$special$$inlined$map$1;->collect(Lkotlinx/coroutines/flow/FlowCollector;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; @@ -44908,7 +45681,7 @@ Lorg/meshtastic/core/ui/viewmodel/UIViewModel$special$$inlined$map$1$1; HSPLorg/meshtastic/core/ui/viewmodel/UIViewModel$special$$inlined$map$1$1;->(Lorg/meshtastic/core/ui/viewmodel/UIViewModel$special$$inlined$map$1;Lkotlin/coroutines/Continuation;)V Lorg/meshtastic/core/ui/viewmodel/UIViewModel$special$$inlined$map$1$2; HSPLorg/meshtastic/core/ui/viewmodel/UIViewModel$special$$inlined$map$1$2;->(Lkotlinx/coroutines/flow/FlowCollector;)V -PLorg/meshtastic/core/ui/viewmodel/UIViewModel$special$$inlined$map$1$2;->emit(Ljava/lang/Object;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; +HSPLorg/meshtastic/core/ui/viewmodel/UIViewModel$special$$inlined$map$1$2;->emit(Ljava/lang/Object;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; PLorg/meshtastic/core/ui/viewmodel/UIViewModel$special$$inlined$map$1$2$1;->(Lorg/meshtastic/core/ui/viewmodel/UIViewModel$special$$inlined$map$1$2;Lkotlin/coroutines/Continuation;)V PLorg/meshtastic/core/ui/viewmodel/UIViewModel$special$$inlined$map$1$2$1;->invokeSuspend(Ljava/lang/Object;)Ljava/lang/Object; Lorg/meshtastic/core/ui/viewmodel/UIViewModel$special$$inlined$map$2; @@ -44969,7 +45742,7 @@ HSPLorg/meshtastic/feature/discovery/DiscoveryInterruptedSessionRecovery$watch$3 Lorg/meshtastic/feature/discovery/DiscoveryMapViewModel; Lorg/meshtastic/feature/discovery/DiscoveryScanEngine; HSPLorg/meshtastic/feature/discovery/DiscoveryScanEngine;->()V -HSPLorg/meshtastic/feature/discovery/DiscoveryScanEngine;->(Lorg/meshtastic/core/repository/RadioController;Lorg/meshtastic/core/repository/ServiceRepository;Lorg/meshtastic/core/repository/NodeRepository;Lorg/meshtastic/core/repository/RadioConfigRepository;Lorg/meshtastic/core/repository/DiscoveryPacketCollectorRegistry;Lorg/meshtastic/core/database/dao/DiscoveryDao;Lorg/meshtastic/feature/discovery/ai/DiscoverySummaryAiProvider;Lorg/meshtastic/core/common/di/ApplicationCoroutineScope;Lorg/meshtastic/core/di/CoroutineDispatchers;Lorg/meshtastic/core/repository/MeshPrefs;)V +HSPLorg/meshtastic/feature/discovery/DiscoveryScanEngine;->(Lorg/meshtastic/core/repository/RadioController;Lorg/meshtastic/core/repository/ServiceRepository;Lorg/meshtastic/core/repository/NodeRepository;Lorg/meshtastic/core/repository/RadioConfigRepository;Lorg/meshtastic/core/repository/DiscoveryPacketCollectorRegistry;Lorg/meshtastic/core/database/dao/DiscoveryDao;Lorg/meshtastic/feature/discovery/ai/DiscoverySummaryAiProvider;Lorg/meshtastic/core/common/di/ApplicationCoroutineScope;Lorg/meshtastic/core/di/CoroutineDispatchers;Lorg/meshtastic/core/repository/MeshPrefs;Lorg/meshtastic/core/common/state/RadioOperationLock;)V HSPLorg/meshtastic/feature/discovery/DiscoveryScanEngine;->restoreInterruptedSessionsOnReconnect(Lkotlin/jvm/functions/Function2;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; Lorg/meshtastic/feature/discovery/DiscoveryScanEngine$$ExternalSyntheticLambda1; HSPLorg/meshtastic/feature/discovery/DiscoveryScanEngine$$ExternalSyntheticLambda1;->(Lorg/meshtastic/feature/discovery/DiscoveryScanEngine;)V @@ -45516,11 +46289,12 @@ Lorg/meshtastic/mqtt/ConnectionState$Reconnecting; Lorg/meshtastic/mqtt/MqttException; Lorg/meshtastic/proto/ChannelSet; HSPLorg/meshtastic/proto/ChannelSet;->()V -HSPLorg/meshtastic/proto/ChannelSet;->(Ljava/util/List;Lorg/meshtastic/proto/Config$LoRaConfig;Lokio/ByteString;)V -HSPLorg/meshtastic/proto/ChannelSet;->(Ljava/util/List;Lorg/meshtastic/proto/Config$LoRaConfig;Lokio/ByteString;ILkotlin/jvm/internal/DefaultConstructorMarker;)V +HSPLorg/meshtastic/proto/ChannelSet;->(Lorg/meshtastic/proto/ChannelSet$Builder;Lokio/ByteString;)V +HSPLorg/meshtastic/proto/ChannelSet;->(Lorg/meshtastic/proto/ChannelSet$Builder;Lokio/ByteString;Lkotlin/jvm/internal/DefaultConstructorMarker;)V HSPLorg/meshtastic/proto/ChannelSet;->equals(Ljava/lang/Object;)Z -HSPLorg/meshtastic/proto/ChannelSet;->getLora_config()Lorg/meshtastic/proto/Config$LoRaConfig; -HSPLorg/meshtastic/proto/ChannelSet;->getSettings()Ljava/util/List; +Lorg/meshtastic/proto/ChannelSet$Builder; +HSPLorg/meshtastic/proto/ChannelSet$Builder;->()V +HSPLorg/meshtastic/proto/ChannelSet$Builder;->build()Lorg/meshtastic/proto/ChannelSet; Lorg/meshtastic/proto/ChannelSet$Companion; HSPLorg/meshtastic/proto/ChannelSet$Companion;->()V HSPLorg/meshtastic/proto/ChannelSet$Companion;->(Lkotlin/jvm/internal/DefaultConstructorMarker;)V @@ -45528,8 +46302,11 @@ Lorg/meshtastic/proto/ChannelSet$Companion$ADAPTER$1; HSPLorg/meshtastic/proto/ChannelSet$Companion$ADAPTER$1;->(Lcom/squareup/wire/FieldEncoding;Lkotlin/reflect/KClass;Lcom/squareup/wire/Syntax;)V Lorg/meshtastic/proto/ChannelSettings; HSPLorg/meshtastic/proto/ChannelSettings;->()V -HSPLorg/meshtastic/proto/ChannelSettings;->(ILokio/ByteString;Ljava/lang/String;IZZLorg/meshtastic/proto/ModuleSettings;Lokio/ByteString;)V -HSPLorg/meshtastic/proto/ChannelSettings;->(ILokio/ByteString;Ljava/lang/String;IZZLorg/meshtastic/proto/ModuleSettings;Lokio/ByteString;ILkotlin/jvm/internal/DefaultConstructorMarker;)V +HSPLorg/meshtastic/proto/ChannelSettings;->(Lorg/meshtastic/proto/ChannelSettings$Builder;Lokio/ByteString;)V +HSPLorg/meshtastic/proto/ChannelSettings;->(Lorg/meshtastic/proto/ChannelSettings$Builder;Lokio/ByteString;Lkotlin/jvm/internal/DefaultConstructorMarker;)V +Lorg/meshtastic/proto/ChannelSettings$Builder; +HSPLorg/meshtastic/proto/ChannelSettings$Builder;->()V +HSPLorg/meshtastic/proto/ChannelSettings$Builder;->build()Lorg/meshtastic/proto/ChannelSettings; Lorg/meshtastic/proto/ChannelSettings$Companion; HSPLorg/meshtastic/proto/ChannelSettings$Companion;->()V HSPLorg/meshtastic/proto/ChannelSettings$Companion;->(Lkotlin/jvm/internal/DefaultConstructorMarker;)V @@ -45549,8 +46326,11 @@ HSPLorg/meshtastic/proto/Config$DeviceConfig$Role$Companion$ADAPTER$1;->(L Lorg/meshtastic/proto/Config$DisplayConfig; Lorg/meshtastic/proto/Config$LoRaConfig; HSPLorg/meshtastic/proto/Config$LoRaConfig;->()V -HSPLorg/meshtastic/proto/Config$LoRaConfig;->(ZLorg/meshtastic/proto/Config$LoRaConfig$ModemPreset;IIIFLorg/meshtastic/proto/Config$LoRaConfig$RegionCode;IZIIZZFZLjava/util/List;ZZLorg/meshtastic/proto/Config$LoRaConfig$FEM_LNA_Mode;ZLokio/ByteString;)V -HSPLorg/meshtastic/proto/Config$LoRaConfig;->(ZLorg/meshtastic/proto/Config$LoRaConfig$ModemPreset;IIIFLorg/meshtastic/proto/Config$LoRaConfig$RegionCode;IZIIZZFZLjava/util/List;ZZLorg/meshtastic/proto/Config$LoRaConfig$FEM_LNA_Mode;ZLokio/ByteString;ILkotlin/jvm/internal/DefaultConstructorMarker;)V +HSPLorg/meshtastic/proto/Config$LoRaConfig;->(Lorg/meshtastic/proto/Config$LoRaConfig$Builder;Lokio/ByteString;)V +HSPLorg/meshtastic/proto/Config$LoRaConfig;->(Lorg/meshtastic/proto/Config$LoRaConfig$Builder;Lokio/ByteString;Lkotlin/jvm/internal/DefaultConstructorMarker;)V +Lorg/meshtastic/proto/Config$LoRaConfig$Builder; +HSPLorg/meshtastic/proto/Config$LoRaConfig$Builder;->()V +HSPLorg/meshtastic/proto/Config$LoRaConfig$Builder;->build()Lorg/meshtastic/proto/Config$LoRaConfig; Lorg/meshtastic/proto/Config$LoRaConfig$Companion; HSPLorg/meshtastic/proto/Config$LoRaConfig$Companion;->()V HSPLorg/meshtastic/proto/Config$LoRaConfig$Companion;->(Lkotlin/jvm/internal/DefaultConstructorMarker;)V @@ -45590,12 +46370,13 @@ Lorg/meshtastic/proto/Config$SecurityConfig; Lorg/meshtastic/proto/DeviceMetrics; Lorg/meshtastic/proto/LocalConfig; HSPLorg/meshtastic/proto/LocalConfig;->()V -HSPLorg/meshtastic/proto/LocalConfig;->(Lorg/meshtastic/proto/Config$DeviceConfig;Lorg/meshtastic/proto/Config$PositionConfig;Lorg/meshtastic/proto/Config$PowerConfig;Lorg/meshtastic/proto/Config$NetworkConfig;Lorg/meshtastic/proto/Config$DisplayConfig;Lorg/meshtastic/proto/Config$LoRaConfig;Lorg/meshtastic/proto/Config$BluetoothConfig;ILorg/meshtastic/proto/Config$SecurityConfig;Lokio/ByteString;)V -HSPLorg/meshtastic/proto/LocalConfig;->(Lorg/meshtastic/proto/Config$DeviceConfig;Lorg/meshtastic/proto/Config$PositionConfig;Lorg/meshtastic/proto/Config$PowerConfig;Lorg/meshtastic/proto/Config$NetworkConfig;Lorg/meshtastic/proto/Config$DisplayConfig;Lorg/meshtastic/proto/Config$LoRaConfig;Lorg/meshtastic/proto/Config$BluetoothConfig;ILorg/meshtastic/proto/Config$SecurityConfig;Lokio/ByteString;ILkotlin/jvm/internal/DefaultConstructorMarker;)V +HSPLorg/meshtastic/proto/LocalConfig;->(Lorg/meshtastic/proto/LocalConfig$Builder;Lokio/ByteString;)V +HSPLorg/meshtastic/proto/LocalConfig;->(Lorg/meshtastic/proto/LocalConfig$Builder;Lokio/ByteString;Lkotlin/jvm/internal/DefaultConstructorMarker;)V HSPLorg/meshtastic/proto/LocalConfig;->equals(Ljava/lang/Object;)Z -HSPLorg/meshtastic/proto/LocalConfig;->getDevice()Lorg/meshtastic/proto/Config$DeviceConfig; -HSPLorg/meshtastic/proto/LocalConfig;->getPower()Lorg/meshtastic/proto/Config$PowerConfig; HSPLorg/meshtastic/proto/LocalConfig;->hashCode()I +Lorg/meshtastic/proto/LocalConfig$Builder; +HSPLorg/meshtastic/proto/LocalConfig$Builder;->()V +HSPLorg/meshtastic/proto/LocalConfig$Builder;->build()Lorg/meshtastic/proto/LocalConfig; Lorg/meshtastic/proto/LocalConfig$Companion; HSPLorg/meshtastic/proto/LocalConfig$Companion;->()V HSPLorg/meshtastic/proto/LocalConfig$Companion;->(Lkotlin/jvm/internal/DefaultConstructorMarker;)V @@ -45603,10 +46384,13 @@ Lorg/meshtastic/proto/LocalConfig$Companion$ADAPTER$1; HSPLorg/meshtastic/proto/LocalConfig$Companion$ADAPTER$1;->(Lcom/squareup/wire/FieldEncoding;Lkotlin/reflect/KClass;Lcom/squareup/wire/Syntax;)V Lorg/meshtastic/proto/LocalModuleConfig; HSPLorg/meshtastic/proto/LocalModuleConfig;->()V -HSPLorg/meshtastic/proto/LocalModuleConfig;->(Lorg/meshtastic/proto/ModuleConfig$MQTTConfig;Lorg/meshtastic/proto/ModuleConfig$SerialConfig;Lorg/meshtastic/proto/ModuleConfig$ExternalNotificationConfig;Lorg/meshtastic/proto/ModuleConfig$StoreForwardConfig;Lorg/meshtastic/proto/ModuleConfig$RangeTestConfig;Lorg/meshtastic/proto/ModuleConfig$TelemetryConfig;Lorg/meshtastic/proto/ModuleConfig$CannedMessageConfig;Lorg/meshtastic/proto/ModuleConfig$AudioConfig;Lorg/meshtastic/proto/ModuleConfig$RemoteHardwareConfig;Lorg/meshtastic/proto/ModuleConfig$NeighborInfoConfig;Lorg/meshtastic/proto/ModuleConfig$AmbientLightingConfig;Lorg/meshtastic/proto/ModuleConfig$DetectionSensorConfig;Lorg/meshtastic/proto/ModuleConfig$PaxcounterConfig;Lorg/meshtastic/proto/ModuleConfig$StatusMessageConfig;Lorg/meshtastic/proto/ModuleConfig$TrafficManagementConfig;Lorg/meshtastic/proto/ModuleConfig$TAKConfig;Lorg/meshtastic/proto/ModuleConfig$MeshBeaconConfig;ILokio/ByteString;)V -HSPLorg/meshtastic/proto/LocalModuleConfig;->(Lorg/meshtastic/proto/ModuleConfig$MQTTConfig;Lorg/meshtastic/proto/ModuleConfig$SerialConfig;Lorg/meshtastic/proto/ModuleConfig$ExternalNotificationConfig;Lorg/meshtastic/proto/ModuleConfig$StoreForwardConfig;Lorg/meshtastic/proto/ModuleConfig$RangeTestConfig;Lorg/meshtastic/proto/ModuleConfig$TelemetryConfig;Lorg/meshtastic/proto/ModuleConfig$CannedMessageConfig;Lorg/meshtastic/proto/ModuleConfig$AudioConfig;Lorg/meshtastic/proto/ModuleConfig$RemoteHardwareConfig;Lorg/meshtastic/proto/ModuleConfig$NeighborInfoConfig;Lorg/meshtastic/proto/ModuleConfig$AmbientLightingConfig;Lorg/meshtastic/proto/ModuleConfig$DetectionSensorConfig;Lorg/meshtastic/proto/ModuleConfig$PaxcounterConfig;Lorg/meshtastic/proto/ModuleConfig$StatusMessageConfig;Lorg/meshtastic/proto/ModuleConfig$TrafficManagementConfig;Lorg/meshtastic/proto/ModuleConfig$TAKConfig;Lorg/meshtastic/proto/ModuleConfig$MeshBeaconConfig;ILokio/ByteString;ILkotlin/jvm/internal/DefaultConstructorMarker;)V +HSPLorg/meshtastic/proto/LocalModuleConfig;->(Lorg/meshtastic/proto/LocalModuleConfig$Builder;Lokio/ByteString;)V +HSPLorg/meshtastic/proto/LocalModuleConfig;->(Lorg/meshtastic/proto/LocalModuleConfig$Builder;Lokio/ByteString;Lkotlin/jvm/internal/DefaultConstructorMarker;)V HSPLorg/meshtastic/proto/LocalModuleConfig;->equals(Ljava/lang/Object;)Z HSPLorg/meshtastic/proto/LocalModuleConfig;->hashCode()I +Lorg/meshtastic/proto/LocalModuleConfig$Builder; +HSPLorg/meshtastic/proto/LocalModuleConfig$Builder;->()V +HSPLorg/meshtastic/proto/LocalModuleConfig$Builder;->build()Lorg/meshtastic/proto/LocalModuleConfig; Lorg/meshtastic/proto/LocalModuleConfig$Companion; HSPLorg/meshtastic/proto/LocalModuleConfig$Companion;->()V HSPLorg/meshtastic/proto/LocalModuleConfig$Companion;->(Lkotlin/jvm/internal/DefaultConstructorMarker;)V @@ -45614,21 +46398,13 @@ Lorg/meshtastic/proto/LocalModuleConfig$Companion$ADAPTER$1; HSPLorg/meshtastic/proto/LocalModuleConfig$Companion$ADAPTER$1;->(Lcom/squareup/wire/FieldEncoding;Lkotlin/reflect/KClass;Lcom/squareup/wire/Syntax;)V Lorg/meshtastic/proto/LocalStats; HSPLorg/meshtastic/proto/LocalStats;->()V -HSPLorg/meshtastic/proto/LocalStats;->(IFFIIIIIIIIIIIILokio/ByteString;)V -HSPLorg/meshtastic/proto/LocalStats;->(IFFIIIIIIIIIIIILokio/ByteString;ILkotlin/jvm/internal/DefaultConstructorMarker;)V +HSPLorg/meshtastic/proto/LocalStats;->(Lorg/meshtastic/proto/LocalStats$Builder;Lokio/ByteString;)V +HSPLorg/meshtastic/proto/LocalStats;->(Lorg/meshtastic/proto/LocalStats$Builder;Lokio/ByteString;Lkotlin/jvm/internal/DefaultConstructorMarker;)V HSPLorg/meshtastic/proto/LocalStats;->equals(Ljava/lang/Object;)Z -HSPLorg/meshtastic/proto/LocalStats;->getHeap_free_bytes()I -HSPLorg/meshtastic/proto/LocalStats;->getHeap_total_bytes()I -HSPLorg/meshtastic/proto/LocalStats;->getNoise_floor()I -HSPLorg/meshtastic/proto/LocalStats;->getNum_packets_rx()I -HSPLorg/meshtastic/proto/LocalStats;->getNum_packets_rx_bad()I -HSPLorg/meshtastic/proto/LocalStats;->getNum_packets_tx()I -HSPLorg/meshtastic/proto/LocalStats;->getNum_rx_dupe()I -HSPLorg/meshtastic/proto/LocalStats;->getNum_tx_dropped()I -HSPLorg/meshtastic/proto/LocalStats;->getNum_tx_relay()I -HSPLorg/meshtastic/proto/LocalStats;->getNum_tx_relay_canceled()I -HSPLorg/meshtastic/proto/LocalStats;->getUptime_seconds()I HSPLorg/meshtastic/proto/LocalStats;->hashCode()I +Lorg/meshtastic/proto/LocalStats$Builder; +HSPLorg/meshtastic/proto/LocalStats$Builder;->()V +HSPLorg/meshtastic/proto/LocalStats$Builder;->build()Lorg/meshtastic/proto/LocalStats; Lorg/meshtastic/proto/LocalStats$Companion; HSPLorg/meshtastic/proto/LocalStats$Companion;->()V HSPLorg/meshtastic/proto/LocalStats$Companion;->(Lkotlin/jvm/internal/DefaultConstructorMarker;)V @@ -45670,84 +46446,4 @@ HSPLorg/meshtastic/proto/Team$Companion;->()V HSPLorg/meshtastic/proto/Team$Companion;->(Lkotlin/jvm/internal/DefaultConstructorMarker;)V Lorg/meshtastic/proto/Team$Companion$ADAPTER$1; HSPLorg/meshtastic/proto/Team$Companion$ADAPTER$1;->(Lkotlin/reflect/KClass;Lcom/squareup/wire/Syntax;Lorg/meshtastic/proto/Team;)V -Lorg/slf4j/ILoggerFactory; -Lorg/slf4j/IMarkerFactory; -Lorg/slf4j/Logger; -Lorg/slf4j/LoggerFactory; -HSPLorg/slf4j/LoggerFactory;->()V -HSPLorg/slf4j/LoggerFactory;->bind()V -HSPLorg/slf4j/LoggerFactory;->earlyBindMDCAdapter()V -HSPLorg/slf4j/LoggerFactory;->findServiceProviders()Ljava/util/List; -HSPLorg/slf4j/LoggerFactory;->fixSubstituteLoggers()V -HSPLorg/slf4j/LoggerFactory;->getILoggerFactory()Lorg/slf4j/ILoggerFactory; -HSPLorg/slf4j/LoggerFactory;->getLogger(Ljava/lang/String;)Lorg/slf4j/Logger; -HSPLorg/slf4j/LoggerFactory;->getProvider()Lorg/slf4j/spi/SLF4JServiceProvider; -HSPLorg/slf4j/LoggerFactory;->getServiceLoader(Ljava/lang/ClassLoader;)Ljava/util/ServiceLoader; -HSPLorg/slf4j/LoggerFactory;->isAmbiguousProviderList(Ljava/util/List;)Z -HSPLorg/slf4j/LoggerFactory;->loadExplicitlySpecified(Ljava/lang/ClassLoader;)Lorg/slf4j/spi/SLF4JServiceProvider; -HSPLorg/slf4j/LoggerFactory;->performInitialization()V -HSPLorg/slf4j/LoggerFactory;->postBindCleanUp()V -HSPLorg/slf4j/LoggerFactory;->replayEvents()V -HSPLorg/slf4j/LoggerFactory;->reportActualBinding(Ljava/util/List;)V -HSPLorg/slf4j/LoggerFactory;->reportMultipleBindingAmbiguity(Ljava/util/List;)V -HSPLorg/slf4j/LoggerFactory;->safelyInstantiate(Ljava/util/List;Ljava/util/Iterator;)V -HSPLorg/slf4j/LoggerFactory;->versionSanityCheck()V -Lorg/slf4j/MDC; -HSPLorg/slf4j/MDC;->setMDCAdapter(Lorg/slf4j/spi/MDCAdapter;)V -Lorg/slf4j/helpers/BasicMDCAdapter; -HSPLorg/slf4j/helpers/BasicMDCAdapter;->()V -Lorg/slf4j/helpers/BasicMDCAdapter$1; -HSPLorg/slf4j/helpers/BasicMDCAdapter$1;->(Lorg/slf4j/helpers/BasicMDCAdapter;)V -Lorg/slf4j/helpers/BasicMarkerFactory; -HSPLorg/slf4j/helpers/BasicMarkerFactory;->()V -Lorg/slf4j/helpers/NOPLogger; -HSPLorg/slf4j/helpers/NOPLogger;->()V -HSPLorg/slf4j/helpers/NOPLogger;->()V -Lorg/slf4j/helpers/NOPLoggerFactory; -HSPLorg/slf4j/helpers/NOPLoggerFactory;->()V -HSPLorg/slf4j/helpers/NOPLoggerFactory;->getLogger(Ljava/lang/String;)Lorg/slf4j/Logger; -Lorg/slf4j/helpers/NOPMDCAdapter; -HSPLorg/slf4j/helpers/NOPMDCAdapter;->()V -Lorg/slf4j/helpers/NOP_FallbackServiceProvider; -HSPLorg/slf4j/helpers/NOP_FallbackServiceProvider;->()V -HSPLorg/slf4j/helpers/NOP_FallbackServiceProvider;->()V -Lorg/slf4j/helpers/NamedLoggerBase; -HSPLorg/slf4j/helpers/NamedLoggerBase;->()V -Lorg/slf4j/helpers/Reporter; -HSPLorg/slf4j/helpers/Reporter;->()V -HSPLorg/slf4j/helpers/Reporter;->debug(Ljava/lang/String;)V -HSPLorg/slf4j/helpers/Reporter;->getTargetChoice()Lorg/slf4j/helpers/Reporter$TargetChoice; -HSPLorg/slf4j/helpers/Reporter;->initVerbosity()Lorg/slf4j/helpers/Reporter$Level; -HSPLorg/slf4j/helpers/Reporter;->isEnabledFor(Lorg/slf4j/helpers/Reporter$Level;)Z -Lorg/slf4j/helpers/Reporter$Level; -HSPLorg/slf4j/helpers/Reporter$Level;->$values()[Lorg/slf4j/helpers/Reporter$Level; -HSPLorg/slf4j/helpers/Reporter$Level;->()V -HSPLorg/slf4j/helpers/Reporter$Level;->(Ljava/lang/String;II)V -Lorg/slf4j/helpers/Reporter$TargetChoice; -HSPLorg/slf4j/helpers/Reporter$TargetChoice;->$values()[Lorg/slf4j/helpers/Reporter$TargetChoice; -HSPLorg/slf4j/helpers/Reporter$TargetChoice;->()V -HSPLorg/slf4j/helpers/Reporter$TargetChoice;->(Ljava/lang/String;I)V -Lorg/slf4j/helpers/SubstituteLoggerFactory; -HSPLorg/slf4j/helpers/SubstituteLoggerFactory;->()V -HSPLorg/slf4j/helpers/SubstituteLoggerFactory;->clear()V -HSPLorg/slf4j/helpers/SubstituteLoggerFactory;->getEventQueue()Ljava/util/concurrent/LinkedBlockingQueue; -HSPLorg/slf4j/helpers/SubstituteLoggerFactory;->getLoggers()Ljava/util/List; -HSPLorg/slf4j/helpers/SubstituteLoggerFactory;->postInitialization()V -Lorg/slf4j/helpers/SubstituteServiceProvider; -HSPLorg/slf4j/helpers/SubstituteServiceProvider;->()V -HSPLorg/slf4j/helpers/SubstituteServiceProvider;->getSubstituteLoggerFactory()Lorg/slf4j/helpers/SubstituteLoggerFactory; -Lorg/slf4j/helpers/ThreadLocalMapOfStacks; -HSPLorg/slf4j/helpers/ThreadLocalMapOfStacks;->()V -Lorg/slf4j/helpers/Util; -HSPLorg/slf4j/helpers/Util;->()V -HSPLorg/slf4j/helpers/Util;->safeGetBooleanSystemProperty(Ljava/lang/String;)Z -HSPLorg/slf4j/helpers/Util;->safeGetSystemProperty(Ljava/lang/String;)Ljava/lang/String; -Lorg/slf4j/nop/NOPServiceProvider; -HSPLorg/slf4j/nop/NOPServiceProvider;->()V -HSPLorg/slf4j/nop/NOPServiceProvider;->()V -HSPLorg/slf4j/nop/NOPServiceProvider;->getLoggerFactory()Lorg/slf4j/ILoggerFactory; -HSPLorg/slf4j/nop/NOPServiceProvider;->getMDCAdapter()Lorg/slf4j/spi/MDCAdapter; -HSPLorg/slf4j/nop/NOPServiceProvider;->getRequestedApiVersion()Ljava/lang/String; -HSPLorg/slf4j/nop/NOPServiceProvider;->initialize()V -Lorg/slf4j/spi/MDCAdapter; -Lorg/slf4j/spi/SLF4JServiceProvider; \ No newline at end of file +Lorg/slf4j/Logger; \ No newline at end of file diff --git a/androidApp/src/googleRelease/generated/baselineProfiles/startup-prof.txt b/androidApp/src/googleRelease/generated/baselineProfiles/startup-prof.txt index 11d46ef98e..35d3663ea0 100644 --- a/androidApp/src/googleRelease/generated/baselineProfiles/startup-prof.txt +++ b/androidApp/src/googleRelease/generated/baselineProfiles/startup-prof.txt @@ -561,10 +561,10 @@ Landroidx/arch/core/internal/SafeIterableMap; HSPLandroidx/arch/core/internal/SafeIterableMap;->()V HSPLandroidx/arch/core/internal/SafeIterableMap;->iteratorWithAdditions()Landroidx/arch/core/internal/SafeIterableMap$IteratorWithAdditions; Landroidx/arch/core/internal/SafeIterableMap$IteratorWithAdditions; -PLandroidx/arch/core/internal/SafeIterableMap$IteratorWithAdditions;->(Landroidx/arch/core/internal/SafeIterableMap;)V +HSPLandroidx/arch/core/internal/SafeIterableMap$IteratorWithAdditions;->(Landroidx/arch/core/internal/SafeIterableMap;)V HSPLandroidx/arch/core/internal/SafeIterableMap$IteratorWithAdditions;->hasNext()Z Landroidx/arch/core/internal/SafeIterableMap$SupportRemove; -PLandroidx/arch/core/internal/SafeIterableMap$SupportRemove;->()V +HSPLandroidx/arch/core/internal/SafeIterableMap$SupportRemove;->()V Landroidx/arch/core/util/Function; Landroidx/camera/view/PreviewView$1$$ExternalSyntheticBackportWithForwarding0; HSPLandroidx/camera/view/PreviewView$1$$ExternalSyntheticBackportWithForwarding0;->m(Ljava/util/concurrent/atomic/AtomicReference;Ljava/lang/Object;Ljava/lang/Object;)Z @@ -1666,7 +1666,7 @@ HSPLandroidx/compose/animation/core/SeekableTransitionState;->recalculateTotalDu HSPLandroidx/compose/animation/core/SeekableTransitionState;->setComposedTargetState$animation_core(Ljava/lang/Object;)V HSPLandroidx/compose/animation/core/SeekableTransitionState;->setCompositionContinuation$animation_core(Lkotlinx/coroutines/CancellableContinuation;)V HSPLandroidx/compose/animation/core/SeekableTransitionState;->setSnapshotStateObserver$animation_core(Landroidx/compose/runtime/snapshots/SnapshotStateObserver;)V -PLandroidx/compose/animation/core/SeekableTransitionState;->snapTo(Ljava/lang/Object;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; +HSPLandroidx/compose/animation/core/SeekableTransitionState;->snapTo(Ljava/lang/Object;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; HSPLandroidx/compose/animation/core/SeekableTransitionState;->transitionConfigured$animation_core(Landroidx/compose/animation/core/Transition;)V Landroidx/compose/animation/core/SeekableTransitionState$$ExternalSyntheticLambda0; HSPLandroidx/compose/animation/core/SeekableTransitionState$$ExternalSyntheticLambda0;->(Landroidx/compose/animation/core/SeekableTransitionState;)V @@ -1709,7 +1709,7 @@ HSPLandroidx/compose/animation/core/SuspendAnimationKt$$ExternalSyntheticLambda7 HSPLandroidx/compose/animation/core/SuspendAnimationKt$$ExternalSyntheticLambda7;->invoke(Ljava/lang/Object;)Ljava/lang/Object; Landroidx/compose/animation/core/SuspendAnimationKt$animate$4; HSPLandroidx/compose/animation/core/SuspendAnimationKt$animate$4;->(Lkotlin/coroutines/Continuation;)V -PLandroidx/compose/animation/core/SuspendAnimationKt$animate$4;->invokeSuspend(Ljava/lang/Object;)Ljava/lang/Object; +HSPLandroidx/compose/animation/core/SuspendAnimationKt$animate$4;->invokeSuspend(Ljava/lang/Object;)Ljava/lang/Object; Landroidx/compose/animation/core/TargetBasedAnimation; HSPLandroidx/compose/animation/core/TargetBasedAnimation;->()V HSPLandroidx/compose/animation/core/TargetBasedAnimation;->(Landroidx/compose/animation/core/AnimationSpec;Landroidx/compose/animation/core/TwoWayConverter;Ljava/lang/Object;Ljava/lang/Object;Landroidx/compose/animation/core/AnimationVector;)V @@ -3306,12 +3306,14 @@ HSPLandroidx/compose/material3/MaterialTheme;->()V HSPLandroidx/compose/material3/MaterialTheme;->()V HSPLandroidx/compose/material3/MaterialTheme;->getColorScheme(Landroidx/compose/runtime/Composer;I)Landroidx/compose/material3/ColorScheme; HSPLandroidx/compose/material3/MaterialTheme;->getLocalMaterialTheme()Landroidx/compose/runtime/CompositionLocal; +HSPLandroidx/compose/material3/MaterialTheme;->getMotionScheme(Landroidx/compose/runtime/Composer;I)Landroidx/compose/material3/MotionScheme; HSPLandroidx/compose/material3/MaterialTheme;->getShapes(Landroidx/compose/runtime/Composer;I)Landroidx/compose/material3/Shapes; HSPLandroidx/compose/material3/MaterialTheme;->getTypography(Landroidx/compose/runtime/Composer;I)Landroidx/compose/material3/Typography; Landroidx/compose/material3/MaterialTheme$Values; HSPLandroidx/compose/material3/MaterialTheme$Values;->()V HSPLandroidx/compose/material3/MaterialTheme$Values;->(Landroidx/compose/material3/ColorScheme;Landroidx/compose/material3/Typography;Landroidx/compose/material3/Shapes;Landroidx/compose/material3/MotionScheme;)V HSPLandroidx/compose/material3/MaterialTheme$Values;->getColorScheme()Landroidx/compose/material3/ColorScheme; +HSPLandroidx/compose/material3/MaterialTheme$Values;->getMotionScheme()Landroidx/compose/material3/MotionScheme; HSPLandroidx/compose/material3/MaterialTheme$Values;->getShapes()Landroidx/compose/material3/Shapes; HSPLandroidx/compose/material3/MaterialTheme$Values;->getTypography()Landroidx/compose/material3/Typography; Landroidx/compose/material3/MaterialThemeKt; @@ -3363,6 +3365,7 @@ HSPLandroidx/compose/material3/MotionScheme$Companion;->expressive()Landroidx/co Landroidx/compose/material3/MotionScheme$ExpressiveMotionSchemeImpl; HSPLandroidx/compose/material3/MotionScheme$ExpressiveMotionSchemeImpl;->()V HSPLandroidx/compose/material3/MotionScheme$ExpressiveMotionSchemeImpl;->()V +HSPLandroidx/compose/material3/MotionScheme$ExpressiveMotionSchemeImpl;->defaultEffectsSpec()Landroidx/compose/animation/core/FiniteAnimationSpec; Landroidx/compose/material3/PrecisionPointer; HSPLandroidx/compose/material3/PrecisionPointer;->()V HSPLandroidx/compose/material3/PrecisionPointer;->getShouldUsePrecisionPointerComponentSizing()Landroidx/compose/runtime/MutableState; @@ -4756,7 +4759,7 @@ Landroidx/compose/runtime/NextFrameEndCallbackQueue; HSPLandroidx/compose/runtime/NextFrameEndCallbackQueue;->()V HSPLandroidx/compose/runtime/NextFrameEndCallbackQueue;->(Lkotlin/jvm/functions/Function0;)V HSPLandroidx/compose/runtime/NextFrameEndCallbackQueue;->getHasAwaiters()Z -HSPLandroidx/compose/runtime/NextFrameEndCallbackQueue;->markFrameComplete()V +PLandroidx/compose/runtime/NextFrameEndCallbackQueue;->markFrameComplete()V PLandroidx/compose/runtime/NextFrameEndCallbackQueue$$ExternalSyntheticLambda0;->()V Landroidx/compose/runtime/NextFrameEndCallbackQueue$$ExternalSyntheticLambda1; HSPLandroidx/compose/runtime/NextFrameEndCallbackQueue$$ExternalSyntheticLambda1;->(Landroidx/compose/runtime/NextFrameEndCallbackQueue;Lkotlin/jvm/functions/Function0;)V @@ -4889,13 +4892,13 @@ HSPLandroidx/compose/runtime/Recomposer;->()V HSPLandroidx/compose/runtime/Recomposer;->(Lkotlin/coroutines/CoroutineContext;)V HSPLandroidx/compose/runtime/Recomposer;->access$awaitWorkAvailable(Landroidx/compose/runtime/Recomposer;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; HSPLandroidx/compose/runtime/Recomposer;->access$deriveStateLocked(Landroidx/compose/runtime/Recomposer;)Lkotlinx/coroutines/CancellableContinuation; -HSPLandroidx/compose/runtime/Recomposer;->access$discardUnusedMovableContentState(Landroidx/compose/runtime/Recomposer;)V +PLandroidx/compose/runtime/Recomposer;->access$discardUnusedMovableContentState(Landroidx/compose/runtime/Recomposer;)V HSPLandroidx/compose/runtime/Recomposer;->access$getBroadcastFrameClock$p(Landroidx/compose/runtime/Recomposer;)Landroidx/compose/runtime/BroadcastFrameClock; HSPLandroidx/compose/runtime/Recomposer;->access$getCompositionInvalidations$p(Landroidx/compose/runtime/Recomposer;)Landroidx/compose/runtime/collection/MutableVector; HSPLandroidx/compose/runtime/Recomposer;->access$getHasBroadcastFrameClockAwaiters(Landroidx/compose/runtime/Recomposer;)Z HSPLandroidx/compose/runtime/Recomposer;->access$getHasSchedulingWork(Landroidx/compose/runtime/Recomposer;)Z HSPLandroidx/compose/runtime/Recomposer;->access$getMovableContentAwaitingInsert$p(Landroidx/compose/runtime/Recomposer;)Ljava/util/List; -HSPLandroidx/compose/runtime/Recomposer;->access$getNextFrameEndCallbackQueue$p(Landroidx/compose/runtime/Recomposer;)Landroidx/compose/runtime/NextFrameEndCallbackQueue; +PLandroidx/compose/runtime/Recomposer;->access$getNextFrameEndCallbackQueue$p(Landroidx/compose/runtime/Recomposer;)Landroidx/compose/runtime/NextFrameEndCallbackQueue; HSPLandroidx/compose/runtime/Recomposer;->access$getRecomposerInfo$p(Landroidx/compose/runtime/Recomposer;)Landroidx/compose/runtime/Recomposer$RecomposerInfoImpl; HSPLandroidx/compose/runtime/Recomposer;->access$getShouldKeepRecomposing(Landroidx/compose/runtime/Recomposer;)Z HSPLandroidx/compose/runtime/Recomposer;->access$getSnapshotInvalidations$p(Landroidx/compose/runtime/Recomposer;)Landroidx/collection/MutableScatterSet; @@ -5829,7 +5832,6 @@ HSPLandroidx/compose/runtime/external/kotlinx/collections/immutable/implementati HSPLandroidx/compose/runtime/external/kotlinx/collections/immutable/implementations/immutableMap/TrieNode;->insertEntryAt(ILjava/lang/Object;Ljava/lang/Object;)Landroidx/compose/runtime/external/kotlinx/collections/immutable/implementations/immutableMap/TrieNode; HSPLandroidx/compose/runtime/external/kotlinx/collections/immutable/implementations/immutableMap/TrieNode;->keyAtIndex(I)Ljava/lang/Object; HSPLandroidx/compose/runtime/external/kotlinx/collections/immutable/implementations/immutableMap/TrieNode;->makeNode(ILjava/lang/Object;Ljava/lang/Object;ILjava/lang/Object;Ljava/lang/Object;ILandroidx/compose/runtime/external/kotlinx/collections/immutable/internal/MutabilityOwnership;)Landroidx/compose/runtime/external/kotlinx/collections/immutable/implementations/immutableMap/TrieNode; -HSPLandroidx/compose/runtime/external/kotlinx/collections/immutable/implementations/immutableMap/TrieNode;->moveEntryToNode(IIILjava/lang/Object;Ljava/lang/Object;I)Landroidx/compose/runtime/external/kotlinx/collections/immutable/implementations/immutableMap/TrieNode; HSPLandroidx/compose/runtime/external/kotlinx/collections/immutable/implementations/immutableMap/TrieNode;->mutableInsertEntryAt(ILjava/lang/Object;Ljava/lang/Object;Landroidx/compose/runtime/external/kotlinx/collections/immutable/internal/MutabilityOwnership;)Landroidx/compose/runtime/external/kotlinx/collections/immutable/implementations/immutableMap/TrieNode; HSPLandroidx/compose/runtime/external/kotlinx/collections/immutable/implementations/immutableMap/TrieNode;->mutableMoveEntryToNode(IIILjava/lang/Object;Ljava/lang/Object;ILandroidx/compose/runtime/external/kotlinx/collections/immutable/internal/MutabilityOwnership;)Landroidx/compose/runtime/external/kotlinx/collections/immutable/implementations/immutableMap/TrieNode; HSPLandroidx/compose/runtime/external/kotlinx/collections/immutable/implementations/immutableMap/TrieNode;->mutablePut(ILjava/lang/Object;Ljava/lang/Object;ILandroidx/compose/runtime/external/kotlinx/collections/immutable/implementations/immutableMap/PersistentHashMapBuilder;)Landroidx/compose/runtime/external/kotlinx/collections/immutable/implementations/immutableMap/TrieNode; @@ -5921,7 +5923,7 @@ HSPLandroidx/compose/runtime/internal/AtomicAwaitersCount$Companion;->(Lko Landroidx/compose/runtime/internal/AtomicBoolean; HSPLandroidx/compose/runtime/internal/AtomicBoolean;->constructor-impl(Landroidx/compose/runtime/internal/AtomicInt;)Landroidx/compose/runtime/internal/AtomicInt; HSPLandroidx/compose/runtime/internal/AtomicBoolean;->constructor-impl(Z)Landroidx/compose/runtime/internal/AtomicInt; -HSPLandroidx/compose/runtime/internal/AtomicBoolean;->set-impl(Landroidx/compose/runtime/internal/AtomicInt;Z)V +PLandroidx/compose/runtime/internal/AtomicBoolean;->set-impl(Landroidx/compose/runtime/internal/AtomicInt;Z)V Landroidx/compose/runtime/internal/AtomicInt; HSPLandroidx/compose/runtime/internal/AtomicInt;->()V HSPLandroidx/compose/runtime/internal/AtomicInt;->(I)V @@ -9915,8 +9917,8 @@ HSPLandroidx/compose/ui/platform/AndroidComposeView$$ExternalSyntheticLambda22;- Landroidx/compose/ui/platform/AndroidComposeView$$ExternalSyntheticLambda23; HSPLandroidx/compose/ui/platform/AndroidComposeView$$ExternalSyntheticLambda23;->(Landroidx/compose/ui/platform/AndroidComposeView;)V HSPLandroidx/compose/ui/platform/AndroidComposeView$$ExternalSyntheticLambda23;->invoke()Ljava/lang/Object; -Landroidx/compose/ui/platform/AndroidComposeView$$ExternalSyntheticLambda7; -HSPLandroidx/compose/ui/platform/AndroidComposeView$$ExternalSyntheticLambda7;->(Landroidx/compose/ui/platform/AndroidComposeView;)V +Landroidx/compose/ui/platform/AndroidComposeView$$ExternalSyntheticLambda8; +HSPLandroidx/compose/ui/platform/AndroidComposeView$$ExternalSyntheticLambda8;->(Landroidx/compose/ui/platform/AndroidComposeView;)V Landroidx/compose/ui/platform/AndroidComposeView$AndroidComposeViewNavigationSoundEffect; HSPLandroidx/compose/ui/platform/AndroidComposeView$AndroidComposeViewNavigationSoundEffect;->(Landroid/view/View;)V Landroidx/compose/ui/platform/AndroidComposeView$Companion; @@ -10038,7 +10040,7 @@ HSPLandroidx/compose/ui/platform/AndroidUiDispatcher;->access$getMain$delegate$c HSPLandroidx/compose/ui/platform/AndroidUiDispatcher;->access$getToRunOnFrame$p(Landroidx/compose/ui/platform/AndroidUiDispatcher;)Ljava/util/List; HSPLandroidx/compose/ui/platform/AndroidUiDispatcher;->access$performFrameDispatch(Landroidx/compose/ui/platform/AndroidUiDispatcher;J)V HSPLandroidx/compose/ui/platform/AndroidUiDispatcher;->access$performTrampolineDispatch(Landroidx/compose/ui/platform/AndroidUiDispatcher;)V -HSPLandroidx/compose/ui/platform/AndroidUiDispatcher;->access$setScheduledFrameDispatch$p(Landroidx/compose/ui/platform/AndroidUiDispatcher;Z)V +PLandroidx/compose/ui/platform/AndroidUiDispatcher;->access$setScheduledFrameDispatch$p(Landroidx/compose/ui/platform/AndroidUiDispatcher;Z)V HSPLandroidx/compose/ui/platform/AndroidUiDispatcher;->dispatch(Lkotlin/coroutines/CoroutineContext;Ljava/lang/Runnable;)V HSPLandroidx/compose/ui/platform/AndroidUiDispatcher;->getChoreographer()Landroid/view/Choreographer; HSPLandroidx/compose/ui/platform/AndroidUiDispatcher;->nextTask()Ljava/lang/Runnable; @@ -12353,15 +12355,15 @@ HSPLandroidx/concurrent/futures/AbstractResolvableFuture;->()V HSPLandroidx/concurrent/futures/AbstractResolvableFuture;->()V HSPLandroidx/concurrent/futures/AbstractResolvableFuture;->addListener(Ljava/lang/Runnable;Ljava/util/concurrent/Executor;)V HSPLandroidx/concurrent/futures/AbstractResolvableFuture;->afterDone()V -HSPLandroidx/concurrent/futures/AbstractResolvableFuture;->cancel(Z)Z +PLandroidx/concurrent/futures/AbstractResolvableFuture;->cancel(Z)Z HSPLandroidx/concurrent/futures/AbstractResolvableFuture;->checkNotNull(Ljava/lang/Object;)Ljava/lang/Object; HSPLandroidx/concurrent/futures/AbstractResolvableFuture;->clearListeners(Landroidx/concurrent/futures/AbstractResolvableFuture$Listener;)Landroidx/concurrent/futures/AbstractResolvableFuture$Listener; HSPLandroidx/concurrent/futures/AbstractResolvableFuture;->complete(Landroidx/concurrent/futures/AbstractResolvableFuture;)V -HSPLandroidx/concurrent/futures/AbstractResolvableFuture;->executeListener(Ljava/lang/Runnable;Ljava/util/concurrent/Executor;)V +PLandroidx/concurrent/futures/AbstractResolvableFuture;->executeListener(Ljava/lang/Runnable;Ljava/util/concurrent/Executor;)V PLandroidx/concurrent/futures/AbstractResolvableFuture;->get()Ljava/lang/Object; PLandroidx/concurrent/futures/AbstractResolvableFuture;->getDoneValue(Ljava/lang/Object;)Ljava/lang/Object; -HSPLandroidx/concurrent/futures/AbstractResolvableFuture;->isCancelled()Z -HSPLandroidx/concurrent/futures/AbstractResolvableFuture;->isDone()Z +PLandroidx/concurrent/futures/AbstractResolvableFuture;->isCancelled()Z +PLandroidx/concurrent/futures/AbstractResolvableFuture;->isDone()Z HSPLandroidx/concurrent/futures/AbstractResolvableFuture;->releaseWaiters()V HSPLandroidx/concurrent/futures/AbstractResolvableFuture;->set(Ljava/lang/Object;)Z Landroidx/concurrent/futures/AbstractResolvableFuture$AtomicHelper; @@ -12388,25 +12390,25 @@ Landroidx/concurrent/futures/CallbackToFutureAdapter$Completer; HSPLandroidx/concurrent/futures/CallbackToFutureAdapter$Completer;->()V HSPLandroidx/concurrent/futures/CallbackToFutureAdapter$Completer;->addCancellationListener(Ljava/lang/Runnable;Ljava/util/concurrent/Executor;)V PLandroidx/concurrent/futures/CallbackToFutureAdapter$Completer;->finalize()V -HSPLandroidx/concurrent/futures/CallbackToFutureAdapter$Completer;->fireCancellationListeners()V +PLandroidx/concurrent/futures/CallbackToFutureAdapter$Completer;->fireCancellationListeners()V HSPLandroidx/concurrent/futures/CallbackToFutureAdapter$Completer;->set(Ljava/lang/Object;)Z -HSPLandroidx/concurrent/futures/CallbackToFutureAdapter$Completer;->setCancelled()Z +PLandroidx/concurrent/futures/CallbackToFutureAdapter$Completer;->setCancelled()Z HSPLandroidx/concurrent/futures/CallbackToFutureAdapter$Completer;->setCompletedNormally()V Landroidx/concurrent/futures/CallbackToFutureAdapter$Resolver; Landroidx/concurrent/futures/CallbackToFutureAdapter$SafeFuture; HSPLandroidx/concurrent/futures/CallbackToFutureAdapter$SafeFuture;->(Landroidx/concurrent/futures/CallbackToFutureAdapter$Completer;)V HSPLandroidx/concurrent/futures/CallbackToFutureAdapter$SafeFuture;->addListener(Ljava/lang/Runnable;Ljava/util/concurrent/Executor;)V -HSPLandroidx/concurrent/futures/CallbackToFutureAdapter$SafeFuture;->cancel(Z)Z +PLandroidx/concurrent/futures/CallbackToFutureAdapter$SafeFuture;->cancel(Z)Z PLandroidx/concurrent/futures/CallbackToFutureAdapter$SafeFuture;->get()Ljava/lang/Object; -HSPLandroidx/concurrent/futures/CallbackToFutureAdapter$SafeFuture;->isCancelled()Z -HSPLandroidx/concurrent/futures/CallbackToFutureAdapter$SafeFuture;->isDone()Z +PLandroidx/concurrent/futures/CallbackToFutureAdapter$SafeFuture;->isCancelled()Z +PLandroidx/concurrent/futures/CallbackToFutureAdapter$SafeFuture;->isDone()Z HSPLandroidx/concurrent/futures/CallbackToFutureAdapter$SafeFuture;->set(Ljava/lang/Object;)Z Landroidx/concurrent/futures/CallbackToFutureAdapter$SafeFuture$1; HSPLandroidx/concurrent/futures/CallbackToFutureAdapter$SafeFuture$1;->(Landroidx/concurrent/futures/CallbackToFutureAdapter$SafeFuture;)V Landroidx/concurrent/futures/ResolvableFuture; HSPLandroidx/concurrent/futures/ResolvableFuture;->()V HSPLandroidx/concurrent/futures/ResolvableFuture;->create()Landroidx/concurrent/futures/ResolvableFuture; -HSPLandroidx/concurrent/futures/ResolvableFuture;->set(Ljava/lang/Object;)Z +PLandroidx/concurrent/futures/ResolvableFuture;->set(Ljava/lang/Object;)Z Landroidx/core/R$id; Landroidx/core/app/ActivityCompat; HSPLandroidx/core/app/ActivityCompat;->shouldShowRequestPermissionRationale(Landroid/app/Activity;Ljava/lang/String;)Z @@ -12527,22 +12529,18 @@ HSPLandroidx/core/graphics/Insets;->(IIII)V HSPLandroidx/core/graphics/Insets;->equals(Ljava/lang/Object;)Z HSPLandroidx/core/graphics/Insets;->of(IIII)Landroidx/core/graphics/Insets; HSPLandroidx/core/graphics/Insets;->toCompatInsets(Landroid/graphics/Insets;)Landroidx/core/graphics/Insets; -Landroidx/core/graphics/TypefaceCompat; -HSPLandroidx/core/graphics/TypefaceCompat;->()V +PLandroidx/core/graphics/TypefaceCompat;->()V PLandroidx/core/graphics/TypefaceCompat;->createFromFontInfo(Landroid/content/Context;Landroid/os/CancellationSignal;[Landroidx/core/provider/FontsContractCompat$FontInfo;I)Landroid/graphics/Typeface; -HSPLandroidx/core/graphics/TypefaceCompat;->getSystemFontFamily(Ljava/lang/String;)Landroid/graphics/Typeface; -Landroidx/core/graphics/TypefaceCompatApi29Impl; -HSPLandroidx/core/graphics/TypefaceCompatApi29Impl;->()V +PLandroidx/core/graphics/TypefaceCompat;->getSystemFontFamily(Ljava/lang/String;)Landroid/graphics/Typeface; +PLandroidx/core/graphics/TypefaceCompatApi29Impl;->()V PLandroidx/core/graphics/TypefaceCompatApi29Impl;->createFromFontInfo(Landroid/content/Context;Landroid/os/CancellationSignal;[Landroidx/core/provider/FontsContractCompat$FontInfo;I)Landroid/graphics/Typeface; PLandroidx/core/graphics/TypefaceCompatApi29Impl;->findBaseFont(Landroid/graphics/fonts/FontFamily;I)Landroid/graphics/fonts/Font; PLandroidx/core/graphics/TypefaceCompatApi29Impl;->getFont(Landroid/os/CancellationSignal;Landroidx/core/provider/FontsContractCompat$FontInfo;Landroid/content/ContentResolver;)Landroid/graphics/fonts/Font; PLandroidx/core/graphics/TypefaceCompatApi29Impl;->getFontFamily(Landroid/os/CancellationSignal;[Landroidx/core/provider/FontsContractCompat$FontInfo;Landroid/content/ContentResolver;)Landroid/graphics/fonts/FontFamily; PLandroidx/core/graphics/TypefaceCompatApi29Impl;->getFontFromProvider(Landroid/os/CancellationSignal;Landroidx/core/provider/FontsContractCompat$FontInfo;Landroid/content/ContentResolver;)Landroid/graphics/fonts/Font; PLandroidx/core/graphics/TypefaceCompatApi29Impl;->getMatchScore(Landroid/graphics/fonts/FontStyle;Landroid/graphics/fonts/FontStyle;)I -Landroidx/core/graphics/TypefaceCompatApi31Impl; -HSPLandroidx/core/graphics/TypefaceCompatApi31Impl;->()V -Landroidx/core/graphics/TypefaceCompatBaseImpl; -HSPLandroidx/core/graphics/TypefaceCompatBaseImpl;->()V +PLandroidx/core/graphics/TypefaceCompatApi31Impl;->()V +PLandroidx/core/graphics/TypefaceCompatBaseImpl;->()V PLandroidx/core/graphics/TypefaceCompatUtil;->mmap(Landroid/content/Context;Landroid/os/CancellationSignal;Landroid/net/Uri;)Ljava/nio/ByteBuffer; Landroidx/core/graphics/drawable/TintAwareDrawable; Landroidx/core/os/BundleCompat; @@ -12579,43 +12577,38 @@ HSPLandroidx/core/os/LocaleListPlatformWrapper;->isEmpty()Z HSPLandroidx/core/os/LocaleListPlatformWrapper;->size()I Landroidx/core/os/TraceCompat; HSPLandroidx/core/os/TraceCompat;->()V -PLandroidx/core/os/TraceCompat;->beginSection(Ljava/lang/String;)V -PLandroidx/core/os/TraceCompat;->endSection()V +HSPLandroidx/core/os/TraceCompat;->beginSection(Ljava/lang/String;)V +HSPLandroidx/core/os/TraceCompat;->endSection()V Landroidx/core/os/UserManagerCompat; HSPLandroidx/core/os/UserManagerCompat;->isUserUnlocked(Landroid/content/Context;)Z Landroidx/core/os/UserManagerCompat$Api24Impl; HSPLandroidx/core/os/UserManagerCompat$Api24Impl;->isUserUnlocked(Landroid/content/Context;)Z -Landroidx/core/provider/FontProvider; -HSPLandroidx/core/provider/FontProvider;->()V +PLandroidx/core/provider/FontProvider;->()V PLandroidx/core/provider/FontProvider;->convertToByteArrayList([Landroid/content/pm/Signature;)Ljava/util/List; PLandroidx/core/provider/FontProvider;->equalsByteArrayList(Ljava/util/List;Ljava/util/List;)Z -HSPLandroidx/core/provider/FontProvider;->getCertificates(Landroidx/core/provider/FontRequest;Landroid/content/res/Resources;)Ljava/util/List; -HSPLandroidx/core/provider/FontProvider;->getFontFamilyResult(Landroid/content/Context;Ljava/util/List;Landroid/os/CancellationSignal;)Landroidx/core/provider/FontsContractCompat$FontFamilyResult; -HSPLandroidx/core/provider/FontProvider;->getProvider(Landroid/content/pm/PackageManager;Landroidx/core/provider/FontRequest;Landroid/content/res/Resources;)Landroid/content/pm/ProviderInfo; +PLandroidx/core/provider/FontProvider;->getCertificates(Landroidx/core/provider/FontRequest;Landroid/content/res/Resources;)Ljava/util/List; +PLandroidx/core/provider/FontProvider;->getFontFamilyResult(Landroid/content/Context;Ljava/util/List;Landroid/os/CancellationSignal;)Landroidx/core/provider/FontsContractCompat$FontFamilyResult; +PLandroidx/core/provider/FontProvider;->getProvider(Landroid/content/pm/PackageManager;Landroidx/core/provider/FontRequest;Landroid/content/res/Resources;)Landroid/content/pm/ProviderInfo; PLandroidx/core/provider/FontProvider;->getSelectionArgs(Landroidx/core/provider/FontRequest;)[Ljava/lang/String; PLandroidx/core/provider/FontProvider;->query(Landroid/content/Context;Landroidx/core/provider/FontRequest;Ljava/lang/String;Landroid/os/CancellationSignal;)[Landroidx/core/provider/FontsContractCompat$FontInfo; -Landroidx/core/provider/FontProvider$$ExternalSyntheticLambda0; -HSPLandroidx/core/provider/FontProvider$$ExternalSyntheticLambda0;->()V +PLandroidx/core/provider/FontProvider$$ExternalSyntheticLambda0;->()V PLandroidx/core/provider/FontProvider$ContentQueryWrapper;->make(Landroid/content/Context;Landroid/net/Uri;)Landroidx/core/provider/FontProvider$ContentQueryWrapper; PLandroidx/core/provider/FontProvider$ContentQueryWrapperApi24Impl;->(Landroid/content/Context;Landroid/net/Uri;)V PLandroidx/core/provider/FontProvider$ContentQueryWrapperApi24Impl;->close()V PLandroidx/core/provider/FontProvider$ContentQueryWrapperApi24Impl;->query(Landroid/net/Uri;[Ljava/lang/String;Ljava/lang/String;[Ljava/lang/String;Ljava/lang/String;Landroid/os/CancellationSignal;)Landroid/database/Cursor; -Landroidx/core/provider/FontProvider$ProviderCacheKey; -HSPLandroidx/core/provider/FontProvider$ProviderCacheKey;->(Ljava/lang/String;Ljava/lang/String;Ljava/util/List;)V +PLandroidx/core/provider/FontProvider$ProviderCacheKey;->(Ljava/lang/String;Ljava/lang/String;Ljava/util/List;)V PLandroidx/core/provider/FontProvider$ProviderCacheKey;->hashCode()I -Landroidx/core/provider/FontRequest; -HSPLandroidx/core/provider/FontRequest;->(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/util/List;)V -HSPLandroidx/core/provider/FontRequest;->(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/util/List;Ljava/lang/String;Ljava/lang/String;)V -HSPLandroidx/core/provider/FontRequest;->createIdentifier(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String; -HSPLandroidx/core/provider/FontRequest;->getCertificates()Ljava/util/List; -HSPLandroidx/core/provider/FontRequest;->getProviderAuthority()Ljava/lang/String; -HSPLandroidx/core/provider/FontRequest;->getProviderPackage()Ljava/lang/String; +PLandroidx/core/provider/FontRequest;->(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/util/List;)V +PLandroidx/core/provider/FontRequest;->(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/util/List;Ljava/lang/String;Ljava/lang/String;)V +PLandroidx/core/provider/FontRequest;->createIdentifier(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String; +PLandroidx/core/provider/FontRequest;->getCertificates()Ljava/util/List; +PLandroidx/core/provider/FontRequest;->getProviderAuthority()Ljava/lang/String; +PLandroidx/core/provider/FontRequest;->getProviderPackage()Ljava/lang/String; PLandroidx/core/provider/FontRequest;->getQuery()Ljava/lang/String; -HSPLandroidx/core/provider/FontRequest;->getSystemFont()Ljava/lang/String; +PLandroidx/core/provider/FontRequest;->getSystemFont()Ljava/lang/String; PLandroidx/core/provider/FontRequest;->getVariationSettings()Ljava/lang/String; -Landroidx/core/provider/FontsContractCompat; PLandroidx/core/provider/FontsContractCompat;->buildTypeface(Landroid/content/Context;Landroid/os/CancellationSignal;[Landroidx/core/provider/FontsContractCompat$FontInfo;)Landroid/graphics/Typeface; -HSPLandroidx/core/provider/FontsContractCompat;->fetchFonts(Landroid/content/Context;Landroid/os/CancellationSignal;Landroidx/core/provider/FontRequest;)Landroidx/core/provider/FontsContractCompat$FontFamilyResult; +PLandroidx/core/provider/FontsContractCompat;->fetchFonts(Landroid/content/Context;Landroid/os/CancellationSignal;Landroidx/core/provider/FontRequest;)Landroidx/core/provider/FontsContractCompat$FontFamilyResult; PLandroidx/core/provider/FontsContractCompat$FontFamilyResult;->(ILjava/util/List;)V PLandroidx/core/provider/FontsContractCompat$FontFamilyResult;->create(ILjava/util/List;)Landroidx/core/provider/FontsContractCompat$FontFamilyResult; PLandroidx/core/provider/FontsContractCompat$FontFamilyResult;->getFonts()[Landroidx/core/provider/FontsContractCompat$FontInfo; @@ -12655,7 +12648,7 @@ Landroidx/core/util/Consumer; Landroidx/core/util/ObjectsCompat; HSPLandroidx/core/util/ObjectsCompat;->requireNonNull(Ljava/lang/Object;Ljava/lang/String;)Ljava/lang/Object; Landroidx/core/util/Preconditions; -PLandroidx/core/util/Preconditions;->checkArgument(ZLjava/lang/Object;)V +HSPLandroidx/core/util/Preconditions;->checkArgument(ZLjava/lang/Object;)V PLandroidx/core/util/Preconditions;->checkArgumentNonnegative(ILjava/lang/String;)I HSPLandroidx/core/util/Preconditions;->checkNotNull(Ljava/lang/Object;)Ljava/lang/Object; HSPLandroidx/core/util/Preconditions;->checkNotNull(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object; @@ -13024,6 +13017,7 @@ HSPLandroidx/datastore/core/DataStoreImpl$handleUpdate$2$1;->invoke(Lkotlinx/cor HSPLandroidx/datastore/core/DataStoreImpl$handleUpdate$2$1;->invokeSuspend(Ljava/lang/Object;)Ljava/lang/Object; Landroidx/datastore/core/DataStoreImpl$incrementCollector$1; HSPLandroidx/datastore/core/DataStoreImpl$incrementCollector$1;->(Landroidx/datastore/core/DataStoreImpl;Lkotlin/coroutines/Continuation;)V +HSPLandroidx/datastore/core/DataStoreImpl$incrementCollector$1;->invokeSuspend(Ljava/lang/Object;)Ljava/lang/Object; Landroidx/datastore/core/DataStoreImpl$incrementCollector$2$1; HSPLandroidx/datastore/core/DataStoreImpl$incrementCollector$2$1;->(Landroidx/datastore/core/DataStoreImpl;Lkotlin/coroutines/Continuation;)V HSPLandroidx/datastore/core/DataStoreImpl$incrementCollector$2$1;->create(Ljava/lang/Object;Lkotlin/coroutines/Continuation;)Lkotlin/coroutines/Continuation; @@ -13066,8 +13060,6 @@ HSPLandroidx/datastore/core/DataStoreImpl$transformAndWrite$2;->invokeSuspend(Lj Landroidx/datastore/core/DataStoreImpl$transformAndWrite$2$newData$1; HSPLandroidx/datastore/core/DataStoreImpl$transformAndWrite$2$newData$1;->(Lkotlin/jvm/functions/Function2;Landroidx/datastore/core/Data;Lkotlin/coroutines/Continuation;)V HSPLandroidx/datastore/core/DataStoreImpl$transformAndWrite$2$newData$1;->create(Ljava/lang/Object;Lkotlin/coroutines/Continuation;)Lkotlin/coroutines/Continuation; -HSPLandroidx/datastore/core/DataStoreImpl$transformAndWrite$2$newData$1;->invoke(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object; -HSPLandroidx/datastore/core/DataStoreImpl$transformAndWrite$2$newData$1;->invoke(Lkotlinx/coroutines/CoroutineScope;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; HSPLandroidx/datastore/core/DataStoreImpl$transformAndWrite$2$newData$1;->invokeSuspend(Ljava/lang/Object;)Ljava/lang/Object; Landroidx/datastore/core/DataStoreImpl$updateData$2; HSPLandroidx/datastore/core/DataStoreImpl$updateData$2;->(Landroidx/datastore/core/DataStoreImpl;Lkotlin/jvm/functions/Function2;Lkotlin/coroutines/Continuation;)V @@ -14112,31 +14104,26 @@ HSPLandroidx/emoji2/text/ConcurrencyHelpers;->createBackgroundPriorityExecutor(L HSPLandroidx/emoji2/text/ConcurrencyHelpers;->lambda$createBackgroundPriorityExecutor$0(Ljava/lang/String;Ljava/lang/Runnable;)Ljava/lang/Thread; HSPLandroidx/emoji2/text/ConcurrencyHelpers;->mainHandlerAsync()Landroid/os/Handler; Landroidx/emoji2/text/ConcurrencyHelpers$$ExternalSyntheticLambda1; -PLandroidx/emoji2/text/ConcurrencyHelpers$$ExternalSyntheticLambda1;->(Ljava/lang/String;)V +HSPLandroidx/emoji2/text/ConcurrencyHelpers$$ExternalSyntheticLambda1;->(Ljava/lang/String;)V HSPLandroidx/emoji2/text/ConcurrencyHelpers$$ExternalSyntheticLambda1;->newThread(Ljava/lang/Runnable;)Ljava/lang/Thread; Landroidx/emoji2/text/ConcurrencyHelpers$Handler28Impl; HSPLandroidx/emoji2/text/ConcurrencyHelpers$Handler28Impl;->createAsync(Landroid/os/Looper;)Landroid/os/Handler; -Landroidx/emoji2/text/DefaultEmojiCompatConfig; PLandroidx/emoji2/text/DefaultEmojiCompatConfig;->create(Landroid/content/Context;)Landroidx/emoji2/text/FontRequestEmojiCompatConfig; -Landroidx/emoji2/text/DefaultEmojiCompatConfig$DefaultEmojiCompatConfigFactory; PLandroidx/emoji2/text/DefaultEmojiCompatConfig$DefaultEmojiCompatConfigFactory;->(Landroidx/emoji2/text/DefaultEmojiCompatConfig$DefaultEmojiCompatConfigHelper;)V PLandroidx/emoji2/text/DefaultEmojiCompatConfig$DefaultEmojiCompatConfigFactory;->configOrNull(Landroid/content/Context;Landroidx/core/provider/FontRequest;)Landroidx/emoji2/text/EmojiCompat$Config; -HSPLandroidx/emoji2/text/DefaultEmojiCompatConfig$DefaultEmojiCompatConfigFactory;->convertToByteArray([Landroid/content/pm/Signature;)Ljava/util/List; +PLandroidx/emoji2/text/DefaultEmojiCompatConfig$DefaultEmojiCompatConfigFactory;->convertToByteArray([Landroid/content/pm/Signature;)Ljava/util/List; PLandroidx/emoji2/text/DefaultEmojiCompatConfig$DefaultEmojiCompatConfigFactory;->create(Landroid/content/Context;)Landroidx/emoji2/text/EmojiCompat$Config; -HSPLandroidx/emoji2/text/DefaultEmojiCompatConfig$DefaultEmojiCompatConfigFactory;->generateFontRequestFrom(Landroid/content/pm/ProviderInfo;Landroid/content/pm/PackageManager;)Landroidx/core/provider/FontRequest; +PLandroidx/emoji2/text/DefaultEmojiCompatConfig$DefaultEmojiCompatConfigFactory;->generateFontRequestFrom(Landroid/content/pm/ProviderInfo;Landroid/content/pm/PackageManager;)Landroidx/core/provider/FontRequest; PLandroidx/emoji2/text/DefaultEmojiCompatConfig$DefaultEmojiCompatConfigFactory;->getHelperForApi()Landroidx/emoji2/text/DefaultEmojiCompatConfig$DefaultEmojiCompatConfigHelper; -HSPLandroidx/emoji2/text/DefaultEmojiCompatConfig$DefaultEmojiCompatConfigFactory;->hasFlagSystem(Landroid/content/pm/ProviderInfo;)Z -HSPLandroidx/emoji2/text/DefaultEmojiCompatConfig$DefaultEmojiCompatConfigFactory;->queryDefaultInstalledContentProvider(Landroid/content/pm/PackageManager;)Landroid/content/pm/ProviderInfo; -HSPLandroidx/emoji2/text/DefaultEmojiCompatConfig$DefaultEmojiCompatConfigFactory;->queryForDefaultFontRequest(Landroid/content/Context;)Landroidx/core/provider/FontRequest; -Landroidx/emoji2/text/DefaultEmojiCompatConfig$DefaultEmojiCompatConfigHelper; +PLandroidx/emoji2/text/DefaultEmojiCompatConfig$DefaultEmojiCompatConfigFactory;->hasFlagSystem(Landroid/content/pm/ProviderInfo;)Z +PLandroidx/emoji2/text/DefaultEmojiCompatConfig$DefaultEmojiCompatConfigFactory;->queryDefaultInstalledContentProvider(Landroid/content/pm/PackageManager;)Landroid/content/pm/ProviderInfo; +PLandroidx/emoji2/text/DefaultEmojiCompatConfig$DefaultEmojiCompatConfigFactory;->queryForDefaultFontRequest(Landroid/content/Context;)Landroidx/core/provider/FontRequest; PLandroidx/emoji2/text/DefaultEmojiCompatConfig$DefaultEmojiCompatConfigHelper;->()V -Landroidx/emoji2/text/DefaultEmojiCompatConfig$DefaultEmojiCompatConfigHelper_API19; PLandroidx/emoji2/text/DefaultEmojiCompatConfig$DefaultEmojiCompatConfigHelper_API19;->()V -HSPLandroidx/emoji2/text/DefaultEmojiCompatConfig$DefaultEmojiCompatConfigHelper_API19;->getProviderInfo(Landroid/content/pm/ResolveInfo;)Landroid/content/pm/ProviderInfo; -HSPLandroidx/emoji2/text/DefaultEmojiCompatConfig$DefaultEmojiCompatConfigHelper_API19;->queryIntentContentProviders(Landroid/content/pm/PackageManager;Landroid/content/Intent;I)Ljava/util/List; -Landroidx/emoji2/text/DefaultEmojiCompatConfig$DefaultEmojiCompatConfigHelper_API28; +PLandroidx/emoji2/text/DefaultEmojiCompatConfig$DefaultEmojiCompatConfigHelper_API19;->getProviderInfo(Landroid/content/pm/ResolveInfo;)Landroid/content/pm/ProviderInfo; +PLandroidx/emoji2/text/DefaultEmojiCompatConfig$DefaultEmojiCompatConfigHelper_API19;->queryIntentContentProviders(Landroid/content/pm/PackageManager;Landroid/content/Intent;I)Ljava/util/List; PLandroidx/emoji2/text/DefaultEmojiCompatConfig$DefaultEmojiCompatConfigHelper_API28;->()V -HSPLandroidx/emoji2/text/DefaultEmojiCompatConfig$DefaultEmojiCompatConfigHelper_API28;->getSigningSignatures(Landroid/content/pm/PackageManager;Ljava/lang/String;)[Landroid/content/pm/Signature; +PLandroidx/emoji2/text/DefaultEmojiCompatConfig$DefaultEmojiCompatConfigHelper_API28;->getSigningSignatures(Landroid/content/pm/PackageManager;Ljava/lang/String;)[Landroid/content/pm/Signature; Landroidx/emoji2/text/DefaultGlyphChecker; HSPLandroidx/emoji2/text/DefaultGlyphChecker;->()V HSPLandroidx/emoji2/text/DefaultGlyphChecker;->()V @@ -14163,7 +14150,7 @@ HSPLandroidx/emoji2/text/EmojiCompat$CompatInternal19;->loadMetadata()V PLandroidx/emoji2/text/EmojiCompat$CompatInternal19;->onMetadataLoadSuccess(Landroidx/emoji2/text/MetadataRepo;)V PLandroidx/emoji2/text/EmojiCompat$CompatInternal19;->process(Ljava/lang/CharSequence;IIIZ)Ljava/lang/CharSequence; Landroidx/emoji2/text/EmojiCompat$CompatInternal19$1; -PLandroidx/emoji2/text/EmojiCompat$CompatInternal19$1;->(Landroidx/emoji2/text/EmojiCompat$CompatInternal19;)V +HSPLandroidx/emoji2/text/EmojiCompat$CompatInternal19$1;->(Landroidx/emoji2/text/EmojiCompat$CompatInternal19;)V PLandroidx/emoji2/text/EmojiCompat$CompatInternal19$1;->onLoaded(Landroidx/emoji2/text/MetadataRepo;)V Landroidx/emoji2/text/EmojiCompat$Config; HSPLandroidx/emoji2/text/EmojiCompat$Config;->(Landroidx/emoji2/text/EmojiCompat$MetadataRepoLoader;)V @@ -14179,7 +14166,7 @@ PLandroidx/emoji2/text/EmojiCompat$ListenerDispatcher;->(Ljava/util/Collec PLandroidx/emoji2/text/EmojiCompat$ListenerDispatcher;->run()V Landroidx/emoji2/text/EmojiCompat$MetadataRepoLoader; Landroidx/emoji2/text/EmojiCompat$MetadataRepoLoaderCallback; -PLandroidx/emoji2/text/EmojiCompat$MetadataRepoLoaderCallback;->()V +HSPLandroidx/emoji2/text/EmojiCompat$MetadataRepoLoaderCallback;->()V Landroidx/emoji2/text/EmojiCompat$SpanFactory; Landroidx/emoji2/text/EmojiCompatInitializer; HSPLandroidx/emoji2/text/EmojiCompatInitializer;->()V @@ -14199,9 +14186,8 @@ HSPLandroidx/emoji2/text/EmojiCompatInitializer$BackgroundDefaultLoader;->doLoad HSPLandroidx/emoji2/text/EmojiCompatInitializer$BackgroundDefaultLoader;->lambda$load$0$androidx-emoji2-text-EmojiCompatInitializer$BackgroundDefaultLoader(Landroidx/emoji2/text/EmojiCompat$MetadataRepoLoaderCallback;Ljava/util/concurrent/ThreadPoolExecutor;)V HSPLandroidx/emoji2/text/EmojiCompatInitializer$BackgroundDefaultLoader;->load(Landroidx/emoji2/text/EmojiCompat$MetadataRepoLoaderCallback;)V Landroidx/emoji2/text/EmojiCompatInitializer$BackgroundDefaultLoader$$ExternalSyntheticLambda0; -PLandroidx/emoji2/text/EmojiCompatInitializer$BackgroundDefaultLoader$$ExternalSyntheticLambda0;->(Landroidx/emoji2/text/EmojiCompatInitializer$BackgroundDefaultLoader;Landroidx/emoji2/text/EmojiCompat$MetadataRepoLoaderCallback;Ljava/util/concurrent/ThreadPoolExecutor;)V +HSPLandroidx/emoji2/text/EmojiCompatInitializer$BackgroundDefaultLoader$$ExternalSyntheticLambda0;->(Landroidx/emoji2/text/EmojiCompatInitializer$BackgroundDefaultLoader;Landroidx/emoji2/text/EmojiCompat$MetadataRepoLoaderCallback;Ljava/util/concurrent/ThreadPoolExecutor;)V HSPLandroidx/emoji2/text/EmojiCompatInitializer$BackgroundDefaultLoader$$ExternalSyntheticLambda0;->run()V -Landroidx/emoji2/text/EmojiCompatInitializer$BackgroundDefaultLoader$1; PLandroidx/emoji2/text/EmojiCompatInitializer$BackgroundDefaultLoader$1;->(Landroidx/emoji2/text/EmojiCompatInitializer$BackgroundDefaultLoader;Landroidx/emoji2/text/EmojiCompat$MetadataRepoLoaderCallback;Ljava/util/concurrent/ThreadPoolExecutor;)V PLandroidx/emoji2/text/EmojiCompatInitializer$BackgroundDefaultLoader$1;->onLoaded(Landroidx/emoji2/text/MetadataRepo;)V Landroidx/emoji2/text/EmojiCompatInitializer$LoadEmojiCompatRunnable; @@ -14221,25 +14207,21 @@ PLandroidx/emoji2/text/EmojiProcessor$ProcessorSm;->(Landroidx/emoji2/text PLandroidx/emoji2/text/EmojiProcessor$ProcessorSm;->check(I)I PLandroidx/emoji2/text/EmojiProcessor$ProcessorSm;->isInFlushableState()Z PLandroidx/emoji2/text/EmojiProcessor$ProcessorSm;->reset()I -Landroidx/emoji2/text/FontRequestEmojiCompatConfig; -HSPLandroidx/emoji2/text/FontRequestEmojiCompatConfig;->()V +PLandroidx/emoji2/text/FontRequestEmojiCompatConfig;->()V PLandroidx/emoji2/text/FontRequestEmojiCompatConfig;->(Landroid/content/Context;Landroidx/core/provider/FontRequest;)V -HSPLandroidx/emoji2/text/FontRequestEmojiCompatConfig;->setLoadingExecutor(Ljava/util/concurrent/Executor;)Landroidx/emoji2/text/FontRequestEmojiCompatConfig; -Landroidx/emoji2/text/FontRequestEmojiCompatConfig$FontProviderHelper; -HSPLandroidx/emoji2/text/FontRequestEmojiCompatConfig$FontProviderHelper;->()V +PLandroidx/emoji2/text/FontRequestEmojiCompatConfig;->setLoadingExecutor(Ljava/util/concurrent/Executor;)Landroidx/emoji2/text/FontRequestEmojiCompatConfig; +PLandroidx/emoji2/text/FontRequestEmojiCompatConfig$FontProviderHelper;->()V PLandroidx/emoji2/text/FontRequestEmojiCompatConfig$FontProviderHelper;->buildTypeface(Landroid/content/Context;Landroidx/core/provider/FontsContractCompat$FontInfo;)Landroid/graphics/Typeface; -HSPLandroidx/emoji2/text/FontRequestEmojiCompatConfig$FontProviderHelper;->fetchFonts(Landroid/content/Context;Landroidx/core/provider/FontRequest;)Landroidx/core/provider/FontsContractCompat$FontFamilyResult; -Landroidx/emoji2/text/FontRequestEmojiCompatConfig$FontRequestMetadataLoader; +PLandroidx/emoji2/text/FontRequestEmojiCompatConfig$FontProviderHelper;->fetchFonts(Landroid/content/Context;Landroidx/core/provider/FontRequest;)Landroidx/core/provider/FontsContractCompat$FontFamilyResult; PLandroidx/emoji2/text/FontRequestEmojiCompatConfig$FontRequestMetadataLoader;->(Landroid/content/Context;Landroidx/core/provider/FontRequest;Landroidx/emoji2/text/FontRequestEmojiCompatConfig$FontProviderHelper;)V PLandroidx/emoji2/text/FontRequestEmojiCompatConfig$FontRequestMetadataLoader;->cleanUp()V -HSPLandroidx/emoji2/text/FontRequestEmojiCompatConfig$FontRequestMetadataLoader;->createMetadata()V -HSPLandroidx/emoji2/text/FontRequestEmojiCompatConfig$FontRequestMetadataLoader;->load(Landroidx/emoji2/text/EmojiCompat$MetadataRepoLoaderCallback;)V -HSPLandroidx/emoji2/text/FontRequestEmojiCompatConfig$FontRequestMetadataLoader;->loadInternal()V -HSPLandroidx/emoji2/text/FontRequestEmojiCompatConfig$FontRequestMetadataLoader;->retrieveFontInfo()Landroidx/core/provider/FontsContractCompat$FontInfo; -HSPLandroidx/emoji2/text/FontRequestEmojiCompatConfig$FontRequestMetadataLoader;->setExecutor(Ljava/util/concurrent/Executor;)V -Landroidx/emoji2/text/FontRequestEmojiCompatConfig$FontRequestMetadataLoader$$ExternalSyntheticLambda0; -HSPLandroidx/emoji2/text/FontRequestEmojiCompatConfig$FontRequestMetadataLoader$$ExternalSyntheticLambda0;->(Landroidx/emoji2/text/FontRequestEmojiCompatConfig$FontRequestMetadataLoader;)V -HSPLandroidx/emoji2/text/FontRequestEmojiCompatConfig$FontRequestMetadataLoader$$ExternalSyntheticLambda0;->run()V +PLandroidx/emoji2/text/FontRequestEmojiCompatConfig$FontRequestMetadataLoader;->createMetadata()V +PLandroidx/emoji2/text/FontRequestEmojiCompatConfig$FontRequestMetadataLoader;->load(Landroidx/emoji2/text/EmojiCompat$MetadataRepoLoaderCallback;)V +PLandroidx/emoji2/text/FontRequestEmojiCompatConfig$FontRequestMetadataLoader;->loadInternal()V +PLandroidx/emoji2/text/FontRequestEmojiCompatConfig$FontRequestMetadataLoader;->retrieveFontInfo()Landroidx/core/provider/FontsContractCompat$FontInfo; +PLandroidx/emoji2/text/FontRequestEmojiCompatConfig$FontRequestMetadataLoader;->setExecutor(Ljava/util/concurrent/Executor;)V +PLandroidx/emoji2/text/FontRequestEmojiCompatConfig$FontRequestMetadataLoader$$ExternalSyntheticLambda0;->(Landroidx/emoji2/text/FontRequestEmojiCompatConfig$FontRequestMetadataLoader;)V +PLandroidx/emoji2/text/FontRequestEmojiCompatConfig$FontRequestMetadataLoader$$ExternalSyntheticLambda0;->run()V PLandroidx/emoji2/text/MetadataListReader;->findOffsetInfo(Landroidx/emoji2/text/MetadataListReader$OpenTypeReader;)Landroidx/emoji2/text/MetadataListReader$OffsetInfo; PLandroidx/emoji2/text/MetadataListReader;->read(Ljava/nio/ByteBuffer;)Landroidx/emoji2/text/flatbuffer/MetadataList; PLandroidx/emoji2/text/MetadataListReader;->toUnsignedInt(I)J @@ -15052,7 +15034,97 @@ HSPLandroidx/media3/muxer/AacWriter$$ExternalSyntheticBackport0;->m(Ljava/lang/O HSPLandroidx/media3/muxer/AacWriter$$ExternalSyntheticBackport0;->m(Ljava/lang/Object;)V HSPLandroidx/media3/muxer/AacWriter$$ExternalSyntheticBackport0;->m([Ljava/lang/Object;)Ljava/util/List; HSPLandroidx/media3/muxer/AacWriter$$ExternalSyntheticBackport0;->m([Ljava/lang/Object;)Ljava/util/Set; +Landroidx/metrics/performance/DelegatingFrameMetricsListener; +HSPLandroidx/metrics/performance/DelegatingFrameMetricsListener;->()V +HSPLandroidx/metrics/performance/DelegatingFrameMetricsListener;->(Ljava/util/List;)V +HSPLandroidx/metrics/performance/DelegatingFrameMetricsListener;->access$getFrameMetricsHandler$cp()Landroid/os/Handler; +HSPLandroidx/metrics/performance/DelegatingFrameMetricsListener;->access$setFrameMetricsHandler$cp(Landroid/os/Handler;)V +HSPLandroidx/metrics/performance/DelegatingFrameMetricsListener;->onFrameMetricsAvailable(Landroid/view/Window;Landroid/view/FrameMetrics;I)V +Landroidx/metrics/performance/DelegatingFrameMetricsListener$Companion; +HSPLandroidx/metrics/performance/DelegatingFrameMetricsListener$Companion;->()V +HSPLandroidx/metrics/performance/DelegatingFrameMetricsListener$Companion;->(Lkotlin/jvm/internal/DefaultConstructorMarker;)V +HSPLandroidx/metrics/performance/DelegatingFrameMetricsListener$Companion;->addDelegateToWindow(Landroid/view/Window;Landroid/view/Window$OnFrameMetricsAvailableListener;)Z +HSPLandroidx/metrics/performance/DelegatingFrameMetricsListener$Companion;->getFrameMetricsHandler$metrics_performance_release()Landroid/os/Handler; +HSPLandroidx/metrics/performance/DelegatingFrameMetricsListener$Companion;->setFrameMetricsHandler$metrics_performance_release(Landroid/os/Handler;)V +Landroidx/metrics/performance/FrameData; +HSPLandroidx/metrics/performance/FrameData;->(JJZLjava/util/List;)V +PLandroidx/metrics/performance/FrameData;->getFrameDurationUiNanos()J +PLandroidx/metrics/performance/FrameData;->getFrameStartNanos()J +PLandroidx/metrics/performance/FrameData;->isJank()Z +PLandroidx/metrics/performance/FrameData;->update$metrics_performance_release(JJZ)V +Landroidx/metrics/performance/FrameDataApi24; +HSPLandroidx/metrics/performance/FrameDataApi24;->(JJJZLjava/util/List;)V +PLandroidx/metrics/performance/FrameDataApi24;->update$metrics_performance_release(JJJZ)V +Landroidx/metrics/performance/FrameDataApi31; +HSPLandroidx/metrics/performance/FrameDataApi31;->(JJJJJZLjava/util/List;)V +PLandroidx/metrics/performance/FrameDataApi31;->update$metrics_performance_release(JJJJJZ)V +Landroidx/metrics/performance/JankStats; +HSPLandroidx/metrics/performance/JankStats;->()V +HSPLandroidx/metrics/performance/JankStats;->(Landroid/view/Window;Landroidx/metrics/performance/JankStats$OnFrameListener;)V +HSPLandroidx/metrics/performance/JankStats;->(Landroid/view/Window;Landroidx/metrics/performance/JankStats$OnFrameListener;Lkotlin/jvm/internal/DefaultConstructorMarker;)V +PLandroidx/metrics/performance/JankStats;->getJankHeuristicMultiplier()F +PLandroidx/metrics/performance/JankStats;->logFrameData$metrics_performance_release(Landroidx/metrics/performance/FrameData;)V +Landroidx/metrics/performance/JankStats$Companion; +HSPLandroidx/metrics/performance/JankStats$Companion;->()V +HSPLandroidx/metrics/performance/JankStats$Companion;->(Lkotlin/jvm/internal/DefaultConstructorMarker;)V +HSPLandroidx/metrics/performance/JankStats$Companion;->createAndTrack(Landroid/view/Window;Landroidx/metrics/performance/JankStats$OnFrameListener;)Landroidx/metrics/performance/JankStats; Landroidx/metrics/performance/JankStats$OnFrameListener; +Landroidx/metrics/performance/JankStatsApi16Impl; +HSPLandroidx/metrics/performance/JankStatsApi16Impl;->(Landroidx/metrics/performance/JankStats;Landroid/view/View;)V +PLandroidx/metrics/performance/JankStatsApi16Impl;->getMetricsStateHolder()Landroidx/metrics/performance/PerformanceMetricsState$Holder; +HSPLandroidx/metrics/performance/JankStatsApi16Impl;->getStateInfo()Ljava/util/List; +Landroidx/metrics/performance/JankStatsApi16Impl$onFrameListenerDelegate$1; +HSPLandroidx/metrics/performance/JankStatsApi16Impl$onFrameListenerDelegate$1;->(Landroidx/metrics/performance/JankStats;Landroidx/metrics/performance/JankStatsApi16Impl;)V +Landroidx/metrics/performance/JankStatsApi24Impl; +HSPLandroidx/metrics/performance/JankStatsApi24Impl;->(Landroidx/metrics/performance/JankStats;Landroid/view/View;Landroid/view/Window;)V +HSPLandroidx/metrics/performance/JankStatsApi24Impl;->frameMetricsAvailableListenerDelegate$lambda$0(Landroidx/metrics/performance/JankStatsApi24Impl;Landroidx/metrics/performance/JankStats;Landroid/view/Window;Landroid/view/FrameMetrics;I)V +PLandroidx/metrics/performance/JankStatsApi24Impl;->getPrevEnd()J +PLandroidx/metrics/performance/JankStatsApi24Impl;->setPrevEnd(J)V +HSPLandroidx/metrics/performance/JankStatsApi24Impl;->setupFrameTimer$lambda$1(ZLandroidx/metrics/performance/JankStatsApi24Impl;)V +HSPLandroidx/metrics/performance/JankStatsApi24Impl;->setupFrameTimer(Z)V +Landroidx/metrics/performance/JankStatsApi24Impl$$ExternalSyntheticLambda0; +HSPLandroidx/metrics/performance/JankStatsApi24Impl$$ExternalSyntheticLambda0;->(ZLandroidx/metrics/performance/JankStatsApi24Impl;)V +HSPLandroidx/metrics/performance/JankStatsApi24Impl$$ExternalSyntheticLambda0;->run()V +Landroidx/metrics/performance/JankStatsApi24Impl$$ExternalSyntheticLambda1; +HSPLandroidx/metrics/performance/JankStatsApi24Impl$$ExternalSyntheticLambda1;->(Landroidx/metrics/performance/JankStatsApi24Impl;Landroidx/metrics/performance/JankStats;)V +HSPLandroidx/metrics/performance/JankStatsApi24Impl$$ExternalSyntheticLambda1;->onFrameMetricsAvailable(Landroid/view/Window;Landroid/view/FrameMetrics;I)V +Landroidx/metrics/performance/JankStatsApi26Impl; +HSPLandroidx/metrics/performance/JankStatsApi26Impl;->(Landroidx/metrics/performance/JankStats;Landroid/view/View;Landroid/view/Window;)V +HSPLandroidx/metrics/performance/JankStatsApi26Impl;->getFrameStartTime$metrics_performance_release(Landroid/view/FrameMetrics;)J +Landroidx/metrics/performance/JankStatsApi31Impl; +HSPLandroidx/metrics/performance/JankStatsApi31Impl;->(Landroidx/metrics/performance/JankStats;Landroid/view/View;Landroid/view/Window;)V +PLandroidx/metrics/performance/JankStatsApi31Impl;->getExpectedFrameDuration(Landroid/view/FrameMetrics;)J +PLandroidx/metrics/performance/JankStatsApi31Impl;->getFrameData$metrics_performance_release(JJLandroid/view/FrameMetrics;)Landroidx/metrics/performance/FrameDataApi24; +PLandroidx/metrics/performance/JankStatsApi31Impl;->getFrameData$metrics_performance_release(JJLandroid/view/FrameMetrics;)Landroidx/metrics/performance/FrameDataApi31; +Landroidx/metrics/performance/JankStatsBaseImpl; +HSPLandroidx/metrics/performance/JankStatsBaseImpl;->()V +HSPLandroidx/metrics/performance/JankStatsBaseImpl;->(Landroidx/metrics/performance/JankStats;)V +Landroidx/metrics/performance/JankStatsBaseImpl$Companion; +HSPLandroidx/metrics/performance/JankStatsBaseImpl$Companion;->()V +HSPLandroidx/metrics/performance/JankStatsBaseImpl$Companion;->(Lkotlin/jvm/internal/DefaultConstructorMarker;)V +Landroidx/metrics/performance/OnFrameListenerDelegate; +HSPLandroidx/metrics/performance/OnFrameListenerDelegate;->()V +Landroidx/metrics/performance/PerformanceMetricsState; +HSPLandroidx/metrics/performance/PerformanceMetricsState;->()V +HSPLandroidx/metrics/performance/PerformanceMetricsState;->()V +HSPLandroidx/metrics/performance/PerformanceMetricsState;->(Lkotlin/jvm/internal/DefaultConstructorMarker;)V +PLandroidx/metrics/performance/PerformanceMetricsState;->addFrameState(JJLjava/util/List;Ljava/util/List;)V +PLandroidx/metrics/performance/PerformanceMetricsState;->getIntervalStates$metrics_performance_release(JJLjava/util/List;)V +Landroidx/metrics/performance/PerformanceMetricsState$Companion; +HSPLandroidx/metrics/performance/PerformanceMetricsState$Companion;->()V +HSPLandroidx/metrics/performance/PerformanceMetricsState$Companion;->(Lkotlin/jvm/internal/DefaultConstructorMarker;)V +HSPLandroidx/metrics/performance/PerformanceMetricsState$Companion;->create$metrics_performance_release(Landroid/view/View;)Landroidx/metrics/performance/PerformanceMetricsState$Holder; +HSPLandroidx/metrics/performance/PerformanceMetricsState$Companion;->getHolderForHierarchy(Landroid/view/View;)Landroidx/metrics/performance/PerformanceMetricsState$Holder; +Landroidx/metrics/performance/PerformanceMetricsState$Holder; +HSPLandroidx/metrics/performance/PerformanceMetricsState$Holder;->()V +HSPLandroidx/metrics/performance/PerformanceMetricsState$Holder;->getState()Landroidx/metrics/performance/PerformanceMetricsState; +HSPLandroidx/metrics/performance/PerformanceMetricsState$Holder;->setState$metrics_performance_release(Landroidx/metrics/performance/PerformanceMetricsState;)V +Landroidx/metrics/performance/R$id; +Landroidx/navigation/ActivityNavigator$Destination; +Landroidx/navigation/FloatingWindow; +Landroidx/navigation/NavDestination; +Landroidx/navigation/fragment/DialogFragmentNavigator$Destination; +Landroidx/navigation/fragment/FragmentNavigator$Destination; Landroidx/navigation3/ListUtilsKt; HSPLandroidx/navigation3/ListUtilsKt;->fastAnyOrAny(Ljava/util/List;Lkotlin/jvm/functions/Function1;)Z Landroidx/navigation3/runtime/DecoratedNavEntriesKt; @@ -15093,10 +15165,10 @@ Landroidx/navigation3/runtime/EntryProviderScope; HSPLandroidx/navigation3/runtime/EntryProviderScope;->()V HSPLandroidx/navigation3/runtime/EntryProviderScope;->(Lkotlin/jvm/functions/Function1;)V HSPLandroidx/navigation3/runtime/EntryProviderScope;->addEntryProvider(Lkotlin/reflect/KClass;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function3;)V -HSPLandroidx/navigation3/runtime/EntryProviderScope;->build$lambda$0(Landroidx/navigation3/runtime/EntryProviderScope;Ljava/lang/Object;)Landroidx/navigation3/runtime/NavEntry; +HSPLandroidx/navigation3/runtime/EntryProviderScope;->build$lambda$0(Landroidx/navigation3/runtime/EntryProviderScope;Ljava/util/Map;Ljava/lang/Object;)Landroidx/navigation3/runtime/NavEntry; HSPLandroidx/navigation3/runtime/EntryProviderScope;->build()Lkotlin/jvm/functions/Function1; Landroidx/navigation3/runtime/EntryProviderScope$$ExternalSyntheticLambda5; -HSPLandroidx/navigation3/runtime/EntryProviderScope$$ExternalSyntheticLambda5;->(Landroidx/navigation3/runtime/EntryProviderScope;)V +HSPLandroidx/navigation3/runtime/EntryProviderScope$$ExternalSyntheticLambda5;->(Landroidx/navigation3/runtime/EntryProviderScope;Ljava/util/Map;)V HSPLandroidx/navigation3/runtime/EntryProviderScope$$ExternalSyntheticLambda5;->invoke(Ljava/lang/Object;)Ljava/lang/Object; Landroidx/navigation3/runtime/ListUtilsKt; HSPLandroidx/navigation3/runtime/ListUtilsKt;->fastDistinctOrDistinct(Ljava/util/List;)Ljava/util/List; @@ -15210,7 +15282,15 @@ HSPLandroidx/navigation3/scene/DialogSceneStrategy$Companion;->(Lkotlin/jv Landroidx/navigation3/scene/DialogSceneStrategy$Companion$DialogKey; HSPLandroidx/navigation3/scene/DialogSceneStrategy$Companion$DialogKey;->()V HSPLandroidx/navigation3/scene/DialogSceneStrategy$Companion$DialogKey;->()V +Landroidx/navigation3/scene/NavigationEventHandlerKt; +HSPLandroidx/navigation3/scene/NavigationEventHandlerKt;->NavigationBackHandler(Landroidx/navigation3/scene/SceneState;Landroidx/navigationevent/compose/NavigationEventState;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function0;Landroidx/compose/runtime/Composer;II)V +Landroidx/navigation3/scene/NavigationEventHandlerKt$$ExternalSyntheticLambda0; +HSPLandroidx/navigation3/scene/NavigationEventHandlerKt$$ExternalSyntheticLambda0;->()V +Landroidx/navigation3/scene/NavigationEventHandlerKt$$ExternalSyntheticLambda1; +HSPLandroidx/navigation3/scene/NavigationEventHandlerKt$$ExternalSyntheticLambda1;->(Landroidx/navigation3/scene/SceneState;Lkotlin/jvm/functions/Function0;)V Landroidx/navigation3/scene/OverlayScene; +Landroidx/navigation3/scene/RememberNavigationEventStateKt; +HSPLandroidx/navigation3/scene/RememberNavigationEventStateKt;->rememberNavigationEventState(Landroidx/navigation3/scene/SceneState;Landroidx/compose/runtime/Composer;I)Landroidx/navigationevent/compose/NavigationEventState; Landroidx/navigation3/scene/Scene; HSPLandroidx/navigation3/scene/Scene;->getMetadata()Ljava/util/Map; Landroidx/navigation3/scene/SceneDecoratorStrategyScope; @@ -15309,16 +15389,16 @@ HSPLandroidx/navigation3/ui/NavDisplayKt;->NavDisplay(Landroidx/navigation3/scen HSPLandroidx/navigation3/ui/NavDisplayKt;->NavDisplay(Ljava/util/List;Landroidx/compose/ui/Modifier;Landroidx/compose/ui/Alignment;Ljava/util/List;Ljava/util/List;Landroidx/compose/animation/SharedTransitionScope;Landroidx/compose/animation/SizeTransform;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function0;Landroidx/compose/runtime/Composer;III)V HSPLandroidx/navigation3/ui/NavDisplayKt;->NavDisplay(Ljava/util/List;Landroidx/compose/ui/Modifier;Landroidx/compose/ui/Alignment;Lkotlin/jvm/functions/Function0;Ljava/util/List;Ljava/util/List;Ljava/util/List;Landroidx/compose/animation/SharedTransitionScope;Landroidx/compose/animation/SizeTransform;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function1;Landroidx/compose/runtime/Composer;III)V Landroidx/navigation3/ui/NavDisplayKt__NavDisplayKt; -HSPLandroidx/navigation3/ui/NavDisplayKt__NavDisplayKt;->$r8$lambda$6WUP_MmF2ACzmwTrp2aBzvnS0Ug(Landroidx/navigation3/scene/Scene;)Ljava/lang/Object; -HSPLandroidx/navigation3/ui/NavDisplayKt__NavDisplayKt;->$r8$lambda$6meK3rcdj_SbQiBBexE_CVd-d90(Landroidx/compose/animation/core/Transition;Landroidx/compose/runtime/snapshots/SnapshotStateList;Lkotlin/jvm/internal/Ref$ObjectRef;Ljava/util/Map;Landroidx/compose/animation/AnimatedContentScope;Landroidx/navigation3/scene/Scene;Landroidx/compose/runtime/Composer;I)Lkotlin/Unit; -HSPLandroidx/navigation3/ui/NavDisplayKt__NavDisplayKt;->$r8$lambda$DEpDt4b-_73nHf6rDOOBatOXNV8(ZLandroidx/navigation3/scene/Scene;ILkotlin/jvm/functions/Function2;ZLkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Landroidx/compose/animation/AnimatedContentTransitionScope;)Landroidx/compose/animation/ContentTransform; -HSPLandroidx/navigation3/ui/NavDisplayKt__NavDisplayKt;->$r8$lambda$OO9GgxnLAIBfMXURmhWq76Z3KmY(Lkotlin/jvm/functions/Function1;FLandroidx/compose/animation/SizeTransform;Landroidx/compose/animation/AnimatedContentTransitionScope;)Landroidx/compose/animation/ContentTransform; -HSPLandroidx/navigation3/ui/NavDisplayKt__NavDisplayKt;->$r8$lambda$XP_-QN-B82CWFFU8xBP8sTt5GYo(Landroidx/navigation3/scene/Scene;Landroidx/compose/runtime/Composer;I)Lkotlin/Unit; -HSPLandroidx/navigation3/ui/NavDisplayKt__NavDisplayKt;->NavDisplay$lambda$22$0$NavDisplayKt__NavDisplayKt(ZLandroidx/navigation3/scene/Scene;ILkotlin/jvm/functions/Function2;ZLkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Landroidx/compose/animation/AnimatedContentTransitionScope;)Landroidx/compose/animation/ContentTransform; -HSPLandroidx/navigation3/ui/NavDisplayKt__NavDisplayKt;->NavDisplay$lambda$23$0$NavDisplayKt__NavDisplayKt(Lkotlin/jvm/functions/Function1;FLandroidx/compose/animation/SizeTransform;Landroidx/compose/animation/AnimatedContentTransitionScope;)Landroidx/compose/animation/ContentTransform; -HSPLandroidx/navigation3/ui/NavDisplayKt__NavDisplayKt;->NavDisplay$lambda$24$0$NavDisplayKt__NavDisplayKt(Landroidx/navigation3/scene/Scene;)Ljava/lang/Object; -HSPLandroidx/navigation3/ui/NavDisplayKt__NavDisplayKt;->NavDisplay$lambda$25$1$NavDisplayKt__NavDisplayKt(Landroidx/navigation3/scene/Scene;Landroidx/compose/runtime/Composer;I)Lkotlin/Unit; -HSPLandroidx/navigation3/ui/NavDisplayKt__NavDisplayKt;->NavDisplay$lambda$25$NavDisplayKt__NavDisplayKt(Landroidx/compose/animation/core/Transition;Landroidx/compose/runtime/snapshots/SnapshotStateList;Lkotlin/jvm/internal/Ref$ObjectRef;Ljava/util/Map;Landroidx/compose/animation/AnimatedContentScope;Landroidx/navigation3/scene/Scene;Landroidx/compose/runtime/Composer;I)Lkotlin/Unit; +HSPLandroidx/navigation3/ui/NavDisplayKt__NavDisplayKt;->$r8$lambda$3YRipLXgCT84bSQWf9dBuZEeUw8(ZLandroidx/navigation3/scene/Scene;ILkotlin/jvm/functions/Function2;ZLkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Landroidx/compose/animation/AnimatedContentTransitionScope;)Landroidx/compose/animation/ContentTransform; +HSPLandroidx/navigation3/ui/NavDisplayKt__NavDisplayKt;->$r8$lambda$LZ9ASXP-YIdn-WuxMmn2u000sMA(Landroidx/navigation3/scene/Scene;)Ljava/lang/Object; +HSPLandroidx/navigation3/ui/NavDisplayKt__NavDisplayKt;->$r8$lambda$aySyOiJnnT1-CCIoFc3GO_djKFw(Landroidx/compose/animation/core/Transition;Landroidx/compose/runtime/snapshots/SnapshotStateList;Lkotlin/jvm/internal/Ref$ObjectRef;Ljava/util/Map;Landroidx/compose/animation/AnimatedContentScope;Landroidx/navigation3/scene/Scene;Landroidx/compose/runtime/Composer;I)Lkotlin/Unit; +HSPLandroidx/navigation3/ui/NavDisplayKt__NavDisplayKt;->$r8$lambda$hH0zCXP8JjnYbce4dkgo7nGOJNY(Landroidx/navigation3/scene/Scene;Landroidx/compose/runtime/Composer;I)Lkotlin/Unit; +HSPLandroidx/navigation3/ui/NavDisplayKt__NavDisplayKt;->$r8$lambda$vImyQbN8SR59TyvDeZ4bBt-ze1I(Lkotlin/jvm/functions/Function1;FLandroidx/compose/animation/SizeTransform;Landroidx/compose/animation/AnimatedContentTransitionScope;)Landroidx/compose/animation/ContentTransform; +HSPLandroidx/navigation3/ui/NavDisplayKt__NavDisplayKt;->NavDisplay$lambda$19$0$NavDisplayKt__NavDisplayKt(ZLandroidx/navigation3/scene/Scene;ILkotlin/jvm/functions/Function2;ZLkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Landroidx/compose/animation/AnimatedContentTransitionScope;)Landroidx/compose/animation/ContentTransform; +HSPLandroidx/navigation3/ui/NavDisplayKt__NavDisplayKt;->NavDisplay$lambda$20$0$NavDisplayKt__NavDisplayKt(Lkotlin/jvm/functions/Function1;FLandroidx/compose/animation/SizeTransform;Landroidx/compose/animation/AnimatedContentTransitionScope;)Landroidx/compose/animation/ContentTransform; +HSPLandroidx/navigation3/ui/NavDisplayKt__NavDisplayKt;->NavDisplay$lambda$21$0$NavDisplayKt__NavDisplayKt(Landroidx/navigation3/scene/Scene;)Ljava/lang/Object; +HSPLandroidx/navigation3/ui/NavDisplayKt__NavDisplayKt;->NavDisplay$lambda$22$1$NavDisplayKt__NavDisplayKt(Landroidx/navigation3/scene/Scene;Landroidx/compose/runtime/Composer;I)Lkotlin/Unit; +HSPLandroidx/navigation3/ui/NavDisplayKt__NavDisplayKt;->NavDisplay$lambda$22$NavDisplayKt__NavDisplayKt(Landroidx/compose/animation/core/Transition;Landroidx/compose/runtime/snapshots/SnapshotStateList;Lkotlin/jvm/internal/Ref$ObjectRef;Ljava/util/Map;Landroidx/compose/animation/AnimatedContentScope;Landroidx/navigation3/scene/Scene;Landroidx/compose/runtime/Composer;I)Lkotlin/Unit; HSPLandroidx/navigation3/ui/NavDisplayKt__NavDisplayKt;->NavDisplay(Landroidx/navigation3/scene/SceneState;Landroidx/navigationevent/compose/NavigationEventState;Landroidx/compose/ui/Modifier;Landroidx/compose/ui/Alignment;Landroidx/compose/animation/SizeTransform;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function2;Landroidx/compose/runtime/Composer;II)V HSPLandroidx/navigation3/ui/NavDisplayKt__NavDisplayKt;->NavDisplay(Ljava/util/List;Landroidx/compose/ui/Modifier;Landroidx/compose/ui/Alignment;Ljava/util/List;Ljava/util/List;Landroidx/compose/animation/SharedTransitionScope;Landroidx/compose/animation/SizeTransform;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function0;Landroidx/compose/runtime/Composer;III)V HSPLandroidx/navigation3/ui/NavDisplayKt__NavDisplayKt;->NavDisplay(Ljava/util/List;Landroidx/compose/ui/Modifier;Landroidx/compose/ui/Alignment;Lkotlin/jvm/functions/Function0;Ljava/util/List;Ljava/util/List;Ljava/util/List;Landroidx/compose/animation/SharedTransitionScope;Landroidx/compose/animation/SizeTransform;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function1;Landroidx/compose/runtime/Composer;III)V @@ -15327,62 +15407,60 @@ HSPLandroidx/navigation3/ui/NavDisplayKt__NavDisplayKt;->isPop$NavDisplayKt__Nav Landroidx/navigation3/ui/NavDisplayKt__NavDisplayKt$$ExternalSyntheticLambda1; HSPLandroidx/navigation3/ui/NavDisplayKt__NavDisplayKt$$ExternalSyntheticLambda1;->(Landroidx/navigation3/scene/SceneState;Landroidx/navigationevent/compose/NavigationEventState;Landroidx/compose/ui/Modifier;Landroidx/compose/ui/Alignment;Landroidx/compose/animation/SizeTransform;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function2;II)V Landroidx/navigation3/ui/NavDisplayKt__NavDisplayKt$$ExternalSyntheticLambda10; -HSPLandroidx/navigation3/ui/NavDisplayKt__NavDisplayKt$$ExternalSyntheticLambda10;->(Lkotlin/jvm/functions/Function1;FLandroidx/compose/animation/SizeTransform;)V +HSPLandroidx/navigation3/ui/NavDisplayKt__NavDisplayKt$$ExternalSyntheticLambda10;->()V HSPLandroidx/navigation3/ui/NavDisplayKt__NavDisplayKt$$ExternalSyntheticLambda10;->invoke(Ljava/lang/Object;)Ljava/lang/Object; Landroidx/navigation3/ui/NavDisplayKt__NavDisplayKt$$ExternalSyntheticLambda11; -HSPLandroidx/navigation3/ui/NavDisplayKt__NavDisplayKt$$ExternalSyntheticLambda11;->()V -HSPLandroidx/navigation3/ui/NavDisplayKt__NavDisplayKt$$ExternalSyntheticLambda11;->invoke(Ljava/lang/Object;)Ljava/lang/Object; -Landroidx/navigation3/ui/NavDisplayKt__NavDisplayKt$$ExternalSyntheticLambda12; -HSPLandroidx/navigation3/ui/NavDisplayKt__NavDisplayKt$$ExternalSyntheticLambda12;->(Landroidx/compose/animation/core/Transition;Landroidx/compose/runtime/snapshots/SnapshotStateList;Lkotlin/jvm/internal/Ref$ObjectRef;Ljava/util/Map;)V -HSPLandroidx/navigation3/ui/NavDisplayKt__NavDisplayKt$$ExternalSyntheticLambda12;->invoke(Ljava/lang/Object;Ljava/lang/Object;Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object; -Landroidx/navigation3/ui/NavDisplayKt__NavDisplayKt$$ExternalSyntheticLambda2; -HSPLandroidx/navigation3/ui/NavDisplayKt__NavDisplayKt$$ExternalSyntheticLambda2;->(Ljava/util/List;Landroidx/navigation3/scene/Scene;Lkotlin/jvm/functions/Function0;)V -Landroidx/navigation3/ui/NavDisplayKt__NavDisplayKt$$ExternalSyntheticLambda6; -HSPLandroidx/navigation3/ui/NavDisplayKt__NavDisplayKt$$ExternalSyntheticLambda6;->(Landroidx/navigation3/scene/Scene;)V -HSPLandroidx/navigation3/ui/NavDisplayKt__NavDisplayKt$$ExternalSyntheticLambda6;->invoke(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object; +HSPLandroidx/navigation3/ui/NavDisplayKt__NavDisplayKt$$ExternalSyntheticLambda11;->(Landroidx/compose/animation/core/Transition;Landroidx/compose/runtime/snapshots/SnapshotStateList;Lkotlin/jvm/internal/Ref$ObjectRef;Ljava/util/Map;)V +HSPLandroidx/navigation3/ui/NavDisplayKt__NavDisplayKt$$ExternalSyntheticLambda11;->invoke(Ljava/lang/Object;Ljava/lang/Object;Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object; +Landroidx/navigation3/ui/NavDisplayKt__NavDisplayKt$$ExternalSyntheticLambda5; +HSPLandroidx/navigation3/ui/NavDisplayKt__NavDisplayKt$$ExternalSyntheticLambda5;->(Landroidx/navigation3/scene/Scene;)V +HSPLandroidx/navigation3/ui/NavDisplayKt__NavDisplayKt$$ExternalSyntheticLambda5;->invoke(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object; +Landroidx/navigation3/ui/NavDisplayKt__NavDisplayKt$$ExternalSyntheticLambda7; +HSPLandroidx/navigation3/ui/NavDisplayKt__NavDisplayKt$$ExternalSyntheticLambda7;->(Ljava/util/List;Landroidx/compose/ui/Modifier;Landroidx/compose/ui/Alignment;Lkotlin/jvm/functions/Function0;Ljava/util/List;Ljava/util/List;Ljava/util/List;Landroidx/compose/animation/SharedTransitionScope;Landroidx/compose/animation/SizeTransform;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function1;III)V Landroidx/navigation3/ui/NavDisplayKt__NavDisplayKt$$ExternalSyntheticLambda8; -HSPLandroidx/navigation3/ui/NavDisplayKt__NavDisplayKt$$ExternalSyntheticLambda8;->(Ljava/util/List;Landroidx/compose/ui/Modifier;Landroidx/compose/ui/Alignment;Lkotlin/jvm/functions/Function0;Ljava/util/List;Ljava/util/List;Ljava/util/List;Landroidx/compose/animation/SharedTransitionScope;Landroidx/compose/animation/SizeTransform;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function1;III)V +HSPLandroidx/navigation3/ui/NavDisplayKt__NavDisplayKt$$ExternalSyntheticLambda8;->(ZLandroidx/navigation3/scene/Scene;ILkotlin/jvm/functions/Function2;ZLkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;)V +HSPLandroidx/navigation3/ui/NavDisplayKt__NavDisplayKt$$ExternalSyntheticLambda8;->invoke(Ljava/lang/Object;)Ljava/lang/Object; Landroidx/navigation3/ui/NavDisplayKt__NavDisplayKt$$ExternalSyntheticLambda9; -HSPLandroidx/navigation3/ui/NavDisplayKt__NavDisplayKt$$ExternalSyntheticLambda9;->(ZLandroidx/navigation3/scene/Scene;ILkotlin/jvm/functions/Function2;ZLkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;)V +HSPLandroidx/navigation3/ui/NavDisplayKt__NavDisplayKt$$ExternalSyntheticLambda9;->(Lkotlin/jvm/functions/Function1;FLandroidx/compose/animation/SizeTransform;)V HSPLandroidx/navigation3/ui/NavDisplayKt__NavDisplayKt$$ExternalSyntheticLambda9;->invoke(Ljava/lang/Object;)Ljava/lang/Object; -Landroidx/navigation3/ui/NavDisplayKt__NavDisplayKt$NavDisplay$10$1; -HSPLandroidx/navigation3/ui/NavDisplayKt__NavDisplayKt$NavDisplay$10$1;->(Ljava/util/List;Landroidx/compose/runtime/snapshots/SnapshotStateList;Lkotlin/coroutines/Continuation;)V -HSPLandroidx/navigation3/ui/NavDisplayKt__NavDisplayKt$NavDisplay$10$1;->create(Ljava/lang/Object;Lkotlin/coroutines/Continuation;)Lkotlin/coroutines/Continuation; -HSPLandroidx/navigation3/ui/NavDisplayKt__NavDisplayKt$NavDisplay$10$1;->invokeSuspend(Ljava/lang/Object;)Ljava/lang/Object; -Landroidx/navigation3/ui/NavDisplayKt__NavDisplayKt$NavDisplay$12$1; -HSPLandroidx/navigation3/ui/NavDisplayKt__NavDisplayKt$NavDisplay$12$1;->(Landroidx/compose/animation/core/SeekableTransitionState;Landroidx/navigation3/scene/Scene;Landroidx/compose/animation/core/Transition;Lkotlin/coroutines/Continuation;)V -HSPLandroidx/navigation3/ui/NavDisplayKt__NavDisplayKt$NavDisplay$12$1;->create(Ljava/lang/Object;Lkotlin/coroutines/Continuation;)Lkotlin/coroutines/Continuation; -HSPLandroidx/navigation3/ui/NavDisplayKt__NavDisplayKt$NavDisplay$12$1;->invokeSuspend$lambda$0(Lkotlinx/coroutines/CoroutineScope;FLandroidx/compose/animation/core/SeekableTransitionState;Landroidx/navigation3/scene/Scene;FF)Lkotlin/Unit; -HSPLandroidx/navigation3/ui/NavDisplayKt__NavDisplayKt$NavDisplay$12$1;->invokeSuspend(Ljava/lang/Object;)Ljava/lang/Object; -Landroidx/navigation3/ui/NavDisplayKt__NavDisplayKt$NavDisplay$12$1$$ExternalSyntheticLambda0; -HSPLandroidx/navigation3/ui/NavDisplayKt__NavDisplayKt$NavDisplay$12$1$$ExternalSyntheticLambda0;->(Lkotlinx/coroutines/CoroutineScope;FLandroidx/compose/animation/core/SeekableTransitionState;Landroidx/navigation3/scene/Scene;)V -HSPLandroidx/navigation3/ui/NavDisplayKt__NavDisplayKt$NavDisplay$12$1$$ExternalSyntheticLambda0;->invoke(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object; -Landroidx/navigation3/ui/NavDisplayKt__NavDisplayKt$NavDisplay$12$1$1$1; -HSPLandroidx/navigation3/ui/NavDisplayKt__NavDisplayKt$NavDisplay$12$1$1$1;->(FFLandroidx/compose/animation/core/SeekableTransitionState;Landroidx/navigation3/scene/Scene;Lkotlin/coroutines/Continuation;)V -HSPLandroidx/navigation3/ui/NavDisplayKt__NavDisplayKt$NavDisplay$12$1$1$1;->create(Ljava/lang/Object;Lkotlin/coroutines/Continuation;)Lkotlin/coroutines/Continuation; -HSPLandroidx/navigation3/ui/NavDisplayKt__NavDisplayKt$NavDisplay$12$1$1$1;->invokeSuspend(Ljava/lang/Object;)Ljava/lang/Object; -Landroidx/navigation3/ui/NavDisplayKt__NavDisplayKt$NavDisplay$16$1; -HSPLandroidx/navigation3/ui/NavDisplayKt__NavDisplayKt$NavDisplay$16$1;->(Landroidx/compose/animation/core/Transition;Landroidx/compose/runtime/snapshots/SnapshotStateMap;Landroidx/collection/MutableObjectFloatMap;Lkotlin/coroutines/Continuation;)V -HSPLandroidx/navigation3/ui/NavDisplayKt__NavDisplayKt$NavDisplay$16$1;->create(Ljava/lang/Object;Lkotlin/coroutines/Continuation;)Lkotlin/coroutines/Continuation; -HSPLandroidx/navigation3/ui/NavDisplayKt__NavDisplayKt$NavDisplay$16$1;->invokeSuspend$lambda$0(Landroidx/compose/animation/core/Transition;)Z -HSPLandroidx/navigation3/ui/NavDisplayKt__NavDisplayKt$NavDisplay$16$1;->invokeSuspend(Ljava/lang/Object;)Ljava/lang/Object; -Landroidx/navigation3/ui/NavDisplayKt__NavDisplayKt$NavDisplay$16$1$$ExternalSyntheticLambda0; -HSPLandroidx/navigation3/ui/NavDisplayKt__NavDisplayKt$NavDisplay$16$1$$ExternalSyntheticLambda0;->(Landroidx/compose/animation/core/Transition;)V -HSPLandroidx/navigation3/ui/NavDisplayKt__NavDisplayKt$NavDisplay$16$1$$ExternalSyntheticLambda0;->invoke()Ljava/lang/Object; -Landroidx/navigation3/ui/NavDisplayKt__NavDisplayKt$NavDisplay$16$1$3; -HSPLandroidx/navigation3/ui/NavDisplayKt__NavDisplayKt$NavDisplay$16$1$3;->(Landroidx/compose/animation/core/Transition;Landroidx/compose/runtime/snapshots/SnapshotStateMap;Landroidx/collection/MutableObjectFloatMap;)V -HSPLandroidx/navigation3/ui/NavDisplayKt__NavDisplayKt$NavDisplay$16$1$3;->emit(Ljava/lang/Object;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; -HSPLandroidx/navigation3/ui/NavDisplayKt__NavDisplayKt$NavDisplay$16$1$3;->emit(ZLkotlin/coroutines/Continuation;)Ljava/lang/Object; -Landroidx/navigation3/ui/NavDisplayKt__NavDisplayKt$NavDisplay$16$1$invokeSuspend$$inlined$filter$1; -HSPLandroidx/navigation3/ui/NavDisplayKt__NavDisplayKt$NavDisplay$16$1$invokeSuspend$$inlined$filter$1;->(Lkotlinx/coroutines/flow/Flow;)V -HSPLandroidx/navigation3/ui/NavDisplayKt__NavDisplayKt$NavDisplay$16$1$invokeSuspend$$inlined$filter$1;->collect(Lkotlinx/coroutines/flow/FlowCollector;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; -Landroidx/navigation3/ui/NavDisplayKt__NavDisplayKt$NavDisplay$16$1$invokeSuspend$$inlined$filter$1$2; -HSPLandroidx/navigation3/ui/NavDisplayKt__NavDisplayKt$NavDisplay$16$1$invokeSuspend$$inlined$filter$1$2;->(Lkotlinx/coroutines/flow/FlowCollector;)V -HSPLandroidx/navigation3/ui/NavDisplayKt__NavDisplayKt$NavDisplay$16$1$invokeSuspend$$inlined$filter$1$2;->emit(Ljava/lang/Object;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; -Landroidx/navigation3/ui/NavDisplayKt__NavDisplayKt$NavDisplay$16$1$invokeSuspend$$inlined$filter$1$2$1; -HSPLandroidx/navigation3/ui/NavDisplayKt__NavDisplayKt$NavDisplay$16$1$invokeSuspend$$inlined$filter$1$2$1;->(Landroidx/navigation3/ui/NavDisplayKt__NavDisplayKt$NavDisplay$16$1$invokeSuspend$$inlined$filter$1$2;Lkotlin/coroutines/Continuation;)V -Landroidx/navigation3/ui/NavDisplayKt__NavDisplayKt$NavDisplay$lambda$19$0$$inlined$sortedByDescending$1; -HSPLandroidx/navigation3/ui/NavDisplayKt__NavDisplayKt$NavDisplay$lambda$19$0$$inlined$sortedByDescending$1;->(Landroidx/collection/MutableObjectFloatMap;)V +Landroidx/navigation3/ui/NavDisplayKt__NavDisplayKt$NavDisplay$11$1; +HSPLandroidx/navigation3/ui/NavDisplayKt__NavDisplayKt$NavDisplay$11$1;->(Landroidx/compose/animation/core/SeekableTransitionState;Landroidx/navigation3/scene/Scene;Landroidx/compose/animation/core/Transition;Lkotlin/coroutines/Continuation;)V +HSPLandroidx/navigation3/ui/NavDisplayKt__NavDisplayKt$NavDisplay$11$1;->create(Ljava/lang/Object;Lkotlin/coroutines/Continuation;)Lkotlin/coroutines/Continuation; +HSPLandroidx/navigation3/ui/NavDisplayKt__NavDisplayKt$NavDisplay$11$1;->invokeSuspend$lambda$0(Lkotlinx/coroutines/CoroutineScope;FLandroidx/compose/animation/core/SeekableTransitionState;Landroidx/navigation3/scene/Scene;FF)Lkotlin/Unit; +HSPLandroidx/navigation3/ui/NavDisplayKt__NavDisplayKt$NavDisplay$11$1;->invokeSuspend(Ljava/lang/Object;)Ljava/lang/Object; +Landroidx/navigation3/ui/NavDisplayKt__NavDisplayKt$NavDisplay$11$1$$ExternalSyntheticLambda0; +HSPLandroidx/navigation3/ui/NavDisplayKt__NavDisplayKt$NavDisplay$11$1$$ExternalSyntheticLambda0;->(Lkotlinx/coroutines/CoroutineScope;FLandroidx/compose/animation/core/SeekableTransitionState;Landroidx/navigation3/scene/Scene;)V +HSPLandroidx/navigation3/ui/NavDisplayKt__NavDisplayKt$NavDisplay$11$1$$ExternalSyntheticLambda0;->invoke(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object; +Landroidx/navigation3/ui/NavDisplayKt__NavDisplayKt$NavDisplay$11$1$1$1; +HSPLandroidx/navigation3/ui/NavDisplayKt__NavDisplayKt$NavDisplay$11$1$1$1;->(FFLandroidx/compose/animation/core/SeekableTransitionState;Landroidx/navigation3/scene/Scene;Lkotlin/coroutines/Continuation;)V +HSPLandroidx/navigation3/ui/NavDisplayKt__NavDisplayKt$NavDisplay$11$1$1$1;->create(Ljava/lang/Object;Lkotlin/coroutines/Continuation;)Lkotlin/coroutines/Continuation; +HSPLandroidx/navigation3/ui/NavDisplayKt__NavDisplayKt$NavDisplay$11$1$1$1;->invokeSuspend(Ljava/lang/Object;)Ljava/lang/Object; +Landroidx/navigation3/ui/NavDisplayKt__NavDisplayKt$NavDisplay$15$1; +HSPLandroidx/navigation3/ui/NavDisplayKt__NavDisplayKt$NavDisplay$15$1;->(Landroidx/compose/animation/core/Transition;Landroidx/compose/runtime/snapshots/SnapshotStateMap;Landroidx/collection/MutableObjectFloatMap;Lkotlin/coroutines/Continuation;)V +HSPLandroidx/navigation3/ui/NavDisplayKt__NavDisplayKt$NavDisplay$15$1;->create(Ljava/lang/Object;Lkotlin/coroutines/Continuation;)Lkotlin/coroutines/Continuation; +HSPLandroidx/navigation3/ui/NavDisplayKt__NavDisplayKt$NavDisplay$15$1;->invokeSuspend$lambda$0(Landroidx/compose/animation/core/Transition;)Z +HSPLandroidx/navigation3/ui/NavDisplayKt__NavDisplayKt$NavDisplay$15$1;->invokeSuspend(Ljava/lang/Object;)Ljava/lang/Object; +Landroidx/navigation3/ui/NavDisplayKt__NavDisplayKt$NavDisplay$15$1$$ExternalSyntheticLambda0; +HSPLandroidx/navigation3/ui/NavDisplayKt__NavDisplayKt$NavDisplay$15$1$$ExternalSyntheticLambda0;->(Landroidx/compose/animation/core/Transition;)V +HSPLandroidx/navigation3/ui/NavDisplayKt__NavDisplayKt$NavDisplay$15$1$$ExternalSyntheticLambda0;->invoke()Ljava/lang/Object; +Landroidx/navigation3/ui/NavDisplayKt__NavDisplayKt$NavDisplay$15$1$3; +HSPLandroidx/navigation3/ui/NavDisplayKt__NavDisplayKt$NavDisplay$15$1$3;->(Landroidx/compose/animation/core/Transition;Landroidx/compose/runtime/snapshots/SnapshotStateMap;Landroidx/collection/MutableObjectFloatMap;)V +HSPLandroidx/navigation3/ui/NavDisplayKt__NavDisplayKt$NavDisplay$15$1$3;->emit(Ljava/lang/Object;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; +HSPLandroidx/navigation3/ui/NavDisplayKt__NavDisplayKt$NavDisplay$15$1$3;->emit(ZLkotlin/coroutines/Continuation;)Ljava/lang/Object; +Landroidx/navigation3/ui/NavDisplayKt__NavDisplayKt$NavDisplay$15$1$invokeSuspend$$inlined$filter$1; +HSPLandroidx/navigation3/ui/NavDisplayKt__NavDisplayKt$NavDisplay$15$1$invokeSuspend$$inlined$filter$1;->(Lkotlinx/coroutines/flow/Flow;)V +HSPLandroidx/navigation3/ui/NavDisplayKt__NavDisplayKt$NavDisplay$15$1$invokeSuspend$$inlined$filter$1;->collect(Lkotlinx/coroutines/flow/FlowCollector;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; +Landroidx/navigation3/ui/NavDisplayKt__NavDisplayKt$NavDisplay$15$1$invokeSuspend$$inlined$filter$1$2; +HSPLandroidx/navigation3/ui/NavDisplayKt__NavDisplayKt$NavDisplay$15$1$invokeSuspend$$inlined$filter$1$2;->(Lkotlinx/coroutines/flow/FlowCollector;)V +HSPLandroidx/navigation3/ui/NavDisplayKt__NavDisplayKt$NavDisplay$15$1$invokeSuspend$$inlined$filter$1$2;->emit(Ljava/lang/Object;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; +Landroidx/navigation3/ui/NavDisplayKt__NavDisplayKt$NavDisplay$15$1$invokeSuspend$$inlined$filter$1$2$1; +HSPLandroidx/navigation3/ui/NavDisplayKt__NavDisplayKt$NavDisplay$15$1$invokeSuspend$$inlined$filter$1$2$1;->(Landroidx/navigation3/ui/NavDisplayKt__NavDisplayKt$NavDisplay$15$1$invokeSuspend$$inlined$filter$1$2;Lkotlin/coroutines/Continuation;)V +Landroidx/navigation3/ui/NavDisplayKt__NavDisplayKt$NavDisplay$9$1; +HSPLandroidx/navigation3/ui/NavDisplayKt__NavDisplayKt$NavDisplay$9$1;->(Ljava/util/List;Landroidx/compose/runtime/snapshots/SnapshotStateList;Lkotlin/coroutines/Continuation;)V +HSPLandroidx/navigation3/ui/NavDisplayKt__NavDisplayKt$NavDisplay$9$1;->create(Ljava/lang/Object;Lkotlin/coroutines/Continuation;)Lkotlin/coroutines/Continuation; +HSPLandroidx/navigation3/ui/NavDisplayKt__NavDisplayKt$NavDisplay$9$1;->invokeSuspend(Ljava/lang/Object;)Ljava/lang/Object; +Landroidx/navigation3/ui/NavDisplayKt__NavDisplayKt$NavDisplay$lambda$16$0$$inlined$sortedByDescending$1; +HSPLandroidx/navigation3/ui/NavDisplayKt__NavDisplayKt$NavDisplay$lambda$16$0$$inlined$sortedByDescending$1;->(Landroidx/collection/MutableObjectFloatMap;)V Landroidx/navigationevent/NavigationEventDispatcher; HSPLandroidx/navigationevent/NavigationEventDispatcher;->()V HSPLandroidx/navigationevent/NavigationEventDispatcher;->(Landroidx/navigationevent/NavigationEventDispatcher;Landroidx/navigationevent/OnBackCompletedFallback;Landroidx/navigationevent/OnForwardCompletedFallback;)V @@ -15493,8 +15571,6 @@ HSPLandroidx/navigationevent/compose/NavigationEventHandlerKt;->NavigationBackHa HSPLandroidx/navigationevent/compose/NavigationEventHandlerKt;->NavigationEventHandler$lambda$6$0(Landroidx/navigationevent/compose/ComposeNavigationEventHandler;ZLkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function0;ZLkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function0;Landroidx/navigationevent/compose/NavigationEventState;)Lkotlin/Unit; HSPLandroidx/navigationevent/compose/NavigationEventHandlerKt;->NavigationEventHandler$lambda$7$0(Landroidx/navigationevent/compose/NavigationEventState;Landroidx/navigationevent/compose/ComposeNavigationEventHandler;Landroidx/navigationevent/NavigationEventDispatcher;Landroidx/compose/runtime/DisposableEffectScope;)Landroidx/compose/runtime/DisposableEffectResult; HSPLandroidx/navigationevent/compose/NavigationEventHandlerKt;->NavigationEventHandler(Landroidx/navigationevent/compose/NavigationEventState;ZLkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function0;ZLkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function0;Landroidx/compose/runtime/Composer;II)V -Landroidx/navigationevent/compose/NavigationEventHandlerKt$$ExternalSyntheticLambda10; -HSPLandroidx/navigationevent/compose/NavigationEventHandlerKt$$ExternalSyntheticLambda10;->()V Landroidx/navigationevent/compose/NavigationEventHandlerKt$$ExternalSyntheticLambda11; HSPLandroidx/navigationevent/compose/NavigationEventHandlerKt$$ExternalSyntheticLambda11;->()V Landroidx/navigationevent/compose/NavigationEventHandlerKt$$ExternalSyntheticLambda12; @@ -16146,7 +16222,7 @@ Landroidx/room3/ObservedTableStates; HSPLandroidx/room3/ObservedTableStates;->(I)V HSPLandroidx/room3/ObservedTableStates;->access$getLock$p(Landroidx/room3/ObservedTableStates;)Ljava/util/concurrent/locks/ReentrantLock; HSPLandroidx/room3/ObservedTableStates;->access$getNeedsSync$p(Landroidx/room3/ObservedTableStates;)Z -HSPLandroidx/room3/ObservedTableStates;->access$getOnSyncLock$p(Landroidx/room3/ObservedTableStates;)Ljava/util/concurrent/locks/ReentrantLock; +HSPLandroidx/room3/ObservedTableStates;->access$getOnSyncMutex$p(Landroidx/room3/ObservedTableStates;)Lkotlinx/coroutines/sync/Mutex; HSPLandroidx/room3/ObservedTableStates;->access$getTableObservedState$p(Landroidx/room3/ObservedTableStates;)[Z HSPLandroidx/room3/ObservedTableStates;->access$getTableObserversCount$p(Landroidx/room3/ObservedTableStates;)[J HSPLandroidx/room3/ObservedTableStates;->access$setInProgressSync$p(Landroidx/room3/ObservedTableStates;Z)V @@ -16427,6 +16503,12 @@ HSPLandroidx/room3/coroutines/ConnectionPoolImpl$useConnection$4;->create(Ljava/ HSPLandroidx/room3/coroutines/ConnectionPoolImpl$useConnection$4;->invoke(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object; HSPLandroidx/room3/coroutines/ConnectionPoolImpl$useConnection$4;->invoke(Lkotlinx/coroutines/CoroutineScope;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; HSPLandroidx/room3/coroutines/ConnectionPoolImpl$useConnection$4;->invokeSuspend(Ljava/lang/Object;)Ljava/lang/Object; +Landroidx/room3/coroutines/ConnectionPoolImpl$useConnection$5; +HSPLandroidx/room3/coroutines/ConnectionPoolImpl$useConnection$5;->(Lkotlin/jvm/internal/Ref$ObjectRef;Lkotlin/jvm/internal/Ref$ObjectRef;Landroidx/room3/coroutines/Pool;Lkotlin/coroutines/Continuation;)V +HSPLandroidx/room3/coroutines/ConnectionPoolImpl$useConnection$5;->create(Ljava/lang/Object;Lkotlin/coroutines/Continuation;)Lkotlin/coroutines/Continuation; +HSPLandroidx/room3/coroutines/ConnectionPoolImpl$useConnection$5;->invoke(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object; +HSPLandroidx/room3/coroutines/ConnectionPoolImpl$useConnection$5;->invoke(Lkotlinx/coroutines/CoroutineScope;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; +HSPLandroidx/room3/coroutines/ConnectionPoolImpl$useConnection$5;->invokeSuspend(Ljava/lang/Object;)Ljava/lang/Object; Landroidx/room3/coroutines/ConnectionPoolImpl_nonWebKt; HSPLandroidx/room3/coroutines/ConnectionPoolImpl_nonWebKt;->newConnectionWrapper(Landroidx/sqlite/SQLiteConnection;I)Landroidx/room3/coroutines/ConnectionWrapper; HSPLandroidx/room3/coroutines/ConnectionPoolImpl_nonWebKt;->newStatementWrapper(Landroidx/sqlite/SQLiteStatement;Lkotlin/jvm/functions/Function0;)Landroidx/room3/coroutines/StatementWrapper; @@ -16492,6 +16574,8 @@ HSPLandroidx/room3/coroutines/Pool$acquireWithTimeout$2;->invoke(Lkotlinx/corout HSPLandroidx/room3/coroutines/Pool$acquireWithTimeout$2;->invokeSuspend(Ljava/lang/Object;)Ljava/lang/Object; Landroidx/room3/coroutines/PooledConnectionImpl; HSPLandroidx/room3/coroutines/PooledConnectionImpl;->(Landroidx/room3/coroutines/ConnectionElementKey;Landroidx/room3/coroutines/ConnectionWrapper;Z)V +HSPLandroidx/room3/coroutines/PooledConnectionImpl;->access$beginTransaction(Landroidx/room3/coroutines/PooledConnectionImpl;Landroidx/room3/Transactor$SQLiteTransactionType;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; +HSPLandroidx/room3/coroutines/PooledConnectionImpl;->access$endTransaction(Landroidx/room3/coroutines/PooledConnectionImpl;ZLkotlin/coroutines/Continuation;)Ljava/lang/Object; HSPLandroidx/room3/coroutines/PooledConnectionImpl;->access$isRecycled$p(Landroidx/room3/coroutines/PooledConnectionImpl;)Z HSPLandroidx/room3/coroutines/PooledConnectionImpl;->beginTransaction(Landroidx/room3/Transactor$SQLiteTransactionType;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; HSPLandroidx/room3/coroutines/PooledConnectionImpl;->endTransaction(ZLkotlin/coroutines/Continuation;)Ljava/lang/Object; @@ -16519,6 +16603,18 @@ Landroidx/room3/coroutines/PooledConnectionImpl$endTransaction$1; HSPLandroidx/room3/coroutines/PooledConnectionImpl$endTransaction$1;->(Landroidx/room3/coroutines/PooledConnectionImpl;Lkotlin/coroutines/Continuation;)V Landroidx/room3/coroutines/PooledConnectionImpl$transaction$1; HSPLandroidx/room3/coroutines/PooledConnectionImpl$transaction$1;->(Landroidx/room3/coroutines/PooledConnectionImpl;Lkotlin/coroutines/Continuation;)V +Landroidx/room3/coroutines/PooledConnectionImpl$transaction$2; +HSPLandroidx/room3/coroutines/PooledConnectionImpl$transaction$2;->(Landroidx/room3/coroutines/PooledConnectionImpl;Landroidx/room3/Transactor$SQLiteTransactionType;Lkotlin/coroutines/Continuation;)V +HSPLandroidx/room3/coroutines/PooledConnectionImpl$transaction$2;->create(Ljava/lang/Object;Lkotlin/coroutines/Continuation;)Lkotlin/coroutines/Continuation; +HSPLandroidx/room3/coroutines/PooledConnectionImpl$transaction$2;->invoke(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object; +HSPLandroidx/room3/coroutines/PooledConnectionImpl$transaction$2;->invoke(Lkotlinx/coroutines/CoroutineScope;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; +HSPLandroidx/room3/coroutines/PooledConnectionImpl$transaction$2;->invokeSuspend(Ljava/lang/Object;)Ljava/lang/Object; +Landroidx/room3/coroutines/PooledConnectionImpl$transaction$3; +HSPLandroidx/room3/coroutines/PooledConnectionImpl$transaction$3;->(Landroidx/room3/coroutines/PooledConnectionImpl;Lkotlin/jvm/internal/Ref$BooleanRef;Lkotlin/jvm/internal/Ref$ObjectRef;Lkotlin/coroutines/Continuation;)V +HSPLandroidx/room3/coroutines/PooledConnectionImpl$transaction$3;->create(Ljava/lang/Object;Lkotlin/coroutines/Continuation;)Lkotlin/coroutines/Continuation; +HSPLandroidx/room3/coroutines/PooledConnectionImpl$transaction$3;->invoke(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object; +HSPLandroidx/room3/coroutines/PooledConnectionImpl$transaction$3;->invoke(Lkotlinx/coroutines/CoroutineScope;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; +HSPLandroidx/room3/coroutines/PooledConnectionImpl$transaction$3;->invokeSuspend(Ljava/lang/Object;)Ljava/lang/Object; Landroidx/room3/coroutines/PooledConnectionImpl$usePrepared$1; HSPLandroidx/room3/coroutines/PooledConnectionImpl$usePrepared$1;->(Landroidx/room3/coroutines/PooledConnectionImpl;Lkotlin/coroutines/Continuation;)V Landroidx/room3/coroutines/PooledConnectionImpl$usePrepared$2$1$1; @@ -17135,7 +17231,7 @@ HSPLandroidx/window/layout/WindowInfoTrackerImpl$windowLayoutInfo$1;->invoke(Lko HSPLandroidx/window/layout/WindowInfoTrackerImpl$windowLayoutInfo$1;->invokeSuspend$lambda$0(Lkotlinx/coroutines/channels/ProducerScope;Landroidx/window/layout/WindowLayoutInfo;)V HSPLandroidx/window/layout/WindowInfoTrackerImpl$windowLayoutInfo$1;->invokeSuspend(Ljava/lang/Object;)Ljava/lang/Object; Landroidx/window/layout/WindowInfoTrackerImpl$windowLayoutInfo$1$$ExternalSyntheticLambda0; -PLandroidx/window/layout/WindowInfoTrackerImpl$windowLayoutInfo$1$$ExternalSyntheticLambda0;->(Lkotlinx/coroutines/channels/ProducerScope;)V +HSPLandroidx/window/layout/WindowInfoTrackerImpl$windowLayoutInfo$1$$ExternalSyntheticLambda0;->(Lkotlinx/coroutines/channels/ProducerScope;)V HSPLandroidx/window/layout/WindowInfoTrackerImpl$windowLayoutInfo$1$$ExternalSyntheticLambda0;->accept(Ljava/lang/Object;)V PLandroidx/window/layout/WindowInfoTrackerImpl$windowLayoutInfo$1$$ExternalSyntheticLambda1;->(Landroidx/window/layout/WindowInfoTrackerImpl;Landroidx/core/util/Consumer;)V PLandroidx/window/layout/WindowLayoutInfo;->(Ljava/util/List;)V @@ -17178,7 +17274,7 @@ PLandroidx/window/layout/adapter/extensions/ExtensionsWindowLayoutInfoAdapter;-> PLandroidx/window/layout/adapter/extensions/ExtensionsWindowLayoutInfoAdapter;->translate$window_release(Landroid/content/Context;Landroidx/window/extensions/layout/WindowLayoutInfo;)Landroidx/window/layout/WindowLayoutInfo; PLandroidx/window/layout/adapter/extensions/ExtensionsWindowLayoutInfoAdapter;->translate$window_release(Landroidx/window/layout/WindowMetrics;Landroidx/window/extensions/layout/WindowLayoutInfo;)Landroidx/window/layout/WindowLayoutInfo; Landroidx/window/layout/adapter/extensions/MulticastConsumerApi2; -PLandroidx/window/layout/adapter/extensions/MulticastConsumerApi2;->(Landroid/content/Context;)V +HSPLandroidx/window/layout/adapter/extensions/MulticastConsumerApi2;->(Landroid/content/Context;)V HSPLandroidx/window/layout/adapter/extensions/MulticastConsumerApi2;->accept(Landroidx/window/extensions/layout/WindowLayoutInfo;)V HSPLandroidx/window/layout/adapter/extensions/MulticastConsumerApi2;->accept(Ljava/lang/Object;)V HSPLandroidx/window/layout/adapter/extensions/MulticastConsumerApi2;->addListener(Landroidx/core/util/Consumer;)V @@ -17297,9 +17393,9 @@ HSPLandroidx/work/Constraints$Companion;->()V HSPLandroidx/work/Constraints$Companion;->(Lkotlin/jvm/internal/DefaultConstructorMarker;)V Landroidx/work/CoroutineWorker; HSPLandroidx/work/CoroutineWorker;->(Landroid/content/Context;Landroidx/work/WorkerParameters;)V -HSPLandroidx/work/CoroutineWorker;->getCoroutineContext()Lkotlinx/coroutines/CoroutineDispatcher; -HSPLandroidx/work/CoroutineWorker;->onStopped()V -HSPLandroidx/work/CoroutineWorker;->startWork()Lcom/google/common/util/concurrent/ListenableFuture; +PLandroidx/work/CoroutineWorker;->getCoroutineContext()Lkotlinx/coroutines/CoroutineDispatcher; +PLandroidx/work/CoroutineWorker;->onStopped()V +PLandroidx/work/CoroutineWorker;->startWork()Lcom/google/common/util/concurrent/ListenableFuture; Landroidx/work/CoroutineWorker$DeprecatedDispatcher; HSPLandroidx/work/CoroutineWorker$DeprecatedDispatcher;->()V HSPLandroidx/work/CoroutineWorker$DeprecatedDispatcher;->()V @@ -17334,7 +17430,7 @@ Landroidx/work/DirectExecutor; HSPLandroidx/work/DirectExecutor;->$values()[Landroidx/work/DirectExecutor; HSPLandroidx/work/DirectExecutor;->()V HSPLandroidx/work/DirectExecutor;->(Ljava/lang/String;I)V -HSPLandroidx/work/DirectExecutor;->execute(Ljava/lang/Runnable;)V +PLandroidx/work/DirectExecutor;->execute(Ljava/lang/Runnable;)V Landroidx/work/ExistingPeriodicWorkPolicy; HSPLandroidx/work/ExistingPeriodicWorkPolicy;->$values()[Landroidx/work/ExistingPeriodicWorkPolicy; HSPLandroidx/work/ExistingPeriodicWorkPolicy;->()V @@ -17345,7 +17441,7 @@ Landroidx/work/InputMergerFactory; HSPLandroidx/work/InputMergerFactory;->()V Landroidx/work/ListenableFutureKt; HSPLandroidx/work/ListenableFutureKt;->launchFuture$default(Lkotlin/coroutines/CoroutineContext;Lkotlinx/coroutines/CoroutineStart;Lkotlin/jvm/functions/Function2;ILjava/lang/Object;)Lcom/google/common/util/concurrent/ListenableFuture; -HSPLandroidx/work/ListenableFutureKt;->launchFuture$lambda$1$lambda$0(Lkotlinx/coroutines/Job;)V +PLandroidx/work/ListenableFutureKt;->launchFuture$lambda$1$lambda$0(Lkotlinx/coroutines/Job;)V HSPLandroidx/work/ListenableFutureKt;->launchFuture$lambda$1(Lkotlin/coroutines/CoroutineContext;Lkotlinx/coroutines/CoroutineStart;Lkotlin/jvm/functions/Function2;Landroidx/concurrent/futures/CallbackToFutureAdapter$Completer;)Ljava/lang/Object; HSPLandroidx/work/ListenableFutureKt;->launchFuture(Lkotlin/coroutines/CoroutineContext;Lkotlinx/coroutines/CoroutineStart;Lkotlin/jvm/functions/Function2;)Lcom/google/common/util/concurrent/ListenableFuture; Landroidx/work/ListenableFutureKt$$ExternalSyntheticLambda1; @@ -17353,7 +17449,7 @@ HSPLandroidx/work/ListenableFutureKt$$ExternalSyntheticLambda1;->(Lkotlin/ HSPLandroidx/work/ListenableFutureKt$$ExternalSyntheticLambda1;->attachCompleter(Landroidx/concurrent/futures/CallbackToFutureAdapter$Completer;)Ljava/lang/Object; Landroidx/work/ListenableFutureKt$$ExternalSyntheticLambda4; HSPLandroidx/work/ListenableFutureKt$$ExternalSyntheticLambda4;->(Lkotlinx/coroutines/Job;)V -HSPLandroidx/work/ListenableFutureKt$$ExternalSyntheticLambda4;->run()V +PLandroidx/work/ListenableFutureKt$$ExternalSyntheticLambda4;->run()V Landroidx/work/ListenableFutureKt$launchFuture$1$2; HSPLandroidx/work/ListenableFutureKt$launchFuture$1$2;->(Lkotlin/jvm/functions/Function2;Landroidx/concurrent/futures/CallbackToFutureAdapter$Completer;Lkotlin/coroutines/Continuation;)V HSPLandroidx/work/ListenableFutureKt$launchFuture$1$2;->create(Ljava/lang/Object;Lkotlin/coroutines/Continuation;)Lkotlin/coroutines/Continuation; @@ -17361,9 +17457,13 @@ HSPLandroidx/work/ListenableFutureKt$launchFuture$1$2;->invokeSuspend(Ljava/lang Landroidx/work/ListenableWorker; HSPLandroidx/work/ListenableWorker;->(Landroid/content/Context;Landroidx/work/WorkerParameters;)V HSPLandroidx/work/ListenableWorker;->isUsed()Z -HSPLandroidx/work/ListenableWorker;->onStopped()V +PLandroidx/work/ListenableWorker;->onStopped()V HSPLandroidx/work/ListenableWorker;->setUsed()V -HSPLandroidx/work/ListenableWorker;->stop(I)V +PLandroidx/work/ListenableWorker;->stop(I)V +PLandroidx/work/ListenableWorker$Result;->()V +PLandroidx/work/ListenableWorker$Result;->success()Landroidx/work/ListenableWorker$Result; +PLandroidx/work/ListenableWorker$Result$Success;->()V +PLandroidx/work/ListenableWorker$Result$Success;->(Landroidx/work/Data;)V Landroidx/work/Logger; HSPLandroidx/work/Logger;->()V HSPLandroidx/work/Logger;->(I)V @@ -17374,7 +17474,7 @@ Landroidx/work/Logger$LogcatLogger; HSPLandroidx/work/Logger$LogcatLogger;->(I)V HSPLandroidx/work/Logger$LogcatLogger;->debug(Ljava/lang/String;Ljava/lang/String;)V PLandroidx/work/Logger$LogcatLogger;->info(Ljava/lang/String;Ljava/lang/String;)V -HSPLandroidx/work/Logger$LogcatLogger;->info(Ljava/lang/String;Ljava/lang/String;Ljava/lang/Throwable;)V +PLandroidx/work/Logger$LogcatLogger;->info(Ljava/lang/String;Ljava/lang/String;Ljava/lang/Throwable;)V Landroidx/work/NetworkType; HSPLandroidx/work/NetworkType;->$values()[Landroidx/work/NetworkType; HSPLandroidx/work/NetworkType;->()V @@ -17477,7 +17577,7 @@ HSPLandroidx/work/WorkerFactory;->createWorkerWithDefaultFallback(Landroid/conte Landroidx/work/WorkerParameters; HSPLandroidx/work/WorkerParameters;->(Ljava/util/UUID;Landroidx/work/Data;Ljava/util/Collection;Landroidx/work/WorkerParameters$RuntimeExtras;IILjava/util/concurrent/Executor;Lkotlin/coroutines/CoroutineContext;Landroidx/work/impl/utils/taskexecutor/TaskExecutor;Landroidx/work/WorkerFactory;Landroidx/work/ProgressUpdater;Landroidx/work/ForegroundUpdater;)V HSPLandroidx/work/WorkerParameters;->getForegroundUpdater()Landroidx/work/ForegroundUpdater; -HSPLandroidx/work/WorkerParameters;->getWorkerContext()Lkotlin/coroutines/CoroutineContext; +PLandroidx/work/WorkerParameters;->getWorkerContext()Lkotlin/coroutines/CoroutineContext; Landroidx/work/WorkerParameters$RuntimeExtras; HSPLandroidx/work/WorkerParameters$RuntimeExtras;->()V Landroidx/work/impl/AutoMigration_14_15; @@ -17528,12 +17628,12 @@ Landroidx/work/impl/Processor; HSPLandroidx/work/impl/Processor;->()V HSPLandroidx/work/impl/Processor;->(Landroid/content/Context;Landroidx/work/Configuration;Landroidx/work/impl/utils/taskexecutor/TaskExecutor;Landroidx/work/impl/WorkDatabase;)V HSPLandroidx/work/impl/Processor;->addExecutionListener(Landroidx/work/impl/ExecutionListener;)V -HSPLandroidx/work/impl/Processor;->cleanUpWorkerUnsafe(Ljava/lang/String;)Landroidx/work/impl/WorkerWrapper; +PLandroidx/work/impl/Processor;->cleanUpWorkerUnsafe(Ljava/lang/String;)Landroidx/work/impl/WorkerWrapper; HSPLandroidx/work/impl/Processor;->getWorkerWrapperUnsafe(Ljava/lang/String;)Landroidx/work/impl/WorkerWrapper; -HSPLandroidx/work/impl/Processor;->interrupt(Ljava/lang/String;Landroidx/work/impl/WorkerWrapper;I)Z +PLandroidx/work/impl/Processor;->interrupt(Ljava/lang/String;Landroidx/work/impl/WorkerWrapper;I)Z PLandroidx/work/impl/Processor;->isCancelled(Ljava/lang/String;)Z HSPLandroidx/work/impl/Processor;->isEnqueued(Ljava/lang/String;)Z -HSPLandroidx/work/impl/Processor;->lambda$runOnExecuted$2$androidx-work-impl-Processor(Landroidx/work/impl/model/WorkGenerationalId;Z)V +PLandroidx/work/impl/Processor;->lambda$runOnExecuted$2$androidx-work-impl-Processor(Landroidx/work/impl/model/WorkGenerationalId;Z)V HSPLandroidx/work/impl/Processor;->lambda$startWork$0$androidx-work-impl-Processor(Ljava/util/ArrayList;Ljava/lang/String;)Landroidx/work/impl/model/WorkSpec; PLandroidx/work/impl/Processor;->lambda$startWork$1$androidx-work-impl-Processor(Lcom/google/common/util/concurrent/ListenableFuture;Landroidx/work/impl/WorkerWrapper;)V PLandroidx/work/impl/Processor;->onExecuted(Landroidx/work/impl/WorkerWrapper;Z)V @@ -17543,27 +17643,27 @@ HSPLandroidx/work/impl/Processor;->startWork(Landroidx/work/impl/StartStopToken; HSPLandroidx/work/impl/Processor;->stopWork(Landroidx/work/impl/StartStopToken;I)Z Landroidx/work/impl/Processor$$ExternalSyntheticLambda0; HSPLandroidx/work/impl/Processor$$ExternalSyntheticLambda0;->(Landroidx/work/impl/Processor;Landroidx/work/impl/model/WorkGenerationalId;Z)V -HSPLandroidx/work/impl/Processor$$ExternalSyntheticLambda0;->run()V +PLandroidx/work/impl/Processor$$ExternalSyntheticLambda0;->run()V Landroidx/work/impl/Processor$$ExternalSyntheticLambda1; HSPLandroidx/work/impl/Processor$$ExternalSyntheticLambda1;->(Landroidx/work/impl/Processor;Ljava/util/ArrayList;Ljava/lang/String;)V HSPLandroidx/work/impl/Processor$$ExternalSyntheticLambda1;->call()Ljava/lang/Object; Landroidx/work/impl/Processor$$ExternalSyntheticLambda2; HSPLandroidx/work/impl/Processor$$ExternalSyntheticLambda2;->(Landroidx/work/impl/Processor;Lcom/google/common/util/concurrent/ListenableFuture;Landroidx/work/impl/WorkerWrapper;)V -HSPLandroidx/work/impl/Processor$$ExternalSyntheticLambda2;->run()V +PLandroidx/work/impl/Processor$$ExternalSyntheticLambda2;->run()V Landroidx/work/impl/RescheduleMigration; HSPLandroidx/work/impl/RescheduleMigration;->(Landroid/content/Context;II)V Landroidx/work/impl/Scheduler; Landroidx/work/impl/Schedulers; HSPLandroidx/work/impl/Schedulers;->()V HSPLandroidx/work/impl/Schedulers;->createBestAvailableBackgroundScheduler(Landroid/content/Context;Landroidx/work/impl/WorkDatabase;Landroidx/work/Configuration;)Landroidx/work/impl/Scheduler; -HSPLandroidx/work/impl/Schedulers;->lambda$registerRescheduling$0(Ljava/util/List;Landroidx/work/impl/model/WorkGenerationalId;Landroidx/work/Configuration;Landroidx/work/impl/WorkDatabase;)V -HSPLandroidx/work/impl/Schedulers;->lambda$registerRescheduling$1(Ljava/util/concurrent/Executor;Ljava/util/List;Landroidx/work/Configuration;Landroidx/work/impl/WorkDatabase;Landroidx/work/impl/model/WorkGenerationalId;Z)V +PLandroidx/work/impl/Schedulers;->lambda$registerRescheduling$0(Ljava/util/List;Landroidx/work/impl/model/WorkGenerationalId;Landroidx/work/Configuration;Landroidx/work/impl/WorkDatabase;)V +PLandroidx/work/impl/Schedulers;->lambda$registerRescheduling$1(Ljava/util/concurrent/Executor;Ljava/util/List;Landroidx/work/Configuration;Landroidx/work/impl/WorkDatabase;Landroidx/work/impl/model/WorkGenerationalId;Z)V HSPLandroidx/work/impl/Schedulers;->markScheduled(Landroidx/work/impl/model/WorkSpecDao;Landroidx/work/Clock;Ljava/util/List;)V HSPLandroidx/work/impl/Schedulers;->registerRescheduling(Ljava/util/List;Landroidx/work/impl/Processor;Ljava/util/concurrent/Executor;Landroidx/work/impl/WorkDatabase;Landroidx/work/Configuration;)V HSPLandroidx/work/impl/Schedulers;->schedule(Landroidx/work/Configuration;Landroidx/work/impl/WorkDatabase;Ljava/util/List;)V Landroidx/work/impl/Schedulers$$ExternalSyntheticLambda0; HSPLandroidx/work/impl/Schedulers$$ExternalSyntheticLambda0;->(Ljava/util/concurrent/Executor;Ljava/util/List;Landroidx/work/Configuration;Landroidx/work/impl/WorkDatabase;)V -HSPLandroidx/work/impl/Schedulers$$ExternalSyntheticLambda0;->onExecuted(Landroidx/work/impl/model/WorkGenerationalId;Z)V +PLandroidx/work/impl/Schedulers$$ExternalSyntheticLambda0;->onExecuted(Landroidx/work/impl/model/WorkGenerationalId;Z)V PLandroidx/work/impl/Schedulers$$ExternalSyntheticLambda1;->(Ljava/util/List;Landroidx/work/impl/model/WorkGenerationalId;Landroidx/work/Configuration;Landroidx/work/impl/WorkDatabase;)V PLandroidx/work/impl/Schedulers$$ExternalSyntheticLambda1;->run()V Landroidx/work/impl/StartStopToken; @@ -17583,13 +17683,13 @@ HSPLandroidx/work/impl/StartStopTokens$Companion;->create(Z)Landroidx/work/impl/ Landroidx/work/impl/StartStopTokensImpl; HSPLandroidx/work/impl/StartStopTokensImpl;->()V HSPLandroidx/work/impl/StartStopTokensImpl;->contains(Landroidx/work/impl/model/WorkGenerationalId;)Z -HSPLandroidx/work/impl/StartStopTokensImpl;->remove(Landroidx/work/impl/model/WorkGenerationalId;)Landroidx/work/impl/StartStopToken; +PLandroidx/work/impl/StartStopTokensImpl;->remove(Landroidx/work/impl/model/WorkGenerationalId;)Landroidx/work/impl/StartStopToken; HSPLandroidx/work/impl/StartStopTokensImpl;->remove(Ljava/lang/String;)Ljava/util/List; HSPLandroidx/work/impl/StartStopTokensImpl;->tokenFor(Landroidx/work/impl/model/WorkGenerationalId;)Landroidx/work/impl/StartStopToken; Landroidx/work/impl/SynchronizedStartStopTokensImpl; HSPLandroidx/work/impl/SynchronizedStartStopTokensImpl;->(Landroidx/work/impl/StartStopTokens;)V HSPLandroidx/work/impl/SynchronizedStartStopTokensImpl;->contains(Landroidx/work/impl/model/WorkGenerationalId;)Z -HSPLandroidx/work/impl/SynchronizedStartStopTokensImpl;->remove(Landroidx/work/impl/model/WorkGenerationalId;)Landroidx/work/impl/StartStopToken; +PLandroidx/work/impl/SynchronizedStartStopTokensImpl;->remove(Landroidx/work/impl/model/WorkGenerationalId;)Landroidx/work/impl/StartStopToken; HSPLandroidx/work/impl/SynchronizedStartStopTokensImpl;->remove(Ljava/lang/String;)Ljava/util/List; HSPLandroidx/work/impl/SynchronizedStartStopTokensImpl;->tokenFor(Landroidx/work/impl/model/WorkGenerationalId;)Landroidx/work/impl/StartStopToken; PLandroidx/work/impl/ToContinuation;->(Lcom/google/common/util/concurrent/ListenableFuture;Lkotlinx/coroutines/CancellableContinuation;)V @@ -17737,8 +17837,8 @@ HSPLandroidx/work/impl/WorkManagerImplExtKt$WorkManagerImpl$1;->invoke(Ljava/lan Landroidx/work/impl/WorkMigration9To10; HSPLandroidx/work/impl/WorkMigration9To10;->(Landroid/content/Context;)V Landroidx/work/impl/WorkerStoppedException; -HSPLandroidx/work/impl/WorkerStoppedException;->(I)V -HSPLandroidx/work/impl/WorkerStoppedException;->getReason()I +PLandroidx/work/impl/WorkerStoppedException;->(I)V +PLandroidx/work/impl/WorkerStoppedException;->getReason()I Landroidx/work/impl/WorkerUpdater; HSPLandroidx/work/impl/WorkerUpdater;->enqueueUniquelyNamedPeriodic$lambda$6(Landroidx/work/impl/WorkManagerImpl;Ljava/lang/String;Landroidx/work/WorkRequest;)Lkotlin/Unit; HSPLandroidx/work/impl/WorkerUpdater;->enqueueUniquelyNamedPeriodic(Landroidx/work/impl/WorkManagerImpl;Ljava/lang/String;Landroidx/work/WorkRequest;)Landroidx/work/Operation; @@ -17754,23 +17854,24 @@ HSPLandroidx/work/impl/WorkerUpdater$$ExternalSyntheticLambda4;->(Landroid HSPLandroidx/work/impl/WorkerUpdater$$ExternalSyntheticLambda4;->invoke()Ljava/lang/Object; Landroidx/work/impl/WorkerWrapper; HSPLandroidx/work/impl/WorkerWrapper;->(Landroidx/work/impl/WorkerWrapper$Builder;)V -HSPLandroidx/work/impl/WorkerWrapper;->access$getAppContext$p(Landroidx/work/impl/WorkerWrapper;)Landroid/content/Context; -HSPLandroidx/work/impl/WorkerWrapper;->access$getWorkDatabase$p(Landroidx/work/impl/WorkerWrapper;)Landroidx/work/impl/WorkDatabase; -HSPLandroidx/work/impl/WorkerWrapper;->access$getWorkTaskExecutor$p(Landroidx/work/impl/WorkerWrapper;)Landroidx/work/impl/utils/taskexecutor/TaskExecutor; +PLandroidx/work/impl/WorkerWrapper;->access$getAppContext$p(Landroidx/work/impl/WorkerWrapper;)Landroid/content/Context; +PLandroidx/work/impl/WorkerWrapper;->access$getWorkDatabase$p(Landroidx/work/impl/WorkerWrapper;)Landroidx/work/impl/WorkDatabase; +PLandroidx/work/impl/WorkerWrapper;->access$getWorkTaskExecutor$p(Landroidx/work/impl/WorkerWrapper;)Landroidx/work/impl/utils/taskexecutor/TaskExecutor; HSPLandroidx/work/impl/WorkerWrapper;->access$getWorkerJob$p(Landroidx/work/impl/WorkerWrapper;)Lkotlinx/coroutines/CompletableJob; -HSPLandroidx/work/impl/WorkerWrapper;->access$resetWorkerStatus(Landroidx/work/impl/WorkerWrapper;I)Z +PLandroidx/work/impl/WorkerWrapper;->access$onWorkFinished(Landroidx/work/impl/WorkerWrapper;Landroidx/work/ListenableWorker$Result;)Z +PLandroidx/work/impl/WorkerWrapper;->access$resetWorkerStatus(Landroidx/work/impl/WorkerWrapper;I)Z HSPLandroidx/work/impl/WorkerWrapper;->access$runWorker(Landroidx/work/impl/WorkerWrapper;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; HSPLandroidx/work/impl/WorkerWrapper;->createWorkDescription(Ljava/util/List;)Ljava/lang/String; -HSPLandroidx/work/impl/WorkerWrapper;->getWorkGenerationalId()Landroidx/work/impl/model/WorkGenerationalId; -HSPLandroidx/work/impl/WorkerWrapper;->getWorkSpec()Landroidx/work/impl/model/WorkSpec; +PLandroidx/work/impl/WorkerWrapper;->getWorkGenerationalId()Landroidx/work/impl/model/WorkGenerationalId; +PLandroidx/work/impl/WorkerWrapper;->getWorkSpec()Landroidx/work/impl/model/WorkSpec; PLandroidx/work/impl/WorkerWrapper;->handleResult(Landroidx/work/ListenableWorker$Result;)Z -HSPLandroidx/work/impl/WorkerWrapper;->interrupt(I)V +PLandroidx/work/impl/WorkerWrapper;->interrupt(I)V HSPLandroidx/work/impl/WorkerWrapper;->launch()Lcom/google/common/util/concurrent/ListenableFuture; PLandroidx/work/impl/WorkerWrapper;->onWorkFinished(Landroidx/work/ListenableWorker$Result;)Z PLandroidx/work/impl/WorkerWrapper;->resetPeriodic()Z -HSPLandroidx/work/impl/WorkerWrapper;->resetWorkerStatus(I)Z +PLandroidx/work/impl/WorkerWrapper;->resetWorkerStatus(I)Z HSPLandroidx/work/impl/WorkerWrapper;->runWorker$lambda$1(Landroidx/work/impl/WorkerWrapper;)Ljava/lang/Boolean; -HSPLandroidx/work/impl/WorkerWrapper;->runWorker$lambda$4(Landroidx/work/ListenableWorker;ZLjava/lang/String;Landroidx/work/impl/WorkerWrapper;Ljava/lang/Throwable;)Lkotlin/Unit; +PLandroidx/work/impl/WorkerWrapper;->runWorker$lambda$4(Landroidx/work/ListenableWorker;ZLjava/lang/String;Landroidx/work/impl/WorkerWrapper;Ljava/lang/Throwable;)Lkotlin/Unit; HSPLandroidx/work/impl/WorkerWrapper;->runWorker(Lkotlin/coroutines/Continuation;)Ljava/lang/Object; HSPLandroidx/work/impl/WorkerWrapper;->trySetRunning$lambda$14(Landroidx/work/impl/WorkerWrapper;)Ljava/lang/Boolean; HSPLandroidx/work/impl/WorkerWrapper;->trySetRunning()Z @@ -17779,7 +17880,7 @@ HSPLandroidx/work/impl/WorkerWrapper$$ExternalSyntheticLambda0;->(Landroid HSPLandroidx/work/impl/WorkerWrapper$$ExternalSyntheticLambda0;->call()Ljava/lang/Object; Landroidx/work/impl/WorkerWrapper$$ExternalSyntheticLambda1; HSPLandroidx/work/impl/WorkerWrapper$$ExternalSyntheticLambda1;->(Landroidx/work/ListenableWorker;ZLjava/lang/String;Landroidx/work/impl/WorkerWrapper;)V -HSPLandroidx/work/impl/WorkerWrapper$$ExternalSyntheticLambda1;->invoke(Ljava/lang/Object;)Ljava/lang/Object; +PLandroidx/work/impl/WorkerWrapper$$ExternalSyntheticLambda1;->invoke(Ljava/lang/Object;)Ljava/lang/Object; Landroidx/work/impl/WorkerWrapper$$ExternalSyntheticLambda2; HSPLandroidx/work/impl/WorkerWrapper$$ExternalSyntheticLambda2;->(Landroidx/work/impl/WorkerWrapper;)V HSPLandroidx/work/impl/WorkerWrapper$$ExternalSyntheticLambda2;->call()Ljava/lang/Object; @@ -17798,6 +17899,7 @@ HSPLandroidx/work/impl/WorkerWrapper$Builder;->getWorker()Landroidx/work/Listena HSPLandroidx/work/impl/WorkerWrapper$Builder;->withRuntimeExtras(Landroidx/work/WorkerParameters$RuntimeExtras;)Landroidx/work/impl/WorkerWrapper$Builder; PLandroidx/work/impl/WorkerWrapper$Resolution;->()V PLandroidx/work/impl/WorkerWrapper$Resolution;->(Lkotlin/jvm/internal/DefaultConstructorMarker;)V +PLandroidx/work/impl/WorkerWrapper$Resolution$Finished;->getResult()Landroidx/work/ListenableWorker$Result; PLandroidx/work/impl/WorkerWrapper$Resolution$ResetWorkerStatus;->(I)V PLandroidx/work/impl/WorkerWrapper$Resolution$ResetWorkerStatus;->(IILkotlin/jvm/internal/DefaultConstructorMarker;)V PLandroidx/work/impl/WorkerWrapper$Resolution$ResetWorkerStatus;->getReason()I @@ -17806,7 +17908,7 @@ HSPLandroidx/work/impl/WorkerWrapper$launch$1;->(Landroidx/work/impl/Worke HSPLandroidx/work/impl/WorkerWrapper$launch$1;->create(Ljava/lang/Object;Lkotlin/coroutines/Continuation;)Lkotlin/coroutines/Continuation; HSPLandroidx/work/impl/WorkerWrapper$launch$1;->invoke(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object; HSPLandroidx/work/impl/WorkerWrapper$launch$1;->invoke(Lkotlinx/coroutines/CoroutineScope;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; -HSPLandroidx/work/impl/WorkerWrapper$launch$1;->invokeSuspend$lambda$1(Landroidx/work/impl/WorkerWrapper$Resolution;Landroidx/work/impl/WorkerWrapper;)Ljava/lang/Boolean; +PLandroidx/work/impl/WorkerWrapper$launch$1;->invokeSuspend$lambda$1(Landroidx/work/impl/WorkerWrapper$Resolution;Landroidx/work/impl/WorkerWrapper;)Ljava/lang/Boolean; HSPLandroidx/work/impl/WorkerWrapper$launch$1;->invokeSuspend(Ljava/lang/Object;)Ljava/lang/Object; PLandroidx/work/impl/WorkerWrapper$launch$1$$ExternalSyntheticLambda0;->(Landroidx/work/impl/WorkerWrapper$Resolution;Landroidx/work/impl/WorkerWrapper;)V PLandroidx/work/impl/WorkerWrapper$launch$1$$ExternalSyntheticLambda0;->call()Ljava/lang/Object; @@ -17818,11 +17920,11 @@ HSPLandroidx/work/impl/WorkerWrapper$launch$1$resolution$1;->invoke(Lkotlinx/cor HSPLandroidx/work/impl/WorkerWrapper$launch$1$resolution$1;->invokeSuspend(Ljava/lang/Object;)Ljava/lang/Object; Landroidx/work/impl/WorkerWrapper$runWorker$1; HSPLandroidx/work/impl/WorkerWrapper$runWorker$1;->(Landroidx/work/impl/WorkerWrapper;Lkotlin/coroutines/Continuation;)V -HSPLandroidx/work/impl/WorkerWrapper$runWorker$1;->invokeSuspend(Ljava/lang/Object;)Ljava/lang/Object; +PLandroidx/work/impl/WorkerWrapper$runWorker$1;->invokeSuspend(Ljava/lang/Object;)Ljava/lang/Object; Landroidx/work/impl/WorkerWrapper$runWorker$result$1; HSPLandroidx/work/impl/WorkerWrapper$runWorker$result$1;->(Landroidx/work/impl/WorkerWrapper;Landroidx/work/ListenableWorker;Landroidx/work/ForegroundUpdater;Lkotlin/coroutines/Continuation;)V HSPLandroidx/work/impl/WorkerWrapper$runWorker$result$1;->create(Ljava/lang/Object;Lkotlin/coroutines/Continuation;)Lkotlin/coroutines/Continuation; -HSPLandroidx/work/impl/WorkerWrapper$runWorker$result$1;->invokeSuspend(Ljava/lang/Object;)Ljava/lang/Object; +PLandroidx/work/impl/WorkerWrapper$runWorker$result$1;->invokeSuspend(Ljava/lang/Object;)Ljava/lang/Object; PLandroidx/work/impl/WorkerWrapperKt;->()V PLandroidx/work/impl/WorkerWrapperKt;->access$getTAG$p()Ljava/lang/String; PLandroidx/work/impl/WorkerWrapperKt;->access$getUninterruptibly(Ljava/util/concurrent/Future;)Ljava/lang/Object; @@ -17845,9 +17947,9 @@ HSPLandroidx/work/impl/background/greedy/GreedyScheduler;->(Landroid/conte HSPLandroidx/work/impl/background/greedy/GreedyScheduler;->cancel(Ljava/lang/String;)V HSPLandroidx/work/impl/background/greedy/GreedyScheduler;->checkDefaultProcess()V HSPLandroidx/work/impl/background/greedy/GreedyScheduler;->hasLimitedSchedulingSlots()Z -HSPLandroidx/work/impl/background/greedy/GreedyScheduler;->onExecuted(Landroidx/work/impl/model/WorkGenerationalId;Z)V +PLandroidx/work/impl/background/greedy/GreedyScheduler;->onExecuted(Landroidx/work/impl/model/WorkGenerationalId;Z)V HSPLandroidx/work/impl/background/greedy/GreedyScheduler;->registerExecutionListenerIfNeeded()V -HSPLandroidx/work/impl/background/greedy/GreedyScheduler;->removeConstraintTrackingFor(Landroidx/work/impl/model/WorkGenerationalId;)V +PLandroidx/work/impl/background/greedy/GreedyScheduler;->removeConstraintTrackingFor(Landroidx/work/impl/model/WorkGenerationalId;)V HSPLandroidx/work/impl/background/greedy/GreedyScheduler;->schedule([Landroidx/work/impl/model/WorkSpec;)V HSPLandroidx/work/impl/background/greedy/GreedyScheduler;->throttleIfNeeded(Landroidx/work/impl/model/WorkSpec;)J Landroidx/work/impl/background/greedy/GreedyScheduler$AttemptData; @@ -17899,13 +18001,13 @@ HSPLandroidx/work/impl/background/systemjob/SystemJobScheduler;->scheduleInterna Landroidx/work/impl/background/systemjob/SystemJobService; HSPLandroidx/work/impl/background/systemjob/SystemJobService;->()V HSPLandroidx/work/impl/background/systemjob/SystemJobService;->()V -HSPLandroidx/work/impl/background/systemjob/SystemJobService;->assertMainThread(Ljava/lang/String;)V +PLandroidx/work/impl/background/systemjob/SystemJobService;->assertMainThread(Ljava/lang/String;)V HSPLandroidx/work/impl/background/systemjob/SystemJobService;->onCreate()V PLandroidx/work/impl/background/systemjob/SystemJobService;->onDestroy()V -HSPLandroidx/work/impl/background/systemjob/SystemJobService;->onExecuted(Landroidx/work/impl/model/WorkGenerationalId;Z)V -HSPLandroidx/work/impl/background/systemjob/SystemJobService;->onStartJob(Landroid/app/job/JobParameters;)Z +PLandroidx/work/impl/background/systemjob/SystemJobService;->onExecuted(Landroidx/work/impl/model/WorkGenerationalId;Z)V +PLandroidx/work/impl/background/systemjob/SystemJobService;->onStartJob(Landroid/app/job/JobParameters;)Z PLandroidx/work/impl/background/systemjob/SystemJobService;->onStopJob(Landroid/app/job/JobParameters;)Z -HSPLandroidx/work/impl/background/systemjob/SystemJobService;->workGenerationalIdFromJobParameters(Landroid/app/job/JobParameters;)Landroidx/work/impl/model/WorkGenerationalId; +PLandroidx/work/impl/background/systemjob/SystemJobService;->workGenerationalIdFromJobParameters(Landroid/app/job/JobParameters;)Landroidx/work/impl/model/WorkGenerationalId; PLandroidx/work/impl/background/systemjob/SystemJobService$Api24Impl;->getTriggeredContentAuthorities(Landroid/app/job/JobParameters;)[Ljava/lang/String; PLandroidx/work/impl/background/systemjob/SystemJobService$Api24Impl;->getTriggeredContentUris(Landroid/app/job/JobParameters;)[Landroid/net/Uri; PLandroidx/work/impl/background/systemjob/SystemJobService$Api28Impl;->getNetwork(Landroid/app/job/JobParameters;)Landroid/net/Network; @@ -18386,7 +18488,7 @@ HSPLco/touchlab/kermit/LogcatWriter$WhenMappings;->()V Lco/touchlab/kermit/Logger; HSPLco/touchlab/kermit/Logger;->()V HSPLco/touchlab/kermit/Logger;->(Lco/touchlab/kermit/LoggerConfig;Ljava/lang/String;)V -HSPLco/touchlab/kermit/Logger;->getTag()Ljava/lang/String; +PLco/touchlab/kermit/Logger;->getTag()Ljava/lang/String; HSPLco/touchlab/kermit/Logger;->withTag(Ljava/lang/String;)Lco/touchlab/kermit/Logger; Lco/touchlab/kermit/Logger$Companion; HSPLco/touchlab/kermit/Logger$Companion;->()V @@ -18456,7 +18558,7 @@ HSPLcom/datadog/android/api/InternalLogger$Target;->values()[Lcom/datadog/androi Lcom/datadog/android/api/SdkCore; Lcom/datadog/android/api/context/AccountInfo; Lcom/datadog/android/api/context/DatadogContext; -HSPLcom/datadog/android/api/context/DatadogContext;->(Lcom/datadog/android/DatadogSite;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;ILjava/lang/String;Ljava/lang/String;Ljava/lang/String;Lcom/datadog/android/api/context/TimeInfo;Lcom/datadog/android/api/context/ProcessInfo;Lcom/datadog/android/api/context/NetworkInfo;Lcom/datadog/android/api/context/DeviceInfo;Lcom/datadog/android/api/context/UserInfo;Lcom/datadog/android/api/context/AccountInfo;Lcom/datadog/android/privacy/TrackingConsent;Ljava/lang/String;Ljava/util/Map;)V +HSPLcom/datadog/android/api/context/DatadogContext;->(Lcom/datadog/android/DatadogSite;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;ILjava/lang/String;Ljava/lang/String;Ljava/lang/String;Lcom/datadog/android/api/context/TimeInfo;Lcom/datadog/android/api/context/ProcessInfo;Lcom/datadog/android/api/context/NetworkInfo;Lcom/datadog/android/api/context/DeviceInfo;Lcom/datadog/android/api/context/UserInfo;Lcom/datadog/android/api/context/AccountInfo;Lcom/datadog/android/privacy/TrackingConsent;Ljava/lang/String;Ljava/lang/String;Ljava/util/Map;)V HSPLcom/datadog/android/api/context/DatadogContext;->getAccountInfo()Lcom/datadog/android/api/context/AccountInfo; HSPLcom/datadog/android/api/context/DatadogContext;->getAppBuildId()Ljava/lang/String; HSPLcom/datadog/android/api/context/DatadogContext;->getClientToken()Ljava/lang/String; @@ -18464,7 +18566,7 @@ HSPLcom/datadog/android/api/context/DatadogContext;->getDeviceInfo()Lcom/datadog HSPLcom/datadog/android/api/context/DatadogContext;->getEnv()Ljava/lang/String; HSPLcom/datadog/android/api/context/DatadogContext;->getFeaturesContext()Ljava/util/Map; HSPLcom/datadog/android/api/context/DatadogContext;->getNetworkInfo()Lcom/datadog/android/api/context/NetworkInfo; -PLcom/datadog/android/api/context/DatadogContext;->getProcessInfo()Lcom/datadog/android/api/context/ProcessInfo; +HSPLcom/datadog/android/api/context/DatadogContext;->getRemoteConfigurationId()Ljava/lang/String; HSPLcom/datadog/android/api/context/DatadogContext;->getSdkVersion()Ljava/lang/String; HSPLcom/datadog/android/api/context/DatadogContext;->getService()Ljava/lang/String; HSPLcom/datadog/android/api/context/DatadogContext;->getSite()Lcom/datadog/android/DatadogSite; @@ -18479,13 +18581,11 @@ Lcom/datadog/android/api/context/DeviceInfo; HSPLcom/datadog/android/api/context/DeviceInfo;->(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Lcom/datadog/android/api/context/DeviceType;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/Integer;Lcom/datadog/android/api/context/LocaleInfo;ILjava/lang/Integer;Ljava/lang/Boolean;)V HSPLcom/datadog/android/api/context/DeviceInfo;->getArchitecture()Ljava/lang/String; HSPLcom/datadog/android/api/context/DeviceInfo;->getDeviceBrand()Ljava/lang/String; -PLcom/datadog/android/api/context/DeviceInfo;->getDeviceBuildId()Ljava/lang/String; HSPLcom/datadog/android/api/context/DeviceInfo;->getDeviceModel()Ljava/lang/String; HSPLcom/datadog/android/api/context/DeviceInfo;->getDeviceName()Ljava/lang/String; HSPLcom/datadog/android/api/context/DeviceInfo;->getDeviceType()Lcom/datadog/android/api/context/DeviceType; HSPLcom/datadog/android/api/context/DeviceInfo;->getLocaleInfo()Lcom/datadog/android/api/context/LocaleInfo; HSPLcom/datadog/android/api/context/DeviceInfo;->getLogicalCpuCount()I -PLcom/datadog/android/api/context/DeviceInfo;->getNumberOfDisplays()Ljava/lang/Integer; HSPLcom/datadog/android/api/context/DeviceInfo;->getOsMajorVersion()Ljava/lang/String; HSPLcom/datadog/android/api/context/DeviceInfo;->getOsName()Ljava/lang/String; HSPLcom/datadog/android/api/context/DeviceInfo;->getOsVersion()Ljava/lang/String; @@ -18521,7 +18621,6 @@ HSPLcom/datadog/android/api/context/NetworkInfo$Connectivity$Companion;->( HSPLcom/datadog/android/api/context/NetworkInfo$Connectivity$Companion;->(Lkotlin/jvm/internal/DefaultConstructorMarker;)V Lcom/datadog/android/api/context/ProcessInfo; HSPLcom/datadog/android/api/context/ProcessInfo;->(Z)V -PLcom/datadog/android/api/context/ProcessInfo;->isMainProcess()Z Lcom/datadog/android/api/context/TimeInfo; HSPLcom/datadog/android/api/context/TimeInfo;->()V HSPLcom/datadog/android/api/context/TimeInfo;->(JJJJ)V @@ -18549,6 +18648,7 @@ Lcom/datadog/android/api/feature/FeatureScope; Lcom/datadog/android/api/feature/FeatureScopeExtKt; HSPLcom/datadog/android/api/feature/FeatureScopeExtKt;->measureMethodCallPerf(Lcom/datadog/android/api/InternalLogger;Ljava/lang/Class;Ljava/lang/String;FLkotlin/jvm/functions/Function0;)Ljava/lang/Object; Lcom/datadog/android/api/feature/FeatureSdkCore; +Lcom/datadog/android/api/feature/FeatureSdkCore$DefaultImpls; Lcom/datadog/android/api/feature/StorageBackedFeature; Lcom/datadog/android/api/net/Request; HSPLcom/datadog/android/api/net/Request;->(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/util/Map;[BLjava/lang/String;)V @@ -18606,9 +18706,8 @@ HSPLcom/datadog/android/core/SdkReference;->(Ljava/lang/String;Lkotlin/jvm HSPLcom/datadog/android/core/SdkReference;->(Ljava/lang/String;Lkotlin/jvm/functions/Function1;ILkotlin/jvm/internal/DefaultConstructorMarker;)V HSPLcom/datadog/android/core/SdkReference;->get()Lcom/datadog/android/api/SdkCore; HSPLcom/datadog/android/core/SdkReference;->tryAcquire()Lcom/datadog/android/api/SdkCore; -Lcom/datadog/android/core/SdkReference$1; -HSPLcom/datadog/android/core/SdkReference$1;->()V -HSPLcom/datadog/android/core/SdkReference$1;->()V +Lcom/datadog/android/core/SdkReference$$ExternalSyntheticLambda0; +HSPLcom/datadog/android/core/SdkReference$$ExternalSyntheticLambda0;->()V Lcom/datadog/android/core/configuration/BackPressureMitigation; HSPLcom/datadog/android/core/configuration/BackPressureMitigation;->$values()[Lcom/datadog/android/core/configuration/BackPressureMitigation; HSPLcom/datadog/android/core/configuration/BackPressureMitigation;->()V @@ -18641,6 +18740,10 @@ HSPLcom/datadog/android/core/configuration/Configuration;->getEnv$dd_sdk_android HSPLcom/datadog/android/core/configuration/Configuration;->getService$dd_sdk_android_core_release()Ljava/lang/String; HSPLcom/datadog/android/core/configuration/Configuration;->getVariant$dd_sdk_android_core_release()Ljava/lang/String; HSPLcom/datadog/android/core/configuration/Configuration;->getVersion$dd_sdk_android_core_release()Ljava/lang/String; +Lcom/datadog/android/core/configuration/Configuration$$ExternalSyntheticLambda0; +HSPLcom/datadog/android/core/configuration/Configuration$$ExternalSyntheticLambda0;->()V +Lcom/datadog/android/core/configuration/Configuration$$ExternalSyntheticLambda1; +HSPLcom/datadog/android/core/configuration/Configuration$$ExternalSyntheticLambda1;->()V Lcom/datadog/android/core/configuration/Configuration$Builder; HSPLcom/datadog/android/core/configuration/Configuration$Builder;->(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)V HSPLcom/datadog/android/core/configuration/Configuration$Builder;->build()Lcom/datadog/android/core/configuration/Configuration; @@ -18651,16 +18754,10 @@ Lcom/datadog/android/core/configuration/Configuration$Companion; HSPLcom/datadog/android/core/configuration/Configuration$Companion;->()V HSPLcom/datadog/android/core/configuration/Configuration$Companion;->(Lkotlin/jvm/internal/DefaultConstructorMarker;)V HSPLcom/datadog/android/core/configuration/Configuration$Companion;->getDEFAULT_CORE_CONFIG$dd_sdk_android_core_release()Lcom/datadog/android/core/configuration/Configuration$Core; -Lcom/datadog/android/core/configuration/Configuration$Companion$DEFAULT_BACKPRESSURE_STRATEGY$1; -HSPLcom/datadog/android/core/configuration/Configuration$Companion$DEFAULT_BACKPRESSURE_STRATEGY$1;->()V -HSPLcom/datadog/android/core/configuration/Configuration$Companion$DEFAULT_BACKPRESSURE_STRATEGY$1;->()V -Lcom/datadog/android/core/configuration/Configuration$Companion$DEFAULT_BACKPRESSURE_STRATEGY$2; -HSPLcom/datadog/android/core/configuration/Configuration$Companion$DEFAULT_BACKPRESSURE_STRATEGY$2;->()V -HSPLcom/datadog/android/core/configuration/Configuration$Companion$DEFAULT_BACKPRESSURE_STRATEGY$2;->()V Lcom/datadog/android/core/configuration/Configuration$Core; -HSPLcom/datadog/android/core/configuration/Configuration$Core;->(ZZLjava/util/Map;Lcom/datadog/android/core/configuration/BatchSize;Lcom/datadog/android/core/configuration/UploadFrequency;Ljava/net/Proxy;Lokhttp3/Authenticator;Lcom/datadog/android/security/Encryption;Lcom/datadog/android/DatadogSite;Lcom/datadog/android/core/configuration/BatchProcessingLevel;Lcom/datadog/android/core/persistence/PersistenceStrategy$Factory;Lcom/datadog/android/core/configuration/BackPressureStrategy;Lcom/datadog/android/core/configuration/UploadSchedulerStrategy;)V -HSPLcom/datadog/android/core/configuration/Configuration$Core;->copy$default(Lcom/datadog/android/core/configuration/Configuration$Core;ZZLjava/util/Map;Lcom/datadog/android/core/configuration/BatchSize;Lcom/datadog/android/core/configuration/UploadFrequency;Ljava/net/Proxy;Lokhttp3/Authenticator;Lcom/datadog/android/security/Encryption;Lcom/datadog/android/DatadogSite;Lcom/datadog/android/core/configuration/BatchProcessingLevel;Lcom/datadog/android/core/persistence/PersistenceStrategy$Factory;Lcom/datadog/android/core/configuration/BackPressureStrategy;Lcom/datadog/android/core/configuration/UploadSchedulerStrategy;ILjava/lang/Object;)Lcom/datadog/android/core/configuration/Configuration$Core; -HSPLcom/datadog/android/core/configuration/Configuration$Core;->copy(ZZLjava/util/Map;Lcom/datadog/android/core/configuration/BatchSize;Lcom/datadog/android/core/configuration/UploadFrequency;Ljava/net/Proxy;Lokhttp3/Authenticator;Lcom/datadog/android/security/Encryption;Lcom/datadog/android/DatadogSite;Lcom/datadog/android/core/configuration/BatchProcessingLevel;Lcom/datadog/android/core/persistence/PersistenceStrategy$Factory;Lcom/datadog/android/core/configuration/BackPressureStrategy;Lcom/datadog/android/core/configuration/UploadSchedulerStrategy;)Lcom/datadog/android/core/configuration/Configuration$Core; +HSPLcom/datadog/android/core/configuration/Configuration$Core;->(ZZLjava/util/Map;Lcom/datadog/android/core/configuration/BatchSize;Lcom/datadog/android/core/configuration/UploadFrequency;Ljava/net/Proxy;Lokhttp3/Authenticator;Lcom/datadog/android/security/Encryption;Lcom/datadog/android/DatadogSite;Lcom/datadog/android/core/configuration/BatchProcessingLevel;Lcom/datadog/android/core/persistence/PersistenceStrategy$Factory;Lcom/datadog/android/core/configuration/BackPressureStrategy;Lcom/datadog/android/core/configuration/UploadSchedulerStrategy;Ljava/lang/String;)V +HSPLcom/datadog/android/core/configuration/Configuration$Core;->copy$default(Lcom/datadog/android/core/configuration/Configuration$Core;ZZLjava/util/Map;Lcom/datadog/android/core/configuration/BatchSize;Lcom/datadog/android/core/configuration/UploadFrequency;Ljava/net/Proxy;Lokhttp3/Authenticator;Lcom/datadog/android/security/Encryption;Lcom/datadog/android/DatadogSite;Lcom/datadog/android/core/configuration/BatchProcessingLevel;Lcom/datadog/android/core/persistence/PersistenceStrategy$Factory;Lcom/datadog/android/core/configuration/BackPressureStrategy;Lcom/datadog/android/core/configuration/UploadSchedulerStrategy;Ljava/lang/String;ILjava/lang/Object;)Lcom/datadog/android/core/configuration/Configuration$Core; +HSPLcom/datadog/android/core/configuration/Configuration$Core;->copy(ZZLjava/util/Map;Lcom/datadog/android/core/configuration/BatchSize;Lcom/datadog/android/core/configuration/UploadFrequency;Ljava/net/Proxy;Lokhttp3/Authenticator;Lcom/datadog/android/security/Encryption;Lcom/datadog/android/DatadogSite;Lcom/datadog/android/core/configuration/BatchProcessingLevel;Lcom/datadog/android/core/persistence/PersistenceStrategy$Factory;Lcom/datadog/android/core/configuration/BackPressureStrategy;Lcom/datadog/android/core/configuration/UploadSchedulerStrategy;Ljava/lang/String;)Lcom/datadog/android/core/configuration/Configuration$Core; HSPLcom/datadog/android/core/configuration/Configuration$Core;->getBackpressureStrategy()Lcom/datadog/android/core/configuration/BackPressureStrategy; PLcom/datadog/android/core/configuration/Configuration$Core;->getBatchProcessingLevel()Lcom/datadog/android/core/configuration/BatchProcessingLevel; HSPLcom/datadog/android/core/configuration/Configuration$Core;->getBatchSize()Lcom/datadog/android/core/configuration/BatchSize; @@ -18670,6 +18767,7 @@ HSPLcom/datadog/android/core/configuration/Configuration$Core;->getFirstPartyHos HSPLcom/datadog/android/core/configuration/Configuration$Core;->getNeedsClearTextHttp()Z HSPLcom/datadog/android/core/configuration/Configuration$Core;->getPersistenceStrategyFactory()Lcom/datadog/android/core/persistence/PersistenceStrategy$Factory; HSPLcom/datadog/android/core/configuration/Configuration$Core;->getProxy()Ljava/net/Proxy; +HSPLcom/datadog/android/core/configuration/Configuration$Core;->getRemoteConfigurationId()Ljava/lang/String; HSPLcom/datadog/android/core/configuration/Configuration$Core;->getSite()Lcom/datadog/android/DatadogSite; HSPLcom/datadog/android/core/configuration/Configuration$Core;->getUploadFrequency()Lcom/datadog/android/core/configuration/UploadFrequency; HSPLcom/datadog/android/core/configuration/Configuration$Core;->getUploadSchedulerStrategy()Lcom/datadog/android/core/configuration/UploadSchedulerStrategy; @@ -18702,48 +18800,42 @@ HSPLcom/datadog/android/core/constraints/DataConstraints$DefaultImpls;->validate Lcom/datadog/android/core/constraints/DatadogDataConstraints; HSPLcom/datadog/android/core/constraints/DatadogDataConstraints;->()V HSPLcom/datadog/android/core/constraints/DatadogDataConstraints;->(Lcom/datadog/android/api/InternalLogger;)V -HSPLcom/datadog/android/core/constraints/DatadogDataConstraints;->access$isKeyReserved(Lcom/datadog/android/core/constraints/DatadogDataConstraints;Ljava/lang/String;)Z HSPLcom/datadog/android/core/constraints/DatadogDataConstraints;->convertAttributeKey(Ljava/lang/String;I)Ljava/lang/String; HSPLcom/datadog/android/core/constraints/DatadogDataConstraints;->convertTag(Ljava/lang/String;)Ljava/lang/String; HSPLcom/datadog/android/core/constraints/DatadogDataConstraints;->isKeyReserved(Ljava/lang/String;)Z +HSPLcom/datadog/android/core/constraints/DatadogDataConstraints;->tagTransforms$lambda$13(Ljava/lang/String;)Ljava/lang/String; +HSPLcom/datadog/android/core/constraints/DatadogDataConstraints;->tagTransforms$lambda$14(Ljava/lang/String;)Ljava/lang/String; +HSPLcom/datadog/android/core/constraints/DatadogDataConstraints;->tagTransforms$lambda$15(Ljava/lang/String;)Ljava/lang/String; +HSPLcom/datadog/android/core/constraints/DatadogDataConstraints;->tagTransforms$lambda$16(Ljava/lang/String;)Ljava/lang/String; +HSPLcom/datadog/android/core/constraints/DatadogDataConstraints;->tagTransforms$lambda$17(Ljava/lang/String;)Ljava/lang/String; +HSPLcom/datadog/android/core/constraints/DatadogDataConstraints;->tagTransforms$lambda$18(Lcom/datadog/android/core/constraints/DatadogDataConstraints;Ljava/lang/String;)Ljava/lang/String; HSPLcom/datadog/android/core/constraints/DatadogDataConstraints;->validateAttributes(Ljava/util/Map;Ljava/lang/String;Ljava/lang/String;Ljava/util/Set;)Ljava/util/Map; +HSPLcom/datadog/android/core/constraints/DatadogDataConstraints;->validateTags$lambda$2$lambda$1(Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String; HSPLcom/datadog/android/core/constraints/DatadogDataConstraints;->validateTags(Ljava/util/List;)Ljava/util/List; +Lcom/datadog/android/core/constraints/DatadogDataConstraints$$ExternalSyntheticLambda0; +HSPLcom/datadog/android/core/constraints/DatadogDataConstraints$$ExternalSyntheticLambda0;->()V +HSPLcom/datadog/android/core/constraints/DatadogDataConstraints$$ExternalSyntheticLambda0;->invoke(Ljava/lang/Object;)Ljava/lang/Object; +Lcom/datadog/android/core/constraints/DatadogDataConstraints$$ExternalSyntheticLambda11; +HSPLcom/datadog/android/core/constraints/DatadogDataConstraints$$ExternalSyntheticLambda11;->(Ljava/lang/String;Ljava/lang/String;)V +HSPLcom/datadog/android/core/constraints/DatadogDataConstraints$$ExternalSyntheticLambda11;->invoke()Ljava/lang/Object; +Lcom/datadog/android/core/constraints/DatadogDataConstraints$$ExternalSyntheticLambda5; +HSPLcom/datadog/android/core/constraints/DatadogDataConstraints$$ExternalSyntheticLambda5;->()V +HSPLcom/datadog/android/core/constraints/DatadogDataConstraints$$ExternalSyntheticLambda5;->invoke(Ljava/lang/Object;)Ljava/lang/Object; +Lcom/datadog/android/core/constraints/DatadogDataConstraints$$ExternalSyntheticLambda6; +HSPLcom/datadog/android/core/constraints/DatadogDataConstraints$$ExternalSyntheticLambda6;->()V +HSPLcom/datadog/android/core/constraints/DatadogDataConstraints$$ExternalSyntheticLambda6;->invoke(Ljava/lang/Object;)Ljava/lang/Object; +Lcom/datadog/android/core/constraints/DatadogDataConstraints$$ExternalSyntheticLambda7; +HSPLcom/datadog/android/core/constraints/DatadogDataConstraints$$ExternalSyntheticLambda7;->()V +HSPLcom/datadog/android/core/constraints/DatadogDataConstraints$$ExternalSyntheticLambda7;->invoke(Ljava/lang/Object;)Ljava/lang/Object; +Lcom/datadog/android/core/constraints/DatadogDataConstraints$$ExternalSyntheticLambda8; +HSPLcom/datadog/android/core/constraints/DatadogDataConstraints$$ExternalSyntheticLambda8;->()V +HSPLcom/datadog/android/core/constraints/DatadogDataConstraints$$ExternalSyntheticLambda8;->invoke(Ljava/lang/Object;)Ljava/lang/Object; +Lcom/datadog/android/core/constraints/DatadogDataConstraints$$ExternalSyntheticLambda9; +HSPLcom/datadog/android/core/constraints/DatadogDataConstraints$$ExternalSyntheticLambda9;->(Lcom/datadog/android/core/constraints/DatadogDataConstraints;)V +HSPLcom/datadog/android/core/constraints/DatadogDataConstraints$$ExternalSyntheticLambda9;->invoke(Ljava/lang/Object;)Ljava/lang/Object; Lcom/datadog/android/core/constraints/DatadogDataConstraints$Companion; HSPLcom/datadog/android/core/constraints/DatadogDataConstraints$Companion;->()V HSPLcom/datadog/android/core/constraints/DatadogDataConstraints$Companion;->(Lkotlin/jvm/internal/DefaultConstructorMarker;)V -Lcom/datadog/android/core/constraints/DatadogDataConstraints$tagTransforms$1; -HSPLcom/datadog/android/core/constraints/DatadogDataConstraints$tagTransforms$1;->()V -HSPLcom/datadog/android/core/constraints/DatadogDataConstraints$tagTransforms$1;->()V -HSPLcom/datadog/android/core/constraints/DatadogDataConstraints$tagTransforms$1;->invoke(Ljava/lang/Object;)Ljava/lang/Object; -HSPLcom/datadog/android/core/constraints/DatadogDataConstraints$tagTransforms$1;->invoke(Ljava/lang/String;)Ljava/lang/String; -Lcom/datadog/android/core/constraints/DatadogDataConstraints$tagTransforms$2; -HSPLcom/datadog/android/core/constraints/DatadogDataConstraints$tagTransforms$2;->()V -HSPLcom/datadog/android/core/constraints/DatadogDataConstraints$tagTransforms$2;->()V -HSPLcom/datadog/android/core/constraints/DatadogDataConstraints$tagTransforms$2;->invoke(Ljava/lang/Object;)Ljava/lang/Object; -HSPLcom/datadog/android/core/constraints/DatadogDataConstraints$tagTransforms$2;->invoke(Ljava/lang/String;)Ljava/lang/String; -Lcom/datadog/android/core/constraints/DatadogDataConstraints$tagTransforms$3; -HSPLcom/datadog/android/core/constraints/DatadogDataConstraints$tagTransforms$3;->()V -HSPLcom/datadog/android/core/constraints/DatadogDataConstraints$tagTransforms$3;->()V -HSPLcom/datadog/android/core/constraints/DatadogDataConstraints$tagTransforms$3;->invoke(Ljava/lang/Object;)Ljava/lang/Object; -HSPLcom/datadog/android/core/constraints/DatadogDataConstraints$tagTransforms$3;->invoke(Ljava/lang/String;)Ljava/lang/String; -Lcom/datadog/android/core/constraints/DatadogDataConstraints$tagTransforms$4; -HSPLcom/datadog/android/core/constraints/DatadogDataConstraints$tagTransforms$4;->()V -HSPLcom/datadog/android/core/constraints/DatadogDataConstraints$tagTransforms$4;->()V -HSPLcom/datadog/android/core/constraints/DatadogDataConstraints$tagTransforms$4;->invoke(Ljava/lang/Object;)Ljava/lang/Object; -HSPLcom/datadog/android/core/constraints/DatadogDataConstraints$tagTransforms$4;->invoke(Ljava/lang/String;)Ljava/lang/String; -Lcom/datadog/android/core/constraints/DatadogDataConstraints$tagTransforms$5; -HSPLcom/datadog/android/core/constraints/DatadogDataConstraints$tagTransforms$5;->()V -HSPLcom/datadog/android/core/constraints/DatadogDataConstraints$tagTransforms$5;->()V -HSPLcom/datadog/android/core/constraints/DatadogDataConstraints$tagTransforms$5;->invoke(Ljava/lang/Object;)Ljava/lang/Object; -HSPLcom/datadog/android/core/constraints/DatadogDataConstraints$tagTransforms$5;->invoke(Ljava/lang/String;)Ljava/lang/String; -Lcom/datadog/android/core/constraints/DatadogDataConstraints$tagTransforms$6; -HSPLcom/datadog/android/core/constraints/DatadogDataConstraints$tagTransforms$6;->(Lcom/datadog/android/core/constraints/DatadogDataConstraints;)V -HSPLcom/datadog/android/core/constraints/DatadogDataConstraints$tagTransforms$6;->invoke(Ljava/lang/Object;)Ljava/lang/Object; -HSPLcom/datadog/android/core/constraints/DatadogDataConstraints$tagTransforms$6;->invoke(Ljava/lang/String;)Ljava/lang/String; -Lcom/datadog/android/core/constraints/DatadogDataConstraints$validateTags$convertedTags$1$2; -HSPLcom/datadog/android/core/constraints/DatadogDataConstraints$validateTags$convertedTags$1$2;->(Ljava/lang/String;Ljava/lang/String;)V -HSPLcom/datadog/android/core/constraints/DatadogDataConstraints$validateTags$convertedTags$1$2;->invoke()Ljava/lang/Object; -HSPLcom/datadog/android/core/constraints/DatadogDataConstraints$validateTags$convertedTags$1$2;->invoke()Ljava/lang/String; Lcom/datadog/android/core/feature/event/JvmCrash; Lcom/datadog/android/core/feature/event/JvmCrash$Rum; Lcom/datadog/android/core/internal/ContextProvider; @@ -18751,13 +18843,9 @@ Lcom/datadog/android/core/internal/CoreFeature; HSPLcom/datadog/android/core/internal/CoreFeature;->()V HSPLcom/datadog/android/core/internal/CoreFeature;->(Lcom/datadog/android/api/InternalLogger;Lcom/datadog/android/core/internal/time/AppStartTimeProvider;Lcom/datadog/android/core/thread/FlushableExecutorService$Factory;Lcom/datadog/android/core/internal/thread/ScheduledExecutorServiceFactory;Lcom/datadog/android/internal/system/BuildSdkVersionProvider;)V HSPLcom/datadog/android/core/internal/CoreFeature;->(Lcom/datadog/android/api/InternalLogger;Lcom/datadog/android/core/internal/time/AppStartTimeProvider;Lcom/datadog/android/core/thread/FlushableExecutorService$Factory;Lcom/datadog/android/core/internal/thread/ScheduledExecutorServiceFactory;Lcom/datadog/android/internal/system/BuildSdkVersionProvider;ILkotlin/jvm/internal/DefaultConstructorMarker;)V -HSPLcom/datadog/android/core/internal/CoreFeature;->DEFAULT_FLUSHABLE_EXECUTOR_SERVICE_FACTORY$lambda$11(Lcom/datadog/android/api/InternalLogger;Ljava/lang/String;Lcom/datadog/android/core/configuration/BackPressureStrategy;Lcom/datadog/android/internal/time/TimeProvider;)Lcom/datadog/android/core/thread/FlushableExecutorService; +HSPLcom/datadog/android/core/internal/CoreFeature;->DEFAULT_FLUSHABLE_EXECUTOR_SERVICE_FACTORY$lambda$28(Lcom/datadog/android/api/InternalLogger;Ljava/lang/String;Lcom/datadog/android/core/configuration/BackPressureStrategy;Lcom/datadog/android/internal/time/TimeProvider;)Lcom/datadog/android/core/thread/FlushableExecutorService; HSPLcom/datadog/android/core/internal/CoreFeature;->access$getDEFAULT_FLUSHABLE_EXECUTOR_SERVICE_FACTORY$cp()Lcom/datadog/android/core/thread/FlushableExecutorService$Factory; HSPLcom/datadog/android/core/internal/CoreFeature;->access$getDEFAULT_SCHEDULED_EXECUTOR_SERVICE_FACTORY$cp()Lcom/datadog/android/core/internal/thread/ScheduledExecutorServiceFactory; -HSPLcom/datadog/android/core/internal/CoreFeature;->access$getInternalLogger$p(Lcom/datadog/android/core/internal/CoreFeature;)Lcom/datadog/android/api/InternalLogger; -HSPLcom/datadog/android/core/internal/CoreFeature;->access$getLazySharedOkHttpClient(Lcom/datadog/android/core/internal/CoreFeature;)Lokhttp3/OkHttpClient; -HSPLcom/datadog/android/core/internal/CoreFeature;->access$getNETWORK_TIMEOUT_MS$cp()J -HSPLcom/datadog/android/core/internal/CoreFeature;->access$getRESTRICTED_CIPHER_SUITES$cp()[Lokhttp3/CipherSuite; HSPLcom/datadog/android/core/internal/CoreFeature;->buildFilePersistenceConfig()Lcom/datadog/android/core/internal/persistence/file/FilePersistenceConfig; HSPLcom/datadog/android/core/internal/CoreFeature;->createExecutorService(Ljava/lang/String;)Ljava/util/concurrent/ExecutorService; HSPLcom/datadog/android/core/internal/CoreFeature;->getAccountInfoProvider$dd_sdk_android_core_release()Lcom/datadog/android/core/internal/account/MutableAccountInfoProvider; @@ -18786,6 +18874,7 @@ HSPLcom/datadog/android/core/internal/CoreFeature;->getPackageInfo(Landroid/cont HSPLcom/datadog/android/core/internal/CoreFeature;->getPackageVersionProvider$dd_sdk_android_core_release()Lcom/datadog/android/core/internal/system/AppVersionProvider; HSPLcom/datadog/android/core/internal/CoreFeature;->getPersistenceExecutorService$dd_sdk_android_core_release()Lcom/datadog/android/core/thread/FlushableExecutorService; HSPLcom/datadog/android/core/internal/CoreFeature;->getPersistenceStrategyFactory$dd_sdk_android_core_release()Lcom/datadog/android/core/persistence/PersistenceStrategy$Factory; +HSPLcom/datadog/android/core/internal/CoreFeature;->getRemoteConfigurationId$dd_sdk_android_core_release()Ljava/lang/String; HSPLcom/datadog/android/core/internal/CoreFeature;->getSafeContext(Landroid/content/Context;)Landroid/content/Context; HSPLcom/datadog/android/core/internal/CoreFeature;->getSdkVersion$dd_sdk_android_core_release()Ljava/lang/String; HSPLcom/datadog/android/core/internal/CoreFeature;->getServiceName$dd_sdk_android_core_release()Ljava/lang/String; @@ -18799,10 +18888,14 @@ HSPLcom/datadog/android/core/internal/CoreFeature;->getUploadExecutorService$dd_ HSPLcom/datadog/android/core/internal/CoreFeature;->getUploadFrequency$dd_sdk_android_core_release()Lcom/datadog/android/core/configuration/UploadFrequency; HSPLcom/datadog/android/core/internal/CoreFeature;->getUserInfoProvider$dd_sdk_android_core_release()Lcom/datadog/android/core/internal/user/MutableUserInfoProvider; HSPLcom/datadog/android/core/internal/CoreFeature;->getVariant$dd_sdk_android_core_release()Ljava/lang/String; -HSPLcom/datadog/android/core/internal/CoreFeature;->initialize$lambda$0(Lcom/datadog/android/core/internal/CoreFeature;Landroid/content/Context;)V +HSPLcom/datadog/android/core/internal/CoreFeature;->initialize$lambda$4(Lcom/datadog/android/core/internal/CoreFeature;Landroid/content/Context;)V +HSPLcom/datadog/android/core/internal/CoreFeature;->initialize$lambda$5(Landroid/content/Context;Ljava/lang/String;)Ljava/io/File; HSPLcom/datadog/android/core/internal/CoreFeature;->initialize(Landroid/content/Context;Ljava/lang/String;Lcom/datadog/android/core/configuration/Configuration;Lcom/datadog/android/privacy/TrackingConsent;)V HSPLcom/datadog/android/core/internal/CoreFeature;->initializeClockSync(Landroid/content/Context;)V HSPLcom/datadog/android/core/internal/CoreFeature;->isMainProcess$dd_sdk_android_core_release()Z +HSPLcom/datadog/android/core/internal/CoreFeature;->lastViewEventFileWriter_delegate$lambda$3(Lcom/datadog/android/core/internal/CoreFeature;)Lcom/datadog/android/core/internal/persistence/file/batch/BatchFileReaderWriter; +HSPLcom/datadog/android/core/internal/CoreFeature;->lastViewEventFile_delegate$lambda$2(Lcom/datadog/android/core/internal/CoreFeature;)Ljava/io/File; +HSPLcom/datadog/android/core/internal/CoreFeature;->lazySharedOkHttpClient_delegate$lambda$0(Lcom/datadog/android/core/internal/CoreFeature;)Lokhttp3/OkHttpClient; HSPLcom/datadog/android/core/internal/CoreFeature;->prepareNdkCrashData(Ljava/lang/String;)V HSPLcom/datadog/android/core/internal/CoreFeature;->readApplicationInformation(Landroid/content/Context;Lcom/datadog/android/core/configuration/Configuration;)V HSPLcom/datadog/android/core/internal/CoreFeature;->readBuildId(Landroid/content/Context;)Ljava/lang/String; @@ -18818,142 +18911,137 @@ HSPLcom/datadog/android/core/internal/CoreFeature;->setUploadExecutorService$dd_ HSPLcom/datadog/android/core/internal/CoreFeature;->setupExecutors()V HSPLcom/datadog/android/core/internal/CoreFeature;->setupInfoProviders(Landroid/content/Context;Lcom/datadog/android/privacy/TrackingConsent;)V HSPLcom/datadog/android/core/internal/CoreFeature;->setupNetworkInfoProviders(Landroid/content/Context;Landroid/os/Handler;)V +HSPLcom/datadog/android/core/internal/CoreFeature;->setupOkHttpClient$lambda$22(Lcom/datadog/android/core/internal/CoreFeature;Lcom/datadog/android/core/configuration/Configuration$Core;)Lokhttp3/OkHttpClient; HSPLcom/datadog/android/core/internal/CoreFeature;->setupOkHttpClient(Lcom/datadog/android/core/configuration/Configuration$Core;)V HSPLcom/datadog/android/core/internal/CoreFeature;->writeLastViewEvent$dd_sdk_android_core_release([B)V -Lcom/datadog/android/core/internal/CoreFeature$$ExternalSyntheticLambda0; -HSPLcom/datadog/android/core/internal/CoreFeature$$ExternalSyntheticLambda0;->(Lcom/datadog/android/core/internal/CoreFeature;Landroid/content/Context;)V -HSPLcom/datadog/android/core/internal/CoreFeature$$ExternalSyntheticLambda0;->run()V -Lcom/datadog/android/core/internal/CoreFeature$$ExternalSyntheticLambda1; -HSPLcom/datadog/android/core/internal/CoreFeature$$ExternalSyntheticLambda1;->()V -HSPLcom/datadog/android/core/internal/CoreFeature$$ExternalSyntheticLambda1;->create(Lcom/datadog/android/api/InternalLogger;Ljava/lang/String;Lcom/datadog/android/core/configuration/BackPressureStrategy;Lcom/datadog/android/internal/time/TimeProvider;)Lcom/datadog/android/core/thread/FlushableExecutorService; -Lcom/datadog/android/core/internal/CoreFeature$$ExternalSyntheticLambda2; -HSPLcom/datadog/android/core/internal/CoreFeature$$ExternalSyntheticLambda2;->()V +Lcom/datadog/android/core/internal/CoreFeature$$ExternalSyntheticLambda10; +HSPLcom/datadog/android/core/internal/CoreFeature$$ExternalSyntheticLambda10;->()V +Lcom/datadog/android/core/internal/CoreFeature$$ExternalSyntheticLambda13; +HSPLcom/datadog/android/core/internal/CoreFeature$$ExternalSyntheticLambda13;->(Lcom/datadog/android/core/internal/CoreFeature;)V +Lcom/datadog/android/core/internal/CoreFeature$$ExternalSyntheticLambda14; +HSPLcom/datadog/android/core/internal/CoreFeature$$ExternalSyntheticLambda14;->(Lcom/datadog/android/core/internal/CoreFeature;)V +HSPLcom/datadog/android/core/internal/CoreFeature$$ExternalSyntheticLambda14;->invoke()Ljava/lang/Object; +Lcom/datadog/android/core/internal/CoreFeature$$ExternalSyntheticLambda15; +HSPLcom/datadog/android/core/internal/CoreFeature$$ExternalSyntheticLambda15;->(Lcom/datadog/android/core/internal/CoreFeature;)V +Lcom/datadog/android/core/internal/CoreFeature$$ExternalSyntheticLambda16; +HSPLcom/datadog/android/core/internal/CoreFeature$$ExternalSyntheticLambda16;->(Lcom/datadog/android/core/internal/CoreFeature;)V +HSPLcom/datadog/android/core/internal/CoreFeature$$ExternalSyntheticLambda16;->invoke()Ljava/lang/Object; +Lcom/datadog/android/core/internal/CoreFeature$$ExternalSyntheticLambda17; +HSPLcom/datadog/android/core/internal/CoreFeature$$ExternalSyntheticLambda17;->(Lcom/datadog/android/core/internal/CoreFeature;)V +HSPLcom/datadog/android/core/internal/CoreFeature$$ExternalSyntheticLambda17;->invoke()Ljava/lang/Object; +Lcom/datadog/android/core/internal/CoreFeature$$ExternalSyntheticLambda19; +HSPLcom/datadog/android/core/internal/CoreFeature$$ExternalSyntheticLambda19;->()V +Lcom/datadog/android/core/internal/CoreFeature$$ExternalSyntheticLambda4; +HSPLcom/datadog/android/core/internal/CoreFeature$$ExternalSyntheticLambda4;->(Lcom/datadog/android/core/internal/CoreFeature;Landroid/content/Context;)V +HSPLcom/datadog/android/core/internal/CoreFeature$$ExternalSyntheticLambda4;->run()V +Lcom/datadog/android/core/internal/CoreFeature$$ExternalSyntheticLambda5; +HSPLcom/datadog/android/core/internal/CoreFeature$$ExternalSyntheticLambda5;->(Landroid/content/Context;Ljava/lang/String;)V +HSPLcom/datadog/android/core/internal/CoreFeature$$ExternalSyntheticLambda5;->invoke()Ljava/lang/Object; +Lcom/datadog/android/core/internal/CoreFeature$$ExternalSyntheticLambda6; +HSPLcom/datadog/android/core/internal/CoreFeature$$ExternalSyntheticLambda6;->(Lcom/datadog/android/core/internal/CoreFeature;Lcom/datadog/android/core/configuration/Configuration$Core;)V +HSPLcom/datadog/android/core/internal/CoreFeature$$ExternalSyntheticLambda6;->invoke()Ljava/lang/Object; +Lcom/datadog/android/core/internal/CoreFeature$$ExternalSyntheticLambda7; +HSPLcom/datadog/android/core/internal/CoreFeature$$ExternalSyntheticLambda7;->()V +Lcom/datadog/android/core/internal/CoreFeature$$ExternalSyntheticLambda8; +HSPLcom/datadog/android/core/internal/CoreFeature$$ExternalSyntheticLambda8;->()V +Lcom/datadog/android/core/internal/CoreFeature$$ExternalSyntheticLambda9; +HSPLcom/datadog/android/core/internal/CoreFeature$$ExternalSyntheticLambda9;->()V +HSPLcom/datadog/android/core/internal/CoreFeature$$ExternalSyntheticLambda9;->create(Lcom/datadog/android/api/InternalLogger;Ljava/lang/String;Lcom/datadog/android/core/configuration/BackPressureStrategy;Lcom/datadog/android/internal/time/TimeProvider;)Lcom/datadog/android/core/thread/FlushableExecutorService; Lcom/datadog/android/core/internal/CoreFeature$Companion; HSPLcom/datadog/android/core/internal/CoreFeature$Companion;->()V HSPLcom/datadog/android/core/internal/CoreFeature$Companion;->(Lkotlin/jvm/internal/DefaultConstructorMarker;)V HSPLcom/datadog/android/core/internal/CoreFeature$Companion;->getDEFAULT_FLUSHABLE_EXECUTOR_SERVICE_FACTORY$dd_sdk_android_core_release()Lcom/datadog/android/core/thread/FlushableExecutorService$Factory; HSPLcom/datadog/android/core/internal/CoreFeature$Companion;->getDEFAULT_SCHEDULED_EXECUTOR_SERVICE_FACTORY$dd_sdk_android_core_release()Lcom/datadog/android/core/internal/thread/ScheduledExecutorServiceFactory; -HSPLcom/datadog/android/core/internal/CoreFeature$Companion;->getNETWORK_TIMEOUT_MS$dd_sdk_android_core_release()J -HSPLcom/datadog/android/core/internal/CoreFeature$Companion;->getRESTRICTED_CIPHER_SUITES$dd_sdk_android_core_release()[Lokhttp3/CipherSuite; Lcom/datadog/android/core/internal/CoreFeature$OkHttpCallFactory; HSPLcom/datadog/android/core/internal/CoreFeature$OkHttpCallFactory;->(Lkotlin/jvm/functions/Function0;)V HSPLcom/datadog/android/core/internal/CoreFeature$OkHttpCallFactory;->getOkhttpClient()Lokhttp3/OkHttpClient; HSPLcom/datadog/android/core/internal/CoreFeature$OkHttpCallFactory;->newCall(Lokhttp3/Request;)Lokhttp3/Call; -Lcom/datadog/android/core/internal/CoreFeature$initialize$2; -HSPLcom/datadog/android/core/internal/CoreFeature$initialize$2;->(Landroid/content/Context;Ljava/lang/String;)V -HSPLcom/datadog/android/core/internal/CoreFeature$initialize$2;->invoke()Ljava/io/File; -HSPLcom/datadog/android/core/internal/CoreFeature$initialize$2;->invoke()Ljava/lang/Object; -Lcom/datadog/android/core/internal/CoreFeature$lastViewEvent$2; -HSPLcom/datadog/android/core/internal/CoreFeature$lastViewEvent$2;->(Lcom/datadog/android/core/internal/CoreFeature;)V -Lcom/datadog/android/core/internal/CoreFeature$lastViewEventFile$2; -HSPLcom/datadog/android/core/internal/CoreFeature$lastViewEventFile$2;->(Lcom/datadog/android/core/internal/CoreFeature;)V -HSPLcom/datadog/android/core/internal/CoreFeature$lastViewEventFile$2;->invoke()Ljava/io/File; -HSPLcom/datadog/android/core/internal/CoreFeature$lastViewEventFile$2;->invoke()Ljava/lang/Object; -Lcom/datadog/android/core/internal/CoreFeature$lastViewEventFileWriter$2; -HSPLcom/datadog/android/core/internal/CoreFeature$lastViewEventFileWriter$2;->(Lcom/datadog/android/core/internal/CoreFeature;)V -HSPLcom/datadog/android/core/internal/CoreFeature$lastViewEventFileWriter$2;->invoke()Lcom/datadog/android/core/internal/persistence/file/batch/BatchFileReaderWriter; -HSPLcom/datadog/android/core/internal/CoreFeature$lastViewEventFileWriter$2;->invoke()Ljava/lang/Object; -Lcom/datadog/android/core/internal/CoreFeature$lazySharedOkHttpClient$2; -HSPLcom/datadog/android/core/internal/CoreFeature$lazySharedOkHttpClient$2;->(Lcom/datadog/android/core/internal/CoreFeature;)V -HSPLcom/datadog/android/core/internal/CoreFeature$lazySharedOkHttpClient$2;->invoke()Ljava/lang/Object; -HSPLcom/datadog/android/core/internal/CoreFeature$lazySharedOkHttpClient$2;->invoke()Lokhttp3/OkHttpClient; -Lcom/datadog/android/core/internal/CoreFeature$prepareNdkCrashData$1; -HSPLcom/datadog/android/core/internal/CoreFeature$prepareNdkCrashData$1;->(Lcom/datadog/android/core/internal/CoreFeature;)V -Lcom/datadog/android/core/internal/CoreFeature$readBuildId$1$2; -HSPLcom/datadog/android/core/internal/CoreFeature$readBuildId$1$2;->()V -HSPLcom/datadog/android/core/internal/CoreFeature$readBuildId$1$2;->()V -Lcom/datadog/android/core/internal/CoreFeature$setupExecutors$contextQueue$1; -HSPLcom/datadog/android/core/internal/CoreFeature$setupExecutors$contextQueue$1;->()V -HSPLcom/datadog/android/core/internal/CoreFeature$setupExecutors$contextQueue$1;->()V -Lcom/datadog/android/core/internal/CoreFeature$setupExecutors$contextQueue$2; -HSPLcom/datadog/android/core/internal/CoreFeature$setupExecutors$contextQueue$2;->()V -HSPLcom/datadog/android/core/internal/CoreFeature$setupExecutors$contextQueue$2;->()V -Lcom/datadog/android/core/internal/CoreFeature$setupOkHttpClient$1; -HSPLcom/datadog/android/core/internal/CoreFeature$setupOkHttpClient$1;->(Lcom/datadog/android/core/internal/CoreFeature;Lcom/datadog/android/core/configuration/Configuration$Core;)V -HSPLcom/datadog/android/core/internal/CoreFeature$setupOkHttpClient$1;->invoke()Ljava/lang/Object; -HSPLcom/datadog/android/core/internal/CoreFeature$setupOkHttpClient$1;->invoke()Lokhttp3/OkHttpClient; Lcom/datadog/android/core/internal/DatadogContextProvider; HSPLcom/datadog/android/core/internal/DatadogContextProvider;->(Lcom/datadog/android/core/internal/CoreFeature;Lcom/datadog/android/core/internal/FeatureContextProvider;)V HSPLcom/datadog/android/core/internal/DatadogContextProvider;->getContext(Ljava/util/Set;)Lcom/datadog/android/api/context/DatadogContext; Lcom/datadog/android/core/internal/DatadogCore; HSPLcom/datadog/android/core/internal/DatadogCore;->()V -HSPLcom/datadog/android/core/internal/DatadogCore;->(Landroid/content/Context;Ljava/lang/String;Ljava/lang/String;Lkotlin/jvm/functions/Function1;Lcom/datadog/android/core/thread/FlushableExecutorService$Factory;Lcom/datadog/android/internal/system/BuildSdkVersionProvider;)V -HSPLcom/datadog/android/core/internal/DatadogCore;->(Landroid/content/Context;Ljava/lang/String;Ljava/lang/String;Lkotlin/jvm/functions/Function1;Lcom/datadog/android/core/thread/FlushableExecutorService$Factory;Lcom/datadog/android/internal/system/BuildSdkVersionProvider;ILkotlin/jvm/internal/DefaultConstructorMarker;)V +HSPLcom/datadog/android/core/internal/DatadogCore;->(Landroid/content/Context;Ljava/lang/String;Ljava/lang/String;Lkotlin/jvm/functions/Function1;Lcom/datadog/android/core/thread/FlushableExecutorService$Factory;Lcom/datadog/android/internal/system/BuildSdkVersionProvider;Lcom/datadog/android/core/internal/remote/RemoteConfigService$Factory;)V +HSPLcom/datadog/android/core/internal/DatadogCore;->(Landroid/content/Context;Ljava/lang/String;Ljava/lang/String;Lkotlin/jvm/functions/Function1;Lcom/datadog/android/core/thread/FlushableExecutorService$Factory;Lcom/datadog/android/internal/system/BuildSdkVersionProvider;Lcom/datadog/android/core/internal/remote/RemoteConfigService$Factory;ILkotlin/jvm/internal/DefaultConstructorMarker;)V +HSPLcom/datadog/android/core/internal/DatadogCore;->_init_$lambda$0(Lcom/datadog/android/api/feature/FeatureSdkCore;)Lcom/datadog/android/core/internal/logger/SdkInternalLogger; HSPLcom/datadog/android/core/internal/DatadogCore;->applyAdditionalConfiguration(Ljava/util/Map;)V HSPLcom/datadog/android/core/internal/DatadogCore;->createSingleThreadExecutorService(Ljava/lang/String;)Ljava/util/concurrent/ExecutorService; HSPLcom/datadog/android/core/internal/DatadogCore;->getAppStartTimeNs()J HSPLcom/datadog/android/core/internal/DatadogCore;->getCoreFeature$dd_sdk_android_core_release()Lcom/datadog/android/core/internal/CoreFeature; HSPLcom/datadog/android/core/internal/DatadogCore;->getFeature(Ljava/lang/String;)Lcom/datadog/android/api/feature/FeatureScope; -HSPLcom/datadog/android/core/internal/DatadogCore;->getFeatureContext$lambda$11(Lcom/datadog/android/core/internal/DatadogCore;Ljava/lang/String;)Ljava/util/Map; +HSPLcom/datadog/android/core/internal/DatadogCore;->getFeatureContext$lambda$16$lambda$15(Lcom/datadog/android/core/internal/SdkFeature;)Ljava/util/HashMap; +HSPLcom/datadog/android/core/internal/DatadogCore;->getFeatureContext$lambda$16(Lcom/datadog/android/core/internal/DatadogCore;Ljava/lang/String;)Ljava/util/Map; HSPLcom/datadog/android/core/internal/DatadogCore;->getFeatureContext(Ljava/lang/String;Z)Ljava/util/Map; -HSPLcom/datadog/android/core/internal/DatadogCore;->getFeatureContextUpdateReceivers$dd_sdk_android_core_release()Ljava/util/Set; -HSPLcom/datadog/android/core/internal/DatadogCore;->getFeatures$dd_sdk_android_core_release()Ljava/util/Map; HSPLcom/datadog/android/core/internal/DatadogCore;->getFirstPartyHostResolver()Lcom/datadog/android/core/internal/net/FirstPartyHostHeaderTypeResolver; HSPLcom/datadog/android/core/internal/DatadogCore;->getInternalLogger()Lcom/datadog/android/api/InternalLogger; HSPLcom/datadog/android/core/internal/DatadogCore;->getName()Ljava/lang/String; +HSPLcom/datadog/android/core/internal/DatadogCore;->getRemoteConfiguration()Lcom/datadog/android/core/internal/remote/model/RemoteConfiguration; HSPLcom/datadog/android/core/internal/DatadogCore;->getService()Ljava/lang/String; HSPLcom/datadog/android/core/internal/DatadogCore;->getTime()Lcom/datadog/android/api/context/TimeInfo; HSPLcom/datadog/android/core/internal/DatadogCore;->getTimeProvider()Lcom/datadog/android/internal/time/TimeProvider; HSPLcom/datadog/android/core/internal/DatadogCore;->initialize$dd_sdk_android_core_release(Lcom/datadog/android/core/configuration/Configuration;)V -HSPLcom/datadog/android/core/internal/DatadogCore;->initialize$lambda$17(Lcom/datadog/android/core/internal/DatadogCore;Ljava/lang/String;)Ljava/util/Map; +HSPLcom/datadog/android/core/internal/DatadogCore;->initialize$lambda$26(Lcom/datadog/android/core/internal/DatadogCore;)Lcom/datadog/android/internal/time/TimeProvider; +HSPLcom/datadog/android/core/internal/DatadogCore;->initialize$lambda$27(Lcom/datadog/android/core/internal/DatadogCore;Ljava/lang/String;)Ljava/util/Map; HSPLcom/datadog/android/core/internal/DatadogCore;->initializeCrashReportFeature()V HSPLcom/datadog/android/core/internal/DatadogCore;->isAppDebuggable(Landroid/content/Context;)Z HSPLcom/datadog/android/core/internal/DatadogCore;->isDeveloperModeEnabled()Z HSPLcom/datadog/android/core/internal/DatadogCore;->isEnvironmentNameValid(Ljava/lang/String;)Z HSPLcom/datadog/android/core/internal/DatadogCore;->registerFeature(Lcom/datadog/android/api/feature/Feature;)V -PLcom/datadog/android/core/internal/DatadogCore;->sendCoreConfigurationTelemetryEvent$lambda$23(Lcom/datadog/android/core/internal/DatadogCore;Lcom/datadog/android/core/configuration/Configuration;)V +PLcom/datadog/android/core/internal/DatadogCore;->sendCoreConfigurationTelemetryEvent$lambda$41(Lcom/datadog/android/core/internal/DatadogCore;Lcom/datadog/android/core/configuration/Configuration;)V HSPLcom/datadog/android/core/internal/DatadogCore;->sendCoreConfigurationTelemetryEvent(Lcom/datadog/android/core/configuration/Configuration;)V -HSPLcom/datadog/android/core/internal/DatadogCore;->setAnonymousId$lambda$13(Lcom/datadog/android/core/internal/DatadogCore;Ljava/util/UUID;)V +HSPLcom/datadog/android/core/internal/DatadogCore;->setAnonymousId$lambda$21(Lcom/datadog/android/core/internal/DatadogCore;Ljava/util/UUID;)V HSPLcom/datadog/android/core/internal/DatadogCore;->setAnonymousId(Ljava/util/UUID;)V HSPLcom/datadog/android/core/internal/DatadogCore;->setCoreFeature$dd_sdk_android_core_release(Lcom/datadog/android/core/internal/CoreFeature;)V HSPLcom/datadog/android/core/internal/DatadogCore;->setEventReceiver(Ljava/lang/String;Lcom/datadog/android/api/feature/FeatureEventReceiver;)V -HSPLcom/datadog/android/core/internal/DatadogCore;->setTrackingConsent$lambda$0(Lcom/datadog/android/core/internal/DatadogCore;Lcom/datadog/android/privacy/TrackingConsent;)V +HSPLcom/datadog/android/core/internal/DatadogCore;->setTrackingConsent$lambda$1(Lcom/datadog/android/core/internal/DatadogCore;Lcom/datadog/android/privacy/TrackingConsent;)V HSPLcom/datadog/android/core/internal/DatadogCore;->setTrackingConsent(Lcom/datadog/android/privacy/TrackingConsent;)V HSPLcom/datadog/android/core/internal/DatadogCore;->setupLifecycleMonitorCallback(Landroid/content/Context;)V +HSPLcom/datadog/android/core/internal/DatadogCore;->setupRemoteConfiguration$dd_sdk_android_core_release(Lcom/datadog/android/core/configuration/Configuration;)V HSPLcom/datadog/android/core/internal/DatadogCore;->setupShutdownHook()V +HSPLcom/datadog/android/core/internal/DatadogCore;->updateFeatureContext$lambda$13$lambda$12(Lcom/datadog/android/core/internal/SdkFeature;Lkotlin/jvm/functions/Function1;Lcom/datadog/android/core/internal/DatadogCore;Ljava/lang/String;)Lkotlin/Unit; +HSPLcom/datadog/android/core/internal/DatadogCore;->updateFeatureContext$lambda$13(Lcom/datadog/android/core/internal/DatadogCore;Ljava/lang/String;Lkotlin/jvm/functions/Function1;)Lkotlin/Unit; +HSPLcom/datadog/android/core/internal/DatadogCore;->updateFeatureContext$lambda$14(Lkotlin/jvm/functions/Function0;)V HSPLcom/datadog/android/core/internal/DatadogCore;->updateFeatureContext(Ljava/lang/String;ZLkotlin/jvm/functions/Function1;)V HSPLcom/datadog/android/core/internal/DatadogCore;->writeLastViewEvent([B)V -Lcom/datadog/android/core/internal/DatadogCore$$ExternalSyntheticLambda10; -HSPLcom/datadog/android/core/internal/DatadogCore$$ExternalSyntheticLambda10;->(Lcom/datadog/android/core/internal/DatadogCore;)V -HSPLcom/datadog/android/core/internal/DatadogCore$$ExternalSyntheticLambda10;->getFeatureContext(Ljava/lang/String;)Ljava/util/Map; -Lcom/datadog/android/core/internal/DatadogCore$$ExternalSyntheticLambda11; -HSPLcom/datadog/android/core/internal/DatadogCore$$ExternalSyntheticLambda11;->(Lcom/datadog/android/core/internal/DatadogCore;Ljava/lang/String;)V -HSPLcom/datadog/android/core/internal/DatadogCore$$ExternalSyntheticLambda11;->call()Ljava/lang/Object; +Lcom/datadog/android/core/internal/DatadogCore$$ExternalSyntheticLambda0; +HSPLcom/datadog/android/core/internal/DatadogCore$$ExternalSyntheticLambda0;->()V +HSPLcom/datadog/android/core/internal/DatadogCore$$ExternalSyntheticLambda0;->invoke(Ljava/lang/Object;)Ljava/lang/Object; +Lcom/datadog/android/core/internal/DatadogCore$$ExternalSyntheticLambda1; +HSPLcom/datadog/android/core/internal/DatadogCore$$ExternalSyntheticLambda1;->(Lcom/datadog/android/core/internal/DatadogCore;Ljava/lang/String;)V +HSPLcom/datadog/android/core/internal/DatadogCore$$ExternalSyntheticLambda1;->call()Ljava/lang/Object; Lcom/datadog/android/core/internal/DatadogCore$$ExternalSyntheticLambda12; -HSPLcom/datadog/android/core/internal/DatadogCore$$ExternalSyntheticLambda12;->(Lcom/datadog/android/core/internal/DatadogCore;Lcom/datadog/android/core/configuration/Configuration;)V -PLcom/datadog/android/core/internal/DatadogCore$$ExternalSyntheticLambda12;->run()V -Lcom/datadog/android/core/internal/DatadogCore$$ExternalSyntheticLambda15; -HSPLcom/datadog/android/core/internal/DatadogCore$$ExternalSyntheticLambda15;->(Lcom/datadog/android/core/internal/DatadogCore;)V -Lcom/datadog/android/core/internal/DatadogCore$$ExternalSyntheticLambda16; -HSPLcom/datadog/android/core/internal/DatadogCore$$ExternalSyntheticLambda16;->(Lcom/datadog/android/core/internal/DatadogCore;Ljava/util/UUID;)V -HSPLcom/datadog/android/core/internal/DatadogCore$$ExternalSyntheticLambda16;->run()V -Lcom/datadog/android/core/internal/DatadogCore$$ExternalSyntheticLambda4; -HSPLcom/datadog/android/core/internal/DatadogCore$$ExternalSyntheticLambda4;->(Lcom/datadog/android/core/internal/DatadogCore;Lcom/datadog/android/privacy/TrackingConsent;)V -HSPLcom/datadog/android/core/internal/DatadogCore$$ExternalSyntheticLambda4;->run()V -Lcom/datadog/android/core/internal/DatadogCore$1; -HSPLcom/datadog/android/core/internal/DatadogCore$1;->()V -HSPLcom/datadog/android/core/internal/DatadogCore$1;->()V -HSPLcom/datadog/android/core/internal/DatadogCore$1;->invoke(Lcom/datadog/android/api/feature/FeatureSdkCore;)Lcom/datadog/android/core/internal/logger/SdkInternalLogger; -HSPLcom/datadog/android/core/internal/DatadogCore$1;->invoke(Ljava/lang/Object;)Ljava/lang/Object; +HSPLcom/datadog/android/core/internal/DatadogCore$$ExternalSyntheticLambda12;->(Lcom/datadog/android/core/internal/DatadogCore;Ljava/util/UUID;)V +HSPLcom/datadog/android/core/internal/DatadogCore$$ExternalSyntheticLambda12;->run()V +Lcom/datadog/android/core/internal/DatadogCore$$ExternalSyntheticLambda13; +HSPLcom/datadog/android/core/internal/DatadogCore$$ExternalSyntheticLambda13;->(Lcom/datadog/android/core/internal/SdkFeature;Lkotlin/jvm/functions/Function1;Lcom/datadog/android/core/internal/DatadogCore;Ljava/lang/String;)V +HSPLcom/datadog/android/core/internal/DatadogCore$$ExternalSyntheticLambda13;->invoke()Ljava/lang/Object; +Lcom/datadog/android/core/internal/DatadogCore$$ExternalSyntheticLambda14; +HSPLcom/datadog/android/core/internal/DatadogCore$$ExternalSyntheticLambda14;->()V +Lcom/datadog/android/core/internal/DatadogCore$$ExternalSyntheticLambda19; +HSPLcom/datadog/android/core/internal/DatadogCore$$ExternalSyntheticLambda19;->(Lcom/datadog/android/core/internal/SdkFeature;)V +HSPLcom/datadog/android/core/internal/DatadogCore$$ExternalSyntheticLambda19;->invoke()Ljava/lang/Object; +Lcom/datadog/android/core/internal/DatadogCore$$ExternalSyntheticLambda2; +HSPLcom/datadog/android/core/internal/DatadogCore$$ExternalSyntheticLambda2;->(Lcom/datadog/android/core/internal/DatadogCore;Lcom/datadog/android/core/configuration/Configuration;)V +PLcom/datadog/android/core/internal/DatadogCore$$ExternalSyntheticLambda2;->run()V +Lcom/datadog/android/core/internal/DatadogCore$$ExternalSyntheticLambda20; +HSPLcom/datadog/android/core/internal/DatadogCore$$ExternalSyntheticLambda20;->(Lcom/datadog/android/core/internal/DatadogCore;Lcom/datadog/android/privacy/TrackingConsent;)V +HSPLcom/datadog/android/core/internal/DatadogCore$$ExternalSyntheticLambda20;->run()V +Lcom/datadog/android/core/internal/DatadogCore$$ExternalSyntheticLambda32; +HSPLcom/datadog/android/core/internal/DatadogCore$$ExternalSyntheticLambda32;->(Lcom/datadog/android/core/internal/DatadogCore;)V +HSPLcom/datadog/android/core/internal/DatadogCore$$ExternalSyntheticLambda32;->invoke()Ljava/lang/Object; +Lcom/datadog/android/core/internal/DatadogCore$$ExternalSyntheticLambda33; +HSPLcom/datadog/android/core/internal/DatadogCore$$ExternalSyntheticLambda33;->(Lcom/datadog/android/core/internal/DatadogCore;)V +HSPLcom/datadog/android/core/internal/DatadogCore$$ExternalSyntheticLambda33;->getFeatureContext(Ljava/lang/String;)Ljava/util/Map; +Lcom/datadog/android/core/internal/DatadogCore$$ExternalSyntheticLambda5; +HSPLcom/datadog/android/core/internal/DatadogCore$$ExternalSyntheticLambda5;->(Lcom/datadog/android/core/internal/DatadogCore;Ljava/lang/String;Lkotlin/jvm/functions/Function1;)V +HSPLcom/datadog/android/core/internal/DatadogCore$$ExternalSyntheticLambda5;->invoke()Ljava/lang/Object; +Lcom/datadog/android/core/internal/DatadogCore$$ExternalSyntheticLambda6; +HSPLcom/datadog/android/core/internal/DatadogCore$$ExternalSyntheticLambda6;->run()V +Lcom/datadog/android/core/internal/DatadogCore$$ExternalSyntheticLambda7; +HSPLcom/datadog/android/core/internal/DatadogCore$$ExternalSyntheticLambda7;->(Lcom/datadog/android/core/internal/DatadogCore;)V Lcom/datadog/android/core/internal/DatadogCore$Companion; HSPLcom/datadog/android/core/internal/DatadogCore$Companion;->()V HSPLcom/datadog/android/core/internal/DatadogCore$Companion;->(Lkotlin/jvm/internal/DefaultConstructorMarker;)V -Lcom/datadog/android/core/internal/DatadogCore$getFeatureContext$callable$1$1; -HSPLcom/datadog/android/core/internal/DatadogCore$getFeatureContext$callable$1$1;->(Lcom/datadog/android/core/internal/SdkFeature;)V -HSPLcom/datadog/android/core/internal/DatadogCore$getFeatureContext$callable$1$1;->invoke()Ljava/lang/Object; -HSPLcom/datadog/android/core/internal/DatadogCore$getFeatureContext$callable$1$1;->invoke()Ljava/util/HashMap; -Lcom/datadog/android/core/internal/DatadogCore$initialize$1; -HSPLcom/datadog/android/core/internal/DatadogCore$initialize$1;->(Lcom/datadog/android/core/internal/DatadogCore;)V -HSPLcom/datadog/android/core/internal/DatadogCore$initialize$1;->invoke()Lcom/datadog/android/internal/time/TimeProvider; -HSPLcom/datadog/android/core/internal/DatadogCore$initialize$1;->invoke()Ljava/lang/Object; -Lcom/datadog/android/core/internal/DatadogCore$updateFeatureContext$runnable$1; -HSPLcom/datadog/android/core/internal/DatadogCore$updateFeatureContext$runnable$1;->(Lcom/datadog/android/core/internal/DatadogCore;Ljava/lang/String;Lkotlin/jvm/functions/Function1;)V -HSPLcom/datadog/android/core/internal/DatadogCore$updateFeatureContext$runnable$1;->invoke()Ljava/lang/Object; -HSPLcom/datadog/android/core/internal/DatadogCore$updateFeatureContext$runnable$1;->invoke()V -Lcom/datadog/android/core/internal/DatadogCore$updateFeatureContext$runnable$1$1; -HSPLcom/datadog/android/core/internal/DatadogCore$updateFeatureContext$runnable$1$1;->(Lcom/datadog/android/core/internal/SdkFeature;Lkotlin/jvm/functions/Function1;Lcom/datadog/android/core/internal/DatadogCore;Ljava/lang/String;)V -HSPLcom/datadog/android/core/internal/DatadogCore$updateFeatureContext$runnable$1$1;->invoke()Ljava/lang/Object; -HSPLcom/datadog/android/core/internal/DatadogCore$updateFeatureContext$runnable$1$1;->invoke()V Lcom/datadog/android/core/internal/FeatureContextProvider; Lcom/datadog/android/core/internal/HashGenerator; Lcom/datadog/android/core/internal/NoOpContextProvider; @@ -18985,28 +19073,27 @@ HSPLcom/datadog/android/core/internal/SdkFeature;->setDataStore(Lcom/datadog/and HSPLcom/datadog/android/core/internal/SdkFeature;->setupMetricsDispatcher(Lcom/datadog/android/core/internal/configuration/DataUploadConfiguration;Lcom/datadog/android/core/internal/persistence/file/FilePersistenceConfig;Landroid/content/Context;)V HSPLcom/datadog/android/core/internal/SdkFeature;->setupUploader(Lcom/datadog/android/api/feature/StorageBackedFeature;Lcom/datadog/android/core/configuration/UploadSchedulerStrategy;I)V HSPLcom/datadog/android/core/internal/SdkFeature;->unwrap()Lcom/datadog/android/api/feature/Feature; -HSPLcom/datadog/android/core/internal/SdkFeature;->withWriteContext$lambda$0(Lcom/datadog/android/core/internal/SdkFeature;Ljava/util/Set;Lkotlin/jvm/functions/Function2;)V +HSPLcom/datadog/android/core/internal/SdkFeature;->withWriteContext$lambda$1(Lcom/datadog/android/core/internal/SdkFeature;Ljava/util/Set;Lkotlin/jvm/functions/Function2;)V HSPLcom/datadog/android/core/internal/SdkFeature;->withWriteContext(Ljava/util/Set;Lkotlin/jvm/functions/Function2;)V -Lcom/datadog/android/core/internal/SdkFeature$$ExternalSyntheticLambda3; -HSPLcom/datadog/android/core/internal/SdkFeature$$ExternalSyntheticLambda3;->(Lcom/datadog/android/core/internal/SdkFeature;Ljava/util/Set;Lkotlin/jvm/functions/Function2;)V -HSPLcom/datadog/android/core/internal/SdkFeature$$ExternalSyntheticLambda3;->run()V +Lcom/datadog/android/core/internal/SdkFeature$$ExternalSyntheticLambda2; +HSPLcom/datadog/android/core/internal/SdkFeature$$ExternalSyntheticLambda2;->(Lcom/datadog/android/core/internal/SdkFeature;)V +Lcom/datadog/android/core/internal/SdkFeature$$ExternalSyntheticLambda5; +HSPLcom/datadog/android/core/internal/SdkFeature$$ExternalSyntheticLambda5;->(Lcom/datadog/android/core/internal/SdkFeature;Ljava/util/Set;Lkotlin/jvm/functions/Function2;)V +HSPLcom/datadog/android/core/internal/SdkFeature$$ExternalSyntheticLambda5;->run()V Lcom/datadog/android/core/internal/SdkFeature$Companion; HSPLcom/datadog/android/core/internal/SdkFeature$Companion;->()V HSPLcom/datadog/android/core/internal/SdkFeature$Companion;->(Lkotlin/jvm/internal/DefaultConstructorMarker;)V -Lcom/datadog/android/core/internal/SdkFeature$createBatchCountBenchmark$1; -HSPLcom/datadog/android/core/internal/SdkFeature$createBatchCountBenchmark$1;->(Lcom/datadog/android/core/internal/SdkFeature;)V Lcom/datadog/android/core/internal/Sha256HashGenerator; HSPLcom/datadog/android/core/internal/Sha256HashGenerator;->()V HSPLcom/datadog/android/core/internal/Sha256HashGenerator;->()V +HSPLcom/datadog/android/core/internal/Sha256HashGenerator;->generate$lambda$0(B)Ljava/lang/CharSequence; HSPLcom/datadog/android/core/internal/Sha256HashGenerator;->generate(Ljava/lang/String;)Ljava/lang/String; +Lcom/datadog/android/core/internal/Sha256HashGenerator$$ExternalSyntheticLambda0; +HSPLcom/datadog/android/core/internal/Sha256HashGenerator$$ExternalSyntheticLambda0;->()V +HSPLcom/datadog/android/core/internal/Sha256HashGenerator$$ExternalSyntheticLambda0;->invoke(Ljava/lang/Object;)Ljava/lang/Object; Lcom/datadog/android/core/internal/Sha256HashGenerator$Companion; HSPLcom/datadog/android/core/internal/Sha256HashGenerator$Companion;->()V HSPLcom/datadog/android/core/internal/Sha256HashGenerator$Companion;->(Lkotlin/jvm/internal/DefaultConstructorMarker;)V -Lcom/datadog/android/core/internal/Sha256HashGenerator$generate$1; -HSPLcom/datadog/android/core/internal/Sha256HashGenerator$generate$1;->()V -HSPLcom/datadog/android/core/internal/Sha256HashGenerator$generate$1;->()V -HSPLcom/datadog/android/core/internal/Sha256HashGenerator$generate$1;->invoke(B)Ljava/lang/CharSequence; -HSPLcom/datadog/android/core/internal/Sha256HashGenerator$generate$1;->invoke(Ljava/lang/Object;)Ljava/lang/Object; Lcom/datadog/android/core/internal/account/AccountInfoProvider; Lcom/datadog/android/core/internal/account/DatadogAccountInfoProvider; HSPLcom/datadog/android/core/internal/account/DatadogAccountInfoProvider;->()V @@ -19031,8 +19118,6 @@ HSPLcom/datadog/android/core/internal/configuration/DataUploadConfiguration$Comp Lcom/datadog/android/core/internal/data/upload/DataOkHttpUploader; HSPLcom/datadog/android/core/internal/data/upload/DataOkHttpUploader;->()V HSPLcom/datadog/android/core/internal/data/upload/DataOkHttpUploader;->(Lcom/datadog/android/api/net/RequestFactory;Lcom/datadog/android/api/InternalLogger;Lokhttp3/Call$Factory;Ljava/lang/String;Lcom/datadog/android/core/internal/system/AndroidInfoProvider;Lcom/datadog/android/internal/profiler/ExecutionTimer;)V -HSPLcom/datadog/android/core/internal/data/upload/DataOkHttpUploader;->access$executeUploadRequest(Lcom/datadog/android/core/internal/data/upload/DataOkHttpUploader;Lcom/datadog/android/api/net/Request;)Lcom/datadog/android/core/internal/data/upload/UploadStatus; -HSPLcom/datadog/android/core/internal/data/upload/DataOkHttpUploader;->access$sanitizeHeaderValue(Lcom/datadog/android/core/internal/data/upload/DataOkHttpUploader;Ljava/lang/String;)Ljava/lang/String; HSPLcom/datadog/android/core/internal/data/upload/DataOkHttpUploader;->buildOkHttpRequest(Lcom/datadog/android/api/net/Request;)Lokhttp3/Request; HSPLcom/datadog/android/core/internal/data/upload/DataOkHttpUploader;->executeUploadRequest(Lcom/datadog/android/api/net/Request;)Lcom/datadog/android/core/internal/data/upload/UploadStatus; HSPLcom/datadog/android/core/internal/data/upload/DataOkHttpUploader;->getUserAgent()Ljava/lang/String; @@ -19041,34 +19126,43 @@ HSPLcom/datadog/android/core/internal/data/upload/DataOkHttpUploader;->isValidHe HSPLcom/datadog/android/core/internal/data/upload/DataOkHttpUploader;->resolveExecutionContext(Lcom/datadog/android/core/internal/persistence/BatchId;)Lcom/datadog/android/api/net/RequestExecutionContext; HSPLcom/datadog/android/core/internal/data/upload/DataOkHttpUploader;->responseCodeToUploadStatus(ILcom/datadog/android/api/net/Request;)Lcom/datadog/android/core/internal/data/upload/UploadStatus; HSPLcom/datadog/android/core/internal/data/upload/DataOkHttpUploader;->sanitizeHeaderValue(Ljava/lang/String;)Ljava/lang/String; +HSPLcom/datadog/android/core/internal/data/upload/DataOkHttpUploader;->upload$lambda$4(Lcom/datadog/android/core/internal/data/upload/DataOkHttpUploader;Lcom/datadog/android/api/net/Request;Lcom/datadog/android/api/context/DatadogContext;)Lcom/datadog/android/core/internal/data/upload/UploadStatus; HSPLcom/datadog/android/core/internal/data/upload/DataOkHttpUploader;->upload(Lcom/datadog/android/api/context/DatadogContext;Ljava/util/List;[BLcom/datadog/android/core/internal/persistence/BatchId;)Lcom/datadog/android/core/internal/data/upload/UploadStatus; +HSPLcom/datadog/android/core/internal/data/upload/DataOkHttpUploader;->userAgent_delegate$lambda$6(Lcom/datadog/android/core/internal/data/upload/DataOkHttpUploader;)Ljava/lang/String; +Lcom/datadog/android/core/internal/data/upload/DataOkHttpUploader$$ExternalSyntheticLambda4; +HSPLcom/datadog/android/core/internal/data/upload/DataOkHttpUploader$$ExternalSyntheticLambda4;->(Lcom/datadog/android/core/internal/data/upload/DataOkHttpUploader;Lcom/datadog/android/api/net/Request;Lcom/datadog/android/api/context/DatadogContext;)V +HSPLcom/datadog/android/core/internal/data/upload/DataOkHttpUploader$$ExternalSyntheticLambda4;->invoke()Ljava/lang/Object; +Lcom/datadog/android/core/internal/data/upload/DataOkHttpUploader$$ExternalSyntheticLambda7; +HSPLcom/datadog/android/core/internal/data/upload/DataOkHttpUploader$$ExternalSyntheticLambda7;->(Lcom/datadog/android/core/internal/data/upload/DataOkHttpUploader;)V +HSPLcom/datadog/android/core/internal/data/upload/DataOkHttpUploader$$ExternalSyntheticLambda7;->invoke()Ljava/lang/Object; Lcom/datadog/android/core/internal/data/upload/DataOkHttpUploader$Companion; HSPLcom/datadog/android/core/internal/data/upload/DataOkHttpUploader$Companion;->()V HSPLcom/datadog/android/core/internal/data/upload/DataOkHttpUploader$Companion;->(Lkotlin/jvm/internal/DefaultConstructorMarker;)V -Lcom/datadog/android/core/internal/data/upload/DataOkHttpUploader$upload$uploadStatus$1; -HSPLcom/datadog/android/core/internal/data/upload/DataOkHttpUploader$upload$uploadStatus$1;->(Lcom/datadog/android/core/internal/data/upload/DataOkHttpUploader;Lcom/datadog/android/api/net/Request;Lcom/datadog/android/api/context/DatadogContext;)V -HSPLcom/datadog/android/core/internal/data/upload/DataOkHttpUploader$upload$uploadStatus$1;->invoke()Lcom/datadog/android/core/internal/data/upload/UploadStatus; -HSPLcom/datadog/android/core/internal/data/upload/DataOkHttpUploader$upload$uploadStatus$1;->invoke()Ljava/lang/Object; -Lcom/datadog/android/core/internal/data/upload/DataOkHttpUploader$userAgent$2; -HSPLcom/datadog/android/core/internal/data/upload/DataOkHttpUploader$userAgent$2;->(Lcom/datadog/android/core/internal/data/upload/DataOkHttpUploader;)V -HSPLcom/datadog/android/core/internal/data/upload/DataOkHttpUploader$userAgent$2;->invoke()Ljava/lang/Object; -HSPLcom/datadog/android/core/internal/data/upload/DataOkHttpUploader$userAgent$2;->invoke()Ljava/lang/String; -Lcom/datadog/android/core/internal/data/upload/DataUploadRunnable; -HSPLcom/datadog/android/core/internal/data/upload/DataUploadRunnable;->()V -HSPLcom/datadog/android/core/internal/data/upload/DataUploadRunnable;->(Ljava/lang/String;Ljava/util/concurrent/ScheduledThreadPoolExecutor;Lcom/datadog/android/core/internal/persistence/Storage;Lcom/datadog/android/core/internal/data/upload/DataUploader;Lcom/datadog/android/core/internal/ContextProvider;Lcom/datadog/android/core/internal/net/info/NetworkInfoProvider;Lcom/datadog/android/core/internal/system/SystemInfoProvider;Lcom/datadog/android/core/configuration/UploadSchedulerStrategy;ILcom/datadog/android/api/InternalLogger;Lcom/datadog/android/core/internal/metrics/BenchmarkUploads;)V -HSPLcom/datadog/android/core/internal/data/upload/DataUploadRunnable;->(Ljava/lang/String;Ljava/util/concurrent/ScheduledThreadPoolExecutor;Lcom/datadog/android/core/internal/persistence/Storage;Lcom/datadog/android/core/internal/data/upload/DataUploader;Lcom/datadog/android/core/internal/ContextProvider;Lcom/datadog/android/core/internal/net/info/NetworkInfoProvider;Lcom/datadog/android/core/internal/system/SystemInfoProvider;Lcom/datadog/android/core/configuration/UploadSchedulerStrategy;ILcom/datadog/android/api/InternalLogger;Lcom/datadog/android/core/internal/metrics/BenchmarkUploads;ILkotlin/jvm/internal/DefaultConstructorMarker;)V -HSPLcom/datadog/android/core/internal/data/upload/DataUploadRunnable;->consumeBatch(Lcom/datadog/android/api/context/DatadogContext;Lcom/datadog/android/core/internal/persistence/BatchId;Ljava/util/List;[B)Lcom/datadog/android/core/internal/data/upload/UploadStatus; -HSPLcom/datadog/android/core/internal/data/upload/DataUploadRunnable;->handleNextBatch(Lcom/datadog/android/api/context/DatadogContext;)Lcom/datadog/android/core/internal/data/upload/UploadStatus; -HSPLcom/datadog/android/core/internal/data/upload/DataUploadRunnable;->isNetworkAvailable()Z -HSPLcom/datadog/android/core/internal/data/upload/DataUploadRunnable;->isSystemReady()Z -HSPLcom/datadog/android/core/internal/data/upload/DataUploadRunnable;->run()V -HSPLcom/datadog/android/core/internal/data/upload/DataUploadRunnable;->scheduleNextUpload(J)V -Lcom/datadog/android/core/internal/data/upload/DataUploadRunnable$Companion; -HSPLcom/datadog/android/core/internal/data/upload/DataUploadRunnable$Companion;->()V -HSPLcom/datadog/android/core/internal/data/upload/DataUploadRunnable$Companion;->(Lkotlin/jvm/internal/DefaultConstructorMarker;)V Lcom/datadog/android/core/internal/data/upload/DataUploadScheduler; -HSPLcom/datadog/android/core/internal/data/upload/DataUploadScheduler;->(Ljava/lang/String;Lcom/datadog/android/core/internal/persistence/Storage;Lcom/datadog/android/core/internal/data/upload/DataUploader;Lcom/datadog/android/core/internal/ContextProvider;Lcom/datadog/android/core/internal/net/info/NetworkInfoProvider;Lcom/datadog/android/core/internal/system/SystemInfoProvider;Lcom/datadog/android/core/configuration/UploadSchedulerStrategy;ILjava/util/concurrent/ScheduledThreadPoolExecutor;Lcom/datadog/android/api/InternalLogger;)V +HSPLcom/datadog/android/core/internal/data/upload/DataUploadScheduler;->(Ljava/lang/String;Lcom/datadog/android/core/internal/persistence/Storage;Lcom/datadog/android/core/internal/data/upload/DataUploader;Lcom/datadog/android/core/internal/ContextProvider;Lcom/datadog/android/core/internal/net/info/NetworkInfoProvider;Lcom/datadog/android/core/internal/system/SystemInfoProvider;Lcom/datadog/android/core/configuration/UploadSchedulerStrategy;ILjava/util/concurrent/ScheduledThreadPoolExecutor;Lcom/datadog/android/api/InternalLogger;Ljava/util/concurrent/Callable;)V +HSPLcom/datadog/android/core/internal/data/upload/DataUploadScheduler;->(Ljava/lang/String;Lcom/datadog/android/core/internal/persistence/Storage;Lcom/datadog/android/core/internal/data/upload/DataUploader;Lcom/datadog/android/core/internal/ContextProvider;Lcom/datadog/android/core/internal/net/info/NetworkInfoProvider;Lcom/datadog/android/core/internal/system/SystemInfoProvider;Lcom/datadog/android/core/configuration/UploadSchedulerStrategy;ILjava/util/concurrent/ScheduledThreadPoolExecutor;Lcom/datadog/android/api/InternalLogger;Ljava/util/concurrent/Callable;ILkotlin/jvm/internal/DefaultConstructorMarker;)V +HSPLcom/datadog/android/core/internal/data/upload/DataUploadScheduler;->access$getScheduleLock$p(Lcom/datadog/android/core/internal/data/upload/DataUploadScheduler;)Ljava/lang/Object; +HSPLcom/datadog/android/core/internal/data/upload/DataUploadScheduler;->access$getStopped$p(Lcom/datadog/android/core/internal/data/upload/DataUploadScheduler;)Z +HSPLcom/datadog/android/core/internal/data/upload/DataUploadScheduler;->access$scheduleNext(Lcom/datadog/android/core/internal/data/upload/DataUploadScheduler;J)V +HSPLcom/datadog/android/core/internal/data/upload/DataUploadScheduler;->getRunnable$dd_sdk_android_core_release()Ljava/util/concurrent/Callable; +HSPLcom/datadog/android/core/internal/data/upload/DataUploadScheduler;->scheduleNext(J)V HSPLcom/datadog/android/core/internal/data/upload/DataUploadScheduler;->startScheduling()V +Lcom/datadog/android/core/internal/data/upload/DataUploadScheduler$uploadTask$1; +HSPLcom/datadog/android/core/internal/data/upload/DataUploadScheduler$uploadTask$1;->(Lcom/datadog/android/core/internal/data/upload/DataUploadScheduler;)V +HSPLcom/datadog/android/core/internal/data/upload/DataUploadScheduler$uploadTask$1;->run()V +Lcom/datadog/android/core/internal/data/upload/DataUploadTask; +HSPLcom/datadog/android/core/internal/data/upload/DataUploadTask;->()V +HSPLcom/datadog/android/core/internal/data/upload/DataUploadTask;->(Ljava/lang/String;Lcom/datadog/android/core/internal/persistence/Storage;Lcom/datadog/android/core/internal/data/upload/DataUploader;Lcom/datadog/android/core/internal/ContextProvider;Lcom/datadog/android/core/internal/net/info/NetworkInfoProvider;Lcom/datadog/android/core/internal/system/SystemInfoProvider;Lcom/datadog/android/core/configuration/UploadSchedulerStrategy;ILcom/datadog/android/core/internal/metrics/BenchmarkUploads;)V +HSPLcom/datadog/android/core/internal/data/upload/DataUploadTask;->(Ljava/lang/String;Lcom/datadog/android/core/internal/persistence/Storage;Lcom/datadog/android/core/internal/data/upload/DataUploader;Lcom/datadog/android/core/internal/ContextProvider;Lcom/datadog/android/core/internal/net/info/NetworkInfoProvider;Lcom/datadog/android/core/internal/system/SystemInfoProvider;Lcom/datadog/android/core/configuration/UploadSchedulerStrategy;ILcom/datadog/android/core/internal/metrics/BenchmarkUploads;ILkotlin/jvm/internal/DefaultConstructorMarker;)V +HSPLcom/datadog/android/core/internal/data/upload/DataUploadTask;->call()Ljava/lang/Long; +HSPLcom/datadog/android/core/internal/data/upload/DataUploadTask;->call()Ljava/lang/Object; +HSPLcom/datadog/android/core/internal/data/upload/DataUploadTask;->consumeBatch(Lcom/datadog/android/api/context/DatadogContext;Lcom/datadog/android/core/internal/persistence/BatchId;Ljava/util/List;[B)Lcom/datadog/android/core/internal/data/upload/UploadStatus; +HSPLcom/datadog/android/core/internal/data/upload/DataUploadTask;->handleNextBatch(Lcom/datadog/android/api/context/DatadogContext;)Lcom/datadog/android/core/internal/data/upload/UploadStatus; +HSPLcom/datadog/android/core/internal/data/upload/DataUploadTask;->isNetworkAvailable()Z +HSPLcom/datadog/android/core/internal/data/upload/DataUploadTask;->isSystemReady()Z +Lcom/datadog/android/core/internal/data/upload/DataUploadTask$Companion; +HSPLcom/datadog/android/core/internal/data/upload/DataUploadTask$Companion;->()V +HSPLcom/datadog/android/core/internal/data/upload/DataUploadTask$Companion;->(Lkotlin/jvm/internal/DefaultConstructorMarker;)V Lcom/datadog/android/core/internal/data/upload/DataUploader; Lcom/datadog/android/core/internal/data/upload/DefaultUploadSchedulerStrategy; HSPLcom/datadog/android/core/internal/data/upload/DefaultUploadSchedulerStrategy;->()V @@ -19108,21 +19202,22 @@ HSPLcom/datadog/android/core/internal/data/upload/RotatingDnsResolver$Companion; HSPLcom/datadog/android/core/internal/data/upload/RotatingDnsResolver$Companion;->(Lkotlin/jvm/internal/DefaultConstructorMarker;)V Lcom/datadog/android/core/internal/data/upload/RotatingDnsResolver$ResolvedHost; HSPLcom/datadog/android/core/internal/data/upload/RotatingDnsResolver$ResolvedHost;->(Ljava/lang/String;Ljava/util/List;J)V -Lcom/datadog/android/core/internal/data/upload/UploadRunnable; Lcom/datadog/android/core/internal/data/upload/UploadScheduler; Lcom/datadog/android/core/internal/data/upload/UploadStatus; HSPLcom/datadog/android/core/internal/data/upload/UploadStatus;->()V HSPLcom/datadog/android/core/internal/data/upload/UploadStatus;->(ZILjava/lang/Throwable;)V HSPLcom/datadog/android/core/internal/data/upload/UploadStatus;->(ZILjava/lang/Throwable;ILkotlin/jvm/internal/DefaultConstructorMarker;)V HSPLcom/datadog/android/core/internal/data/upload/UploadStatus;->(ZILjava/lang/Throwable;Lkotlin/jvm/internal/DefaultConstructorMarker;)V -HSPLcom/datadog/android/core/internal/data/upload/UploadStatus;->access$buildStatusMessage(Lcom/datadog/android/core/internal/data/upload/UploadStatus;Ljava/lang/String;ILjava/lang/String;Ljava/lang/Throwable;I)Ljava/lang/String; HSPLcom/datadog/android/core/internal/data/upload/UploadStatus;->buildStatusMessage(Ljava/lang/String;ILjava/lang/String;Ljava/lang/Throwable;I)Ljava/lang/String; HSPLcom/datadog/android/core/internal/data/upload/UploadStatus;->getCode()I HSPLcom/datadog/android/core/internal/data/upload/UploadStatus;->getShouldRetry()Z -HSPLcom/datadog/android/core/internal/data/upload/UploadStatus;->getThrowable()Ljava/lang/Throwable; +HSPLcom/datadog/android/core/internal/data/upload/UploadStatus;->logStatus$lambda$0(Lcom/datadog/android/core/internal/data/upload/UploadStatus;Ljava/lang/String;ILjava/lang/String;I)Ljava/lang/String; HSPLcom/datadog/android/core/internal/data/upload/UploadStatus;->logStatus(Ljava/lang/String;ILcom/datadog/android/api/InternalLogger;ILjava/lang/String;)V HSPLcom/datadog/android/core/internal/data/upload/UploadStatus;->resolveInternalLogLevel()Lcom/datadog/android/api/InternalLogger$Level; HSPLcom/datadog/android/core/internal/data/upload/UploadStatus;->resolveInternalLogTarget()Ljava/util/List; +Lcom/datadog/android/core/internal/data/upload/UploadStatus$$ExternalSyntheticLambda0; +HSPLcom/datadog/android/core/internal/data/upload/UploadStatus$$ExternalSyntheticLambda0;->(Lcom/datadog/android/core/internal/data/upload/UploadStatus;Ljava/lang/String;ILjava/lang/String;I)V +HSPLcom/datadog/android/core/internal/data/upload/UploadStatus$$ExternalSyntheticLambda0;->invoke()Ljava/lang/Object; Lcom/datadog/android/core/internal/data/upload/UploadStatus$Companion; HSPLcom/datadog/android/core/internal/data/upload/UploadStatus$Companion;->()V HSPLcom/datadog/android/core/internal/data/upload/UploadStatus$Companion;->(Lkotlin/jvm/internal/DefaultConstructorMarker;)V @@ -19136,10 +19231,6 @@ HSPLcom/datadog/android/core/internal/data/upload/UploadStatus$InvalidTokenError Lcom/datadog/android/core/internal/data/upload/UploadStatus$RequestCreationError; Lcom/datadog/android/core/internal/data/upload/UploadStatus$Success; Lcom/datadog/android/core/internal/data/upload/UploadStatus$UnknownStatus; -Lcom/datadog/android/core/internal/data/upload/UploadStatus$logStatus$1; -HSPLcom/datadog/android/core/internal/data/upload/UploadStatus$logStatus$1;->(Lcom/datadog/android/core/internal/data/upload/UploadStatus;Ljava/lang/String;ILjava/lang/String;I)V -HSPLcom/datadog/android/core/internal/data/upload/UploadStatus$logStatus$1;->invoke()Ljava/lang/Object; -HSPLcom/datadog/android/core/internal/data/upload/UploadStatus$logStatus$1;->invoke()Ljava/lang/String; Lcom/datadog/android/core/internal/lifecycle/ProcessLifecycleCallback; HSPLcom/datadog/android/core/internal/lifecycle/ProcessLifecycleCallback;->(Landroid/content/Context;Ljava/lang/String;Lcom/datadog/android/api/InternalLogger;)V HSPLcom/datadog/android/core/internal/lifecycle/ProcessLifecycleCallback;->onResumed()V @@ -19157,6 +19248,9 @@ Lcom/datadog/android/core/internal/logger/SdkInternalLogger; HSPLcom/datadog/android/core/internal/logger/SdkInternalLogger;->()V HSPLcom/datadog/android/core/internal/logger/SdkInternalLogger;->(Lcom/datadog/android/api/feature/FeatureSdkCore;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function0;)V HSPLcom/datadog/android/core/internal/logger/SdkInternalLogger;->(Lcom/datadog/android/api/feature/FeatureSdkCore;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function0;ILkotlin/jvm/internal/DefaultConstructorMarker;)V +HSPLcom/datadog/android/core/internal/logger/SdkInternalLogger;->_init_$lambda$1$lambda$0(I)Z +HSPLcom/datadog/android/core/internal/logger/SdkInternalLogger;->_init_$lambda$1()Lcom/datadog/android/core/internal/logger/LogcatLogHandler; +HSPLcom/datadog/android/core/internal/logger/SdkInternalLogger;->_init_$lambda$2()Lcom/datadog/android/core/internal/logger/LogcatLogHandler; HSPLcom/datadog/android/core/internal/logger/SdkInternalLogger;->log(Lcom/datadog/android/api/InternalLogger$Level;Lcom/datadog/android/api/InternalLogger$Target;Lkotlin/jvm/functions/Function0;Ljava/lang/Throwable;ZLjava/util/Map;)V HSPLcom/datadog/android/core/internal/logger/SdkInternalLogger;->log(Lcom/datadog/android/api/InternalLogger$Level;Ljava/util/List;Lkotlin/jvm/functions/Function0;Ljava/lang/Throwable;ZLjava/util/Map;)V HSPLcom/datadog/android/core/internal/logger/SdkInternalLogger;->logApiUsage(FLkotlin/jvm/functions/Function0;)V @@ -19168,21 +19262,15 @@ HSPLcom/datadog/android/core/internal/logger/SdkInternalLogger;->sendToLogHandle HSPLcom/datadog/android/core/internal/logger/SdkInternalLogger;->startPerformanceMeasure(Ljava/lang/String;Lcom/datadog/android/core/metrics/TelemetryMetricType;FLjava/lang/String;)Lcom/datadog/android/core/metrics/PerformanceMetric; HSPLcom/datadog/android/core/internal/logger/SdkInternalLogger;->toLogLevel(Lcom/datadog/android/api/InternalLogger$Level;)I HSPLcom/datadog/android/core/internal/logger/SdkInternalLogger;->withSdkName(Ljava/lang/String;)Ljava/lang/String; -Lcom/datadog/android/core/internal/logger/SdkInternalLogger$1; -HSPLcom/datadog/android/core/internal/logger/SdkInternalLogger$1;->()V -HSPLcom/datadog/android/core/internal/logger/SdkInternalLogger$1;->()V -HSPLcom/datadog/android/core/internal/logger/SdkInternalLogger$1;->invoke()Lcom/datadog/android/core/internal/logger/LogcatLogHandler; -HSPLcom/datadog/android/core/internal/logger/SdkInternalLogger$1;->invoke()Ljava/lang/Object; -Lcom/datadog/android/core/internal/logger/SdkInternalLogger$1$1; -HSPLcom/datadog/android/core/internal/logger/SdkInternalLogger$1$1;->()V -HSPLcom/datadog/android/core/internal/logger/SdkInternalLogger$1$1;->()V -HSPLcom/datadog/android/core/internal/logger/SdkInternalLogger$1$1;->invoke(I)Ljava/lang/Boolean; -HSPLcom/datadog/android/core/internal/logger/SdkInternalLogger$1$1;->invoke(Ljava/lang/Object;)Ljava/lang/Object; -Lcom/datadog/android/core/internal/logger/SdkInternalLogger$2; -HSPLcom/datadog/android/core/internal/logger/SdkInternalLogger$2;->()V -HSPLcom/datadog/android/core/internal/logger/SdkInternalLogger$2;->()V -HSPLcom/datadog/android/core/internal/logger/SdkInternalLogger$2;->invoke()Lcom/datadog/android/core/internal/logger/LogcatLogHandler; -HSPLcom/datadog/android/core/internal/logger/SdkInternalLogger$2;->invoke()Ljava/lang/Object; +Lcom/datadog/android/core/internal/logger/SdkInternalLogger$$ExternalSyntheticLambda0; +HSPLcom/datadog/android/core/internal/logger/SdkInternalLogger$$ExternalSyntheticLambda0;->()V +HSPLcom/datadog/android/core/internal/logger/SdkInternalLogger$$ExternalSyntheticLambda0;->invoke()Ljava/lang/Object; +Lcom/datadog/android/core/internal/logger/SdkInternalLogger$$ExternalSyntheticLambda1; +HSPLcom/datadog/android/core/internal/logger/SdkInternalLogger$$ExternalSyntheticLambda1;->()V +HSPLcom/datadog/android/core/internal/logger/SdkInternalLogger$$ExternalSyntheticLambda1;->invoke()Ljava/lang/Object; +Lcom/datadog/android/core/internal/logger/SdkInternalLogger$$ExternalSyntheticLambda2; +HSPLcom/datadog/android/core/internal/logger/SdkInternalLogger$$ExternalSyntheticLambda2;->()V +HSPLcom/datadog/android/core/internal/logger/SdkInternalLogger$$ExternalSyntheticLambda2;->invoke(Ljava/lang/Object;)Ljava/lang/Object; Lcom/datadog/android/core/internal/logger/SdkInternalLogger$Companion; HSPLcom/datadog/android/core/internal/logger/SdkInternalLogger$Companion;->()V HSPLcom/datadog/android/core/internal/logger/SdkInternalLogger$Companion;->(Lkotlin/jvm/internal/DefaultConstructorMarker;)V @@ -19193,16 +19281,16 @@ HSPLcom/datadog/android/core/internal/metrics/BatchMetricsDispatcher;->( HSPLcom/datadog/android/core/internal/metrics/BatchMetricsDispatcher;->(Ljava/lang/String;Lcom/datadog/android/core/internal/configuration/DataUploadConfiguration;Lcom/datadog/android/core/internal/persistence/file/FilePersistenceConfig;Lcom/datadog/android/api/InternalLogger;Lcom/datadog/android/internal/time/TimeProvider;)V HSPLcom/datadog/android/core/internal/metrics/BatchMetricsDispatcher;->nameAsTimestampSafe(Ljava/io/File;Lcom/datadog/android/api/InternalLogger;)Ljava/lang/Long; HSPLcom/datadog/android/core/internal/metrics/BatchMetricsDispatcher;->onResumed()V +HSPLcom/datadog/android/core/internal/metrics/BatchMetricsDispatcher;->onStarted()V HSPLcom/datadog/android/core/internal/metrics/BatchMetricsDispatcher;->resolveBatchDeletedMetricAttributes(Ljava/io/File;Lcom/datadog/android/core/internal/metrics/RemovalReason;I)Ljava/util/Map; HSPLcom/datadog/android/core/internal/metrics/BatchMetricsDispatcher;->resolveFileOriginAsConsent(Ljava/io/File;)Ljava/lang/String; HSPLcom/datadog/android/core/internal/metrics/BatchMetricsDispatcher;->resolveTrackName(Ljava/lang/String;)Ljava/lang/String; HSPLcom/datadog/android/core/internal/metrics/BatchMetricsDispatcher;->sendBatchDeletedMetric(Ljava/io/File;Lcom/datadog/android/core/internal/metrics/RemovalReason;I)V +Lcom/datadog/android/core/internal/metrics/BatchMetricsDispatcher$$ExternalSyntheticLambda0; +HSPLcom/datadog/android/core/internal/metrics/BatchMetricsDispatcher$$ExternalSyntheticLambda0;->()V Lcom/datadog/android/core/internal/metrics/BatchMetricsDispatcher$Companion; HSPLcom/datadog/android/core/internal/metrics/BatchMetricsDispatcher$Companion;->()V HSPLcom/datadog/android/core/internal/metrics/BatchMetricsDispatcher$Companion;->(Lkotlin/jvm/internal/DefaultConstructorMarker;)V -Lcom/datadog/android/core/internal/metrics/BatchMetricsDispatcher$sendBatchDeletedMetric$1$1; -HSPLcom/datadog/android/core/internal/metrics/BatchMetricsDispatcher$sendBatchDeletedMetric$1$1;->()V -HSPLcom/datadog/android/core/internal/metrics/BatchMetricsDispatcher$sendBatchDeletedMetric$1$1;->()V Lcom/datadog/android/core/internal/metrics/BenchmarkUploads; HSPLcom/datadog/android/core/internal/metrics/BenchmarkUploads;->()V HSPLcom/datadog/android/core/internal/metrics/BenchmarkUploads;->(Lcom/datadog/android/internal/profiler/BenchmarkSdkUploads;)V @@ -19228,6 +19316,7 @@ HSPLcom/datadog/android/core/internal/metrics/RemovalReason$IntakeCode;->toStrin Lcom/datadog/android/core/internal/net/DefaultFirstPartyHostHeaderTypeResolver; HSPLcom/datadog/android/core/internal/net/DefaultFirstPartyHostHeaderTypeResolver;->(Ljava/util/Map;)V HSPLcom/datadog/android/core/internal/net/DefaultFirstPartyHostHeaderTypeResolver;->addKnownHostsWithHeaderTypes$dd_sdk_android_core_release(Ljava/util/Map;)V +HSPLcom/datadog/android/core/internal/net/DefaultFirstPartyHostHeaderTypeResolver;->getAllHeaderTypes()Ljava/util/Set; HSPLcom/datadog/android/core/internal/net/DefaultFirstPartyHostHeaderTypeResolver;->isEmpty()Z HSPLcom/datadog/android/core/internal/net/DefaultFirstPartyHostHeaderTypeResolver;->isWildcardPattern(Ljava/lang/String;)Z HSPLcom/datadog/android/core/internal/net/DefaultFirstPartyHostHeaderTypeResolver;->updateKnownHosts(Ljava/util/Map;)V @@ -19347,53 +19436,47 @@ HSPLcom/datadog/android/core/internal/persistence/datastore/DatastoreFileWriter$ Lcom/datadog/android/core/internal/persistence/datastore/NoOpDataStoreHandler; HSPLcom/datadog/android/core/internal/persistence/datastore/NoOpDataStoreHandler;->()V Lcom/datadog/android/core/internal/persistence/file/FileExtKt; +HSPLcom/datadog/android/core/internal/persistence/file/FileExtKt;->canWriteSafe$lambda$2(Ljava/io/File;)Z HSPLcom/datadog/android/core/internal/persistence/file/FileExtKt;->canWriteSafe(Ljava/io/File;Lcom/datadog/android/api/InternalLogger;)Z +HSPLcom/datadog/android/core/internal/persistence/file/FileExtKt;->existsSafe$lambda$5(Ljava/io/File;)Z HSPLcom/datadog/android/core/internal/persistence/file/FileExtKt;->existsSafe(Ljava/io/File;Lcom/datadog/android/api/InternalLogger;)Z +HSPLcom/datadog/android/core/internal/persistence/file/FileExtKt;->isDirectorySafe$lambda$7(Ljava/io/File;)Z HSPLcom/datadog/android/core/internal/persistence/file/FileExtKt;->isDirectorySafe(Ljava/io/File;Lcom/datadog/android/api/InternalLogger;)Z +HSPLcom/datadog/android/core/internal/persistence/file/FileExtKt;->lengthSafe$lambda$11(Ljava/io/File;)J HSPLcom/datadog/android/core/internal/persistence/file/FileExtKt;->lengthSafe(Ljava/io/File;Lcom/datadog/android/api/InternalLogger;)J +HSPLcom/datadog/android/core/internal/persistence/file/FileExtKt;->listFilesSafe$lambda$8(Ljava/io/File;)[Ljava/io/File; +HSPLcom/datadog/android/core/internal/persistence/file/FileExtKt;->listFilesSafe$lambda$9(Ljava/io/FileFilter;Ljava/io/File;)[Ljava/io/File; HSPLcom/datadog/android/core/internal/persistence/file/FileExtKt;->listFilesSafe(Ljava/io/File;Lcom/datadog/android/api/InternalLogger;)[Ljava/io/File; HSPLcom/datadog/android/core/internal/persistence/file/FileExtKt;->listFilesSafe(Ljava/io/File;Ljava/io/FileFilter;Lcom/datadog/android/api/InternalLogger;)[Ljava/io/File; +HSPLcom/datadog/android/core/internal/persistence/file/FileExtKt;->mkdirsSafe$lambda$12(Ljava/io/File;)Z HSPLcom/datadog/android/core/internal/persistence/file/FileExtKt;->mkdirsSafe(Ljava/io/File;Lcom/datadog/android/api/InternalLogger;)Z +HSPLcom/datadog/android/core/internal/persistence/file/FileExtKt;->renameToSafe$lambda$13(Ljava/io/File;Ljava/io/File;)Z HSPLcom/datadog/android/core/internal/persistence/file/FileExtKt;->renameToSafe(Ljava/io/File;Ljava/io/File;Lcom/datadog/android/api/InternalLogger;)Z HSPLcom/datadog/android/core/internal/persistence/file/FileExtKt;->safeCall(Ljava/io/File;Ljava/lang/Object;Lcom/datadog/android/api/InternalLogger;Lkotlin/jvm/functions/Function1;)Ljava/lang/Object; -Lcom/datadog/android/core/internal/persistence/file/FileExtKt$canWriteSafe$1; -HSPLcom/datadog/android/core/internal/persistence/file/FileExtKt$canWriteSafe$1;->()V -HSPLcom/datadog/android/core/internal/persistence/file/FileExtKt$canWriteSafe$1;->()V -HSPLcom/datadog/android/core/internal/persistence/file/FileExtKt$canWriteSafe$1;->invoke(Ljava/io/File;)Ljava/lang/Boolean; -HSPLcom/datadog/android/core/internal/persistence/file/FileExtKt$canWriteSafe$1;->invoke(Ljava/lang/Object;)Ljava/lang/Object; -Lcom/datadog/android/core/internal/persistence/file/FileExtKt$existsSafe$1; -HSPLcom/datadog/android/core/internal/persistence/file/FileExtKt$existsSafe$1;->()V -HSPLcom/datadog/android/core/internal/persistence/file/FileExtKt$existsSafe$1;->()V -HSPLcom/datadog/android/core/internal/persistence/file/FileExtKt$existsSafe$1;->invoke(Ljava/io/File;)Ljava/lang/Boolean; -HSPLcom/datadog/android/core/internal/persistence/file/FileExtKt$existsSafe$1;->invoke(Ljava/lang/Object;)Ljava/lang/Object; -Lcom/datadog/android/core/internal/persistence/file/FileExtKt$isDirectorySafe$1; -HSPLcom/datadog/android/core/internal/persistence/file/FileExtKt$isDirectorySafe$1;->()V -HSPLcom/datadog/android/core/internal/persistence/file/FileExtKt$isDirectorySafe$1;->()V -HSPLcom/datadog/android/core/internal/persistence/file/FileExtKt$isDirectorySafe$1;->invoke(Ljava/io/File;)Ljava/lang/Boolean; -HSPLcom/datadog/android/core/internal/persistence/file/FileExtKt$isDirectorySafe$1;->invoke(Ljava/lang/Object;)Ljava/lang/Object; -Lcom/datadog/android/core/internal/persistence/file/FileExtKt$lengthSafe$1; -HSPLcom/datadog/android/core/internal/persistence/file/FileExtKt$lengthSafe$1;->()V -HSPLcom/datadog/android/core/internal/persistence/file/FileExtKt$lengthSafe$1;->()V -HSPLcom/datadog/android/core/internal/persistence/file/FileExtKt$lengthSafe$1;->invoke(Ljava/io/File;)Ljava/lang/Long; -HSPLcom/datadog/android/core/internal/persistence/file/FileExtKt$lengthSafe$1;->invoke(Ljava/lang/Object;)Ljava/lang/Object; -Lcom/datadog/android/core/internal/persistence/file/FileExtKt$listFilesSafe$1; -HSPLcom/datadog/android/core/internal/persistence/file/FileExtKt$listFilesSafe$1;->()V -HSPLcom/datadog/android/core/internal/persistence/file/FileExtKt$listFilesSafe$1;->()V -HSPLcom/datadog/android/core/internal/persistence/file/FileExtKt$listFilesSafe$1;->invoke(Ljava/io/File;)[Ljava/io/File; -HSPLcom/datadog/android/core/internal/persistence/file/FileExtKt$listFilesSafe$1;->invoke(Ljava/lang/Object;)Ljava/lang/Object; -Lcom/datadog/android/core/internal/persistence/file/FileExtKt$listFilesSafe$2; -HSPLcom/datadog/android/core/internal/persistence/file/FileExtKt$listFilesSafe$2;->(Ljava/io/FileFilter;)V -HSPLcom/datadog/android/core/internal/persistence/file/FileExtKt$listFilesSafe$2;->invoke(Ljava/io/File;)[Ljava/io/File; -HSPLcom/datadog/android/core/internal/persistence/file/FileExtKt$listFilesSafe$2;->invoke(Ljava/lang/Object;)Ljava/lang/Object; -Lcom/datadog/android/core/internal/persistence/file/FileExtKt$mkdirsSafe$1; -HSPLcom/datadog/android/core/internal/persistence/file/FileExtKt$mkdirsSafe$1;->()V -HSPLcom/datadog/android/core/internal/persistence/file/FileExtKt$mkdirsSafe$1;->()V -HSPLcom/datadog/android/core/internal/persistence/file/FileExtKt$mkdirsSafe$1;->invoke(Ljava/io/File;)Ljava/lang/Boolean; -HSPLcom/datadog/android/core/internal/persistence/file/FileExtKt$mkdirsSafe$1;->invoke(Ljava/lang/Object;)Ljava/lang/Object; -Lcom/datadog/android/core/internal/persistence/file/FileExtKt$renameToSafe$1; -HSPLcom/datadog/android/core/internal/persistence/file/FileExtKt$renameToSafe$1;->(Ljava/io/File;)V -HSPLcom/datadog/android/core/internal/persistence/file/FileExtKt$renameToSafe$1;->invoke(Ljava/io/File;)Ljava/lang/Boolean; -HSPLcom/datadog/android/core/internal/persistence/file/FileExtKt$renameToSafe$1;->invoke(Ljava/lang/Object;)Ljava/lang/Object; +Lcom/datadog/android/core/internal/persistence/file/FileExtKt$$ExternalSyntheticLambda1; +HSPLcom/datadog/android/core/internal/persistence/file/FileExtKt$$ExternalSyntheticLambda1;->()V +HSPLcom/datadog/android/core/internal/persistence/file/FileExtKt$$ExternalSyntheticLambda1;->invoke(Ljava/lang/Object;)Ljava/lang/Object; +Lcom/datadog/android/core/internal/persistence/file/FileExtKt$$ExternalSyntheticLambda11; +HSPLcom/datadog/android/core/internal/persistence/file/FileExtKt$$ExternalSyntheticLambda11;->(Ljava/io/FileFilter;)V +HSPLcom/datadog/android/core/internal/persistence/file/FileExtKt$$ExternalSyntheticLambda11;->invoke(Ljava/lang/Object;)Ljava/lang/Object; +Lcom/datadog/android/core/internal/persistence/file/FileExtKt$$ExternalSyntheticLambda13; +HSPLcom/datadog/android/core/internal/persistence/file/FileExtKt$$ExternalSyntheticLambda13;->()V +HSPLcom/datadog/android/core/internal/persistence/file/FileExtKt$$ExternalSyntheticLambda13;->invoke(Ljava/lang/Object;)Ljava/lang/Object; +Lcom/datadog/android/core/internal/persistence/file/FileExtKt$$ExternalSyntheticLambda17; +HSPLcom/datadog/android/core/internal/persistence/file/FileExtKt$$ExternalSyntheticLambda17;->(Ljava/io/File;)V +HSPLcom/datadog/android/core/internal/persistence/file/FileExtKt$$ExternalSyntheticLambda17;->invoke(Ljava/lang/Object;)Ljava/lang/Object; +Lcom/datadog/android/core/internal/persistence/file/FileExtKt$$ExternalSyntheticLambda3; +HSPLcom/datadog/android/core/internal/persistence/file/FileExtKt$$ExternalSyntheticLambda3;->()V +HSPLcom/datadog/android/core/internal/persistence/file/FileExtKt$$ExternalSyntheticLambda3;->invoke(Ljava/lang/Object;)Ljava/lang/Object; +Lcom/datadog/android/core/internal/persistence/file/FileExtKt$$ExternalSyntheticLambda5; +HSPLcom/datadog/android/core/internal/persistence/file/FileExtKt$$ExternalSyntheticLambda5;->()V +HSPLcom/datadog/android/core/internal/persistence/file/FileExtKt$$ExternalSyntheticLambda5;->invoke(Ljava/lang/Object;)Ljava/lang/Object; +Lcom/datadog/android/core/internal/persistence/file/FileExtKt$$ExternalSyntheticLambda6; +HSPLcom/datadog/android/core/internal/persistence/file/FileExtKt$$ExternalSyntheticLambda6;->()V +HSPLcom/datadog/android/core/internal/persistence/file/FileExtKt$$ExternalSyntheticLambda6;->invoke(Ljava/lang/Object;)Ljava/lang/Object; +Lcom/datadog/android/core/internal/persistence/file/FileExtKt$$ExternalSyntheticLambda8; +HSPLcom/datadog/android/core/internal/persistence/file/FileExtKt$$ExternalSyntheticLambda8;->()V +HSPLcom/datadog/android/core/internal/persistence/file/FileExtKt$$ExternalSyntheticLambda8;->invoke(Ljava/lang/Object;)Ljava/lang/Object; Lcom/datadog/android/core/internal/persistence/file/FileMover; HSPLcom/datadog/android/core/internal/persistence/file/FileMover;->()V HSPLcom/datadog/android/core/internal/persistence/file/FileMover;->(Lcom/datadog/android/api/InternalLogger;)V @@ -19479,30 +19562,25 @@ HSPLcom/datadog/android/core/internal/persistence/file/advanced/FeatureFileOrche Lcom/datadog/android/core/internal/persistence/file/advanced/MoveDataMigrationOperation; HSPLcom/datadog/android/core/internal/persistence/file/advanced/MoveDataMigrationOperation;->()V HSPLcom/datadog/android/core/internal/persistence/file/advanced/MoveDataMigrationOperation;->(Ljava/io/File;Ljava/io/File;Lcom/datadog/android/core/internal/persistence/file/FileMover;Lcom/datadog/android/api/InternalLogger;)V -HSPLcom/datadog/android/core/internal/persistence/file/advanced/MoveDataMigrationOperation;->getFileMover$dd_sdk_android_core_release()Lcom/datadog/android/core/internal/persistence/file/FileMover; -HSPLcom/datadog/android/core/internal/persistence/file/advanced/MoveDataMigrationOperation;->getFromDir$dd_sdk_android_core_release()Ljava/io/File; -HSPLcom/datadog/android/core/internal/persistence/file/advanced/MoveDataMigrationOperation;->getToDir$dd_sdk_android_core_release()Ljava/io/File; +HSPLcom/datadog/android/core/internal/persistence/file/advanced/MoveDataMigrationOperation;->run$lambda$2(Lcom/datadog/android/core/internal/persistence/file/advanced/MoveDataMigrationOperation;)Z HSPLcom/datadog/android/core/internal/persistence/file/advanced/MoveDataMigrationOperation;->run()V +Lcom/datadog/android/core/internal/persistence/file/advanced/MoveDataMigrationOperation$$ExternalSyntheticLambda2; +HSPLcom/datadog/android/core/internal/persistence/file/advanced/MoveDataMigrationOperation$$ExternalSyntheticLambda2;->(Lcom/datadog/android/core/internal/persistence/file/advanced/MoveDataMigrationOperation;)V +HSPLcom/datadog/android/core/internal/persistence/file/advanced/MoveDataMigrationOperation$$ExternalSyntheticLambda2;->invoke()Ljava/lang/Object; Lcom/datadog/android/core/internal/persistence/file/advanced/MoveDataMigrationOperation$Companion; HSPLcom/datadog/android/core/internal/persistence/file/advanced/MoveDataMigrationOperation$Companion;->()V HSPLcom/datadog/android/core/internal/persistence/file/advanced/MoveDataMigrationOperation$Companion;->(Lkotlin/jvm/internal/DefaultConstructorMarker;)V -Lcom/datadog/android/core/internal/persistence/file/advanced/MoveDataMigrationOperation$run$3; -HSPLcom/datadog/android/core/internal/persistence/file/advanced/MoveDataMigrationOperation$run$3;->(Lcom/datadog/android/core/internal/persistence/file/advanced/MoveDataMigrationOperation;)V -HSPLcom/datadog/android/core/internal/persistence/file/advanced/MoveDataMigrationOperation$run$3;->invoke()Ljava/lang/Boolean; -HSPLcom/datadog/android/core/internal/persistence/file/advanced/MoveDataMigrationOperation$run$3;->invoke()Ljava/lang/Object; Lcom/datadog/android/core/internal/persistence/file/advanced/WipeDataMigrationOperation; HSPLcom/datadog/android/core/internal/persistence/file/advanced/WipeDataMigrationOperation;->()V HSPLcom/datadog/android/core/internal/persistence/file/advanced/WipeDataMigrationOperation;->(Ljava/io/File;Lcom/datadog/android/core/internal/persistence/file/FileMover;Lcom/datadog/android/api/InternalLogger;)V -HSPLcom/datadog/android/core/internal/persistence/file/advanced/WipeDataMigrationOperation;->getFileMover$dd_sdk_android_core_release()Lcom/datadog/android/core/internal/persistence/file/FileMover; -HSPLcom/datadog/android/core/internal/persistence/file/advanced/WipeDataMigrationOperation;->getTargetDir$dd_sdk_android_core_release()Ljava/io/File; +HSPLcom/datadog/android/core/internal/persistence/file/advanced/WipeDataMigrationOperation;->run$lambda$1(Lcom/datadog/android/core/internal/persistence/file/advanced/WipeDataMigrationOperation;)Z HSPLcom/datadog/android/core/internal/persistence/file/advanced/WipeDataMigrationOperation;->run()V +Lcom/datadog/android/core/internal/persistence/file/advanced/WipeDataMigrationOperation$$ExternalSyntheticLambda1; +HSPLcom/datadog/android/core/internal/persistence/file/advanced/WipeDataMigrationOperation$$ExternalSyntheticLambda1;->(Lcom/datadog/android/core/internal/persistence/file/advanced/WipeDataMigrationOperation;)V +HSPLcom/datadog/android/core/internal/persistence/file/advanced/WipeDataMigrationOperation$$ExternalSyntheticLambda1;->invoke()Ljava/lang/Object; Lcom/datadog/android/core/internal/persistence/file/advanced/WipeDataMigrationOperation$Companion; HSPLcom/datadog/android/core/internal/persistence/file/advanced/WipeDataMigrationOperation$Companion;->()V HSPLcom/datadog/android/core/internal/persistence/file/advanced/WipeDataMigrationOperation$Companion;->(Lkotlin/jvm/internal/DefaultConstructorMarker;)V -Lcom/datadog/android/core/internal/persistence/file/advanced/WipeDataMigrationOperation$run$2; -HSPLcom/datadog/android/core/internal/persistence/file/advanced/WipeDataMigrationOperation$run$2;->(Lcom/datadog/android/core/internal/persistence/file/advanced/WipeDataMigrationOperation;)V -HSPLcom/datadog/android/core/internal/persistence/file/advanced/WipeDataMigrationOperation$run$2;->invoke()Ljava/lang/Boolean; -HSPLcom/datadog/android/core/internal/persistence/file/advanced/WipeDataMigrationOperation$run$2;->invoke()Ljava/lang/Object; Lcom/datadog/android/core/internal/persistence/file/batch/BatchFileOrchestrator; HSPLcom/datadog/android/core/internal/persistence/file/batch/BatchFileOrchestrator;->()V HSPLcom/datadog/android/core/internal/persistence/file/batch/BatchFileOrchestrator;->(Ljava/io/File;Lcom/datadog/android/core/internal/persistence/file/FilePersistenceConfig;Lcom/datadog/android/api/InternalLogger;Lcom/datadog/android/core/internal/metrics/MetricsDispatcher;Lcom/datadog/android/internal/time/TimeProvider;Ljava/util/concurrent/atomic/AtomicInteger;)V @@ -19591,8 +19669,8 @@ HSPLcom/datadog/android/core/internal/persistence/tlvformat/TLVBlockType;->$valu HSPLcom/datadog/android/core/internal/persistence/tlvformat/TLVBlockType;->()V HSPLcom/datadog/android/core/internal/persistence/tlvformat/TLVBlockType;->(Ljava/lang/String;IS)V HSPLcom/datadog/android/core/internal/persistence/tlvformat/TLVBlockType;->access$getMap$cp()Ljava/util/Map; +HSPLcom/datadog/android/core/internal/persistence/tlvformat/TLVBlockType;->getEntries()Lkotlin/enums/EnumEntries; HSPLcom/datadog/android/core/internal/persistence/tlvformat/TLVBlockType;->getRawValue-Mh2AYeg()S -HSPLcom/datadog/android/core/internal/persistence/tlvformat/TLVBlockType;->values()[Lcom/datadog/android/core/internal/persistence/tlvformat/TLVBlockType; Lcom/datadog/android/core/internal/persistence/tlvformat/TLVBlockType$Companion; HSPLcom/datadog/android/core/internal/persistence/tlvformat/TLVBlockType$Companion;->()V HSPLcom/datadog/android/core/internal/persistence/tlvformat/TLVBlockType$Companion;->(Lkotlin/jvm/internal/DefaultConstructorMarker;)V @@ -19614,6 +19692,7 @@ HSPLcom/datadog/android/core/internal/receiver/ThreadSafeReceiver;->registerRece Lcom/datadog/android/core/internal/receiver/ThreadSafeReceiver$Companion; HSPLcom/datadog/android/core/internal/receiver/ThreadSafeReceiver$Companion;->()V HSPLcom/datadog/android/core/internal/receiver/ThreadSafeReceiver$Companion;->(Lkotlin/jvm/internal/DefaultConstructorMarker;)V +Lcom/datadog/android/core/internal/remote/RemoteConfigService$Factory; Lcom/datadog/android/core/internal/storage/TelemetryAwareEventBatchWriter; Lcom/datadog/android/core/internal/system/AndroidInfoProvider; Lcom/datadog/android/core/internal/system/AppVersionProvider; @@ -19633,7 +19712,11 @@ HSPLcom/datadog/android/core/internal/system/DefaultAndroidInfoProvider;->(Landroid/content/Context;)V HSPLcom/datadog/android/core/internal/system/DefaultAndroidInfoProvider;->(Landroid/content/Context;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Lcom/datadog/android/internal/system/BuildSdkVersionProvider;)V HSPLcom/datadog/android/core/internal/system/DefaultAndroidInfoProvider;->(Landroid/content/Context;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Lcom/datadog/android/internal/system/BuildSdkVersionProvider;ILkotlin/jvm/internal/DefaultConstructorMarker;)V -HSPLcom/datadog/android/core/internal/system/DefaultAndroidInfoProvider;->access$getBuildSdkVersionProvider$p(Lcom/datadog/android/core/internal/system/DefaultAndroidInfoProvider;)Lcom/datadog/android/internal/system/BuildSdkVersionProvider; +HSPLcom/datadog/android/core/internal/system/DefaultAndroidInfoProvider;->architecture_delegate$lambda$10()Ljava/lang/String; +HSPLcom/datadog/android/core/internal/system/DefaultAndroidInfoProvider;->currentLocale_delegate$lambda$5(Landroid/content/Context;Lcom/datadog/android/core/internal/system/DefaultAndroidInfoProvider;)Ljava/lang/String; +HSPLcom/datadog/android/core/internal/system/DefaultAndroidInfoProvider;->deviceBrand_delegate$lambda$8(Ljava/lang/String;)Ljava/lang/String; +HSPLcom/datadog/android/core/internal/system/DefaultAndroidInfoProvider;->deviceName_delegate$lambda$1(Lcom/datadog/android/core/internal/system/DefaultAndroidInfoProvider;)Ljava/lang/String; +HSPLcom/datadog/android/core/internal/system/DefaultAndroidInfoProvider;->deviceType_delegate$lambda$0(Ljava/lang/String;Landroid/content/Context;)Lcom/datadog/android/api/context/DeviceType; HSPLcom/datadog/android/core/internal/system/DefaultAndroidInfoProvider;->getArchitecture()Ljava/lang/String; HSPLcom/datadog/android/core/internal/system/DefaultAndroidInfoProvider;->getCurrentLocale()Ljava/lang/String; HSPLcom/datadog/android/core/internal/system/DefaultAndroidInfoProvider;->getDeviceBrand()Ljava/lang/String; @@ -19650,6 +19733,49 @@ HSPLcom/datadog/android/core/internal/system/DefaultAndroidInfoProvider;->getOsV HSPLcom/datadog/android/core/internal/system/DefaultAndroidInfoProvider;->getTimeZone()Ljava/lang/String; HSPLcom/datadog/android/core/internal/system/DefaultAndroidInfoProvider;->getTotalRam()Ljava/lang/Integer; HSPLcom/datadog/android/core/internal/system/DefaultAndroidInfoProvider;->isLowRam()Ljava/lang/Boolean; +HSPLcom/datadog/android/core/internal/system/DefaultAndroidInfoProvider;->isLowRam_delegate$lambda$15(Landroid/content/Context;)Ljava/lang/Boolean; +HSPLcom/datadog/android/core/internal/system/DefaultAndroidInfoProvider;->locales_delegate$lambda$4(Landroid/content/Context;Lcom/datadog/android/core/internal/system/DefaultAndroidInfoProvider;)Ljava/util/List; +HSPLcom/datadog/android/core/internal/system/DefaultAndroidInfoProvider;->logicalCpuCount_delegate$lambda$13()I +HSPLcom/datadog/android/core/internal/system/DefaultAndroidInfoProvider;->numberOfDisplays_delegate$lambda$12(Landroid/content/Context;)Ljava/lang/Integer; +HSPLcom/datadog/android/core/internal/system/DefaultAndroidInfoProvider;->osMajorVersion_delegate$lambda$9(Lcom/datadog/android/core/internal/system/DefaultAndroidInfoProvider;)Ljava/lang/String; +HSPLcom/datadog/android/core/internal/system/DefaultAndroidInfoProvider;->timeZone_delegate$lambda$6()Ljava/lang/String; +HSPLcom/datadog/android/core/internal/system/DefaultAndroidInfoProvider;->totalRam_delegate$lambda$14(Landroid/content/Context;)Ljava/lang/Integer; +Lcom/datadog/android/core/internal/system/DefaultAndroidInfoProvider$$ExternalSyntheticLambda0; +HSPLcom/datadog/android/core/internal/system/DefaultAndroidInfoProvider$$ExternalSyntheticLambda0;->(Ljava/lang/String;Landroid/content/Context;)V +HSPLcom/datadog/android/core/internal/system/DefaultAndroidInfoProvider$$ExternalSyntheticLambda0;->invoke()Ljava/lang/Object; +Lcom/datadog/android/core/internal/system/DefaultAndroidInfoProvider$$ExternalSyntheticLambda1; +HSPLcom/datadog/android/core/internal/system/DefaultAndroidInfoProvider$$ExternalSyntheticLambda1;->(Landroid/content/Context;)V +HSPLcom/datadog/android/core/internal/system/DefaultAndroidInfoProvider$$ExternalSyntheticLambda1;->invoke()Ljava/lang/Object; +Lcom/datadog/android/core/internal/system/DefaultAndroidInfoProvider$$ExternalSyntheticLambda10; +HSPLcom/datadog/android/core/internal/system/DefaultAndroidInfoProvider$$ExternalSyntheticLambda10;->(Lcom/datadog/android/core/internal/system/DefaultAndroidInfoProvider;)V +HSPLcom/datadog/android/core/internal/system/DefaultAndroidInfoProvider$$ExternalSyntheticLambda10;->invoke()Ljava/lang/Object; +Lcom/datadog/android/core/internal/system/DefaultAndroidInfoProvider$$ExternalSyntheticLambda11; +HSPLcom/datadog/android/core/internal/system/DefaultAndroidInfoProvider$$ExternalSyntheticLambda11;->()V +HSPLcom/datadog/android/core/internal/system/DefaultAndroidInfoProvider$$ExternalSyntheticLambda11;->invoke()Ljava/lang/Object; +Lcom/datadog/android/core/internal/system/DefaultAndroidInfoProvider$$ExternalSyntheticLambda2; +HSPLcom/datadog/android/core/internal/system/DefaultAndroidInfoProvider$$ExternalSyntheticLambda2;->()V +HSPLcom/datadog/android/core/internal/system/DefaultAndroidInfoProvider$$ExternalSyntheticLambda2;->invoke()Ljava/lang/Object; +Lcom/datadog/android/core/internal/system/DefaultAndroidInfoProvider$$ExternalSyntheticLambda3; +HSPLcom/datadog/android/core/internal/system/DefaultAndroidInfoProvider$$ExternalSyntheticLambda3;->(Landroid/content/Context;)V +HSPLcom/datadog/android/core/internal/system/DefaultAndroidInfoProvider$$ExternalSyntheticLambda3;->invoke()Ljava/lang/Object; +Lcom/datadog/android/core/internal/system/DefaultAndroidInfoProvider$$ExternalSyntheticLambda4; +HSPLcom/datadog/android/core/internal/system/DefaultAndroidInfoProvider$$ExternalSyntheticLambda4;->(Landroid/content/Context;)V +HSPLcom/datadog/android/core/internal/system/DefaultAndroidInfoProvider$$ExternalSyntheticLambda4;->invoke()Ljava/lang/Object; +Lcom/datadog/android/core/internal/system/DefaultAndroidInfoProvider$$ExternalSyntheticLambda5; +HSPLcom/datadog/android/core/internal/system/DefaultAndroidInfoProvider$$ExternalSyntheticLambda5;->(Lcom/datadog/android/core/internal/system/DefaultAndroidInfoProvider;)V +HSPLcom/datadog/android/core/internal/system/DefaultAndroidInfoProvider$$ExternalSyntheticLambda5;->invoke()Ljava/lang/Object; +Lcom/datadog/android/core/internal/system/DefaultAndroidInfoProvider$$ExternalSyntheticLambda6; +HSPLcom/datadog/android/core/internal/system/DefaultAndroidInfoProvider$$ExternalSyntheticLambda6;->(Landroid/content/Context;Lcom/datadog/android/core/internal/system/DefaultAndroidInfoProvider;)V +HSPLcom/datadog/android/core/internal/system/DefaultAndroidInfoProvider$$ExternalSyntheticLambda6;->invoke()Ljava/lang/Object; +Lcom/datadog/android/core/internal/system/DefaultAndroidInfoProvider$$ExternalSyntheticLambda7; +HSPLcom/datadog/android/core/internal/system/DefaultAndroidInfoProvider$$ExternalSyntheticLambda7;->(Landroid/content/Context;Lcom/datadog/android/core/internal/system/DefaultAndroidInfoProvider;)V +HSPLcom/datadog/android/core/internal/system/DefaultAndroidInfoProvider$$ExternalSyntheticLambda7;->invoke()Ljava/lang/Object; +Lcom/datadog/android/core/internal/system/DefaultAndroidInfoProvider$$ExternalSyntheticLambda8; +HSPLcom/datadog/android/core/internal/system/DefaultAndroidInfoProvider$$ExternalSyntheticLambda8;->()V +HSPLcom/datadog/android/core/internal/system/DefaultAndroidInfoProvider$$ExternalSyntheticLambda8;->invoke()Ljava/lang/Object; +Lcom/datadog/android/core/internal/system/DefaultAndroidInfoProvider$$ExternalSyntheticLambda9; +HSPLcom/datadog/android/core/internal/system/DefaultAndroidInfoProvider$$ExternalSyntheticLambda9;->(Ljava/lang/String;)V +HSPLcom/datadog/android/core/internal/system/DefaultAndroidInfoProvider$$ExternalSyntheticLambda9;->invoke()Ljava/lang/Object; Lcom/datadog/android/core/internal/system/DefaultAndroidInfoProvider$Companion; HSPLcom/datadog/android/core/internal/system/DefaultAndroidInfoProvider$Companion;->()V HSPLcom/datadog/android/core/internal/system/DefaultAndroidInfoProvider$Companion;->(Lkotlin/jvm/internal/DefaultConstructorMarker;)V @@ -19659,57 +19785,6 @@ HSPLcom/datadog/android/core/internal/system/DefaultAndroidInfoProvider$Companio HSPLcom/datadog/android/core/internal/system/DefaultAndroidInfoProvider$Companion;->isTablet(Ljava/lang/String;Landroid/content/Context;)Z HSPLcom/datadog/android/core/internal/system/DefaultAndroidInfoProvider$Companion;->isTv(Landroid/content/Context;)Z HSPLcom/datadog/android/core/internal/system/DefaultAndroidInfoProvider$Companion;->resolveDeviceType(Ljava/lang/String;Landroid/content/Context;)Lcom/datadog/android/api/context/DeviceType; -Lcom/datadog/android/core/internal/system/DefaultAndroidInfoProvider$architecture$2; -HSPLcom/datadog/android/core/internal/system/DefaultAndroidInfoProvider$architecture$2;->()V -HSPLcom/datadog/android/core/internal/system/DefaultAndroidInfoProvider$architecture$2;->()V -HSPLcom/datadog/android/core/internal/system/DefaultAndroidInfoProvider$architecture$2;->invoke()Ljava/lang/Object; -HSPLcom/datadog/android/core/internal/system/DefaultAndroidInfoProvider$architecture$2;->invoke()Ljava/lang/String; -Lcom/datadog/android/core/internal/system/DefaultAndroidInfoProvider$currentLocale$2; -HSPLcom/datadog/android/core/internal/system/DefaultAndroidInfoProvider$currentLocale$2;->(Landroid/content/Context;Lcom/datadog/android/core/internal/system/DefaultAndroidInfoProvider;)V -HSPLcom/datadog/android/core/internal/system/DefaultAndroidInfoProvider$currentLocale$2;->invoke()Ljava/lang/Object; -HSPLcom/datadog/android/core/internal/system/DefaultAndroidInfoProvider$currentLocale$2;->invoke()Ljava/lang/String; -Lcom/datadog/android/core/internal/system/DefaultAndroidInfoProvider$deviceBrand$2; -HSPLcom/datadog/android/core/internal/system/DefaultAndroidInfoProvider$deviceBrand$2;->(Ljava/lang/String;)V -HSPLcom/datadog/android/core/internal/system/DefaultAndroidInfoProvider$deviceBrand$2;->invoke()Ljava/lang/Object; -HSPLcom/datadog/android/core/internal/system/DefaultAndroidInfoProvider$deviceBrand$2;->invoke()Ljava/lang/String; -Lcom/datadog/android/core/internal/system/DefaultAndroidInfoProvider$deviceName$2; -HSPLcom/datadog/android/core/internal/system/DefaultAndroidInfoProvider$deviceName$2;->(Lcom/datadog/android/core/internal/system/DefaultAndroidInfoProvider;)V -HSPLcom/datadog/android/core/internal/system/DefaultAndroidInfoProvider$deviceName$2;->invoke()Ljava/lang/Object; -HSPLcom/datadog/android/core/internal/system/DefaultAndroidInfoProvider$deviceName$2;->invoke()Ljava/lang/String; -Lcom/datadog/android/core/internal/system/DefaultAndroidInfoProvider$deviceType$2; -HSPLcom/datadog/android/core/internal/system/DefaultAndroidInfoProvider$deviceType$2;->(Ljava/lang/String;Landroid/content/Context;)V -HSPLcom/datadog/android/core/internal/system/DefaultAndroidInfoProvider$deviceType$2;->invoke()Lcom/datadog/android/api/context/DeviceType; -HSPLcom/datadog/android/core/internal/system/DefaultAndroidInfoProvider$deviceType$2;->invoke()Ljava/lang/Object; -Lcom/datadog/android/core/internal/system/DefaultAndroidInfoProvider$isLowRam$2; -HSPLcom/datadog/android/core/internal/system/DefaultAndroidInfoProvider$isLowRam$2;->(Landroid/content/Context;)V -HSPLcom/datadog/android/core/internal/system/DefaultAndroidInfoProvider$isLowRam$2;->invoke()Ljava/lang/Boolean; -HSPLcom/datadog/android/core/internal/system/DefaultAndroidInfoProvider$isLowRam$2;->invoke()Ljava/lang/Object; -Lcom/datadog/android/core/internal/system/DefaultAndroidInfoProvider$locales$2; -HSPLcom/datadog/android/core/internal/system/DefaultAndroidInfoProvider$locales$2;->(Landroid/content/Context;Lcom/datadog/android/core/internal/system/DefaultAndroidInfoProvider;)V -HSPLcom/datadog/android/core/internal/system/DefaultAndroidInfoProvider$locales$2;->invoke()Ljava/lang/Object; -HSPLcom/datadog/android/core/internal/system/DefaultAndroidInfoProvider$locales$2;->invoke()Ljava/util/List; -Lcom/datadog/android/core/internal/system/DefaultAndroidInfoProvider$logicalCpuCount$2; -HSPLcom/datadog/android/core/internal/system/DefaultAndroidInfoProvider$logicalCpuCount$2;->()V -HSPLcom/datadog/android/core/internal/system/DefaultAndroidInfoProvider$logicalCpuCount$2;->()V -HSPLcom/datadog/android/core/internal/system/DefaultAndroidInfoProvider$logicalCpuCount$2;->invoke()Ljava/lang/Integer; -HSPLcom/datadog/android/core/internal/system/DefaultAndroidInfoProvider$logicalCpuCount$2;->invoke()Ljava/lang/Object; -Lcom/datadog/android/core/internal/system/DefaultAndroidInfoProvider$numberOfDisplays$2; -HSPLcom/datadog/android/core/internal/system/DefaultAndroidInfoProvider$numberOfDisplays$2;->(Landroid/content/Context;)V -HSPLcom/datadog/android/core/internal/system/DefaultAndroidInfoProvider$numberOfDisplays$2;->invoke()Ljava/lang/Integer; -HSPLcom/datadog/android/core/internal/system/DefaultAndroidInfoProvider$numberOfDisplays$2;->invoke()Ljava/lang/Object; -Lcom/datadog/android/core/internal/system/DefaultAndroidInfoProvider$osMajorVersion$2; -HSPLcom/datadog/android/core/internal/system/DefaultAndroidInfoProvider$osMajorVersion$2;->(Lcom/datadog/android/core/internal/system/DefaultAndroidInfoProvider;)V -HSPLcom/datadog/android/core/internal/system/DefaultAndroidInfoProvider$osMajorVersion$2;->invoke()Ljava/lang/Object; -HSPLcom/datadog/android/core/internal/system/DefaultAndroidInfoProvider$osMajorVersion$2;->invoke()Ljava/lang/String; -Lcom/datadog/android/core/internal/system/DefaultAndroidInfoProvider$timeZone$2; -HSPLcom/datadog/android/core/internal/system/DefaultAndroidInfoProvider$timeZone$2;->()V -HSPLcom/datadog/android/core/internal/system/DefaultAndroidInfoProvider$timeZone$2;->()V -HSPLcom/datadog/android/core/internal/system/DefaultAndroidInfoProvider$timeZone$2;->invoke()Ljava/lang/Object; -HSPLcom/datadog/android/core/internal/system/DefaultAndroidInfoProvider$timeZone$2;->invoke()Ljava/lang/String; -Lcom/datadog/android/core/internal/system/DefaultAndroidInfoProvider$totalRam$2; -HSPLcom/datadog/android/core/internal/system/DefaultAndroidInfoProvider$totalRam$2;->(Landroid/content/Context;)V -HSPLcom/datadog/android/core/internal/system/DefaultAndroidInfoProvider$totalRam$2;->invoke()Ljava/lang/Integer; -HSPLcom/datadog/android/core/internal/system/DefaultAndroidInfoProvider$totalRam$2;->invoke()Ljava/lang/Object; Lcom/datadog/android/core/internal/system/DefaultAppVersionProvider; HSPLcom/datadog/android/core/internal/system/DefaultAppVersionProvider;->(Ljava/lang/String;I)V HSPLcom/datadog/android/core/internal/system/DefaultAppVersionProvider;->getVersion()Ljava/lang/String; @@ -19747,13 +19822,12 @@ HSPLcom/datadog/android/core/internal/thread/BackPressureExecutorService$Compani Lcom/datadog/android/core/internal/thread/BackPressuredBlockingQueue; HSPLcom/datadog/android/core/internal/thread/BackPressuredBlockingQueue;->(Lcom/datadog/android/api/InternalLogger;Ljava/lang/String;IILkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function1;Lcom/datadog/android/core/configuration/BackPressureMitigation;Lcom/datadog/android/internal/time/TimeProvider;)V HSPLcom/datadog/android/core/internal/thread/BackPressuredBlockingQueue;->(Lcom/datadog/android/api/InternalLogger;Ljava/lang/String;Lcom/datadog/android/core/configuration/BackPressureStrategy;Lcom/datadog/android/internal/time/TimeProvider;)V -HSPLcom/datadog/android/core/internal/thread/BackPressuredBlockingQueue;->access$offer$s-727953952(Lcom/datadog/android/core/internal/thread/BackPressuredBlockingQueue;Ljava/lang/Object;)Z HSPLcom/datadog/android/core/internal/thread/BackPressuredBlockingQueue;->addWithBackPressure(Ljava/lang/Object;Lkotlin/jvm/functions/Function1;)Z +HSPLcom/datadog/android/core/internal/thread/BackPressuredBlockingQueue;->offer$lambda$0(Lcom/datadog/android/core/internal/thread/BackPressuredBlockingQueue;Ljava/lang/Object;)Z HSPLcom/datadog/android/core/internal/thread/BackPressuredBlockingQueue;->offer(Ljava/lang/Object;)Z -Lcom/datadog/android/core/internal/thread/BackPressuredBlockingQueue$offer$1; -HSPLcom/datadog/android/core/internal/thread/BackPressuredBlockingQueue$offer$1;->(Lcom/datadog/android/core/internal/thread/BackPressuredBlockingQueue;)V -HSPLcom/datadog/android/core/internal/thread/BackPressuredBlockingQueue$offer$1;->invoke(Ljava/lang/Object;)Ljava/lang/Boolean; -HSPLcom/datadog/android/core/internal/thread/BackPressuredBlockingQueue$offer$1;->invoke(Ljava/lang/Object;)Ljava/lang/Object; +Lcom/datadog/android/core/internal/thread/BackPressuredBlockingQueue$$ExternalSyntheticLambda2; +HSPLcom/datadog/android/core/internal/thread/BackPressuredBlockingQueue$$ExternalSyntheticLambda2;->(Lcom/datadog/android/core/internal/thread/BackPressuredBlockingQueue;)V +HSPLcom/datadog/android/core/internal/thread/BackPressuredBlockingQueue$$ExternalSyntheticLambda2;->invoke(Ljava/lang/Object;)Ljava/lang/Object; Lcom/datadog/android/core/internal/thread/BroadcastReceiverThread; HSPLcom/datadog/android/core/internal/thread/BroadcastReceiverThread;->()V Lcom/datadog/android/core/internal/thread/DatadogThreadFactory; @@ -19762,8 +19836,8 @@ HSPLcom/datadog/android/core/internal/thread/DatadogThreadFactory;->newThread(Lj Lcom/datadog/android/core/internal/thread/LoggingScheduledThreadPoolExecutor; HSPLcom/datadog/android/core/internal/thread/LoggingScheduledThreadPoolExecutor;->(ILjava/lang/String;Lcom/datadog/android/api/InternalLogger;Lcom/datadog/android/core/configuration/BackPressureStrategy;)V HSPLcom/datadog/android/core/internal/thread/LoggingScheduledThreadPoolExecutor;->afterExecute(Ljava/lang/Runnable;Ljava/lang/Throwable;)V -Lcom/datadog/android/core/internal/thread/LoggingScheduledThreadPoolExecutor$$ExternalSyntheticLambda0; -HSPLcom/datadog/android/core/internal/thread/LoggingScheduledThreadPoolExecutor$$ExternalSyntheticLambda0;->(Lcom/datadog/android/api/InternalLogger;Ljava/lang/String;Lcom/datadog/android/core/configuration/BackPressureStrategy;)V +Lcom/datadog/android/core/internal/thread/LoggingScheduledThreadPoolExecutor$$ExternalSyntheticLambda1; +HSPLcom/datadog/android/core/internal/thread/LoggingScheduledThreadPoolExecutor$$ExternalSyntheticLambda1;->(Lcom/datadog/android/api/InternalLogger;Ljava/lang/String;Lcom/datadog/android/core/configuration/BackPressureStrategy;)V Lcom/datadog/android/core/internal/thread/NamedFutureTask; HSPLcom/datadog/android/core/internal/thread/NamedFutureTask;->(Ljava/lang/String;Ljava/util/concurrent/Callable;)V Lcom/datadog/android/core/internal/thread/ObservableLinkedBlockingQueue; @@ -19787,17 +19861,14 @@ Lcom/datadog/android/core/internal/time/DefaultAppStartTimeProvider; HSPLcom/datadog/android/core/internal/time/DefaultAppStartTimeProvider;->()V HSPLcom/datadog/android/core/internal/time/DefaultAppStartTimeProvider;->(Lkotlin/jvm/functions/Function0;Lcom/datadog/android/internal/system/BuildSdkVersionProvider;)V HSPLcom/datadog/android/core/internal/time/DefaultAppStartTimeProvider;->(Lkotlin/jvm/functions/Function0;Lcom/datadog/android/internal/system/BuildSdkVersionProvider;ILkotlin/jvm/internal/DefaultConstructorMarker;)V -HSPLcom/datadog/android/core/internal/time/DefaultAppStartTimeProvider;->access$getPROCESS_START_TO_CP_START_DIFF_THRESHOLD_NS$cp()J -HSPLcom/datadog/android/core/internal/time/DefaultAppStartTimeProvider;->access$getTimeProviderFactory$p(Lcom/datadog/android/core/internal/time/DefaultAppStartTimeProvider;)Lkotlin/jvm/functions/Function0; +HSPLcom/datadog/android/core/internal/time/DefaultAppStartTimeProvider;->appStartTimeNs_delegate$lambda$0(Lcom/datadog/android/internal/system/BuildSdkVersionProvider;Lcom/datadog/android/core/internal/time/DefaultAppStartTimeProvider;)J HSPLcom/datadog/android/core/internal/time/DefaultAppStartTimeProvider;->getAppStartTimeNs()J +Lcom/datadog/android/core/internal/time/DefaultAppStartTimeProvider$$ExternalSyntheticLambda0; +HSPLcom/datadog/android/core/internal/time/DefaultAppStartTimeProvider$$ExternalSyntheticLambda0;->(Lcom/datadog/android/internal/system/BuildSdkVersionProvider;Lcom/datadog/android/core/internal/time/DefaultAppStartTimeProvider;)V +HSPLcom/datadog/android/core/internal/time/DefaultAppStartTimeProvider$$ExternalSyntheticLambda0;->invoke()Ljava/lang/Object; Lcom/datadog/android/core/internal/time/DefaultAppStartTimeProvider$Companion; HSPLcom/datadog/android/core/internal/time/DefaultAppStartTimeProvider$Companion;->()V HSPLcom/datadog/android/core/internal/time/DefaultAppStartTimeProvider$Companion;->(Lkotlin/jvm/internal/DefaultConstructorMarker;)V -HSPLcom/datadog/android/core/internal/time/DefaultAppStartTimeProvider$Companion;->getPROCESS_START_TO_CP_START_DIFF_THRESHOLD_NS$dd_sdk_android_core_release()J -Lcom/datadog/android/core/internal/time/DefaultAppStartTimeProvider$appStartTimeNs$2; -HSPLcom/datadog/android/core/internal/time/DefaultAppStartTimeProvider$appStartTimeNs$2;->(Lcom/datadog/android/internal/system/BuildSdkVersionProvider;Lcom/datadog/android/core/internal/time/DefaultAppStartTimeProvider;)V -HSPLcom/datadog/android/core/internal/time/DefaultAppStartTimeProvider$appStartTimeNs$2;->invoke()Ljava/lang/Long; -HSPLcom/datadog/android/core/internal/time/DefaultAppStartTimeProvider$appStartTimeNs$2;->invoke()Ljava/lang/Object; Lcom/datadog/android/core/internal/time/KronosTimeProvider; HSPLcom/datadog/android/core/internal/time/KronosTimeProvider;->()V HSPLcom/datadog/android/core/internal/time/KronosTimeProvider;->(Lcom/lyft/kronos/Clock;Lcom/datadog/android/api/InternalLogger;)V @@ -19876,12 +19947,12 @@ Lcom/datadog/android/core/sampling/DeterministicSampler; HSPLcom/datadog/android/core/sampling/DeterministicSampler;->()V HSPLcom/datadog/android/core/sampling/DeterministicSampler;->(Lkotlin/jvm/functions/Function1;F)V HSPLcom/datadog/android/core/sampling/DeterministicSampler;->(Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function0;)V +HSPLcom/datadog/android/core/sampling/DeterministicSampler;->_init_$lambda$0(F)F HSPLcom/datadog/android/core/sampling/DeterministicSampler;->getSampleRate()Ljava/lang/Float; HSPLcom/datadog/android/core/sampling/DeterministicSampler;->sample(Ljava/lang/Object;)Z -Lcom/datadog/android/core/sampling/DeterministicSampler$1; -HSPLcom/datadog/android/core/sampling/DeterministicSampler$1;->(F)V -HSPLcom/datadog/android/core/sampling/DeterministicSampler$1;->invoke()Ljava/lang/Float; -HSPLcom/datadog/android/core/sampling/DeterministicSampler$1;->invoke()Ljava/lang/Object; +Lcom/datadog/android/core/sampling/DeterministicSampler$$ExternalSyntheticLambda0; +HSPLcom/datadog/android/core/sampling/DeterministicSampler$$ExternalSyntheticLambda0;->(F)V +HSPLcom/datadog/android/core/sampling/DeterministicSampler$$ExternalSyntheticLambda0;->invoke()Ljava/lang/Object; Lcom/datadog/android/core/sampling/DeterministicSampler$Companion; HSPLcom/datadog/android/core/sampling/DeterministicSampler$Companion;->()V HSPLcom/datadog/android/core/sampling/DeterministicSampler$Companion;->(Lkotlin/jvm/internal/DefaultConstructorMarker;)V @@ -19889,21 +19960,20 @@ Lcom/datadog/android/core/sampling/RateBasedSampler; HSPLcom/datadog/android/core/sampling/RateBasedSampler;->()V HSPLcom/datadog/android/core/sampling/RateBasedSampler;->(F)V HSPLcom/datadog/android/core/sampling/RateBasedSampler;->(Lkotlin/jvm/functions/Function0;)V +HSPLcom/datadog/android/core/sampling/RateBasedSampler;->_init_$lambda$0(F)F HSPLcom/datadog/android/core/sampling/RateBasedSampler;->getRandom()Ljava/security/SecureRandom; HSPLcom/datadog/android/core/sampling/RateBasedSampler;->getSampleRate()Ljava/lang/Float; +HSPLcom/datadog/android/core/sampling/RateBasedSampler;->random_delegate$lambda$1()Ljava/security/SecureRandom; HSPLcom/datadog/android/core/sampling/RateBasedSampler;->sample(Ljava/lang/Object;)Z -Lcom/datadog/android/core/sampling/RateBasedSampler$1; -HSPLcom/datadog/android/core/sampling/RateBasedSampler$1;->(F)V -HSPLcom/datadog/android/core/sampling/RateBasedSampler$1;->invoke()Ljava/lang/Float; -HSPLcom/datadog/android/core/sampling/RateBasedSampler$1;->invoke()Ljava/lang/Object; +Lcom/datadog/android/core/sampling/RateBasedSampler$$ExternalSyntheticLambda2; +HSPLcom/datadog/android/core/sampling/RateBasedSampler$$ExternalSyntheticLambda2;->(F)V +HSPLcom/datadog/android/core/sampling/RateBasedSampler$$ExternalSyntheticLambda2;->invoke()Ljava/lang/Object; +Lcom/datadog/android/core/sampling/RateBasedSampler$$ExternalSyntheticLambda3; +HSPLcom/datadog/android/core/sampling/RateBasedSampler$$ExternalSyntheticLambda3;->()V +HSPLcom/datadog/android/core/sampling/RateBasedSampler$$ExternalSyntheticLambda3;->invoke()Ljava/lang/Object; Lcom/datadog/android/core/sampling/RateBasedSampler$Companion; HSPLcom/datadog/android/core/sampling/RateBasedSampler$Companion;->()V HSPLcom/datadog/android/core/sampling/RateBasedSampler$Companion;->(Lkotlin/jvm/internal/DefaultConstructorMarker;)V -Lcom/datadog/android/core/sampling/RateBasedSampler$random$2; -HSPLcom/datadog/android/core/sampling/RateBasedSampler$random$2;->()V -HSPLcom/datadog/android/core/sampling/RateBasedSampler$random$2;->()V -HSPLcom/datadog/android/core/sampling/RateBasedSampler$random$2;->invoke()Ljava/lang/Object; -HSPLcom/datadog/android/core/sampling/RateBasedSampler$random$2;->invoke()Ljava/security/SecureRandom; Lcom/datadog/android/core/sampling/Sampler; Lcom/datadog/android/core/thread/FlushableExecutorService; Lcom/datadog/android/core/thread/FlushableExecutorService$Factory; @@ -19936,12 +20006,15 @@ Lcom/datadog/android/internal/attributes/LocalAttribute$Key; HSPLcom/datadog/android/internal/attributes/LocalAttribute$Key;->$values()[Lcom/datadog/android/internal/attributes/LocalAttribute$Key; HSPLcom/datadog/android/internal/attributes/LocalAttribute$Key;->()V HSPLcom/datadog/android/internal/attributes/LocalAttribute$Key;->(Ljava/lang/String;ILjava/lang/String;)V +HSPLcom/datadog/android/internal/attributes/LocalAttribute$Key;->toString()Ljava/lang/String; Lcom/datadog/android/internal/attributes/LocalAttributeKt; Lcom/datadog/android/internal/attributes/ViewScopeInstrumentationType; Lcom/datadog/android/internal/attributes/ViewScopeInstrumentationType$Native; HSPLcom/datadog/android/internal/attributes/ViewScopeInstrumentationType$Native;->$values()[Lcom/datadog/android/internal/attributes/ViewScopeInstrumentationType$Native; HSPLcom/datadog/android/internal/attributes/ViewScopeInstrumentationType$Native;->()V HSPLcom/datadog/android/internal/attributes/ViewScopeInstrumentationType$Native;->(Ljava/lang/String;ILjava/lang/String;)V +HSPLcom/datadog/android/internal/attributes/ViewScopeInstrumentationType$Native;->getValue()Ljava/lang/String; +PLcom/datadog/android/internal/collections/EvictingQueue;->(ILjava/util/Deque;)V Lcom/datadog/android/internal/heatmaps/HeatmapIdentifierRegistry; HSPLcom/datadog/android/internal/heatmaps/HeatmapIdentifierRegistry;->()V Lcom/datadog/android/internal/heatmaps/HeatmapIdentifierRegistry$Companion; @@ -19996,9 +20069,11 @@ HSPLcom/datadog/android/internal/system/BuildSdkVersionProvider$Companion;->getD Lcom/datadog/android/internal/system/BuildSdkVersionProvider$Companion$DEFAULT$1; HSPLcom/datadog/android/internal/system/BuildSdkVersionProvider$Companion$DEFAULT$1;->()V HSPLcom/datadog/android/internal/system/BuildSdkVersionProvider$Companion$DEFAULT$1;->isAtLeastN()Z +HSPLcom/datadog/android/internal/system/BuildSdkVersionProvider$Companion$DEFAULT$1;->isAtLeastO()Z HSPLcom/datadog/android/internal/system/BuildSdkVersionProvider$Companion$DEFAULT$1;->isAtLeastP()Z HSPLcom/datadog/android/internal/system/BuildSdkVersionProvider$Companion$DEFAULT$1;->isAtLeastQ()Z HSPLcom/datadog/android/internal/system/BuildSdkVersionProvider$Companion$DEFAULT$1;->isAtLeastR()Z +HSPLcom/datadog/android/internal/system/BuildSdkVersionProvider$Companion$DEFAULT$1;->isAtLeastS()Z HSPLcom/datadog/android/internal/system/BuildSdkVersionProvider$Companion$DEFAULT$1;->isAtLeastTiramisu()Z Lcom/datadog/android/internal/telemetry/InternalTelemetryEvent; HSPLcom/datadog/android/internal/telemetry/InternalTelemetryEvent;->()V @@ -20015,12 +20090,6 @@ HSPLcom/datadog/android/internal/telemetry/InternalTelemetryEvent$ApiUsage$Netwo HSPLcom/datadog/android/internal/telemetry/InternalTelemetryEvent$ApiUsage$NetworkInstrumentation$LibraryType;->(Ljava/lang/String;I)V Lcom/datadog/android/internal/telemetry/InternalTelemetryEvent$Configuration; PLcom/datadog/android/internal/telemetry/InternalTelemetryEvent$Configuration;->(ZJJZZI)V -PLcom/datadog/android/internal/telemetry/InternalTelemetryEvent$Configuration;->getBatchProcessingLevel()I -PLcom/datadog/android/internal/telemetry/InternalTelemetryEvent$Configuration;->getBatchSize()J -PLcom/datadog/android/internal/telemetry/InternalTelemetryEvent$Configuration;->getBatchUploadFrequency()J -PLcom/datadog/android/internal/telemetry/InternalTelemetryEvent$Configuration;->getTrackErrors()Z -PLcom/datadog/android/internal/telemetry/InternalTelemetryEvent$Configuration;->getUseLocalEncryption()Z -PLcom/datadog/android/internal/telemetry/InternalTelemetryEvent$Configuration;->getUseProxy()Z Lcom/datadog/android/internal/telemetry/InternalTelemetryEvent$InterceptorInstantiated; HSPLcom/datadog/android/internal/telemetry/InternalTelemetryEvent$InterceptorInstantiated;->()V HSPLcom/datadog/android/internal/telemetry/InternalTelemetryEvent$InterceptorInstantiated;->()V @@ -20039,10 +20108,8 @@ Lcom/datadog/android/internal/telemetry/TracingHeaderType; HSPLcom/datadog/android/internal/telemetry/TracingHeaderType;->$values()[Lcom/datadog/android/internal/telemetry/TracingHeaderType; HSPLcom/datadog/android/internal/telemetry/TracingHeaderType;->()V HSPLcom/datadog/android/internal/telemetry/TracingHeaderType;->(Ljava/lang/String;I)V -PLcom/datadog/android/internal/telemetry/TracingHeaderType;->values()[Lcom/datadog/android/internal/telemetry/TracingHeaderType; Lcom/datadog/android/internal/telemetry/TracingHeaderTypesSet; HSPLcom/datadog/android/internal/telemetry/TracingHeaderTypesSet;->(Ljava/util/Set;)V -PLcom/datadog/android/internal/telemetry/TracingHeaderTypesSet;->getTypes()Ljava/util/Set; Lcom/datadog/android/internal/thread/NamedCallable; HSPLcom/datadog/android/internal/thread/NamedCallable;->(Ljava/lang/String;Ljava/util/concurrent/Callable;)V HSPLcom/datadog/android/internal/thread/NamedCallable;->call()Ljava/lang/Object; @@ -20057,11 +20124,13 @@ HSPLcom/datadog/android/internal/thread/NamedRunnable;->run()V Lcom/datadog/android/internal/time/BaseTimeProvider; HSPLcom/datadog/android/internal/time/BaseTimeProvider;->()V HSPLcom/datadog/android/internal/time/BaseTimeProvider;->getDeviceElapsedRealtimeMillis()J +HSPLcom/datadog/android/internal/time/BaseTimeProvider;->getDeviceElapsedRealtimeNanos()J HSPLcom/datadog/android/internal/time/BaseTimeProvider;->getDeviceElapsedTimeNanos()J HSPLcom/datadog/android/internal/time/BaseTimeProvider;->getDeviceTimestampMillis()J HSPLcom/datadog/android/internal/time/BaseTimeProvider;->getDeviceUptimeMillis()J Lcom/datadog/android/internal/time/DefaultTimeProvider; HSPLcom/datadog/android/internal/time/DefaultTimeProvider;->()V +HSPLcom/datadog/android/internal/time/DefaultTimeProvider;->getServerTimestampMillis()J Lcom/datadog/android/internal/time/TimeProvider; Lcom/datadog/android/internal/utils/ByteArrayExtKt; HSPLcom/datadog/android/internal/utils/ByteArrayExtKt;->toHexString([B)Ljava/lang/String; @@ -20106,7 +20175,6 @@ HSPLcom/datadog/android/log/LogsConfiguration$Builder;->build()Lcom/datadog/andr Lcom/datadog/android/log/internal/LogsFeature; HSPLcom/datadog/android/log/internal/LogsFeature;->()V HSPLcom/datadog/android/log/internal/LogsFeature;->(Lcom/datadog/android/api/feature/FeatureSdkCore;Ljava/lang/String;Lcom/datadog/android/event/EventMapper;)V -HSPLcom/datadog/android/log/internal/LogsFeature;->access$getSdkCore$p(Lcom/datadog/android/log/internal/LogsFeature;)Lcom/datadog/android/api/feature/FeatureSdkCore; HSPLcom/datadog/android/log/internal/LogsFeature;->createDataWriter(Lcom/datadog/android/event/EventMapper;)Lcom/datadog/android/api/storage/DataWriter; HSPLcom/datadog/android/log/internal/LogsFeature;->getAttributes$dd_sdk_android_logs_release()Ljava/util/Map; HSPLcom/datadog/android/log/internal/LogsFeature;->getDataWriter$dd_sdk_android_logs_release()Lcom/datadog/android/api/storage/DataWriter; @@ -20115,13 +20183,13 @@ HSPLcom/datadog/android/log/internal/LogsFeature;->getPackageName$dd_sdk_android HSPLcom/datadog/android/log/internal/LogsFeature;->getRequestFactory()Lcom/datadog/android/api/net/RequestFactory; HSPLcom/datadog/android/log/internal/LogsFeature;->getStorageConfiguration()Lcom/datadog/android/api/storage/FeatureStorageConfiguration; HSPLcom/datadog/android/log/internal/LogsFeature;->onInitialize(Landroid/content/Context;)V +HSPLcom/datadog/android/log/internal/LogsFeature;->requestFactory_delegate$lambda$0(Ljava/lang/String;Lcom/datadog/android/log/internal/LogsFeature;)Lcom/datadog/android/log/internal/net/LogsRequestFactory; +Lcom/datadog/android/log/internal/LogsFeature$$ExternalSyntheticLambda2; +HSPLcom/datadog/android/log/internal/LogsFeature$$ExternalSyntheticLambda2;->(Ljava/lang/String;Lcom/datadog/android/log/internal/LogsFeature;)V +HSPLcom/datadog/android/log/internal/LogsFeature$$ExternalSyntheticLambda2;->invoke()Ljava/lang/Object; Lcom/datadog/android/log/internal/LogsFeature$Companion; HSPLcom/datadog/android/log/internal/LogsFeature$Companion;->()V HSPLcom/datadog/android/log/internal/LogsFeature$Companion;->(Lkotlin/jvm/internal/DefaultConstructorMarker;)V -Lcom/datadog/android/log/internal/LogsFeature$requestFactory$2; -HSPLcom/datadog/android/log/internal/LogsFeature$requestFactory$2;->(Ljava/lang/String;Lcom/datadog/android/log/internal/LogsFeature;)V -HSPLcom/datadog/android/log/internal/LogsFeature$requestFactory$2;->invoke()Lcom/datadog/android/log/internal/net/LogsRequestFactory; -HSPLcom/datadog/android/log/internal/LogsFeature$requestFactory$2;->invoke()Ljava/lang/Object; Lcom/datadog/android/log/internal/domain/DatadogLogGenerator; HSPLcom/datadog/android/log/internal/domain/DatadogLogGenerator;->()V HSPLcom/datadog/android/log/internal/domain/DatadogLogGenerator;->(Ljava/lang/String;Lcom/datadog/android/api/InternalLogger;)V @@ -20169,21 +20237,19 @@ HSPLcom/datadog/android/log/internal/domain/event/LogEventSerializer$Companion;- Lcom/datadog/android/log/internal/logger/DatadogLogHandler; HSPLcom/datadog/android/log/internal/logger/DatadogLogHandler;->()V HSPLcom/datadog/android/log/internal/logger/DatadogLogHandler;->(Ljava/lang/String;Lcom/datadog/android/log/internal/domain/LogGenerator;Lcom/datadog/android/api/feature/FeatureSdkCore;Lcom/datadog/android/api/storage/DataWriter;ZZZLcom/datadog/android/core/sampling/Sampler;I)V -HSPLcom/datadog/android/log/internal/logger/DatadogLogHandler;->access$createLog(Lcom/datadog/android/log/internal/logger/DatadogLogHandler;ILcom/datadog/android/api/context/DatadogContext;Ljava/lang/String;Ljava/lang/Throwable;Ljava/util/Map;Ljava/util/Set;Ljava/lang/String;J)Lcom/datadog/android/log/model/LogEvent; HSPLcom/datadog/android/log/internal/logger/DatadogLogHandler;->createLog(ILcom/datadog/android/api/context/DatadogContext;Ljava/lang/String;Ljava/lang/Throwable;Ljava/util/Map;Ljava/util/Set;Ljava/lang/String;J)Lcom/datadog/android/log/model/LogEvent; -HSPLcom/datadog/android/log/internal/logger/DatadogLogHandler;->getWriter$dd_sdk_android_logs_release()Lcom/datadog/android/api/storage/DataWriter; +HSPLcom/datadog/android/log/internal/logger/DatadogLogHandler;->handleLog$lambda$1$lambda$0(Lcom/datadog/android/log/internal/logger/DatadogLogHandler;Lcom/datadog/android/log/model/LogEvent;Lcom/datadog/android/api/storage/EventBatchWriter;)Lkotlin/Unit; +HSPLcom/datadog/android/log/internal/logger/DatadogLogHandler;->handleLog$lambda$1(Lcom/datadog/android/log/internal/logger/DatadogLogHandler;ILjava/lang/String;Ljava/lang/Throwable;Ljava/util/Map;Ljava/util/Set;Ljava/lang/String;JLcom/datadog/android/api/context/DatadogContext;Lkotlin/jvm/functions/Function1;)Lkotlin/Unit; HSPLcom/datadog/android/log/internal/logger/DatadogLogHandler;->handleLog(ILjava/lang/String;Ljava/lang/Throwable;Ljava/util/Map;Ljava/util/Set;Ljava/lang/Long;)V +Lcom/datadog/android/log/internal/logger/DatadogLogHandler$$ExternalSyntheticLambda2; +HSPLcom/datadog/android/log/internal/logger/DatadogLogHandler$$ExternalSyntheticLambda2;->(Lcom/datadog/android/log/internal/logger/DatadogLogHandler;Lcom/datadog/android/log/model/LogEvent;)V +HSPLcom/datadog/android/log/internal/logger/DatadogLogHandler$$ExternalSyntheticLambda2;->invoke(Ljava/lang/Object;)Ljava/lang/Object; +Lcom/datadog/android/log/internal/logger/DatadogLogHandler$$ExternalSyntheticLambda5; +HSPLcom/datadog/android/log/internal/logger/DatadogLogHandler$$ExternalSyntheticLambda5;->(Lcom/datadog/android/log/internal/logger/DatadogLogHandler;ILjava/lang/String;Ljava/lang/Throwable;Ljava/util/Map;Ljava/util/Set;Ljava/lang/String;J)V +HSPLcom/datadog/android/log/internal/logger/DatadogLogHandler$$ExternalSyntheticLambda5;->invoke(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object; Lcom/datadog/android/log/internal/logger/DatadogLogHandler$Companion; HSPLcom/datadog/android/log/internal/logger/DatadogLogHandler$Companion;->()V HSPLcom/datadog/android/log/internal/logger/DatadogLogHandler$Companion;->(Lkotlin/jvm/internal/DefaultConstructorMarker;)V -Lcom/datadog/android/log/internal/logger/DatadogLogHandler$handleLog$1; -HSPLcom/datadog/android/log/internal/logger/DatadogLogHandler$handleLog$1;->(Lcom/datadog/android/log/internal/logger/DatadogLogHandler;ILjava/lang/String;Ljava/lang/Throwable;Ljava/util/Map;Ljava/util/Set;Ljava/lang/String;J)V -HSPLcom/datadog/android/log/internal/logger/DatadogLogHandler$handleLog$1;->invoke(Lcom/datadog/android/api/context/DatadogContext;Lkotlin/jvm/functions/Function1;)V -HSPLcom/datadog/android/log/internal/logger/DatadogLogHandler$handleLog$1;->invoke(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object; -Lcom/datadog/android/log/internal/logger/DatadogLogHandler$handleLog$1$1; -HSPLcom/datadog/android/log/internal/logger/DatadogLogHandler$handleLog$1$1;->(Lcom/datadog/android/log/internal/logger/DatadogLogHandler;Lcom/datadog/android/log/model/LogEvent;)V -HSPLcom/datadog/android/log/internal/logger/DatadogLogHandler$handleLog$1$1;->invoke(Lcom/datadog/android/api/storage/EventBatchWriter;)V -HSPLcom/datadog/android/log/internal/logger/DatadogLogHandler$handleLog$1$1;->invoke(Ljava/lang/Object;)Ljava/lang/Object; Lcom/datadog/android/log/internal/logger/LogHandler; Lcom/datadog/android/log/internal/net/LogsRequestFactory; HSPLcom/datadog/android/log/internal/net/LogsRequestFactory;->()V @@ -20288,12 +20354,12 @@ HSPLcom/datadog/android/ndk/internal/DatadogNdkCrashHandler;->handleNdkCrash(Lco HSPLcom/datadog/android/ndk/internal/DatadogNdkCrashHandler;->prepareData$lambda$0(Lcom/datadog/android/ndk/internal/DatadogNdkCrashHandler;)V HSPLcom/datadog/android/ndk/internal/DatadogNdkCrashHandler;->prepareData()V HSPLcom/datadog/android/ndk/internal/DatadogNdkCrashHandler;->readCrashData()V -Lcom/datadog/android/ndk/internal/DatadogNdkCrashHandler$$ExternalSyntheticLambda0; -HSPLcom/datadog/android/ndk/internal/DatadogNdkCrashHandler$$ExternalSyntheticLambda0;->(Lcom/datadog/android/ndk/internal/DatadogNdkCrashHandler;)V -HSPLcom/datadog/android/ndk/internal/DatadogNdkCrashHandler$$ExternalSyntheticLambda0;->run()V Lcom/datadog/android/ndk/internal/DatadogNdkCrashHandler$$ExternalSyntheticLambda1; -HSPLcom/datadog/android/ndk/internal/DatadogNdkCrashHandler$$ExternalSyntheticLambda1;->(Lcom/datadog/android/ndk/internal/DatadogNdkCrashHandler;Lcom/datadog/android/api/feature/FeatureSdkCore;)V +HSPLcom/datadog/android/ndk/internal/DatadogNdkCrashHandler$$ExternalSyntheticLambda1;->(Lcom/datadog/android/ndk/internal/DatadogNdkCrashHandler;)V HSPLcom/datadog/android/ndk/internal/DatadogNdkCrashHandler$$ExternalSyntheticLambda1;->run()V +Lcom/datadog/android/ndk/internal/DatadogNdkCrashHandler$$ExternalSyntheticLambda3; +HSPLcom/datadog/android/ndk/internal/DatadogNdkCrashHandler$$ExternalSyntheticLambda3;->(Lcom/datadog/android/ndk/internal/DatadogNdkCrashHandler;Lcom/datadog/android/api/feature/FeatureSdkCore;)V +HSPLcom/datadog/android/ndk/internal/DatadogNdkCrashHandler$$ExternalSyntheticLambda3;->run()V Lcom/datadog/android/ndk/internal/DatadogNdkCrashHandler$Companion; HSPLcom/datadog/android/ndk/internal/DatadogNdkCrashHandler$Companion;->()V HSPLcom/datadog/android/ndk/internal/DatadogNdkCrashHandler$Companion;->(Lkotlin/jvm/internal/DefaultConstructorMarker;)V @@ -20319,8 +20385,11 @@ Lcom/datadog/android/okhttp/DatadogEventListener$Factory$Companion$NO_OP_EVENT_L HSPLcom/datadog/android/okhttp/DatadogEventListener$Factory$Companion$NO_OP_EVENT_LISTENER$1;->()V Lcom/datadog/android/okhttp/DatadogInterceptor; HSPLcom/datadog/android/okhttp/DatadogInterceptor;->()V -HSPLcom/datadog/android/okhttp/DatadogInterceptor;->(Ljava/lang/String;Ljava/util/Map;Lcom/datadog/android/okhttp/trace/TracedRequestListener;Lcom/datadog/android/rum/RumResourceAttributesProvider;Lcom/datadog/android/core/sampling/Sampler;Lcom/datadog/android/trace/TraceContextInjection;ZLkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function0;Lcom/datadog/android/rum/resource/ResourceHeadersExtractor;)V +HSPLcom/datadog/android/okhttp/DatadogInterceptor;->(Ljava/lang/String;Ljava/util/Map;Lcom/datadog/android/okhttp/trace/TracedRequestListener;Lcom/datadog/android/rum/RumResourceAttributesProvider;Lcom/datadog/android/core/sampling/Sampler;Lcom/datadog/android/trace/TraceContextInjection;ZLkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function0;Lcom/datadog/android/rum/resource/ResourceHeadersExtractor;Lkotlin/jvm/functions/Function1;)V +HSPLcom/datadog/android/okhttp/DatadogInterceptor;->(Ljava/lang/String;Ljava/util/Map;Lcom/datadog/android/okhttp/trace/TracedRequestListener;Lcom/datadog/android/rum/RumResourceAttributesProvider;Lcom/datadog/android/core/sampling/Sampler;Lcom/datadog/android/trace/TraceContextInjection;ZLkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function0;Lcom/datadog/android/rum/resource/ResourceHeadersExtractor;Lkotlin/jvm/functions/Function1;ILkotlin/jvm/internal/DefaultConstructorMarker;)V HSPLcom/datadog/android/okhttp/DatadogInterceptor;->onSdkInstanceReady$dd_sdk_android_okhttp_release(Lcom/datadog/android/core/InternalSdkCore;)V +Lcom/datadog/android/okhttp/DatadogInterceptor$1; +HSPLcom/datadog/android/okhttp/DatadogInterceptor$1;->(Ljava/lang/Object;)V Lcom/datadog/android/okhttp/DatadogInterceptor$Builder; HSPLcom/datadog/android/okhttp/DatadogInterceptor$Builder;->(Ljava/util/List;)V HSPLcom/datadog/android/okhttp/DatadogInterceptor$Builder;->(Ljava/util/Map;)V @@ -20344,12 +20413,22 @@ HSPLcom/datadog/android/okhttp/trace/NoOpTracedRequestListener;->()V Lcom/datadog/android/okhttp/trace/TracedRequestListener; Lcom/datadog/android/okhttp/trace/TracingInterceptor; HSPLcom/datadog/android/okhttp/trace/TracingInterceptor;->()V -HSPLcom/datadog/android/okhttp/trace/TracingInterceptor;->(Ljava/lang/String;Ljava/util/Map;Lcom/datadog/android/okhttp/trace/TracedRequestListener;Ljava/lang/String;Lcom/datadog/android/core/sampling/Sampler;Lcom/datadog/android/trace/TraceContextInjection;ZLkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function0;)V +HSPLcom/datadog/android/okhttp/trace/TracingInterceptor;->(Ljava/lang/String;Ljava/util/Map;Lcom/datadog/android/okhttp/trace/TracedRequestListener;Ljava/lang/String;Lcom/datadog/android/core/sampling/Sampler;Lcom/datadog/android/trace/TraceContextInjection;ZLkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function1;)V HSPLcom/datadog/android/okhttp/trace/TracingInterceptor;->access$getDEFAULT_LOCAL_TRACER_FACTORY$cp()Lkotlin/jvm/functions/Function2; -HSPLcom/datadog/android/okhttp/trace/TracingInterceptor;->getTraceSampler$dd_sdk_android_okhttp_release()Lcom/datadog/android/core/sampling/Sampler; -HSPLcom/datadog/android/okhttp/trace/TracingInterceptor;->getTracedHosts$dd_sdk_android_okhttp_release()Ljava/util/Map; HSPLcom/datadog/android/okhttp/trace/TracingInterceptor;->onSdkInstanceReady$dd_sdk_android_okhttp_release(Lcom/datadog/android/core/InternalSdkCore;)V +HSPLcom/datadog/android/okhttp/trace/TracingInterceptor;->sdkCoreReference$lambda$1(Lcom/datadog/android/okhttp/trace/TracingInterceptor;Lcom/datadog/android/api/SdkCore;)Lkotlin/Unit; +HSPLcom/datadog/android/okhttp/trace/TracingInterceptor;->updateTracingFeatureContext$lambda$10(Lcom/datadog/android/okhttp/trace/TracingInterceptor;Ljava/util/Map;)Lkotlin/Unit; HSPLcom/datadog/android/okhttp/trace/TracingInterceptor;->updateTracingFeatureContext(Lcom/datadog/android/api/feature/FeatureSdkCore;)V +Lcom/datadog/android/okhttp/trace/TracingInterceptor$$ExternalSyntheticLambda0; +HSPLcom/datadog/android/okhttp/trace/TracingInterceptor$$ExternalSyntheticLambda0;->(Lcom/datadog/android/okhttp/trace/TracingInterceptor;)V +Lcom/datadog/android/okhttp/trace/TracingInterceptor$$ExternalSyntheticLambda11; +HSPLcom/datadog/android/okhttp/trace/TracingInterceptor$$ExternalSyntheticLambda11;->()V +Lcom/datadog/android/okhttp/trace/TracingInterceptor$$ExternalSyntheticLambda12; +HSPLcom/datadog/android/okhttp/trace/TracingInterceptor$$ExternalSyntheticLambda12;->(Lcom/datadog/android/okhttp/trace/TracingInterceptor;)V +HSPLcom/datadog/android/okhttp/trace/TracingInterceptor$$ExternalSyntheticLambda12;->invoke(Ljava/lang/Object;)Ljava/lang/Object; +Lcom/datadog/android/okhttp/trace/TracingInterceptor$$ExternalSyntheticLambda2; +HSPLcom/datadog/android/okhttp/trace/TracingInterceptor$$ExternalSyntheticLambda2;->(Lcom/datadog/android/okhttp/trace/TracingInterceptor;)V +HSPLcom/datadog/android/okhttp/trace/TracingInterceptor$$ExternalSyntheticLambda2;->invoke(Ljava/lang/Object;)Ljava/lang/Object; Lcom/datadog/android/okhttp/trace/TracingInterceptor$BaseBuilder; HSPLcom/datadog/android/okhttp/trace/TracingInterceptor$BaseBuilder;->(Ljava/util/Map;)V HSPLcom/datadog/android/okhttp/trace/TracingInterceptor$BaseBuilder;->getGlobalTracerProvider$dd_sdk_android_okhttp_release()Lkotlin/jvm/functions/Function0; @@ -20360,25 +20439,11 @@ HSPLcom/datadog/android/okhttp/trace/TracingInterceptor$BaseBuilder;->getTraceCo HSPLcom/datadog/android/okhttp/trace/TracingInterceptor$BaseBuilder;->getTraceSampler$dd_sdk_android_okhttp_release()Lcom/datadog/android/core/sampling/Sampler; HSPLcom/datadog/android/okhttp/trace/TracingInterceptor$BaseBuilder;->getTracedHostsWithHeaderType$dd_sdk_android_okhttp_release()Ljava/util/Map; HSPLcom/datadog/android/okhttp/trace/TracingInterceptor$BaseBuilder;->getTracedRequestListener$dd_sdk_android_okhttp_release()Lcom/datadog/android/okhttp/trace/TracedRequestListener; -Lcom/datadog/android/okhttp/trace/TracingInterceptor$BaseBuilder$globalTracerProvider$1; -HSPLcom/datadog/android/okhttp/trace/TracingInterceptor$BaseBuilder$globalTracerProvider$1;->()V -HSPLcom/datadog/android/okhttp/trace/TracingInterceptor$BaseBuilder$globalTracerProvider$1;->()V +Lcom/datadog/android/okhttp/trace/TracingInterceptor$BaseBuilder$$ExternalSyntheticLambda0; +HSPLcom/datadog/android/okhttp/trace/TracingInterceptor$BaseBuilder$$ExternalSyntheticLambda0;->()V Lcom/datadog/android/okhttp/trace/TracingInterceptor$Companion; HSPLcom/datadog/android/okhttp/trace/TracingInterceptor$Companion;->()V HSPLcom/datadog/android/okhttp/trace/TracingInterceptor$Companion;->(Lkotlin/jvm/internal/DefaultConstructorMarker;)V -Lcom/datadog/android/okhttp/trace/TracingInterceptor$Companion$DEFAULT_LOCAL_TRACER_FACTORY$1; -HSPLcom/datadog/android/okhttp/trace/TracingInterceptor$Companion$DEFAULT_LOCAL_TRACER_FACTORY$1;->()V -HSPLcom/datadog/android/okhttp/trace/TracingInterceptor$Companion$DEFAULT_LOCAL_TRACER_FACTORY$1;->()V -Lcom/datadog/android/okhttp/trace/TracingInterceptor$rumContextPropagator$1; -HSPLcom/datadog/android/okhttp/trace/TracingInterceptor$rumContextPropagator$1;->(Lcom/datadog/android/okhttp/trace/TracingInterceptor;)V -Lcom/datadog/android/okhttp/trace/TracingInterceptor$sdkCoreReference$1; -HSPLcom/datadog/android/okhttp/trace/TracingInterceptor$sdkCoreReference$1;->(Lcom/datadog/android/okhttp/trace/TracingInterceptor;)V -HSPLcom/datadog/android/okhttp/trace/TracingInterceptor$sdkCoreReference$1;->invoke(Lcom/datadog/android/api/SdkCore;)V -HSPLcom/datadog/android/okhttp/trace/TracingInterceptor$sdkCoreReference$1;->invoke(Ljava/lang/Object;)Ljava/lang/Object; -Lcom/datadog/android/okhttp/trace/TracingInterceptor$updateTracingFeatureContext$1; -HSPLcom/datadog/android/okhttp/trace/TracingInterceptor$updateTracingFeatureContext$1;->(Lcom/datadog/android/okhttp/trace/TracingInterceptor;)V -HSPLcom/datadog/android/okhttp/trace/TracingInterceptor$updateTracingFeatureContext$1;->invoke(Ljava/lang/Object;)Ljava/lang/Object; -HSPLcom/datadog/android/okhttp/trace/TracingInterceptor$updateTracingFeatureContext$1;->invoke(Ljava/util/Map;)V Lcom/datadog/android/privacy/TrackingConsent; HSPLcom/datadog/android/privacy/TrackingConsent;->$values()[Lcom/datadog/android/privacy/TrackingConsent; HSPLcom/datadog/android/privacy/TrackingConsent;->()V @@ -20408,15 +20473,15 @@ HSPLcom/datadog/android/rum/NoOpRumResourceAttributesProvider;->()V Lcom/datadog/android/rum/Rum; HSPLcom/datadog/android/rum/Rum;->()V HSPLcom/datadog/android/rum/Rum;->()V +HSPLcom/datadog/android/rum/Rum;->createMonitor$lambda$5(Lcom/datadog/android/rum/internal/domain/scope/RumVitalAppLaunchEventHelper;Lcom/datadog/android/core/InternalSdkCore;Lcom/datadog/android/rum/internal/startup/RumAppStartupTelemetryReporter;)Lcom/datadog/android/rum/internal/startup/RumSessionScopeStartupManager; HSPLcom/datadog/android/rum/Rum;->createMonitor(Lcom/datadog/android/core/InternalSdkCore;Lcom/datadog/android/rum/internal/RumFeature;)Lcom/datadog/android/rum/internal/monitor/DatadogRumMonitor; HSPLcom/datadog/android/rum/Rum;->createTelemetryEventHandler(Lcom/datadog/android/core/InternalSdkCore;Lcom/datadog/android/rum/internal/RumFeature;Lcom/datadog/android/rum/internal/metric/SessionEndedMetricDispatcher;)Lcom/datadog/android/telemetry/internal/TelemetryEventHandler; HSPLcom/datadog/android/rum/Rum;->enable$default(Lcom/datadog/android/rum/RumConfiguration;Lcom/datadog/android/api/SdkCore;ILjava/lang/Object;)V HSPLcom/datadog/android/rum/Rum;->enable(Lcom/datadog/android/rum/RumConfiguration;Lcom/datadog/android/api/SdkCore;)V HSPLcom/datadog/android/rum/Rum;->isTimeseriesConfigured(Lcom/datadog/android/rum/internal/RumFeature$Configuration;)Z -Lcom/datadog/android/rum/Rum$createMonitor$1; -HSPLcom/datadog/android/rum/Rum$createMonitor$1;->(Lcom/datadog/android/rum/internal/domain/scope/RumVitalAppLaunchEventHelper;Lcom/datadog/android/core/InternalSdkCore;Lcom/datadog/android/rum/internal/startup/RumAppStartupTelemetryReporter;)V -HSPLcom/datadog/android/rum/Rum$createMonitor$1;->invoke()Lcom/datadog/android/rum/internal/startup/RumSessionScopeStartupManager; -HSPLcom/datadog/android/rum/Rum$createMonitor$1;->invoke()Ljava/lang/Object; +Lcom/datadog/android/rum/Rum$$ExternalSyntheticLambda4; +HSPLcom/datadog/android/rum/Rum$$ExternalSyntheticLambda4;->(Lcom/datadog/android/rum/internal/domain/scope/RumVitalAppLaunchEventHelper;Lcom/datadog/android/core/InternalSdkCore;Lcom/datadog/android/rum/internal/startup/RumAppStartupTelemetryReporter;)V +HSPLcom/datadog/android/rum/Rum$$ExternalSyntheticLambda4;->invoke()Ljava/lang/Object; Lcom/datadog/android/rum/Rum$createMonitor$sessionSampler$1; HSPLcom/datadog/android/rum/Rum$createMonitor$sessionSampler$1;->(Ljava/lang/Object;)V Lcom/datadog/android/rum/RumActionType; @@ -20472,12 +20537,19 @@ HSPLcom/datadog/android/rum/_RumInternalProxy;->(Lcom/datadog/android/rum/ Lcom/datadog/android/rum/_RumInternalProxy$Companion; HSPLcom/datadog/android/rum/_RumInternalProxy$Companion;->()V HSPLcom/datadog/android/rum/_RumInternalProxy$Companion;->(Lkotlin/jvm/internal/DefaultConstructorMarker;)V +Lcom/datadog/android/rum/configuration/RumViewEventWriteConfig; +Lcom/datadog/android/rum/configuration/RumViewEventWriteConfig$AlwaysFullView; +HSPLcom/datadog/android/rum/configuration/RumViewEventWriteConfig$AlwaysFullView;->()V +HSPLcom/datadog/android/rum/configuration/RumViewEventWriteConfig$AlwaysFullView;->()V Lcom/datadog/android/rum/configuration/SlowFramesConfiguration; HSPLcom/datadog/android/rum/configuration/SlowFramesConfiguration;->()V HSPLcom/datadog/android/rum/configuration/SlowFramesConfiguration;->(IJJJJ)V HSPLcom/datadog/android/rum/configuration/SlowFramesConfiguration;->(IJJJJILkotlin/jvm/internal/DefaultConstructorMarker;)V HSPLcom/datadog/android/rum/configuration/SlowFramesConfiguration;->access$getDEFAULT$cp()Lcom/datadog/android/rum/configuration/SlowFramesConfiguration; HSPLcom/datadog/android/rum/configuration/SlowFramesConfiguration;->getFreezeDurationThresholdNs$dd_sdk_android_rum_release()J +PLcom/datadog/android/rum/configuration/SlowFramesConfiguration;->getMaxSlowFrameThresholdNs$dd_sdk_android_rum_release()J +PLcom/datadog/android/rum/configuration/SlowFramesConfiguration;->getMaxSlowFramesAmount$dd_sdk_android_rum_release()I +PLcom/datadog/android/rum/configuration/SlowFramesConfiguration;->getMinViewLifetimeThresholdNs$dd_sdk_android_rum_release()J Lcom/datadog/android/rum/configuration/SlowFramesConfiguration$Companion; HSPLcom/datadog/android/rum/configuration/SlowFramesConfiguration$Companion;->()V HSPLcom/datadog/android/rum/configuration/SlowFramesConfiguration$Companion;->(Lkotlin/jvm/internal/DefaultConstructorMarker;)V @@ -20486,7 +20558,7 @@ Lcom/datadog/android/rum/configuration/VitalsUpdateFrequency; HSPLcom/datadog/android/rum/configuration/VitalsUpdateFrequency;->$values()[Lcom/datadog/android/rum/configuration/VitalsUpdateFrequency; HSPLcom/datadog/android/rum/configuration/VitalsUpdateFrequency;->()V HSPLcom/datadog/android/rum/configuration/VitalsUpdateFrequency;->(Ljava/lang/String;IJ)V -PLcom/datadog/android/rum/configuration/VitalsUpdateFrequency;->getPeriodInMs$dd_sdk_android_rum_release()J +Lcom/datadog/android/rum/event/ViewEventMapper; Lcom/datadog/android/rum/internal/AnonymousIdentifierDeserializer; HSPLcom/datadog/android/rum/internal/AnonymousIdentifierDeserializer;->()V HSPLcom/datadog/android/rum/internal/AnonymousIdentifierDeserializer;->deserialize(Ljava/lang/Object;)Ljava/lang/Object; @@ -20515,17 +20587,18 @@ HSPLcom/datadog/android/rum/internal/NoOpRumSessionListener;->onSessionStarted(L Lcom/datadog/android/rum/internal/RumAnonymousIdentifierManager; HSPLcom/datadog/android/rum/internal/RumAnonymousIdentifierManager;->()V HSPLcom/datadog/android/rum/internal/RumAnonymousIdentifierManager;->(Lcom/datadog/android/api/storage/datastore/DataStoreHandler;Lcom/datadog/android/api/feature/FeatureSdkCore;)V -HSPLcom/datadog/android/rum/internal/RumAnonymousIdentifierManager;->access$createAndStoreAnonymousId(Lcom/datadog/android/rum/internal/RumAnonymousIdentifierManager;Lcom/datadog/android/api/storage/datastore/DataStoreHandler;Lcom/datadog/android/api/feature/FeatureSdkCore;)V HSPLcom/datadog/android/rum/internal/RumAnonymousIdentifierManager;->createAndStoreAnonymousId(Lcom/datadog/android/api/storage/datastore/DataStoreHandler;Lcom/datadog/android/api/feature/FeatureSdkCore;)V +HSPLcom/datadog/android/rum/internal/RumAnonymousIdentifierManager;->handleAnonymousIdTracking$lambda$0(Lcom/datadog/android/rum/internal/RumAnonymousIdentifierManager;Lcom/datadog/android/api/storage/datastore/DataStoreHandler;Lcom/datadog/android/api/feature/FeatureSdkCore;Ljava/util/UUID;)Lkotlin/Unit; HSPLcom/datadog/android/rum/internal/RumAnonymousIdentifierManager;->handleAnonymousIdTracking(Lcom/datadog/android/api/storage/datastore/DataStoreHandler;Lcom/datadog/android/api/feature/FeatureSdkCore;)V HSPLcom/datadog/android/rum/internal/RumAnonymousIdentifierManager;->manageAnonymousId(Z)V +Lcom/datadog/android/rum/internal/RumAnonymousIdentifierManager$$ExternalSyntheticLambda0; +HSPLcom/datadog/android/rum/internal/RumAnonymousIdentifierManager$$ExternalSyntheticLambda0;->(Lcom/datadog/android/rum/internal/RumAnonymousIdentifierManager;Lcom/datadog/android/api/storage/datastore/DataStoreHandler;Lcom/datadog/android/api/feature/FeatureSdkCore;)V +HSPLcom/datadog/android/rum/internal/RumAnonymousIdentifierManager$$ExternalSyntheticLambda0;->invoke(Ljava/lang/Object;)Ljava/lang/Object; Lcom/datadog/android/rum/internal/RumAnonymousIdentifierManager$Companion; HSPLcom/datadog/android/rum/internal/RumAnonymousIdentifierManager$Companion;->()V HSPLcom/datadog/android/rum/internal/RumAnonymousIdentifierManager$Companion;->(Lkotlin/jvm/internal/DefaultConstructorMarker;)V -Lcom/datadog/android/rum/internal/RumAnonymousIdentifierManager$handleAnonymousIdTracking$1; -HSPLcom/datadog/android/rum/internal/RumAnonymousIdentifierManager$handleAnonymousIdTracking$1;->(Lcom/datadog/android/rum/internal/RumAnonymousIdentifierManager;Lcom/datadog/android/api/storage/datastore/DataStoreHandler;Lcom/datadog/android/api/feature/FeatureSdkCore;)V -HSPLcom/datadog/android/rum/internal/RumAnonymousIdentifierManager$handleAnonymousIdTracking$1;->invoke(Ljava/lang/Object;)Ljava/lang/Object; -HSPLcom/datadog/android/rum/internal/RumAnonymousIdentifierManager$handleAnonymousIdTracking$1;->invoke(Ljava/util/UUID;)V +Lcom/datadog/android/rum/internal/RumConfigurationExtKt; +HSPLcom/datadog/android/rum/internal/RumConfigurationExtKt;->applyRemoteConfiguration(Lcom/datadog/android/rum/RumConfiguration;Lcom/datadog/android/core/internal/remote/model/RemoteConfiguration;)Lcom/datadog/android/rum/RumConfiguration; Lcom/datadog/android/rum/internal/RumErrorSourceType; HSPLcom/datadog/android/rum/internal/RumErrorSourceType;->$values()[Lcom/datadog/android/rum/internal/RumErrorSourceType; HSPLcom/datadog/android/rum/internal/RumErrorSourceType;->()V @@ -20535,11 +20608,11 @@ Lcom/datadog/android/rum/internal/RumFeature; HSPLcom/datadog/android/rum/internal/RumFeature;->()V HSPLcom/datadog/android/rum/internal/RumFeature;->(Lcom/datadog/android/api/feature/FeatureSdkCore;Ljava/lang/String;Lcom/datadog/android/rum/internal/RumFeature$Configuration;Lkotlin/jvm/functions/Function1;Lcom/datadog/android/internal/system/BuildSdkVersionProvider;Landroid/os/Handler;)V HSPLcom/datadog/android/rum/internal/RumFeature;->(Lcom/datadog/android/api/feature/FeatureSdkCore;Ljava/lang/String;Lcom/datadog/android/rum/internal/RumFeature$Configuration;Lkotlin/jvm/functions/Function1;Lcom/datadog/android/internal/system/BuildSdkVersionProvider;Landroid/os/Handler;ILkotlin/jvm/internal/DefaultConstructorMarker;)V +HSPLcom/datadog/android/rum/internal/RumFeature;->DEFAULT_RUM_CONFIG$lambda$25(Lcom/datadog/android/rum/model/ViewEvent;)Lcom/datadog/android/rum/model/ViewEvent; HSPLcom/datadog/android/rum/internal/RumFeature;->access$getDEFAULT_RUM_CONFIG$cp()Lcom/datadog/android/rum/internal/RumFeature$Configuration; -HSPLcom/datadog/android/rum/internal/RumFeature;->access$getSdkCore$p(Lcom/datadog/android/rum/internal/RumFeature;)Lcom/datadog/android/api/feature/FeatureSdkCore; HSPLcom/datadog/android/rum/internal/RumFeature;->consumeLastFatalAnr$dd_sdk_android_rum_release(Ljava/util/concurrent/ExecutorService;)V HSPLcom/datadog/android/rum/internal/RumFeature;->createDataWriter(Lcom/datadog/android/rum/internal/RumFeature$Configuration;Lcom/datadog/android/core/InternalSdkCore;)Lcom/datadog/android/api/storage/DataWriter; -HSPLcom/datadog/android/rum/internal/RumFeature;->getAccessibilitySnapshotManager$dd_sdk_android_rum_release()Lcom/datadog/android/rum/internal/domain/accessibility/AccessibilitySnapshotManager; +HSPLcom/datadog/android/rum/internal/RumFeature;->getAccessibilityReader$dd_sdk_android_rum_release()Lcom/datadog/android/rum/internal/domain/InfoProvider; HSPLcom/datadog/android/rum/internal/RumFeature;->getAppContext$dd_sdk_android_rum_release()Landroid/content/Context; HSPLcom/datadog/android/rum/internal/RumFeature;->getApplicationId$dd_sdk_android_rum_release()Ljava/lang/String; HSPLcom/datadog/android/rum/internal/RumFeature;->getBackgroundEventTracking$dd_sdk_android_rum_release()Z @@ -20573,10 +20646,20 @@ HSPLcom/datadog/android/rum/internal/RumFeature;->initializeSlowFrameListener(Lc HSPLcom/datadog/android/rum/internal/RumFeature;->onInitialize(Landroid/content/Context;)V HSPLcom/datadog/android/rum/internal/RumFeature;->onReceive(Ljava/lang/Object;)V HSPLcom/datadog/android/rum/internal/RumFeature;->registerTrackingStrategies(Landroid/content/Context;)V +HSPLcom/datadog/android/rum/internal/RumFeature;->requestFactory_delegate$lambda$8(Lcom/datadog/android/rum/internal/RumFeature;)Lcom/datadog/android/rum/internal/net/RumRequestFactory; HSPLcom/datadog/android/rum/internal/RumFeature;->setAppContext$dd_sdk_android_rum_release(Landroid/content/Context;)V -Lcom/datadog/android/rum/internal/RumFeature$1; -HSPLcom/datadog/android/rum/internal/RumFeature$1;->()V -HSPLcom/datadog/android/rum/internal/RumFeature$1;->()V +Lcom/datadog/android/rum/internal/RumFeature$$ExternalSyntheticLambda11; +HSPLcom/datadog/android/rum/internal/RumFeature$$ExternalSyntheticLambda11;->()V +HSPLcom/datadog/android/rum/internal/RumFeature$$ExternalSyntheticLambda11;->map(Lcom/datadog/android/rum/model/ViewEvent;)Lcom/datadog/android/rum/model/ViewEvent; +Lcom/datadog/android/rum/internal/RumFeature$$ExternalSyntheticLambda2; +HSPLcom/datadog/android/rum/internal/RumFeature$$ExternalSyntheticLambda2;->(Lcom/datadog/android/rum/internal/RumFeature;)V +Lcom/datadog/android/rum/internal/RumFeature$$ExternalSyntheticLambda3; +HSPLcom/datadog/android/rum/internal/RumFeature$$ExternalSyntheticLambda3;->(Lcom/datadog/android/rum/internal/RumFeature;)V +HSPLcom/datadog/android/rum/internal/RumFeature$$ExternalSyntheticLambda3;->invoke()Ljava/lang/Object; +Lcom/datadog/android/rum/internal/RumFeature$$ExternalSyntheticLambda6; +HSPLcom/datadog/android/rum/internal/RumFeature$$ExternalSyntheticLambda6;->()V +Lcom/datadog/android/rum/internal/RumFeature$$ExternalSyntheticLambda9; +HSPLcom/datadog/android/rum/internal/RumFeature$$ExternalSyntheticLambda9;->()V Lcom/datadog/android/rum/internal/RumFeature$Companion; HSPLcom/datadog/android/rum/internal/RumFeature$Companion;->()V HSPLcom/datadog/android/rum/internal/RumFeature$Companion;->(Lkotlin/jvm/internal/DefaultConstructorMarker;)V @@ -20587,9 +20670,9 @@ HSPLcom/datadog/android/rum/internal/RumFeature$Companion;->isTrackNonFatalAnrsE HSPLcom/datadog/android/rum/internal/RumFeature$Companion;->provideGestureTracker([Lcom/datadog/android/rum/tracking/ViewAttributesProvider;Lcom/datadog/android/rum/tracking/InteractionPredicate;Lcom/datadog/android/rum/tracking/ActionTrackingStrategy;Lcom/datadog/android/api/InternalLogger;)Lcom/datadog/android/rum/internal/instrumentation/gestures/DatadogGesturesTracker; HSPLcom/datadog/android/rum/internal/RumFeature$Companion;->provideUserTrackingStrategy([Lcom/datadog/android/rum/tracking/ViewAttributesProvider;Lcom/datadog/android/rum/tracking/InteractionPredicate;Lcom/datadog/android/rum/tracking/ActionTrackingStrategy;Lcom/datadog/android/internal/system/BuildSdkVersionProvider;Lcom/datadog/android/api/InternalLogger;)Lcom/datadog/android/rum/internal/tracking/UserActionTrackingStrategy; Lcom/datadog/android/rum/internal/RumFeature$Configuration; -HSPLcom/datadog/android/rum/internal/RumFeature$Configuration;->(Ljava/lang/String;FFFZLjava/util/List;Lcom/datadog/android/rum/tracking/InteractionPredicate;Lcom/datadog/android/rum/tracking/ViewTrackingStrategy;Lcom/datadog/android/rum/tracking/TrackingStrategy;Lcom/datadog/android/event/EventMapper;Lcom/datadog/android/event/EventMapper;Lcom/datadog/android/event/EventMapper;Lcom/datadog/android/event/EventMapper;Lcom/datadog/android/event/EventMapper;Lcom/datadog/android/event/EventMapper;Lcom/datadog/android/event/EventMapper;Lcom/datadog/android/event/EventMapper;ZZZLcom/datadog/android/rum/configuration/VitalsUpdateFrequency;Lcom/datadog/android/rum/RumSessionListener;Lcom/datadog/android/rum/metric/networksettled/InitialResourceIdentifier;Lcom/datadog/android/rum/metric/interactiontonextview/LastInteractionIdentifier;Lcom/datadog/android/rum/configuration/SlowFramesConfiguration;Lcom/datadog/android/rum/tracking/ActionTrackingStrategy;Ljava/util/Map;ZLcom/datadog/android/rum/RumSessionType;ZZLcom/datadog/android/rum/internal/instrumentation/insights/InsightsCollector;Lcom/datadog/android/rum/startup/AppStartupActivityPredicate;Lcom/datadog/android/rum/timeseries/TimeseriesConfiguration;)V -HSPLcom/datadog/android/rum/internal/RumFeature$Configuration;->copy$default(Lcom/datadog/android/rum/internal/RumFeature$Configuration;Ljava/lang/String;FFFZLjava/util/List;Lcom/datadog/android/rum/tracking/InteractionPredicate;Lcom/datadog/android/rum/tracking/ViewTrackingStrategy;Lcom/datadog/android/rum/tracking/TrackingStrategy;Lcom/datadog/android/event/EventMapper;Lcom/datadog/android/event/EventMapper;Lcom/datadog/android/event/EventMapper;Lcom/datadog/android/event/EventMapper;Lcom/datadog/android/event/EventMapper;Lcom/datadog/android/event/EventMapper;Lcom/datadog/android/event/EventMapper;Lcom/datadog/android/event/EventMapper;ZZZLcom/datadog/android/rum/configuration/VitalsUpdateFrequency;Lcom/datadog/android/rum/RumSessionListener;Lcom/datadog/android/rum/metric/networksettled/InitialResourceIdentifier;Lcom/datadog/android/rum/metric/interactiontonextview/LastInteractionIdentifier;Lcom/datadog/android/rum/configuration/SlowFramesConfiguration;Lcom/datadog/android/rum/tracking/ActionTrackingStrategy;Ljava/util/Map;ZLcom/datadog/android/rum/RumSessionType;ZZLcom/datadog/android/rum/internal/instrumentation/insights/InsightsCollector;Lcom/datadog/android/rum/startup/AppStartupActivityPredicate;Lcom/datadog/android/rum/timeseries/TimeseriesConfiguration;IILjava/lang/Object;)Lcom/datadog/android/rum/internal/RumFeature$Configuration; -HSPLcom/datadog/android/rum/internal/RumFeature$Configuration;->copy(Ljava/lang/String;FFFZLjava/util/List;Lcom/datadog/android/rum/tracking/InteractionPredicate;Lcom/datadog/android/rum/tracking/ViewTrackingStrategy;Lcom/datadog/android/rum/tracking/TrackingStrategy;Lcom/datadog/android/event/EventMapper;Lcom/datadog/android/event/EventMapper;Lcom/datadog/android/event/EventMapper;Lcom/datadog/android/event/EventMapper;Lcom/datadog/android/event/EventMapper;Lcom/datadog/android/event/EventMapper;Lcom/datadog/android/event/EventMapper;Lcom/datadog/android/event/EventMapper;ZZZLcom/datadog/android/rum/configuration/VitalsUpdateFrequency;Lcom/datadog/android/rum/RumSessionListener;Lcom/datadog/android/rum/metric/networksettled/InitialResourceIdentifier;Lcom/datadog/android/rum/metric/interactiontonextview/LastInteractionIdentifier;Lcom/datadog/android/rum/configuration/SlowFramesConfiguration;Lcom/datadog/android/rum/tracking/ActionTrackingStrategy;Ljava/util/Map;ZLcom/datadog/android/rum/RumSessionType;ZZLcom/datadog/android/rum/internal/instrumentation/insights/InsightsCollector;Lcom/datadog/android/rum/startup/AppStartupActivityPredicate;Lcom/datadog/android/rum/timeseries/TimeseriesConfiguration;)Lcom/datadog/android/rum/internal/RumFeature$Configuration; +HSPLcom/datadog/android/rum/internal/RumFeature$Configuration;->(Ljava/lang/String;FFFZLjava/util/List;Lcom/datadog/android/rum/tracking/InteractionPredicate;Lcom/datadog/android/rum/tracking/ViewTrackingStrategy;Lcom/datadog/android/rum/tracking/TrackingStrategy;Lcom/datadog/android/rum/event/ViewEventMapper;Lcom/datadog/android/event/EventMapper;Lcom/datadog/android/event/EventMapper;Lcom/datadog/android/event/EventMapper;Lcom/datadog/android/event/EventMapper;Lcom/datadog/android/event/EventMapper;Lcom/datadog/android/event/EventMapper;Lcom/datadog/android/event/EventMapper;ZZZLcom/datadog/android/rum/configuration/VitalsUpdateFrequency;Lcom/datadog/android/rum/RumSessionListener;Lcom/datadog/android/rum/metric/networksettled/InitialResourceIdentifier;Lcom/datadog/android/rum/metric/interactiontonextview/LastInteractionIdentifier;Lcom/datadog/android/rum/configuration/SlowFramesConfiguration;Lcom/datadog/android/rum/tracking/ActionTrackingStrategy;Ljava/util/Map;ZLcom/datadog/android/rum/RumSessionType;ZZLcom/datadog/android/rum/internal/instrumentation/insights/InsightsCollector;Lcom/datadog/android/rum/startup/AppStartupActivityPredicate;Lcom/datadog/android/rum/configuration/RumViewEventWriteConfig;Lcom/datadog/android/rum/timeseries/TimeseriesConfiguration;)V +HSPLcom/datadog/android/rum/internal/RumFeature$Configuration;->copy$default(Lcom/datadog/android/rum/internal/RumFeature$Configuration;Ljava/lang/String;FFFZLjava/util/List;Lcom/datadog/android/rum/tracking/InteractionPredicate;Lcom/datadog/android/rum/tracking/ViewTrackingStrategy;Lcom/datadog/android/rum/tracking/TrackingStrategy;Lcom/datadog/android/rum/event/ViewEventMapper;Lcom/datadog/android/event/EventMapper;Lcom/datadog/android/event/EventMapper;Lcom/datadog/android/event/EventMapper;Lcom/datadog/android/event/EventMapper;Lcom/datadog/android/event/EventMapper;Lcom/datadog/android/event/EventMapper;Lcom/datadog/android/event/EventMapper;ZZZLcom/datadog/android/rum/configuration/VitalsUpdateFrequency;Lcom/datadog/android/rum/RumSessionListener;Lcom/datadog/android/rum/metric/networksettled/InitialResourceIdentifier;Lcom/datadog/android/rum/metric/interactiontonextview/LastInteractionIdentifier;Lcom/datadog/android/rum/configuration/SlowFramesConfiguration;Lcom/datadog/android/rum/tracking/ActionTrackingStrategy;Ljava/util/Map;ZLcom/datadog/android/rum/RumSessionType;ZZLcom/datadog/android/rum/internal/instrumentation/insights/InsightsCollector;Lcom/datadog/android/rum/startup/AppStartupActivityPredicate;Lcom/datadog/android/rum/configuration/RumViewEventWriteConfig;Lcom/datadog/android/rum/timeseries/TimeseriesConfiguration;IILjava/lang/Object;)Lcom/datadog/android/rum/internal/RumFeature$Configuration; +HSPLcom/datadog/android/rum/internal/RumFeature$Configuration;->copy(Ljava/lang/String;FFFZLjava/util/List;Lcom/datadog/android/rum/tracking/InteractionPredicate;Lcom/datadog/android/rum/tracking/ViewTrackingStrategy;Lcom/datadog/android/rum/tracking/TrackingStrategy;Lcom/datadog/android/rum/event/ViewEventMapper;Lcom/datadog/android/event/EventMapper;Lcom/datadog/android/event/EventMapper;Lcom/datadog/android/event/EventMapper;Lcom/datadog/android/event/EventMapper;Lcom/datadog/android/event/EventMapper;Lcom/datadog/android/event/EventMapper;Lcom/datadog/android/event/EventMapper;ZZZLcom/datadog/android/rum/configuration/VitalsUpdateFrequency;Lcom/datadog/android/rum/RumSessionListener;Lcom/datadog/android/rum/metric/networksettled/InitialResourceIdentifier;Lcom/datadog/android/rum/metric/interactiontonextview/LastInteractionIdentifier;Lcom/datadog/android/rum/configuration/SlowFramesConfiguration;Lcom/datadog/android/rum/tracking/ActionTrackingStrategy;Ljava/util/Map;ZLcom/datadog/android/rum/RumSessionType;ZZLcom/datadog/android/rum/internal/instrumentation/insights/InsightsCollector;Lcom/datadog/android/rum/startup/AppStartupActivityPredicate;Lcom/datadog/android/rum/configuration/RumViewEventWriteConfig;Lcom/datadog/android/rum/timeseries/TimeseriesConfiguration;)Lcom/datadog/android/rum/internal/RumFeature$Configuration; HSPLcom/datadog/android/rum/internal/RumFeature$Configuration;->getActionEventMapper()Lcom/datadog/android/event/EventMapper; HSPLcom/datadog/android/rum/internal/RumFeature$Configuration;->getAdditionalConfig()Ljava/util/Map; HSPLcom/datadog/android/rum/internal/RumFeature$Configuration;->getAppStartupActivityPredicate()Lcom/datadog/android/rum/startup/AppStartupActivityPredicate; @@ -20607,6 +20690,7 @@ HSPLcom/datadog/android/rum/internal/RumFeature$Configuration;->getLongTaskEvent HSPLcom/datadog/android/rum/internal/RumFeature$Configuration;->getLongTaskTrackingStrategy()Lcom/datadog/android/rum/tracking/TrackingStrategy; HSPLcom/datadog/android/rum/internal/RumFeature$Configuration;->getResourceEventMapper()Lcom/datadog/android/event/EventMapper; HSPLcom/datadog/android/rum/internal/RumFeature$Configuration;->getRumSessionTypeOverride()Lcom/datadog/android/rum/RumSessionType; +HSPLcom/datadog/android/rum/internal/RumFeature$Configuration;->getRumViewEventWriteConfig()Lcom/datadog/android/rum/configuration/RumViewEventWriteConfig; HSPLcom/datadog/android/rum/internal/RumFeature$Configuration;->getSampleRate()F HSPLcom/datadog/android/rum/internal/RumFeature$Configuration;->getSessionListener()Lcom/datadog/android/rum/RumSessionListener; HSPLcom/datadog/android/rum/internal/RumFeature$Configuration;->getSlowFramesConfiguration()Lcom/datadog/android/rum/configuration/SlowFramesConfiguration; @@ -20619,22 +20703,13 @@ HSPLcom/datadog/android/rum/internal/RumFeature$Configuration;->getTrackAnonymou HSPLcom/datadog/android/rum/internal/RumFeature$Configuration;->getTrackFrustrations()Z HSPLcom/datadog/android/rum/internal/RumFeature$Configuration;->getTrackNonFatalAnrs()Z HSPLcom/datadog/android/rum/internal/RumFeature$Configuration;->getUserActionTracking()Z -HSPLcom/datadog/android/rum/internal/RumFeature$Configuration;->getViewEventMapper()Lcom/datadog/android/event/EventMapper; +HSPLcom/datadog/android/rum/internal/RumFeature$Configuration;->getViewEventMapper()Lcom/datadog/android/rum/event/ViewEventMapper; HSPLcom/datadog/android/rum/internal/RumFeature$Configuration;->getViewTrackingStrategy()Lcom/datadog/android/rum/tracking/ViewTrackingStrategy; HSPLcom/datadog/android/rum/internal/RumFeature$Configuration;->getVitalAppLaunchEventMapper()Lcom/datadog/android/event/EventMapper; HSPLcom/datadog/android/rum/internal/RumFeature$Configuration;->getVitalOperationStepEventMapper()Lcom/datadog/android/event/EventMapper; HSPLcom/datadog/android/rum/internal/RumFeature$Configuration;->getVitalsMonitorUpdateFrequency()Lcom/datadog/android/rum/configuration/VitalsUpdateFrequency; Lcom/datadog/android/rum/internal/RumFeature$initRumAppStartupDetector$1; HSPLcom/datadog/android/rum/internal/RumFeature$initRumAppStartupDetector$1;->(Lcom/datadog/android/rum/internal/RumFeature;)V -Lcom/datadog/android/rum/internal/RumFeature$initializeSlowFrameListener$1; -HSPLcom/datadog/android/rum/internal/RumFeature$initializeSlowFrameListener$1;->()V -HSPLcom/datadog/android/rum/internal/RumFeature$initializeSlowFrameListener$1;->()V -Lcom/datadog/android/rum/internal/RumFeature$lateCrashEventHandler$2; -HSPLcom/datadog/android/rum/internal/RumFeature$lateCrashEventHandler$2;->(Lcom/datadog/android/rum/internal/RumFeature;)V -Lcom/datadog/android/rum/internal/RumFeature$requestFactory$2; -HSPLcom/datadog/android/rum/internal/RumFeature$requestFactory$2;->(Lcom/datadog/android/rum/internal/RumFeature;)V -HSPLcom/datadog/android/rum/internal/RumFeature$requestFactory$2;->invoke()Lcom/datadog/android/rum/internal/net/RumRequestFactory; -HSPLcom/datadog/android/rum/internal/RumFeature$requestFactory$2;->invoke()Ljava/lang/Object; Lcom/datadog/android/rum/internal/anr/ANRDetectorRunnable; HSPLcom/datadog/android/rum/internal/anr/ANRDetectorRunnable;->()V HSPLcom/datadog/android/rum/internal/anr/ANRDetectorRunnable;->(Lcom/datadog/android/api/feature/FeatureSdkCore;Landroid/os/Handler;JJ)V @@ -20650,6 +20725,22 @@ Lcom/datadog/android/rum/internal/domain/FrameMetricsData; HSPLcom/datadog/android/rum/internal/domain/FrameMetricsData;->()V HSPLcom/datadog/android/rum/internal/domain/FrameMetricsData;->(IJJJJJJJJJZJJJJD)V HSPLcom/datadog/android/rum/internal/domain/FrameMetricsData;->(IJJJJJJJJJZJJJJDILkotlin/jvm/internal/DefaultConstructorMarker;)V +HSPLcom/datadog/android/rum/internal/domain/FrameMetricsData;->setAnimationDuration(J)V +HSPLcom/datadog/android/rum/internal/domain/FrameMetricsData;->setCommandIssueDuration(J)V +HSPLcom/datadog/android/rum/internal/domain/FrameMetricsData;->setDeadline(J)V +HSPLcom/datadog/android/rum/internal/domain/FrameMetricsData;->setDisplayRefreshRate(D)V +HSPLcom/datadog/android/rum/internal/domain/FrameMetricsData;->setDrawDuration(J)V +HSPLcom/datadog/android/rum/internal/domain/FrameMetricsData;->setDroppedFrames(I)V +HSPLcom/datadog/android/rum/internal/domain/FrameMetricsData;->setFirstDrawFrame(Z)V +HSPLcom/datadog/android/rum/internal/domain/FrameMetricsData;->setGpuDuration(J)V +HSPLcom/datadog/android/rum/internal/domain/FrameMetricsData;->setInputHandlingDuration(J)V +HSPLcom/datadog/android/rum/internal/domain/FrameMetricsData;->setIntendedVsyncTimestamp(J)V +HSPLcom/datadog/android/rum/internal/domain/FrameMetricsData;->setLayoutMeasureDuration(J)V +HSPLcom/datadog/android/rum/internal/domain/FrameMetricsData;->setSwapBuffersDuration(J)V +HSPLcom/datadog/android/rum/internal/domain/FrameMetricsData;->setSyncDuration(J)V +HSPLcom/datadog/android/rum/internal/domain/FrameMetricsData;->setTotalDuration(J)V +HSPLcom/datadog/android/rum/internal/domain/FrameMetricsData;->setUnknownDelayDuration(J)V +HSPLcom/datadog/android/rum/internal/domain/FrameMetricsData;->setVsyncTimestamp(J)V Lcom/datadog/android/rum/internal/domain/FrameMetricsData$Companion; HSPLcom/datadog/android/rum/internal/domain/FrameMetricsData$Companion;->()V HSPLcom/datadog/android/rum/internal/domain/FrameMetricsData$Companion;->(Lkotlin/jvm/internal/DefaultConstructorMarker;)V @@ -20674,14 +20765,28 @@ HSPLcom/datadog/android/rum/internal/domain/RumContext;->toMap()Ljava/util/Map; Lcom/datadog/android/rum/internal/domain/RumContext$Companion; HSPLcom/datadog/android/rum/internal/domain/RumContext$Companion;->()V HSPLcom/datadog/android/rum/internal/domain/RumContext$Companion;->(Lkotlin/jvm/internal/DefaultConstructorMarker;)V -PLcom/datadog/android/rum/internal/domain/RumContext$Companion;->fromFeatureContext(Ljava/util/Map;)Lcom/datadog/android/rum/internal/domain/RumContext; Lcom/datadog/android/rum/internal/domain/RumDataWriter; HSPLcom/datadog/android/rum/internal/domain/RumDataWriter;->()V -HSPLcom/datadog/android/rum/internal/domain/RumDataWriter;->(Lcom/datadog/android/core/persistence/Serializer;Lcom/datadog/android/core/persistence/Serializer;Lcom/datadog/android/core/InternalSdkCore;)V -HSPLcom/datadog/android/rum/internal/domain/RumDataWriter;->onDataWritten$dd_sdk_android_rum_release(Ljava/lang/Object;[B)V +HSPLcom/datadog/android/rum/internal/domain/RumDataWriter;->(Lcom/datadog/android/rum/internal/domain/event/RumEventMapper;Lcom/datadog/android/rum/internal/domain/event/RumEventSerializer;Lcom/datadog/android/core/persistence/Serializer;Lcom/datadog/android/core/InternalSdkCore;)V +HSPLcom/datadog/android/rum/internal/domain/RumDataWriter;->onDataWritten$dd_sdk_android_rum_release(Lcom/datadog/android/rum/model/ViewEvent;[B)V HSPLcom/datadog/android/rum/internal/domain/RumDataWriter;->onViewEventSubmitted$dd_sdk_android_rum_release(Lcom/datadog/android/rum/model/ViewEvent;)V HSPLcom/datadog/android/rum/internal/domain/RumDataWriter;->onViewEventWritten(Lcom/datadog/android/rum/model/ViewEvent;[B)V +HSPLcom/datadog/android/rum/internal/domain/RumDataWriter;->resolveHasAccessibility(Lcom/datadog/android/rum/model/ViewEvent;)Z +HSPLcom/datadog/android/rum/internal/domain/RumDataWriter;->serializeViewEvent(Lcom/datadog/android/rum/model/ViewEvent;)Lkotlin/Pair; +HSPLcom/datadog/android/rum/internal/domain/RumDataWriter;->telemetryContext(Ljava/lang/Object;)Lcom/datadog/android/internal/telemetry/TelemetryContext; HSPLcom/datadog/android/rum/internal/domain/RumDataWriter;->write(Lcom/datadog/android/api/storage/EventBatchWriter;Ljava/lang/Object;Lcom/datadog/android/api/storage/EventType;)Z +HSPLcom/datadog/android/rum/internal/domain/RumDataWriter;->writeBatchEvent$default(Lcom/datadog/android/rum/internal/domain/RumDataWriter;Lcom/datadog/android/api/storage/EventBatchWriter;Lcom/datadog/android/api/storage/RawBatchEvent;Lcom/datadog/android/api/storage/EventType;Lcom/datadog/android/internal/telemetry/TelemetryContext;Lkotlin/jvm/functions/Function0;ILjava/lang/Object;)Z +HSPLcom/datadog/android/rum/internal/domain/RumDataWriter;->writeBatchEvent$lambda$5()Lkotlin/Unit; +HSPLcom/datadog/android/rum/internal/domain/RumDataWriter;->writeBatchEvent(Lcom/datadog/android/api/storage/EventBatchWriter;Lcom/datadog/android/api/storage/RawBatchEvent;Lcom/datadog/android/api/storage/EventType;Lcom/datadog/android/internal/telemetry/TelemetryContext;Lkotlin/jvm/functions/Function0;)Z +HSPLcom/datadog/android/rum/internal/domain/RumDataWriter;->writeMappedViewEvent$lambda$0(Lcom/datadog/android/rum/internal/domain/RumDataWriter;Lcom/datadog/android/rum/model/ViewEvent;[B)Lkotlin/Unit; +HSPLcom/datadog/android/rum/internal/domain/RumDataWriter;->writeMappedViewEvent(Lcom/datadog/android/api/storage/EventBatchWriter;Lcom/datadog/android/rum/model/ViewEvent;Lcom/datadog/android/api/storage/EventType;)Z +HSPLcom/datadog/android/rum/internal/domain/RumDataWriter;->writeOtherEvent(Lcom/datadog/android/api/storage/EventBatchWriter;Ljava/lang/Object;Lcom/datadog/android/api/storage/EventType;)Z +Lcom/datadog/android/rum/internal/domain/RumDataWriter$$ExternalSyntheticLambda0; +HSPLcom/datadog/android/rum/internal/domain/RumDataWriter$$ExternalSyntheticLambda0;->()V +HSPLcom/datadog/android/rum/internal/domain/RumDataWriter$$ExternalSyntheticLambda0;->invoke()Ljava/lang/Object; +Lcom/datadog/android/rum/internal/domain/RumDataWriter$$ExternalSyntheticLambda1; +HSPLcom/datadog/android/rum/internal/domain/RumDataWriter$$ExternalSyntheticLambda1;->(Lcom/datadog/android/rum/internal/domain/RumDataWriter;Lcom/datadog/android/rum/model/ViewEvent;[B)V +HSPLcom/datadog/android/rum/internal/domain/RumDataWriter$$ExternalSyntheticLambda1;->invoke()Ljava/lang/Object; Lcom/datadog/android/rum/internal/domain/RumDataWriter$Companion; HSPLcom/datadog/android/rum/internal/domain/RumDataWriter$Companion;->()V HSPLcom/datadog/android/rum/internal/domain/RumDataWriter$Companion;->(Lkotlin/jvm/internal/DefaultConstructorMarker;)V @@ -20696,12 +20801,20 @@ Lcom/datadog/android/rum/internal/domain/Time$Companion; HSPLcom/datadog/android/rum/internal/domain/Time$Companion;->()V HSPLcom/datadog/android/rum/internal/domain/Time$Companion;->(Lkotlin/jvm/internal/DefaultConstructorMarker;)V HSPLcom/datadog/android/rum/internal/domain/Time$Companion;->now(Lcom/datadog/android/internal/time/TimeProvider;)Lcom/datadog/android/rum/internal/domain/Time; -Lcom/datadog/android/rum/internal/domain/accessibility/AccessibilitySnapshotManager; +Lcom/datadog/android/rum/internal/domain/accessibility/AccessibilityInfo; +HSPLcom/datadog/android/rum/internal/domain/accessibility/AccessibilityInfo;->(Ljava/lang/String;Ljava/lang/Boolean;Ljava/lang/Boolean;Ljava/lang/Boolean;Ljava/lang/Boolean;Ljava/lang/Boolean;Ljava/lang/Boolean;)V +HSPLcom/datadog/android/rum/internal/domain/accessibility/AccessibilityInfo;->(Ljava/lang/String;Ljava/lang/Boolean;Ljava/lang/Boolean;Ljava/lang/Boolean;Ljava/lang/Boolean;Ljava/lang/Boolean;Ljava/lang/Boolean;ILkotlin/jvm/internal/DefaultConstructorMarker;)V +HSPLcom/datadog/android/rum/internal/domain/accessibility/AccessibilityInfo;->getTextSize()Ljava/lang/String; +HSPLcom/datadog/android/rum/internal/domain/accessibility/AccessibilityInfo;->isClosedCaptioningEnabled()Ljava/lang/Boolean; +HSPLcom/datadog/android/rum/internal/domain/accessibility/AccessibilityInfo;->isColorInversionEnabled()Ljava/lang/Boolean; +HSPLcom/datadog/android/rum/internal/domain/accessibility/AccessibilityInfo;->isReducedAnimationsEnabled()Ljava/lang/Boolean; +HSPLcom/datadog/android/rum/internal/domain/accessibility/AccessibilityInfo;->isRtlEnabled()Ljava/lang/Boolean; +HSPLcom/datadog/android/rum/internal/domain/accessibility/AccessibilityInfo;->isScreenPinningEnabled()Ljava/lang/Boolean; +HSPLcom/datadog/android/rum/internal/domain/accessibility/AccessibilityInfo;->isScreenReaderEnabled()Ljava/lang/Boolean; Lcom/datadog/android/rum/internal/domain/accessibility/NoOpAccessibilityReader; HSPLcom/datadog/android/rum/internal/domain/accessibility/NoOpAccessibilityReader;->()V -Lcom/datadog/android/rum/internal/domain/accessibility/NoOpAccessibilitySnapshotManager; -HSPLcom/datadog/android/rum/internal/domain/accessibility/NoOpAccessibilitySnapshotManager;->()V -HSPLcom/datadog/android/rum/internal/domain/accessibility/NoOpAccessibilitySnapshotManager;->getIfChanged()Lcom/datadog/android/rum/internal/domain/accessibility/AccessibilityInfo; +HSPLcom/datadog/android/rum/internal/domain/accessibility/NoOpAccessibilityReader;->getState()Lcom/datadog/android/rum/internal/domain/InfoData; +HSPLcom/datadog/android/rum/internal/domain/accessibility/NoOpAccessibilityReader;->getState()Lcom/datadog/android/rum/internal/domain/accessibility/AccessibilityInfo; Lcom/datadog/android/rum/internal/domain/battery/BatteryInfo; HSPLcom/datadog/android/rum/internal/domain/battery/BatteryInfo;->(Ljava/lang/Float;Ljava/lang/Boolean;)V HSPLcom/datadog/android/rum/internal/domain/battery/BatteryInfo;->getBatteryLevel()Ljava/lang/Float; @@ -20772,6 +20885,7 @@ HSPLcom/datadog/android/rum/internal/domain/event/RumEventMeta$View;->getHasAcce HSPLcom/datadog/android/rum/internal/domain/event/RumEventMeta$View;->getType()Ljava/lang/String; HSPLcom/datadog/android/rum/internal/domain/event/RumEventMeta$View;->getViewId()Ljava/lang/String; HSPLcom/datadog/android/rum/internal/domain/event/RumEventMeta$View;->toJson()Lcom/google/gson/JsonObject; +Lcom/datadog/android/rum/internal/domain/event/RumEventMeta$ViewUpdate; Lcom/datadog/android/rum/internal/domain/event/RumEventMetaDeserializer; HSPLcom/datadog/android/rum/internal/domain/event/RumEventMetaDeserializer;->()V HSPLcom/datadog/android/rum/internal/domain/event/RumEventMetaDeserializer;->(Lcom/datadog/android/api/InternalLogger;)V @@ -20800,15 +20914,20 @@ HSPLcom/datadog/android/rum/internal/domain/event/RumEventSerializer$Companion;- Lcom/datadog/android/rum/internal/domain/event/RumViewEventFilter; HSPLcom/datadog/android/rum/internal/domain/event/RumViewEventFilter;->(Lcom/datadog/android/core/internal/persistence/Deserializer;)V HSPLcom/datadog/android/rum/internal/domain/event/RumViewEventFilter;->filterOutRedundantViewEvents(Ljava/util/List;)Ljava/util/List; +Lcom/datadog/android/rum/internal/domain/scope/DiffThenFullView; +Lcom/datadog/android/rum/internal/domain/scope/MappedViewEvent; +HSPLcom/datadog/android/rum/internal/domain/scope/MappedViewEvent;->(Lcom/datadog/android/rum/model/ViewEvent;)V +HSPLcom/datadog/android/rum/internal/domain/scope/MappedViewEvent;->getViewEvent()Lcom/datadog/android/rum/model/ViewEvent; Lcom/datadog/android/rum/internal/domain/scope/RumActionScope; Lcom/datadog/android/rum/internal/domain/scope/RumApplicationScope; HSPLcom/datadog/android/rum/internal/domain/scope/RumApplicationScope;->()V -HSPLcom/datadog/android/rum/internal/domain/scope/RumApplicationScope;->(Ljava/lang/String;Lcom/datadog/android/core/InternalSdkCore;Lcom/datadog/android/core/sampling/Sampler;ZZLcom/datadog/android/core/internal/net/FirstPartyHostHeaderTypeResolver;Lcom/datadog/android/rum/internal/vitals/VitalMonitor;Lcom/datadog/android/rum/internal/vitals/VitalMonitor;Lcom/datadog/android/rum/internal/vitals/VitalMonitor;Lcom/datadog/android/rum/internal/metric/SessionMetricDispatcher;Lcom/datadog/android/rum/RumSessionListener;Lcom/datadog/android/rum/metric/networksettled/InitialResourceIdentifier;Lcom/datadog/android/rum/metric/interactiontonextview/LastInteractionIdentifier;Lcom/datadog/android/rum/internal/metric/slowframes/SlowFramesListener;Lcom/datadog/android/rum/RumSessionType;Lcom/datadog/android/rum/internal/domain/accessibility/AccessibilitySnapshotManager;Lcom/datadog/android/rum/internal/domain/InfoProvider;Lcom/datadog/android/rum/internal/domain/InfoProvider;Lkotlin/jvm/functions/Function0;Lcom/datadog/android/rum/internal/instrumentation/insights/InsightsCollector;Lcom/datadog/android/internal/heatmaps/HeatmapIdentifierRegistry;Lcom/datadog/android/rum/internal/timeseries/TimeseriesCollector$Factory;)V +HSPLcom/datadog/android/rum/internal/domain/scope/RumApplicationScope;->(Ljava/lang/String;Lcom/datadog/android/core/InternalSdkCore;Lcom/datadog/android/core/sampling/Sampler;ZZLcom/datadog/android/core/internal/net/FirstPartyHostHeaderTypeResolver;Lcom/datadog/android/rum/internal/vitals/VitalMonitor;Lcom/datadog/android/rum/internal/vitals/VitalMonitor;Lcom/datadog/android/rum/internal/vitals/VitalMonitor;Lcom/datadog/android/rum/internal/metric/SessionMetricDispatcher;Lcom/datadog/android/rum/RumSessionListener;Lcom/datadog/android/rum/metric/networksettled/InitialResourceIdentifier;Lcom/datadog/android/rum/metric/interactiontonextview/LastInteractionIdentifier;Lcom/datadog/android/rum/internal/metric/slowframes/SlowFramesListener;Lcom/datadog/android/rum/RumSessionType;Lcom/datadog/android/rum/internal/domain/InfoProvider;Lcom/datadog/android/rum/internal/domain/InfoProvider;Lcom/datadog/android/rum/internal/domain/InfoProvider;Lkotlin/jvm/functions/Function0;Lcom/datadog/android/rum/internal/instrumentation/insights/InsightsCollector;Lcom/datadog/android/rum/event/ViewEventMapper;Lcom/datadog/android/rum/configuration/RumViewEventWriteConfig;Lcom/datadog/android/internal/heatmaps/HeatmapIdentifierRegistry;Lcom/datadog/android/rum/internal/timeseries/TimeseriesCollector$Factory;)V HSPLcom/datadog/android/rum/internal/domain/scope/RumApplicationScope;->delegateToChildren(Lcom/datadog/android/rum/internal/domain/scope/RumRawEvent;Lcom/datadog/android/api/context/DatadogContext;Lkotlin/jvm/functions/Function1;Lcom/datadog/android/api/storage/DataWriter;)V HSPLcom/datadog/android/rum/internal/domain/scope/RumApplicationScope;->getActiveSession()Lcom/datadog/android/rum/internal/domain/scope/RumSessionScope; HSPLcom/datadog/android/rum/internal/domain/scope/RumApplicationScope;->getCustomAttributes()Ljava/util/Map; HSPLcom/datadog/android/rum/internal/domain/scope/RumApplicationScope;->getRumContext()Lcom/datadog/android/rum/internal/domain/RumContext; HSPLcom/datadog/android/rum/internal/domain/scope/RumApplicationScope;->handleEvent(Lcom/datadog/android/rum/internal/domain/scope/RumRawEvent;Lcom/datadog/android/api/context/DatadogContext;Lkotlin/jvm/functions/Function1;Lcom/datadog/android/api/storage/DataWriter;)Lcom/datadog/android/rum/internal/domain/scope/RumScope; +HSPLcom/datadog/android/rum/internal/domain/scope/RumApplicationScope;->onViewChanged(Lcom/datadog/android/rum/internal/domain/scope/RumViewInfo;)V HSPLcom/datadog/android/rum/internal/domain/scope/RumApplicationScope;->sendApplicationStartEvent(Lcom/datadog/android/rum/internal/domain/Time;Lcom/datadog/android/api/context/DatadogContext;Lkotlin/jvm/functions/Function1;Lcom/datadog/android/api/storage/DataWriter;)V Lcom/datadog/android/rum/internal/domain/scope/RumApplicationScope$Companion; HSPLcom/datadog/android/rum/internal/domain/scope/RumApplicationScope$Companion;->()V @@ -20871,6 +20990,10 @@ Lcom/datadog/android/rum/internal/domain/scope/RumRawEvent$StartAction; Lcom/datadog/android/rum/internal/domain/scope/RumRawEvent$StartOperation; Lcom/datadog/android/rum/internal/domain/scope/RumRawEvent$StartResource; Lcom/datadog/android/rum/internal/domain/scope/RumRawEvent$StartView; +HSPLcom/datadog/android/rum/internal/domain/scope/RumRawEvent$StartView;->(Lcom/datadog/android/rum/internal/domain/scope/RumScopeKey;Ljava/util/Map;Lcom/datadog/android/rum/internal/domain/Time;)V +HSPLcom/datadog/android/rum/internal/domain/scope/RumRawEvent$StartView;->getAttributes()Ljava/util/Map; +HSPLcom/datadog/android/rum/internal/domain/scope/RumRawEvent$StartView;->getEventTime()Lcom/datadog/android/rum/internal/domain/Time; +HSPLcom/datadog/android/rum/internal/domain/scope/RumRawEvent$StartView;->getKey()Lcom/datadog/android/rum/internal/domain/scope/RumScopeKey; Lcom/datadog/android/rum/internal/domain/scope/RumRawEvent$StopOperation; Lcom/datadog/android/rum/internal/domain/scope/RumRawEvent$StopSession; Lcom/datadog/android/rum/internal/domain/scope/RumRawEvent$StopView; @@ -20891,10 +21014,13 @@ HSPLcom/datadog/android/rum/internal/domain/scope/RumScopeKey;->getUrl()Ljava/la Lcom/datadog/android/rum/internal/domain/scope/RumScopeKey$Companion; HSPLcom/datadog/android/rum/internal/domain/scope/RumScopeKey$Companion;->()V HSPLcom/datadog/android/rum/internal/domain/scope/RumScopeKey$Companion;->(Lkotlin/jvm/internal/DefaultConstructorMarker;)V +HSPLcom/datadog/android/rum/internal/domain/scope/RumScopeKey$Companion;->from(Ljava/lang/Object;Ljava/lang/String;)Lcom/datadog/android/rum/internal/domain/scope/RumScopeKey; +HSPLcom/datadog/android/rum/internal/domain/scope/RumScopeKey$Companion;->resolveComponent(Ljava/lang/Object;)Landroid/content/ComponentName; +HSPLcom/datadog/android/rum/internal/domain/scope/RumScopeKey$Companion;->resolveComponentUrl(Landroid/content/ComponentName;)Ljava/lang/String; Lcom/datadog/android/rum/internal/domain/scope/RumSessionScope; HSPLcom/datadog/android/rum/internal/domain/scope/RumSessionScope;->()V -HSPLcom/datadog/android/rum/internal/domain/scope/RumSessionScope;->(Lcom/datadog/android/rum/internal/domain/scope/RumScope;Lcom/datadog/android/core/InternalSdkCore;Lcom/datadog/android/rum/internal/metric/SessionMetricDispatcher;Lcom/datadog/android/core/sampling/Sampler;ZZLcom/datadog/android/rum/internal/domain/scope/RumViewChangedListener;Lcom/datadog/android/core/internal/net/FirstPartyHostHeaderTypeResolver;Lcom/datadog/android/rum/internal/vitals/VitalMonitor;Lcom/datadog/android/rum/internal/vitals/VitalMonitor;Lcom/datadog/android/rum/internal/vitals/VitalMonitor;Lcom/datadog/android/rum/RumSessionListener;ZLcom/datadog/android/rum/metric/networksettled/InitialResourceIdentifier;Lcom/datadog/android/rum/metric/interactiontonextview/LastInteractionIdentifier;Lcom/datadog/android/rum/internal/metric/slowframes/SlowFramesListener;Lcom/datadog/android/rum/internal/domain/accessibility/AccessibilitySnapshotManager;Lcom/datadog/android/rum/internal/domain/InfoProvider;Lcom/datadog/android/rum/internal/domain/InfoProvider;JJLcom/datadog/android/rum/RumSessionType;Lkotlin/jvm/functions/Function0;Lcom/datadog/android/rum/internal/instrumentation/insights/InsightsCollector;Lcom/datadog/android/internal/heatmaps/HeatmapIdentifierRegistry;Lcom/datadog/android/rum/internal/timeseries/TimeseriesCollector$Factory;)V -HSPLcom/datadog/android/rum/internal/domain/scope/RumSessionScope;->(Lcom/datadog/android/rum/internal/domain/scope/RumScope;Lcom/datadog/android/core/InternalSdkCore;Lcom/datadog/android/rum/internal/metric/SessionMetricDispatcher;Lcom/datadog/android/core/sampling/Sampler;ZZLcom/datadog/android/rum/internal/domain/scope/RumViewChangedListener;Lcom/datadog/android/core/internal/net/FirstPartyHostHeaderTypeResolver;Lcom/datadog/android/rum/internal/vitals/VitalMonitor;Lcom/datadog/android/rum/internal/vitals/VitalMonitor;Lcom/datadog/android/rum/internal/vitals/VitalMonitor;Lcom/datadog/android/rum/RumSessionListener;ZLcom/datadog/android/rum/metric/networksettled/InitialResourceIdentifier;Lcom/datadog/android/rum/metric/interactiontonextview/LastInteractionIdentifier;Lcom/datadog/android/rum/internal/metric/slowframes/SlowFramesListener;Lcom/datadog/android/rum/internal/domain/accessibility/AccessibilitySnapshotManager;Lcom/datadog/android/rum/internal/domain/InfoProvider;Lcom/datadog/android/rum/internal/domain/InfoProvider;JJLcom/datadog/android/rum/RumSessionType;Lkotlin/jvm/functions/Function0;Lcom/datadog/android/rum/internal/instrumentation/insights/InsightsCollector;Lcom/datadog/android/internal/heatmaps/HeatmapIdentifierRegistry;Lcom/datadog/android/rum/internal/timeseries/TimeseriesCollector$Factory;ILkotlin/jvm/internal/DefaultConstructorMarker;)V +HSPLcom/datadog/android/rum/internal/domain/scope/RumSessionScope;->(Lcom/datadog/android/rum/internal/domain/scope/RumScope;Lcom/datadog/android/core/InternalSdkCore;Lcom/datadog/android/rum/internal/metric/SessionMetricDispatcher;Lcom/datadog/android/core/sampling/Sampler;ZZLcom/datadog/android/rum/internal/domain/scope/RumViewChangedListener;Lcom/datadog/android/core/internal/net/FirstPartyHostHeaderTypeResolver;Lcom/datadog/android/rum/internal/vitals/VitalMonitor;Lcom/datadog/android/rum/internal/vitals/VitalMonitor;Lcom/datadog/android/rum/internal/vitals/VitalMonitor;Lcom/datadog/android/rum/RumSessionListener;ZLcom/datadog/android/rum/metric/networksettled/InitialResourceIdentifier;Lcom/datadog/android/rum/metric/interactiontonextview/LastInteractionIdentifier;Lcom/datadog/android/rum/internal/metric/slowframes/SlowFramesListener;Lcom/datadog/android/rum/internal/domain/InfoProvider;Lcom/datadog/android/rum/internal/domain/InfoProvider;Lcom/datadog/android/rum/internal/domain/InfoProvider;JJLcom/datadog/android/rum/RumSessionType;Lkotlin/jvm/functions/Function0;Lcom/datadog/android/rum/internal/instrumentation/insights/InsightsCollector;Lcom/datadog/android/rum/event/ViewEventMapper;Lcom/datadog/android/rum/configuration/RumViewEventWriteConfig;Lcom/datadog/android/internal/heatmaps/HeatmapIdentifierRegistry;Lcom/datadog/android/rum/internal/timeseries/TimeseriesCollector$Factory;)V +HSPLcom/datadog/android/rum/internal/domain/scope/RumSessionScope;->(Lcom/datadog/android/rum/internal/domain/scope/RumScope;Lcom/datadog/android/core/InternalSdkCore;Lcom/datadog/android/rum/internal/metric/SessionMetricDispatcher;Lcom/datadog/android/core/sampling/Sampler;ZZLcom/datadog/android/rum/internal/domain/scope/RumViewChangedListener;Lcom/datadog/android/core/internal/net/FirstPartyHostHeaderTypeResolver;Lcom/datadog/android/rum/internal/vitals/VitalMonitor;Lcom/datadog/android/rum/internal/vitals/VitalMonitor;Lcom/datadog/android/rum/internal/vitals/VitalMonitor;Lcom/datadog/android/rum/RumSessionListener;ZLcom/datadog/android/rum/metric/networksettled/InitialResourceIdentifier;Lcom/datadog/android/rum/metric/interactiontonextview/LastInteractionIdentifier;Lcom/datadog/android/rum/internal/metric/slowframes/SlowFramesListener;Lcom/datadog/android/rum/internal/domain/InfoProvider;Lcom/datadog/android/rum/internal/domain/InfoProvider;Lcom/datadog/android/rum/internal/domain/InfoProvider;JJLcom/datadog/android/rum/RumSessionType;Lkotlin/jvm/functions/Function0;Lcom/datadog/android/rum/internal/instrumentation/insights/InsightsCollector;Lcom/datadog/android/rum/event/ViewEventMapper;Lcom/datadog/android/rum/configuration/RumViewEventWriteConfig;Lcom/datadog/android/internal/heatmaps/HeatmapIdentifierRegistry;Lcom/datadog/android/rum/internal/timeseries/TimeseriesCollector$Factory;ILkotlin/jvm/internal/DefaultConstructorMarker;)V HSPLcom/datadog/android/rum/internal/domain/scope/RumSessionScope;->getActiveRumContext()Lcom/datadog/android/rum/internal/domain/RumContext; HSPLcom/datadog/android/rum/internal/domain/scope/RumSessionScope;->getActiveView$dd_sdk_android_rum_release()Lcom/datadog/android/rum/internal/domain/scope/RumViewScope; HSPLcom/datadog/android/rum/internal/domain/scope/RumSessionScope;->getCustomAttributes()Ljava/util/Map; @@ -20921,22 +21047,44 @@ HSPLcom/datadog/android/rum/internal/domain/scope/RumSessionScope$StartReason;-> Lcom/datadog/android/rum/internal/domain/scope/RumSessionScope$StartReason$Companion; HSPLcom/datadog/android/rum/internal/domain/scope/RumSessionScope$StartReason$Companion;->()V HSPLcom/datadog/android/rum/internal/domain/scope/RumSessionScope$StartReason$Companion;->(Lkotlin/jvm/internal/DefaultConstructorMarker;)V -PLcom/datadog/android/rum/internal/domain/scope/RumSessionScope$StartReason$Companion;->fromString(Ljava/lang/String;)Lcom/datadog/android/rum/internal/domain/scope/RumSessionScope$StartReason; Lcom/datadog/android/rum/internal/domain/scope/RumSessionScope$State; HSPLcom/datadog/android/rum/internal/domain/scope/RumSessionScope$State;->$values()[Lcom/datadog/android/rum/internal/domain/scope/RumSessionScope$State; HSPLcom/datadog/android/rum/internal/domain/scope/RumSessionScope$State;->()V HSPLcom/datadog/android/rum/internal/domain/scope/RumSessionScope$State;->(Ljava/lang/String;ILjava/lang/String;)V HSPLcom/datadog/android/rum/internal/domain/scope/RumSessionScope$State;->getAsString()Ljava/lang/String; -PLcom/datadog/android/rum/internal/domain/scope/RumSessionScope$State;->values()[Lcom/datadog/android/rum/internal/domain/scope/RumSessionScope$State; Lcom/datadog/android/rum/internal/domain/scope/RumSessionScope$State$Companion; HSPLcom/datadog/android/rum/internal/domain/scope/RumSessionScope$State$Companion;->()V HSPLcom/datadog/android/rum/internal/domain/scope/RumSessionScope$State$Companion;->(Lkotlin/jvm/internal/DefaultConstructorMarker;)V -PLcom/datadog/android/rum/internal/domain/scope/RumSessionScope$State$Companion;->fromString(Ljava/lang/String;)Lcom/datadog/android/rum/internal/domain/scope/RumSessionScope$State; Lcom/datadog/android/rum/internal/domain/scope/RumViewChangedListener; +Lcom/datadog/android/rum/internal/domain/scope/RumViewEventWriter; +HSPLcom/datadog/android/rum/internal/domain/scope/RumViewEventWriter;->()V +Lcom/datadog/android/rum/internal/domain/scope/RumViewEventWriter$Companion; +HSPLcom/datadog/android/rum/internal/domain/scope/RumViewEventWriter$Companion;->()V +HSPLcom/datadog/android/rum/internal/domain/scope/RumViewEventWriter$Companion;->()V +HSPLcom/datadog/android/rum/internal/domain/scope/RumViewEventWriter$Companion;->create(Lcom/datadog/android/rum/configuration/RumViewEventWriteConfig;Lcom/datadog/android/rum/event/ViewEventMapper;Lcom/datadog/android/core/InternalSdkCore;)Lcom/datadog/android/rum/internal/domain/scope/RumViewEventWriter; +Lcom/datadog/android/rum/internal/domain/scope/RumViewEventWriterImpl; +HSPLcom/datadog/android/rum/internal/domain/scope/RumViewEventWriterImpl;->()V +HSPLcom/datadog/android/rum/internal/domain/scope/RumViewEventWriterImpl;->(Lcom/datadog/android/rum/configuration/RumViewEventWriteConfig;Lcom/datadog/android/rum/event/ViewEventMapper;Lcom/datadog/android/core/InternalSdkCore;)V +HSPLcom/datadog/android/rum/internal/domain/scope/RumViewEventWriterImpl;->writeViewEvent$lambda$2(Lcom/datadog/android/rum/internal/domain/scope/RumViewEventWriterImpl;Lcom/datadog/android/rum/model/ViewEvent;Lkotlin/jvm/internal/Ref$ObjectRef;)Ljava/lang/Object; +HSPLcom/datadog/android/rum/internal/domain/scope/RumViewEventWriterImpl;->writeViewEvent$lambda$4(Lkotlin/jvm/internal/Ref$ObjectRef;Lcom/datadog/android/rum/internal/domain/scope/RumViewEventWriterImpl;Lcom/datadog/android/rum/internal/monitor/AdvancedRumMonitor;)Lkotlin/Unit; +HSPLcom/datadog/android/rum/internal/domain/scope/RumViewEventWriterImpl;->writeViewEvent(Lcom/datadog/android/rum/model/ViewEvent;Lcom/datadog/android/api/context/DatadogContext;Lkotlin/jvm/functions/Function1;Lcom/datadog/android/api/storage/DataWriter;Lcom/datadog/android/api/storage/EventType;)V +Lcom/datadog/android/rum/internal/domain/scope/RumViewEventWriterImpl$$ExternalSyntheticLambda0; +HSPLcom/datadog/android/rum/internal/domain/scope/RumViewEventWriterImpl$$ExternalSyntheticLambda0;->(Lcom/datadog/android/rum/internal/domain/scope/RumViewEventWriterImpl;Lcom/datadog/android/rum/model/ViewEvent;Lkotlin/jvm/internal/Ref$ObjectRef;)V +HSPLcom/datadog/android/rum/internal/domain/scope/RumViewEventWriterImpl$$ExternalSyntheticLambda0;->invoke()Ljava/lang/Object; +Lcom/datadog/android/rum/internal/domain/scope/RumViewEventWriterImpl$$ExternalSyntheticLambda1; +HSPLcom/datadog/android/rum/internal/domain/scope/RumViewEventWriterImpl$$ExternalSyntheticLambda1;->(Lkotlin/jvm/internal/Ref$ObjectRef;Lcom/datadog/android/rum/internal/domain/scope/RumViewEventWriterImpl;)V +HSPLcom/datadog/android/rum/internal/domain/scope/RumViewEventWriterImpl$$ExternalSyntheticLambda1;->invoke(Ljava/lang/Object;)Ljava/lang/Object; +Lcom/datadog/android/rum/internal/domain/scope/RumViewEventWriterImpl$Companion; +HSPLcom/datadog/android/rum/internal/domain/scope/RumViewEventWriterImpl$Companion;->()V +HSPLcom/datadog/android/rum/internal/domain/scope/RumViewEventWriterImpl$Companion;->(Lkotlin/jvm/internal/DefaultConstructorMarker;)V +Lcom/datadog/android/rum/internal/domain/scope/RumViewInfo; +HSPLcom/datadog/android/rum/internal/domain/scope/RumViewInfo;->(Lcom/datadog/android/rum/internal/domain/scope/RumScopeKey;Ljava/util/Map;Z)V +HSPLcom/datadog/android/rum/internal/domain/scope/RumViewInfo;->isActive()Z Lcom/datadog/android/rum/internal/domain/scope/RumViewManagerScope; HSPLcom/datadog/android/rum/internal/domain/scope/RumViewManagerScope;->()V -HSPLcom/datadog/android/rum/internal/domain/scope/RumViewManagerScope;->(Lcom/datadog/android/rum/internal/domain/scope/RumScope;Lcom/datadog/android/core/InternalSdkCore;Lcom/datadog/android/rum/internal/metric/SessionMetricDispatcher;ZZLcom/datadog/android/rum/internal/domain/scope/RumViewChangedListener;Lcom/datadog/android/core/internal/net/FirstPartyHostHeaderTypeResolver;Lcom/datadog/android/rum/internal/vitals/VitalMonitor;Lcom/datadog/android/rum/internal/vitals/VitalMonitor;Lcom/datadog/android/rum/internal/vitals/VitalMonitor;ZFLcom/datadog/android/rum/metric/networksettled/InitialResourceIdentifier;Lcom/datadog/android/rum/internal/metric/slowframes/SlowFramesListener;Lcom/datadog/android/rum/metric/interactiontonextview/LastInteractionIdentifier;Lcom/datadog/android/rum/RumSessionType;Lcom/datadog/android/rum/internal/domain/accessibility/AccessibilitySnapshotManager;Lcom/datadog/android/rum/internal/domain/InfoProvider;Lcom/datadog/android/rum/internal/domain/InfoProvider;Lcom/datadog/android/rum/internal/instrumentation/insights/InsightsCollector;Lcom/datadog/android/internal/heatmaps/HeatmapIdentifierRegistry;)V +HSPLcom/datadog/android/rum/internal/domain/scope/RumViewManagerScope;->(Lcom/datadog/android/rum/internal/domain/scope/RumScope;Lcom/datadog/android/core/InternalSdkCore;Lcom/datadog/android/rum/internal/metric/SessionMetricDispatcher;ZZLcom/datadog/android/rum/internal/domain/scope/RumViewChangedListener;Lcom/datadog/android/core/internal/net/FirstPartyHostHeaderTypeResolver;Lcom/datadog/android/rum/internal/vitals/VitalMonitor;Lcom/datadog/android/rum/internal/vitals/VitalMonitor;Lcom/datadog/android/rum/internal/vitals/VitalMonitor;ZFLcom/datadog/android/rum/metric/networksettled/InitialResourceIdentifier;Lcom/datadog/android/rum/internal/metric/slowframes/SlowFramesListener;Lcom/datadog/android/rum/metric/interactiontonextview/LastInteractionIdentifier;Lcom/datadog/android/rum/RumSessionType;Lcom/datadog/android/rum/internal/domain/InfoProvider;Lcom/datadog/android/rum/internal/domain/InfoProvider;Lcom/datadog/android/rum/internal/domain/InfoProvider;Lcom/datadog/android/rum/internal/instrumentation/insights/InsightsCollector;Lcom/datadog/android/rum/event/ViewEventMapper;Lcom/datadog/android/rum/configuration/RumViewEventWriteConfig;Lcom/datadog/android/internal/heatmaps/HeatmapIdentifierRegistry;)V HSPLcom/datadog/android/rum/internal/domain/scope/RumViewManagerScope;->access$getValidBackgroundEventTypes$cp()[Ljava/lang/Class; +HSPLcom/datadog/android/rum/internal/domain/scope/RumViewManagerScope;->createAppLaunchViewScope$lambda$16(Lcom/datadog/android/rum/internal/domain/scope/RumViewManagerScope;)Lcom/datadog/android/rum/internal/domain/scope/RumViewEventWriter; HSPLcom/datadog/android/rum/internal/domain/scope/RumViewManagerScope;->createAppLaunchViewScope(Lcom/datadog/android/rum/internal/domain/Time;)Lcom/datadog/android/rum/internal/domain/scope/RumViewScope; HSPLcom/datadog/android/rum/internal/domain/scope/RumViewManagerScope;->delegateToChildren(Lcom/datadog/android/rum/internal/domain/scope/RumRawEvent;Lcom/datadog/android/api/context/DatadogContext;Lkotlin/jvm/functions/Function1;Lcom/datadog/android/api/storage/DataWriter;)V HSPLcom/datadog/android/rum/internal/domain/scope/RumViewManagerScope;->getActiveView$dd_sdk_android_rum_release()Lcom/datadog/android/rum/internal/domain/scope/RumViewScope; @@ -20947,79 +21095,90 @@ HSPLcom/datadog/android/rum/internal/domain/scope/RumViewManagerScope;->handleEv HSPLcom/datadog/android/rum/internal/domain/scope/RumViewManagerScope;->isActive()Z HSPLcom/datadog/android/rum/internal/domain/scope/RumViewManagerScope;->isViewManagerComplete()Z HSPLcom/datadog/android/rum/internal/domain/scope/RumViewManagerScope;->renewViewScopes$dd_sdk_android_rum_release(Lcom/datadog/android/rum/internal/domain/Time;)V +HSPLcom/datadog/android/rum/internal/domain/scope/RumViewManagerScope;->sendViewUpdateToChildren(Lcom/datadog/android/rum/internal/domain/scope/RumRawEvent;Lcom/datadog/android/api/context/DatadogContext;Lkotlin/jvm/functions/Function1;Lcom/datadog/android/api/storage/DataWriter;)V HSPLcom/datadog/android/rum/internal/domain/scope/RumViewManagerScope;->startApplicationLaunchView(Lcom/datadog/android/rum/internal/domain/scope/RumRawEvent$ApplicationStarted;Lcom/datadog/android/api/context/DatadogContext;Lkotlin/jvm/functions/Function1;Lcom/datadog/android/api/storage/DataWriter;)V +HSPLcom/datadog/android/rum/internal/domain/scope/RumViewManagerScope;->startForegroundView(Lcom/datadog/android/rum/internal/domain/scope/RumRawEvent$StartView;)V +Lcom/datadog/android/rum/internal/domain/scope/RumViewManagerScope$$ExternalSyntheticLambda6; +HSPLcom/datadog/android/rum/internal/domain/scope/RumViewManagerScope$$ExternalSyntheticLambda6;->(Lcom/datadog/android/rum/internal/domain/scope/RumViewManagerScope;)V +HSPLcom/datadog/android/rum/internal/domain/scope/RumViewManagerScope$$ExternalSyntheticLambda6;->invoke()Ljava/lang/Object; Lcom/datadog/android/rum/internal/domain/scope/RumViewManagerScope$Companion; HSPLcom/datadog/android/rum/internal/domain/scope/RumViewManagerScope$Companion;->()V HSPLcom/datadog/android/rum/internal/domain/scope/RumViewManagerScope$Companion;->(Lkotlin/jvm/internal/DefaultConstructorMarker;)V HSPLcom/datadog/android/rum/internal/domain/scope/RumViewManagerScope$Companion;->getValidBackgroundEventTypes$dd_sdk_android_rum_release()[Ljava/lang/Class; Lcom/datadog/android/rum/internal/domain/scope/RumViewScope; HSPLcom/datadog/android/rum/internal/domain/scope/RumViewScope;->()V -HSPLcom/datadog/android/rum/internal/domain/scope/RumViewScope;->(Lcom/datadog/android/rum/internal/domain/scope/RumScope;Lcom/datadog/android/core/InternalSdkCore;Lcom/datadog/android/rum/internal/metric/SessionMetricDispatcher;Lcom/datadog/android/rum/internal/domain/scope/RumScopeKey;Lcom/datadog/android/rum/internal/domain/Time;Ljava/util/Map;Lcom/datadog/android/rum/internal/domain/scope/RumViewChangedListener;Lcom/datadog/android/core/internal/net/FirstPartyHostHeaderTypeResolver;Lcom/datadog/android/rum/internal/vitals/VitalMonitor;Lcom/datadog/android/rum/internal/vitals/VitalMonitor;Lcom/datadog/android/rum/internal/vitals/VitalMonitor;Lcom/datadog/android/rum/internal/FeaturesContextResolver;Lcom/datadog/android/rum/internal/domain/scope/RumViewType;ZFLcom/datadog/android/rum/internal/metric/interactiontonextview/InteractionToNextViewMetricResolver;Lcom/datadog/android/rum/internal/metric/networksettled/NetworkSettledMetricResolver;Lcom/datadog/android/rum/internal/metric/slowframes/SlowFramesListener;Lcom/datadog/android/rum/internal/metric/ViewMetricDispatcher;Lcom/datadog/android/rum/RumSessionType;Lcom/datadog/android/rum/internal/domain/accessibility/AccessibilitySnapshotManager;Lcom/datadog/android/rum/internal/domain/InfoProvider;Lcom/datadog/android/rum/internal/domain/InfoProvider;Lcom/datadog/android/rum/internal/instrumentation/insights/InsightsCollector;Lcom/datadog/android/internal/heatmaps/HeatmapIdentifierRegistry;)V -HSPLcom/datadog/android/rum/internal/domain/scope/RumViewScope;->(Lcom/datadog/android/rum/internal/domain/scope/RumScope;Lcom/datadog/android/core/InternalSdkCore;Lcom/datadog/android/rum/internal/metric/SessionMetricDispatcher;Lcom/datadog/android/rum/internal/domain/scope/RumScopeKey;Lcom/datadog/android/rum/internal/domain/Time;Ljava/util/Map;Lcom/datadog/android/rum/internal/domain/scope/RumViewChangedListener;Lcom/datadog/android/core/internal/net/FirstPartyHostHeaderTypeResolver;Lcom/datadog/android/rum/internal/vitals/VitalMonitor;Lcom/datadog/android/rum/internal/vitals/VitalMonitor;Lcom/datadog/android/rum/internal/vitals/VitalMonitor;Lcom/datadog/android/rum/internal/FeaturesContextResolver;Lcom/datadog/android/rum/internal/domain/scope/RumViewType;ZFLcom/datadog/android/rum/internal/metric/interactiontonextview/InteractionToNextViewMetricResolver;Lcom/datadog/android/rum/internal/metric/networksettled/NetworkSettledMetricResolver;Lcom/datadog/android/rum/internal/metric/slowframes/SlowFramesListener;Lcom/datadog/android/rum/internal/metric/ViewMetricDispatcher;Lcom/datadog/android/rum/RumSessionType;Lcom/datadog/android/rum/internal/domain/accessibility/AccessibilitySnapshotManager;Lcom/datadog/android/rum/internal/domain/InfoProvider;Lcom/datadog/android/rum/internal/domain/InfoProvider;Lcom/datadog/android/rum/internal/instrumentation/insights/InsightsCollector;Lcom/datadog/android/internal/heatmaps/HeatmapIdentifierRegistry;ILkotlin/jvm/internal/DefaultConstructorMarker;)V -HSPLcom/datadog/android/rum/internal/domain/scope/RumViewScope;->access$getONE_SECOND_NS$cp()J -HSPLcom/datadog/android/rum/internal/domain/scope/RumViewScope;->access$getRumSessionTypeOverride$p(Lcom/datadog/android/rum/internal/domain/scope/RumViewScope;)Lcom/datadog/android/rum/RumSessionType; -HSPLcom/datadog/android/rum/internal/domain/scope/RumViewScope;->access$getSdkCore$p(Lcom/datadog/android/rum/internal/domain/scope/RumViewScope;)Lcom/datadog/android/core/InternalSdkCore; -HSPLcom/datadog/android/rum/internal/domain/scope/RumViewScope;->access$getSessionEndedMetricDispatcher$p(Lcom/datadog/android/rum/internal/domain/scope/RumViewScope;)Lcom/datadog/android/rum/internal/metric/SessionMetricDispatcher; -HSPLcom/datadog/android/rum/internal/domain/scope/RumViewScope;->access$getSessionId$p(Lcom/datadog/android/rum/internal/domain/scope/RumViewScope;)Ljava/lang/String; -HSPLcom/datadog/android/rum/internal/domain/scope/RumViewScope;->access$resolveLongTaskProfilingStatus(Lcom/datadog/android/rum/internal/domain/scope/RumViewScope;Lcom/datadog/android/api/context/DatadogContext;)Lcom/datadog/android/rum/model/LongTaskEvent$Profiling; -HSPLcom/datadog/android/rum/internal/domain/scope/RumViewScope;->access$resolveViewProfilingStatus(Lcom/datadog/android/rum/internal/domain/scope/RumViewScope;Lcom/datadog/android/api/context/DatadogContext;)Lcom/datadog/android/rum/model/ViewEvent$Profiling; +HSPLcom/datadog/android/rum/internal/domain/scope/RumViewScope;->(Lcom/datadog/android/rum/internal/domain/scope/RumScope;Lcom/datadog/android/core/InternalSdkCore;Lcom/datadog/android/rum/internal/metric/SessionMetricDispatcher;Lcom/datadog/android/rum/internal/domain/scope/RumScopeKey;Lcom/datadog/android/rum/internal/domain/Time;Ljava/util/Map;Lcom/datadog/android/rum/internal/domain/scope/RumViewChangedListener;Lcom/datadog/android/core/internal/net/FirstPartyHostHeaderTypeResolver;Lcom/datadog/android/rum/internal/vitals/VitalMonitor;Lcom/datadog/android/rum/internal/vitals/VitalMonitor;Lcom/datadog/android/rum/internal/vitals/VitalMonitor;Lcom/datadog/android/rum/internal/FeaturesContextResolver;Lcom/datadog/android/rum/internal/domain/scope/RumViewType;ZFLcom/datadog/android/rum/internal/metric/interactiontonextview/InteractionToNextViewMetricResolver;Lcom/datadog/android/rum/internal/metric/networksettled/NetworkSettledMetricResolver;Lcom/datadog/android/rum/internal/metric/slowframes/SlowFramesListener;Lcom/datadog/android/rum/internal/metric/ViewMetricDispatcher;Lcom/datadog/android/rum/RumSessionType;Lcom/datadog/android/rum/internal/domain/InfoProvider;Lcom/datadog/android/rum/internal/domain/InfoProvider;Lcom/datadog/android/rum/internal/domain/InfoProvider;Lcom/datadog/android/rum/internal/instrumentation/insights/InsightsCollector;Lkotlin/jvm/functions/Function0;Lcom/datadog/android/internal/heatmaps/HeatmapIdentifierRegistry;)V +HSPLcom/datadog/android/rum/internal/domain/scope/RumViewScope;->(Lcom/datadog/android/rum/internal/domain/scope/RumScope;Lcom/datadog/android/core/InternalSdkCore;Lcom/datadog/android/rum/internal/metric/SessionMetricDispatcher;Lcom/datadog/android/rum/internal/domain/scope/RumScopeKey;Lcom/datadog/android/rum/internal/domain/Time;Ljava/util/Map;Lcom/datadog/android/rum/internal/domain/scope/RumViewChangedListener;Lcom/datadog/android/core/internal/net/FirstPartyHostHeaderTypeResolver;Lcom/datadog/android/rum/internal/vitals/VitalMonitor;Lcom/datadog/android/rum/internal/vitals/VitalMonitor;Lcom/datadog/android/rum/internal/vitals/VitalMonitor;Lcom/datadog/android/rum/internal/FeaturesContextResolver;Lcom/datadog/android/rum/internal/domain/scope/RumViewType;ZFLcom/datadog/android/rum/internal/metric/interactiontonextview/InteractionToNextViewMetricResolver;Lcom/datadog/android/rum/internal/metric/networksettled/NetworkSettledMetricResolver;Lcom/datadog/android/rum/internal/metric/slowframes/SlowFramesListener;Lcom/datadog/android/rum/internal/metric/ViewMetricDispatcher;Lcom/datadog/android/rum/RumSessionType;Lcom/datadog/android/rum/internal/domain/InfoProvider;Lcom/datadog/android/rum/internal/domain/InfoProvider;Lcom/datadog/android/rum/internal/domain/InfoProvider;Lcom/datadog/android/rum/internal/instrumentation/insights/InsightsCollector;Lkotlin/jvm/functions/Function0;Lcom/datadog/android/internal/heatmaps/HeatmapIdentifierRegistry;ILkotlin/jvm/internal/DefaultConstructorMarker;)V HSPLcom/datadog/android/rum/internal/domain/scope/RumViewScope;->delegateEventToAction(Lcom/datadog/android/rum/internal/domain/scope/RumRawEvent;Lcom/datadog/android/api/context/DatadogContext;Lkotlin/jvm/functions/Function1;Lcom/datadog/android/api/storage/DataWriter;)V HSPLcom/datadog/android/rum/internal/domain/scope/RumViewScope;->delegateEventToChildren(Lcom/datadog/android/rum/internal/domain/scope/RumRawEvent;Lcom/datadog/android/api/context/DatadogContext;Lkotlin/jvm/functions/Function1;Lcom/datadog/android/api/storage/DataWriter;)V HSPLcom/datadog/android/rum/internal/domain/scope/RumViewScope;->delegateEventToResources(Lcom/datadog/android/rum/internal/domain/scope/RumRawEvent;Lcom/datadog/android/api/context/DatadogContext;Lkotlin/jvm/functions/Function1;Lcom/datadog/android/api/storage/DataWriter;)V HSPLcom/datadog/android/rum/internal/domain/scope/RumViewScope;->getCustomAttributes()Ljava/util/Map; -HSPLcom/datadog/android/rum/internal/domain/scope/RumViewScope;->getEventTimestamp$dd_sdk_android_rum_release()J -HSPLcom/datadog/android/rum/internal/domain/scope/RumViewScope;->getHasReplay$dd_sdk_android_rum_release()Z HSPLcom/datadog/android/rum/internal/domain/scope/RumViewScope;->getParentScope()Lcom/datadog/android/rum/internal/domain/scope/RumScope; HSPLcom/datadog/android/rum/internal/domain/scope/RumViewScope;->getRumContext()Lcom/datadog/android/rum/internal/domain/RumContext; -HSPLcom/datadog/android/rum/internal/domain/scope/RumViewScope;->getSampleRate$dd_sdk_android_rum_release()F -HSPLcom/datadog/android/rum/internal/domain/scope/RumViewScope;->getViewLoadingTime$dd_sdk_android_rum_release()Ljava/lang/Long; HSPLcom/datadog/android/rum/internal/domain/scope/RumViewScope;->handleEvent(Lcom/datadog/android/rum/internal/domain/scope/RumRawEvent;Lcom/datadog/android/api/context/DatadogContext;Lkotlin/jvm/functions/Function1;Lcom/datadog/android/api/storage/DataWriter;)Lcom/datadog/android/rum/internal/domain/scope/RumScope; HSPLcom/datadog/android/rum/internal/domain/scope/RumViewScope;->isActive()Z HSPLcom/datadog/android/rum/internal/domain/scope/RumViewScope;->isViewComplete()Z +HSPLcom/datadog/android/rum/internal/domain/scope/RumViewScope;->onAddLongTask$lambda$36(Lcom/datadog/android/api/context/DatadogContext;Lcom/datadog/android/rum/internal/domain/RumContext;Lcom/datadog/android/rum/internal/domain/scope/RumViewScope;JLcom/datadog/android/rum/internal/domain/scope/RumRawEvent$AddLongTask;Ljava/lang/String;ZLjava/util/Map;)Ljava/lang/Object; +HSPLcom/datadog/android/rum/internal/domain/scope/RumViewScope;->onAddLongTask$lambda$39$lambda$38(Lcom/datadog/android/rum/internal/domain/RumContext;Lcom/datadog/android/rum/internal/monitor/StorageEvent;Lcom/datadog/android/rum/internal/domain/scope/RumViewScope;Ljava/lang/String;JLcom/datadog/android/rum/internal/domain/scope/RumRawEvent$AddLongTask;Lcom/datadog/android/rum/internal/monitor/AdvancedRumMonitor;)Lkotlin/Unit; HSPLcom/datadog/android/rum/internal/domain/scope/RumViewScope;->onAddLongTask(Lcom/datadog/android/rum/internal/domain/scope/RumRawEvent$AddLongTask;Lcom/datadog/android/api/context/DatadogContext;Lkotlin/jvm/functions/Function1;Lcom/datadog/android/api/storage/DataWriter;)V HSPLcom/datadog/android/rum/internal/domain/scope/RumViewScope;->onLongTaskSent(Lcom/datadog/android/rum/internal/domain/scope/RumRawEvent$LongTaskSent;Lcom/datadog/android/api/context/DatadogContext;Lkotlin/jvm/functions/Function1;Lcom/datadog/android/api/storage/DataWriter;)V +HSPLcom/datadog/android/rum/internal/domain/scope/RumViewScope;->onStartView$lambda$13(Lcom/datadog/android/rum/internal/domain/scope/RumViewScope;)Lkotlin/Unit; HSPLcom/datadog/android/rum/internal/domain/scope/RumViewScope;->resolveCustomTimings()Lcom/datadog/android/rum/model/ViewEvent$CustomTimings; HSPLcom/datadog/android/rum/internal/domain/scope/RumViewScope;->resolveLongTaskProfilingStatus(Lcom/datadog/android/api/context/DatadogContext;)Lcom/datadog/android/rum/model/LongTaskEvent$Profiling; HSPLcom/datadog/android/rum/internal/domain/scope/RumViewScope;->resolveRefreshRateInfo(Lcom/datadog/android/rum/internal/vitals/VitalInfo;)Ljava/lang/Boolean; HSPLcom/datadog/android/rum/internal/domain/scope/RumViewScope;->resolveViewDuration(Lcom/datadog/android/rum/internal/domain/scope/RumRawEvent;)V HSPLcom/datadog/android/rum/internal/domain/scope/RumViewScope;->resolveViewProfilingStatus(Lcom/datadog/android/api/context/DatadogContext;)Lcom/datadog/android/rum/model/ViewEvent$Profiling; +HSPLcom/datadog/android/rum/internal/domain/scope/RumViewScope;->sendViewChanged()V HSPLcom/datadog/android/rum/internal/domain/scope/RumViewScope;->sendViewUpdate$dd_sdk_android_rum_release$default(Lcom/datadog/android/rum/internal/domain/scope/RumViewScope;Lcom/datadog/android/rum/internal/domain/scope/RumRawEvent;Lcom/datadog/android/api/context/DatadogContext;Lkotlin/jvm/functions/Function1;Lcom/datadog/android/api/storage/DataWriter;Lcom/datadog/android/api/storage/EventType;ILjava/lang/Object;)V HSPLcom/datadog/android/rum/internal/domain/scope/RumViewScope;->sendViewUpdate$dd_sdk_android_rum_release(Lcom/datadog/android/rum/internal/domain/scope/RumRawEvent;Lcom/datadog/android/api/context/DatadogContext;Lkotlin/jvm/functions/Function1;Lcom/datadog/android/api/storage/DataWriter;Lcom/datadog/android/api/storage/EventType;)V +HSPLcom/datadog/android/rum/internal/domain/scope/RumViewScope;->stopScope(Lcom/datadog/android/rum/internal/domain/scope/RumRawEvent;Lcom/datadog/android/api/context/DatadogContext;Lkotlin/jvm/functions/Function1;Lcom/datadog/android/api/storage/DataWriter;Lkotlin/jvm/functions/Function0;)V +Lcom/datadog/android/rum/internal/domain/scope/RumViewScope$$ExternalSyntheticLambda12; +HSPLcom/datadog/android/rum/internal/domain/scope/RumViewScope$$ExternalSyntheticLambda12;->invoke()Ljava/lang/Object; +Lcom/datadog/android/rum/internal/domain/scope/RumViewScope$$ExternalSyntheticLambda2; +Lcom/datadog/android/rum/internal/domain/scope/RumViewScope$$ExternalSyntheticLambda6; +HSPLcom/datadog/android/rum/internal/domain/scope/RumViewScope$$ExternalSyntheticLambda6;->(Lcom/datadog/android/api/context/DatadogContext;Lcom/datadog/android/rum/internal/domain/RumContext;Lcom/datadog/android/rum/internal/domain/scope/RumViewScope;JLcom/datadog/android/rum/internal/domain/scope/RumRawEvent$AddLongTask;Ljava/lang/String;ZLjava/util/Map;)V +HSPLcom/datadog/android/rum/internal/domain/scope/RumViewScope$$ExternalSyntheticLambda6;->invoke()Ljava/lang/Object; +Lcom/datadog/android/rum/internal/domain/scope/RumViewScope$$ExternalSyntheticLambda7; +HSPLcom/datadog/android/rum/internal/domain/scope/RumViewScope$$ExternalSyntheticLambda7;->(Lcom/datadog/android/rum/internal/domain/RumContext;Lcom/datadog/android/rum/internal/monitor/StorageEvent;)V +Lcom/datadog/android/rum/internal/domain/scope/RumViewScope$$ExternalSyntheticLambda8; +HSPLcom/datadog/android/rum/internal/domain/scope/RumViewScope$$ExternalSyntheticLambda8;->(Lcom/datadog/android/rum/internal/domain/RumContext;Lcom/datadog/android/rum/internal/monitor/StorageEvent;Lcom/datadog/android/rum/internal/domain/scope/RumViewScope;Ljava/lang/String;JLcom/datadog/android/rum/internal/domain/scope/RumRawEvent$AddLongTask;)V +HSPLcom/datadog/android/rum/internal/domain/scope/RumViewScope$$ExternalSyntheticLambda8;->invoke(Ljava/lang/Object;)Ljava/lang/Object; Lcom/datadog/android/rum/internal/domain/scope/RumViewScope$Companion; HSPLcom/datadog/android/rum/internal/domain/scope/RumViewScope$Companion;->()V HSPLcom/datadog/android/rum/internal/domain/scope/RumViewScope$Companion;->(Lkotlin/jvm/internal/DefaultConstructorMarker;)V -HSPLcom/datadog/android/rum/internal/domain/scope/RumViewScope$Companion;->getONE_SECOND_NS$dd_sdk_android_rum_release()J +HSPLcom/datadog/android/rum/internal/domain/scope/RumViewScope$Companion;->fromEvent$dd_sdk_android_rum_release(Lcom/datadog/android/rum/internal/domain/scope/RumScope;Lcom/datadog/android/rum/internal/metric/SessionMetricDispatcher;Lcom/datadog/android/core/InternalSdkCore;Lcom/datadog/android/rum/internal/domain/scope/RumRawEvent$StartView;Lcom/datadog/android/rum/internal/domain/scope/RumViewChangedListener;Lcom/datadog/android/core/internal/net/FirstPartyHostHeaderTypeResolver;Lcom/datadog/android/rum/internal/vitals/VitalMonitor;Lcom/datadog/android/rum/internal/vitals/VitalMonitor;Lcom/datadog/android/rum/internal/vitals/VitalMonitor;ZFLcom/datadog/android/rum/internal/metric/interactiontonextview/InteractionToNextViewMetricResolver;Lcom/datadog/android/rum/metric/networksettled/InitialResourceIdentifier;Lcom/datadog/android/rum/internal/metric/slowframes/SlowFramesListener;Lcom/datadog/android/rum/RumSessionType;Lcom/datadog/android/rum/internal/domain/InfoProvider;Lcom/datadog/android/rum/internal/domain/InfoProvider;Lcom/datadog/android/rum/internal/domain/InfoProvider;Lcom/datadog/android/rum/internal/instrumentation/insights/InsightsCollector;Lcom/datadog/android/rum/event/ViewEventMapper;Lcom/datadog/android/rum/configuration/RumViewEventWriteConfig;Lcom/datadog/android/internal/heatmaps/HeatmapIdentifierRegistry;)Lcom/datadog/android/rum/internal/domain/scope/RumViewScope; +HSPLcom/datadog/android/rum/internal/domain/scope/RumViewScope$Companion;->fromEvent$lambda$0(Lcom/datadog/android/rum/configuration/RumViewEventWriteConfig;Lcom/datadog/android/rum/event/ViewEventMapper;Lcom/datadog/android/core/InternalSdkCore;)Lcom/datadog/android/rum/internal/domain/scope/RumViewEventWriter; +HSPLcom/datadog/android/rum/internal/domain/scope/RumViewScope$Companion;->tryResolveInstrumentationType(Lcom/datadog/android/rum/internal/domain/scope/RumRawEvent$StartView;)Lcom/datadog/android/internal/attributes/ViewScopeInstrumentationType; +Lcom/datadog/android/rum/internal/domain/scope/RumViewScope$Companion$$ExternalSyntheticLambda0; +HSPLcom/datadog/android/rum/internal/domain/scope/RumViewScope$Companion$$ExternalSyntheticLambda0;->(Lcom/datadog/android/rum/configuration/RumViewEventWriteConfig;Lcom/datadog/android/rum/event/ViewEventMapper;Lcom/datadog/android/core/InternalSdkCore;)V +HSPLcom/datadog/android/rum/internal/domain/scope/RumViewScope$Companion$$ExternalSyntheticLambda0;->invoke()Ljava/lang/Object; Lcom/datadog/android/rum/internal/domain/scope/RumViewScope$cpuVitalListener$1; HSPLcom/datadog/android/rum/internal/domain/scope/RumViewScope$cpuVitalListener$1;->(Lcom/datadog/android/rum/internal/domain/scope/RumViewScope;)V Lcom/datadog/android/rum/internal/domain/scope/RumViewScope$frameRateVitalListener$1; HSPLcom/datadog/android/rum/internal/domain/scope/RumViewScope$frameRateVitalListener$1;->(Lcom/datadog/android/rum/internal/domain/scope/RumViewScope;)V Lcom/datadog/android/rum/internal/domain/scope/RumViewScope$memoryVitalListener$1; HSPLcom/datadog/android/rum/internal/domain/scope/RumViewScope$memoryVitalListener$1;->(Lcom/datadog/android/rum/internal/domain/scope/RumViewScope;)V -Lcom/datadog/android/rum/internal/domain/scope/RumViewScope$onAddLongTask$1; -HSPLcom/datadog/android/rum/internal/domain/scope/RumViewScope$onAddLongTask$1;->(Lcom/datadog/android/api/context/DatadogContext;Lcom/datadog/android/rum/internal/domain/RumContext;Lcom/datadog/android/rum/internal/domain/scope/RumViewScope;JLcom/datadog/android/rum/internal/domain/scope/RumRawEvent$AddLongTask;Ljava/lang/String;ZLjava/util/Map;)V -HSPLcom/datadog/android/rum/internal/domain/scope/RumViewScope$onAddLongTask$1;->invoke()Ljava/lang/Object; -Lcom/datadog/android/rum/internal/domain/scope/RumViewScope$onAddLongTask$2$1; -HSPLcom/datadog/android/rum/internal/domain/scope/RumViewScope$onAddLongTask$2$1;->(Lcom/datadog/android/rum/internal/domain/RumContext;Lcom/datadog/android/rum/internal/monitor/StorageEvent;)V -Lcom/datadog/android/rum/internal/domain/scope/RumViewScope$onAddLongTask$2$2; -HSPLcom/datadog/android/rum/internal/domain/scope/RumViewScope$onAddLongTask$2$2;->(Lcom/datadog/android/rum/internal/domain/RumContext;Lcom/datadog/android/rum/internal/monitor/StorageEvent;Lcom/datadog/android/rum/internal/domain/scope/RumViewScope;Ljava/lang/String;JLcom/datadog/android/rum/internal/domain/scope/RumRawEvent$AddLongTask;)V -HSPLcom/datadog/android/rum/internal/domain/scope/RumViewScope$onAddLongTask$2$2;->invoke(Lcom/datadog/android/rum/internal/monitor/AdvancedRumMonitor;)V -HSPLcom/datadog/android/rum/internal/domain/scope/RumViewScope$onAddLongTask$2$2;->invoke(Ljava/lang/Object;)Ljava/lang/Object; -Lcom/datadog/android/rum/internal/domain/scope/RumViewScope$sendViewUpdate$1; -HSPLcom/datadog/android/rum/internal/domain/scope/RumViewScope$sendViewUpdate$1;->(Lcom/datadog/android/api/context/DatadogContext;JLcom/datadog/android/rum/internal/domain/RumContext;Lcom/datadog/android/rum/internal/domain/scope/RumViewScope;Ljava/util/Map;JJJJJJZJLjava/lang/Double;Lcom/datadog/android/rum/internal/vitals/VitalInfo;Lcom/datadog/android/rum/internal/vitals/VitalInfo;ILkotlin/jvm/internal/Ref$ObjectRef;Ljava/lang/String;Ljava/lang/Long;Lcom/datadog/android/rum/model/ViewEvent$CustomTimings;ZLjava/util/List;Ljava/lang/Double;Ljava/lang/Double;Lcom/datadog/android/rum/model/ViewEvent$FlutterBuildTime;Lcom/datadog/android/rum/model/ViewEvent$FlutterBuildTime;Lcom/datadog/android/rum/model/ViewEvent$FlutterBuildTime;Lcom/datadog/android/rum/model/ViewEvent$Performance;Lcom/datadog/android/rum/model/ViewEvent$Accessibility;Lcom/datadog/android/rum/internal/domain/battery/BatteryInfo;Lcom/datadog/android/rum/internal/domain/display/DisplayInfo;Ljava/util/Map;J)V -HSPLcom/datadog/android/rum/internal/domain/scope/RumViewScope$sendViewUpdate$1;->invoke()Ljava/lang/Object; Lcom/datadog/android/rum/internal/domain/scope/RumViewType; HSPLcom/datadog/android/rum/internal/domain/scope/RumViewType;->$values()[Lcom/datadog/android/rum/internal/domain/scope/RumViewType; HSPLcom/datadog/android/rum/internal/domain/scope/RumViewType;->()V HSPLcom/datadog/android/rum/internal/domain/scope/RumViewType;->(Ljava/lang/String;ILjava/lang/String;)V HSPLcom/datadog/android/rum/internal/domain/scope/RumViewType;->getAsString()Ljava/lang/String; -PLcom/datadog/android/rum/internal/domain/scope/RumViewType;->values()[Lcom/datadog/android/rum/internal/domain/scope/RumViewType; +HSPLcom/datadog/android/rum/internal/domain/scope/RumViewType;->values()[Lcom/datadog/android/rum/internal/domain/scope/RumViewType; Lcom/datadog/android/rum/internal/domain/scope/RumViewType$Companion; HSPLcom/datadog/android/rum/internal/domain/scope/RumViewType$Companion;->()V HSPLcom/datadog/android/rum/internal/domain/scope/RumViewType$Companion;->(Lkotlin/jvm/internal/DefaultConstructorMarker;)V -PLcom/datadog/android/rum/internal/domain/scope/RumViewType$Companion;->fromString(Ljava/lang/String;)Lcom/datadog/android/rum/internal/domain/scope/RumViewType; +Lcom/datadog/android/rum/internal/domain/scope/RumViewUpdateData; Lcom/datadog/android/rum/internal/domain/scope/RumVitalAppLaunchEventHelper; HSPLcom/datadog/android/rum/internal/domain/scope/RumVitalAppLaunchEventHelper;->(Lcom/datadog/android/rum/RumSessionType;Lcom/datadog/android/rum/internal/domain/InfoProvider;Lcom/datadog/android/rum/internal/domain/InfoProvider;FLcom/datadog/android/api/InternalLogger;)V Lcom/datadog/android/rum/internal/domain/state/ViewUIPerformanceReport; +PLcom/datadog/android/rum/internal/domain/state/ViewUIPerformanceReport;->()V +PLcom/datadog/android/rum/internal/domain/state/ViewUIPerformanceReport;->(JIJ)V +PLcom/datadog/android/rum/internal/domain/state/ViewUIPerformanceReport;->(JLcom/datadog/android/internal/collections/EvictingQueue;JJJJ)V +PLcom/datadog/android/rum/internal/domain/state/ViewUIPerformanceReport;->(JLcom/datadog/android/internal/collections/EvictingQueue;JJJJILkotlin/jvm/internal/DefaultConstructorMarker;)V +PLcom/datadog/android/rum/internal/domain/state/ViewUIPerformanceReport;->getTotalFramesDurationNs()J +PLcom/datadog/android/rum/internal/domain/state/ViewUIPerformanceReport;->setTotalFramesDurationNs(J)V +PLcom/datadog/android/rum/internal/domain/state/ViewUIPerformanceReport$Companion;->()V +PLcom/datadog/android/rum/internal/domain/state/ViewUIPerformanceReport$Companion;->(Lkotlin/jvm/internal/DefaultConstructorMarker;)V Lcom/datadog/android/rum/internal/instrumentation/MainLooperLongTaskStrategy; HSPLcom/datadog/android/rum/internal/instrumentation/MainLooperLongTaskStrategy;->()V HSPLcom/datadog/android/rum/internal/instrumentation/MainLooperLongTaskStrategy;->(J)V @@ -21060,6 +21219,12 @@ Lcom/datadog/android/rum/internal/metric/NoValueReason$InteractionToNextView; HSPLcom/datadog/android/rum/internal/metric/NoValueReason$InteractionToNextView;->$values()[Lcom/datadog/android/rum/internal/metric/NoValueReason$InteractionToNextView; HSPLcom/datadog/android/rum/internal/metric/NoValueReason$InteractionToNextView;->()V HSPLcom/datadog/android/rum/internal/metric/NoValueReason$InteractionToNextView;->(Ljava/lang/String;I)V +HSPLcom/datadog/android/rum/internal/metric/NoValueReason$InteractionToNextView;->values()[Lcom/datadog/android/rum/internal/metric/NoValueReason$InteractionToNextView; +Lcom/datadog/android/rum/internal/metric/NoValueReason$TimeToNetworkSettle; +HSPLcom/datadog/android/rum/internal/metric/NoValueReason$TimeToNetworkSettle;->$values()[Lcom/datadog/android/rum/internal/metric/NoValueReason$TimeToNetworkSettle; +HSPLcom/datadog/android/rum/internal/metric/NoValueReason$TimeToNetworkSettle;->()V +HSPLcom/datadog/android/rum/internal/metric/NoValueReason$TimeToNetworkSettle;->(Ljava/lang/String;I)V +HSPLcom/datadog/android/rum/internal/metric/NoValueReason$TimeToNetworkSettle;->values()[Lcom/datadog/android/rum/internal/metric/NoValueReason$TimeToNetworkSettle; Lcom/datadog/android/rum/internal/metric/SessionEndedMetric; HSPLcom/datadog/android/rum/internal/metric/SessionEndedMetric;->()V HSPLcom/datadog/android/rum/internal/metric/SessionEndedMetric;->(Ljava/lang/String;Lcom/datadog/android/rum/internal/domain/scope/RumSessionScope$StartReason;JZ)V @@ -21069,6 +21234,8 @@ HSPLcom/datadog/android/rum/internal/metric/SessionEndedMetric$Companion;->(Lkotlin/jvm/internal/DefaultConstructorMarker;)V Lcom/datadog/android/rum/internal/metric/SessionEndedMetric$TrackedView; HSPLcom/datadog/android/rum/internal/metric/SessionEndedMetric$TrackedView;->(Ljava/lang/String;JJZ)V +PLcom/datadog/android/rum/internal/metric/SessionEndedMetric$TrackedView;->getStartMs()J +PLcom/datadog/android/rum/internal/metric/SessionEndedMetric$TrackedView;->getViewUrl()Ljava/lang/String; Lcom/datadog/android/rum/internal/metric/SessionEndedMetricDispatcher; HSPLcom/datadog/android/rum/internal/metric/SessionEndedMetricDispatcher;->(Lcom/datadog/android/api/InternalLogger;F)V HSPLcom/datadog/android/rum/internal/metric/SessionEndedMetricDispatcher;->onViewTracked(Ljava/lang/String;Lcom/datadog/android/rum/model/ViewEvent;)V @@ -21078,16 +21245,31 @@ Lcom/datadog/android/rum/internal/metric/ViewEndedMetricDispatcher; HSPLcom/datadog/android/rum/internal/metric/ViewEndedMetricDispatcher;->()V HSPLcom/datadog/android/rum/internal/metric/ViewEndedMetricDispatcher;->(Lcom/datadog/android/rum/internal/domain/scope/RumViewType;Lcom/datadog/android/api/InternalLogger;Lcom/datadog/android/internal/attributes/ViewScopeInstrumentationType;F)V HSPLcom/datadog/android/rum/internal/metric/ViewEndedMetricDispatcher;->(Lcom/datadog/android/rum/internal/domain/scope/RumViewType;Lcom/datadog/android/api/InternalLogger;Lcom/datadog/android/internal/attributes/ViewScopeInstrumentationType;FILkotlin/jvm/internal/DefaultConstructorMarker;)V +HSPLcom/datadog/android/rum/internal/metric/ViewEndedMetricDispatcher;->buildAttributesMap(Lcom/datadog/android/rum/internal/metric/ViewInitializationMetricsState;Lcom/datadog/android/rum/internal/metric/ViewInitializationMetricsState;)Ljava/util/Map; +HSPLcom/datadog/android/rum/internal/metric/ViewEndedMetricDispatcher;->buildMetricAttributesMap(Lcom/datadog/android/rum/internal/metric/ViewInitializationMetricsState;Lcom/datadog/android/rum/internal/metric/ViewInitializationMetricsState;)Ljava/util/Map; HSPLcom/datadog/android/rum/internal/metric/ViewEndedMetricDispatcher;->onDurationResolved(J)V +HSPLcom/datadog/android/rum/internal/metric/ViewEndedMetricDispatcher;->sendViewEnded(Lcom/datadog/android/rum/internal/metric/ViewInitializationMetricsState;Lcom/datadog/android/rum/internal/metric/ViewInitializationMetricsState;)V +Lcom/datadog/android/rum/internal/metric/ViewEndedMetricDispatcher$$ExternalSyntheticLambda1; +HSPLcom/datadog/android/rum/internal/metric/ViewEndedMetricDispatcher$$ExternalSyntheticLambda1;->()V Lcom/datadog/android/rum/internal/metric/ViewEndedMetricDispatcher$Companion; HSPLcom/datadog/android/rum/internal/metric/ViewEndedMetricDispatcher$Companion;->()V HSPLcom/datadog/android/rum/internal/metric/ViewEndedMetricDispatcher$Companion;->(Lkotlin/jvm/internal/DefaultConstructorMarker;)V +HSPLcom/datadog/android/rum/internal/metric/ViewEndedMetricDispatcher$Companion;->access$putNonNull(Lcom/datadog/android/rum/internal/metric/ViewEndedMetricDispatcher$Companion;Ljava/util/Map;Ljava/lang/Object;Ljava/lang/Object;)V +HSPLcom/datadog/android/rum/internal/metric/ViewEndedMetricDispatcher$Companion;->putNonNull(Ljava/util/Map;Ljava/lang/Object;Ljava/lang/Object;)V +HSPLcom/datadog/android/rum/internal/metric/ViewEndedMetricDispatcher$Companion;->toAttributeValue$dd_sdk_android_rum_release(Lcom/datadog/android/rum/internal/domain/scope/RumViewType;)Ljava/lang/String; +HSPLcom/datadog/android/rum/internal/metric/ViewEndedMetricDispatcher$Companion;->toAttributeValue$dd_sdk_android_rum_release(Lcom/datadog/android/rum/internal/metric/NoValueReason;)Ljava/lang/String; +HSPLcom/datadog/android/rum/internal/metric/ViewEndedMetricDispatcher$Companion;->toAttributeValue$dd_sdk_android_rum_release(Lcom/datadog/android/rum/internal/metric/ViewInitializationMetricsConfig;)Ljava/lang/String; +Lcom/datadog/android/rum/internal/metric/ViewEndedMetricDispatcher$Companion$WhenMappings; +HSPLcom/datadog/android/rum/internal/metric/ViewEndedMetricDispatcher$Companion$WhenMappings;->()V Lcom/datadog/android/rum/internal/metric/ViewInitializationMetricsConfig; HSPLcom/datadog/android/rum/internal/metric/ViewInitializationMetricsConfig;->$values()[Lcom/datadog/android/rum/internal/metric/ViewInitializationMetricsConfig; HSPLcom/datadog/android/rum/internal/metric/ViewInitializationMetricsConfig;->()V HSPLcom/datadog/android/rum/internal/metric/ViewInitializationMetricsConfig;->(Ljava/lang/String;I)V +HSPLcom/datadog/android/rum/internal/metric/ViewInitializationMetricsConfig;->values()[Lcom/datadog/android/rum/internal/metric/ViewInitializationMetricsConfig; Lcom/datadog/android/rum/internal/metric/ViewInitializationMetricsState; HSPLcom/datadog/android/rum/internal/metric/ViewInitializationMetricsState;->(Ljava/lang/Long;Lcom/datadog/android/rum/internal/metric/ViewInitializationMetricsConfig;Lcom/datadog/android/rum/internal/metric/NoValueReason;)V +HSPLcom/datadog/android/rum/internal/metric/ViewInitializationMetricsState;->getConfig()Lcom/datadog/android/rum/internal/metric/ViewInitializationMetricsConfig; +HSPLcom/datadog/android/rum/internal/metric/ViewInitializationMetricsState;->getInitializationTime()Ljava/lang/Long; HSPLcom/datadog/android/rum/internal/metric/ViewInitializationMetricsState;->getNoValueReason()Lcom/datadog/android/rum/internal/metric/NoValueReason; Lcom/datadog/android/rum/internal/metric/ViewMetricDispatcher; Lcom/datadog/android/rum/internal/metric/interactiontonextview/ActionTypeInteractionValidator; @@ -21113,27 +21295,46 @@ HSPLcom/datadog/android/rum/internal/metric/interactiontonextview/InteractionToN HSPLcom/datadog/android/rum/internal/metric/interactiontonextview/InteractionToNextViewMetricResolver$Companion;->(Lkotlin/jvm/internal/DefaultConstructorMarker;)V HSPLcom/datadog/android/rum/internal/metric/interactiontonextview/InteractionToNextViewMetricResolver$Companion;->access$toConfig(Lcom/datadog/android/rum/internal/metric/interactiontonextview/InteractionToNextViewMetricResolver$Companion;Lcom/datadog/android/rum/metric/interactiontonextview/LastInteractionIdentifier;)Lcom/datadog/android/rum/internal/metric/ViewInitializationMetricsConfig; HSPLcom/datadog/android/rum/internal/metric/interactiontonextview/InteractionToNextViewMetricResolver$Companion;->toConfig(Lcom/datadog/android/rum/metric/interactiontonextview/LastInteractionIdentifier;)Lcom/datadog/android/rum/internal/metric/ViewInitializationMetricsConfig; +Lcom/datadog/android/rum/internal/metric/interactiontonextview/InternalInteractionContext; Lcom/datadog/android/rum/internal/metric/networksettled/NetworkSettledMetricResolver; HSPLcom/datadog/android/rum/internal/metric/networksettled/NetworkSettledMetricResolver;->()V HSPLcom/datadog/android/rum/internal/metric/networksettled/NetworkSettledMetricResolver;->(Lcom/datadog/android/rum/metric/networksettled/InitialResourceIdentifier;Lcom/datadog/android/api/InternalLogger;)V HSPLcom/datadog/android/rum/internal/metric/networksettled/NetworkSettledMetricResolver;->computeMetric()Ljava/lang/Long; +HSPLcom/datadog/android/rum/internal/metric/networksettled/NetworkSettledMetricResolver;->getState()Lcom/datadog/android/rum/internal/metric/ViewInitializationMetricsState; HSPLcom/datadog/android/rum/internal/metric/networksettled/NetworkSettledMetricResolver;->resolveMetric()Ljava/lang/Long; HSPLcom/datadog/android/rum/internal/metric/networksettled/NetworkSettledMetricResolver;->viewWasCreated(J)V +HSPLcom/datadog/android/rum/internal/metric/networksettled/NetworkSettledMetricResolver;->viewWasStopped()V Lcom/datadog/android/rum/internal/metric/networksettled/NetworkSettledMetricResolver$Companion; HSPLcom/datadog/android/rum/internal/metric/networksettled/NetworkSettledMetricResolver$Companion;->()V HSPLcom/datadog/android/rum/internal/metric/networksettled/NetworkSettledMetricResolver$Companion;->(Lkotlin/jvm/internal/DefaultConstructorMarker;)V +HSPLcom/datadog/android/rum/internal/metric/networksettled/NetworkSettledMetricResolver$Companion;->access$toConfig(Lcom/datadog/android/rum/internal/metric/networksettled/NetworkSettledMetricResolver$Companion;Lcom/datadog/android/rum/metric/networksettled/InitialResourceIdentifier;)Lcom/datadog/android/rum/internal/metric/ViewInitializationMetricsConfig; +HSPLcom/datadog/android/rum/internal/metric/networksettled/NetworkSettledMetricResolver$Companion;->toConfig(Lcom/datadog/android/rum/metric/networksettled/InitialResourceIdentifier;)Lcom/datadog/android/rum/internal/metric/ViewInitializationMetricsConfig; Lcom/datadog/android/rum/internal/metric/networksettled/NetworkSettledMetricResolver$Companion$Diagnostic; HSPLcom/datadog/android/rum/internal/metric/networksettled/NetworkSettledMetricResolver$Companion$Diagnostic;->(Ljava/util/concurrent/atomic/AtomicInteger;Ljava/util/concurrent/atomic/AtomicInteger;Ljava/util/concurrent/atomic/AtomicInteger;Ljava/util/concurrent/atomic/AtomicInteger;)V HSPLcom/datadog/android/rum/internal/metric/networksettled/NetworkSettledMetricResolver$Companion$Diagnostic;->(Ljava/util/concurrent/atomic/AtomicInteger;Ljava/util/concurrent/atomic/AtomicInteger;Ljava/util/concurrent/atomic/AtomicInteger;Ljava/util/concurrent/atomic/AtomicInteger;ILkotlin/jvm/internal/DefaultConstructorMarker;)V +HSPLcom/datadog/android/rum/internal/metric/networksettled/NetworkSettledMetricResolver$Companion$Diagnostic;->resolveNoValueReason()Lcom/datadog/android/rum/internal/metric/NoValueReason$TimeToNetworkSettle; Lcom/datadog/android/rum/internal/metric/slowframes/DefaultSlowFramesListener; HSPLcom/datadog/android/rum/internal/metric/slowframes/DefaultSlowFramesListener;->(Lcom/datadog/android/rum/configuration/SlowFramesConfiguration;Lcom/datadog/android/rum/internal/metric/slowframes/UISlownessMetricDispatcher;Lcom/datadog/android/rum/internal/instrumentation/insights/InsightsCollector;Lcom/datadog/android/internal/time/TimeProvider;)V +PLcom/datadog/android/rum/internal/metric/slowframes/DefaultSlowFramesListener;->getViewPerformanceReport$lambda$3(Lcom/datadog/android/rum/internal/metric/slowframes/DefaultSlowFramesListener;)Lcom/datadog/android/rum/internal/domain/state/ViewUIPerformanceReport; +PLcom/datadog/android/rum/internal/metric/slowframes/DefaultSlowFramesListener;->getViewPerformanceReport$lambda$4(Lkotlin/jvm/functions/Function0;Ljava/lang/String;)Lcom/datadog/android/rum/internal/domain/state/ViewUIPerformanceReport; +PLcom/datadog/android/rum/internal/metric/slowframes/DefaultSlowFramesListener;->getViewPerformanceReport$lambda$5(Lkotlin/jvm/functions/Function1;Ljava/lang/Object;)Lcom/datadog/android/rum/internal/domain/state/ViewUIPerformanceReport; +PLcom/datadog/android/rum/internal/metric/slowframes/DefaultSlowFramesListener;->getViewPerformanceReport(Ljava/lang/String;)Lcom/datadog/android/rum/internal/domain/state/ViewUIPerformanceReport; HSPLcom/datadog/android/rum/internal/metric/slowframes/DefaultSlowFramesListener;->onAddLongTask(J)V +PLcom/datadog/android/rum/internal/metric/slowframes/DefaultSlowFramesListener;->onFrame(Landroidx/metrics/performance/FrameData;)V +HSPLcom/datadog/android/rum/internal/metric/slowframes/DefaultSlowFramesListener;->onFrameMetricsData(Lcom/datadog/android/rum/internal/domain/FrameMetricsData;)V HSPLcom/datadog/android/rum/internal/metric/slowframes/DefaultSlowFramesListener;->onViewCreated(Ljava/lang/String;J)V HSPLcom/datadog/android/rum/internal/metric/slowframes/DefaultSlowFramesListener;->resolveReport(Ljava/lang/String;ZJ)Lcom/datadog/android/rum/internal/domain/state/ViewUIPerformanceReport$Snapshot; +PLcom/datadog/android/rum/internal/metric/slowframes/DefaultSlowFramesListener$$ExternalSyntheticLambda0;->(Lcom/datadog/android/rum/internal/metric/slowframes/DefaultSlowFramesListener;)V +PLcom/datadog/android/rum/internal/metric/slowframes/DefaultSlowFramesListener$$ExternalSyntheticLambda0;->invoke()Ljava/lang/Object; +PLcom/datadog/android/rum/internal/metric/slowframes/DefaultSlowFramesListener$$ExternalSyntheticLambda1;->(Lkotlin/jvm/functions/Function0;)V +PLcom/datadog/android/rum/internal/metric/slowframes/DefaultSlowFramesListener$$ExternalSyntheticLambda1;->invoke(Ljava/lang/Object;)Ljava/lang/Object; +PLcom/datadog/android/rum/internal/metric/slowframes/DefaultSlowFramesListener$$ExternalSyntheticLambda2;->(Lkotlin/jvm/functions/Function1;)V +PLcom/datadog/android/rum/internal/metric/slowframes/DefaultSlowFramesListener$$ExternalSyntheticLambda2;->apply(Ljava/lang/Object;)Ljava/lang/Object; Lcom/datadog/android/rum/internal/metric/slowframes/DefaultUISlownessMetricDispatcher; HSPLcom/datadog/android/rum/internal/metric/slowframes/DefaultUISlownessMetricDispatcher;->()V HSPLcom/datadog/android/rum/internal/metric/slowframes/DefaultUISlownessMetricDispatcher;->(Lcom/datadog/android/rum/configuration/SlowFramesConfiguration;Lcom/datadog/android/api/InternalLogger;F)V HSPLcom/datadog/android/rum/internal/metric/slowframes/DefaultUISlownessMetricDispatcher;->(Lcom/datadog/android/rum/configuration/SlowFramesConfiguration;Lcom/datadog/android/api/InternalLogger;FILkotlin/jvm/internal/DefaultConstructorMarker;)V +PLcom/datadog/android/rum/internal/metric/slowframes/DefaultUISlownessMetricDispatcher;->incrementIgnoredFrameCount(Ljava/lang/String;)V HSPLcom/datadog/android/rum/internal/metric/slowframes/DefaultUISlownessMetricDispatcher;->onViewCreated(Ljava/lang/String;)V Lcom/datadog/android/rum/internal/metric/slowframes/DefaultUISlownessMetricDispatcher$Companion; HSPLcom/datadog/android/rum/internal/metric/slowframes/DefaultUISlownessMetricDispatcher$Companion;->()V @@ -21141,56 +21342,53 @@ HSPLcom/datadog/android/rum/internal/metric/slowframes/DefaultUISlownessMetricDi Lcom/datadog/android/rum/internal/metric/slowframes/DefaultUISlownessMetricDispatcher$SlowFramesTelemetry; HSPLcom/datadog/android/rum/internal/metric/slowframes/DefaultUISlownessMetricDispatcher$SlowFramesTelemetry;->(Ljava/util/concurrent/atomic/AtomicInteger;Ljava/util/concurrent/atomic/AtomicInteger;Ljava/util/concurrent/atomic/AtomicInteger;)V HSPLcom/datadog/android/rum/internal/metric/slowframes/DefaultUISlownessMetricDispatcher$SlowFramesTelemetry;->(Ljava/util/concurrent/atomic/AtomicInteger;Ljava/util/concurrent/atomic/AtomicInteger;Ljava/util/concurrent/atomic/AtomicInteger;ILkotlin/jvm/internal/DefaultConstructorMarker;)V +PLcom/datadog/android/rum/internal/metric/slowframes/DefaultUISlownessMetricDispatcher$SlowFramesTelemetry;->getIgnoredFramesCount()Ljava/util/concurrent/atomic/AtomicInteger; Lcom/datadog/android/rum/internal/metric/slowframes/SlowFramesListener; Lcom/datadog/android/rum/internal/metric/slowframes/UISlownessMetricDispatcher; Lcom/datadog/android/rum/internal/monitor/AdvancedNetworkRumMonitor; Lcom/datadog/android/rum/internal/monitor/AdvancedRumMonitor; Lcom/datadog/android/rum/internal/monitor/DatadogRumMonitor; HSPLcom/datadog/android/rum/internal/monitor/DatadogRumMonitor;->()V -HSPLcom/datadog/android/rum/internal/monitor/DatadogRumMonitor;->(Ljava/lang/String;Lcom/datadog/android/core/InternalSdkCore;Ljava/lang/String;Lcom/datadog/android/core/sampling/Sampler;ZZLcom/datadog/android/api/storage/DataWriter;Landroid/os/Handler;Lcom/datadog/android/telemetry/internal/TelemetryEventHandler;Lcom/datadog/android/rum/internal/metric/SessionMetricDispatcher;Lcom/datadog/android/core/internal/net/FirstPartyHostHeaderTypeResolver;Lcom/datadog/android/rum/internal/vitals/VitalMonitor;Lcom/datadog/android/rum/internal/vitals/VitalMonitor;Lcom/datadog/android/rum/internal/vitals/VitalMonitor;Lcom/datadog/android/rum/RumSessionListener;Ljava/util/concurrent/ExecutorService;Lcom/datadog/android/rum/metric/networksettled/InitialResourceIdentifier;Lcom/datadog/android/rum/metric/interactiontonextview/LastInteractionIdentifier;Lcom/datadog/android/rum/internal/metric/slowframes/SlowFramesListener;Lcom/datadog/android/rum/RumSessionType;Lcom/datadog/android/rum/internal/domain/accessibility/AccessibilitySnapshotManager;Lcom/datadog/android/rum/internal/domain/InfoProvider;Lcom/datadog/android/rum/internal/domain/InfoProvider;Lkotlin/jvm/functions/Function0;Lcom/datadog/android/rum/internal/instrumentation/insights/InsightsCollector;Lcom/datadog/android/internal/heatmaps/HeatmapIdentifierRegistry;Lcom/datadog/android/rum/internal/timeseries/TimeseriesCollector$Factory;)V -HSPLcom/datadog/android/rum/internal/monitor/DatadogRumMonitor;->access$currentRumContext(Lcom/datadog/android/rum/internal/monitor/DatadogRumMonitor;)Lcom/datadog/android/rum/internal/domain/RumContext; -HSPLcom/datadog/android/rum/internal/monitor/DatadogRumMonitor;->access$getSdkCore$p(Lcom/datadog/android/rum/internal/monitor/DatadogRumMonitor;)Lcom/datadog/android/core/InternalSdkCore; -HSPLcom/datadog/android/rum/internal/monitor/DatadogRumMonitor;->access$getWriter$p(Lcom/datadog/android/rum/internal/monitor/DatadogRumMonitor;)Lcom/datadog/android/api/storage/DataWriter; -HSPLcom/datadog/android/rum/internal/monitor/DatadogRumMonitor;->access$handleEventWithMethodCallPerf(Lcom/datadog/android/rum/internal/monitor/DatadogRumMonitor;Lcom/datadog/android/rum/internal/domain/scope/RumRawEvent;Lcom/datadog/android/api/context/DatadogContext;Lkotlin/jvm/functions/Function1;)V -HSPLcom/datadog/android/rum/internal/monitor/DatadogRumMonitor;->access$updateCachedViewUrl(Lcom/datadog/android/rum/internal/monitor/DatadogRumMonitor;Lcom/datadog/android/rum/internal/domain/RumContext;)V +HSPLcom/datadog/android/rum/internal/monitor/DatadogRumMonitor;->(Ljava/lang/String;Lcom/datadog/android/core/InternalSdkCore;Ljava/lang/String;Lcom/datadog/android/core/sampling/Sampler;ZZLcom/datadog/android/api/storage/DataWriter;Landroid/os/Handler;Lcom/datadog/android/telemetry/internal/TelemetryEventHandler;Lcom/datadog/android/rum/internal/metric/SessionMetricDispatcher;Lcom/datadog/android/core/internal/net/FirstPartyHostHeaderTypeResolver;Lcom/datadog/android/rum/internal/vitals/VitalMonitor;Lcom/datadog/android/rum/internal/vitals/VitalMonitor;Lcom/datadog/android/rum/internal/vitals/VitalMonitor;Lcom/datadog/android/rum/RumSessionListener;Ljava/util/concurrent/ExecutorService;Lcom/datadog/android/rum/metric/networksettled/InitialResourceIdentifier;Lcom/datadog/android/rum/metric/interactiontonextview/LastInteractionIdentifier;Lcom/datadog/android/rum/internal/metric/slowframes/SlowFramesListener;Lcom/datadog/android/rum/RumSessionType;Lcom/datadog/android/rum/internal/domain/InfoProvider;Lcom/datadog/android/rum/internal/domain/InfoProvider;Lcom/datadog/android/rum/internal/domain/InfoProvider;Lkotlin/jvm/functions/Function0;Lcom/datadog/android/rum/internal/instrumentation/insights/InsightsCollector;Lcom/datadog/android/rum/event/ViewEventMapper;Lcom/datadog/android/rum/configuration/RumViewEventWriteConfig;Lcom/datadog/android/internal/heatmaps/HeatmapIdentifierRegistry;Lcom/datadog/android/rum/internal/timeseries/TimeseriesCollector$Factory;)V HSPLcom/datadog/android/rum/internal/monitor/DatadogRumMonitor;->addLongTask(JLjava/lang/String;)V HSPLcom/datadog/android/rum/internal/monitor/DatadogRumMonitor;->currentRumContext()Lcom/datadog/android/rum/internal/domain/RumContext; HSPLcom/datadog/android/rum/internal/monitor/DatadogRumMonitor;->eventSent(Ljava/lang/String;Lcom/datadog/android/rum/internal/monitor/StorageEvent;)V HSPLcom/datadog/android/rum/internal/monitor/DatadogRumMonitor;->getAttributes()Ljava/util/Map; +HSPLcom/datadog/android/rum/internal/monitor/DatadogRumMonitor;->getEventTime(Ljava/util/Map;)Lcom/datadog/android/rum/internal/domain/Time; HSPLcom/datadog/android/rum/internal/monitor/DatadogRumMonitor;->getExecutorService$dd_sdk_android_rum_release()Ljava/util/concurrent/ExecutorService; -HSPLcom/datadog/android/rum/internal/monitor/DatadogRumMonitor;->getRootScope$dd_sdk_android_rum_release()Lcom/datadog/android/rum/internal/domain/scope/RumApplicationScope; HSPLcom/datadog/android/rum/internal/monitor/DatadogRumMonitor;->handleEvent$dd_sdk_android_rum_release(Lcom/datadog/android/rum/internal/domain/scope/RumRawEvent;)V +HSPLcom/datadog/android/rum/internal/monitor/DatadogRumMonitor;->handleEvent$lambda$22$lambda$19(Lcom/datadog/android/rum/internal/monitor/DatadogRumMonitor;Lcom/datadog/android/rum/internal/domain/scope/RumRawEvent;Lcom/datadog/android/api/context/DatadogContext;Lkotlin/jvm/functions/Function1;)Lcom/datadog/android/rum/internal/domain/RumContext; +HSPLcom/datadog/android/rum/internal/monitor/DatadogRumMonitor;->handleEvent$lambda$22$lambda$21(Lcom/datadog/android/rum/internal/domain/RumContext;Ljava/util/Map;)Lkotlin/Unit; +HSPLcom/datadog/android/rum/internal/monitor/DatadogRumMonitor;->handleEvent$lambda$22(Lcom/datadog/android/rum/internal/monitor/DatadogRumMonitor;Lcom/datadog/android/rum/internal/domain/scope/RumRawEvent;Lcom/datadog/android/api/context/DatadogContext;Lkotlin/jvm/functions/Function1;)Lkotlin/Unit; +HSPLcom/datadog/android/rum/internal/monitor/DatadogRumMonitor;->handleEventWithMethodCallPerf$lambda$23(Lcom/datadog/android/rum/internal/monitor/DatadogRumMonitor;Lcom/datadog/android/rum/internal/domain/scope/RumRawEvent;Lcom/datadog/android/api/context/DatadogContext;Lkotlin/jvm/functions/Function1;)Lcom/datadog/android/rum/internal/domain/scope/RumScope; HSPLcom/datadog/android/rum/internal/monitor/DatadogRumMonitor;->handleEventWithMethodCallPerf(Lcom/datadog/android/rum/internal/domain/scope/RumRawEvent;Lcom/datadog/android/api/context/DatadogContext;Lkotlin/jvm/functions/Function1;)V HSPLcom/datadog/android/rum/internal/monitor/DatadogRumMonitor;->notifyDebugListenerWithState$dd_sdk_android_rum_release()V HSPLcom/datadog/android/rum/internal/monitor/DatadogRumMonitor;->notifyInterceptorInstantiated()V HSPLcom/datadog/android/rum/internal/monitor/DatadogRumMonitor;->now()Lcom/datadog/android/rum/internal/domain/Time; +HSPLcom/datadog/android/rum/internal/monitor/DatadogRumMonitor;->reportNetworkingLibraryType$lambda$5(Lcom/datadog/android/internal/telemetry/InternalTelemetryEvent$ApiUsage$NetworkInstrumentation$LibraryType;)Lcom/datadog/android/internal/telemetry/InternalTelemetryEvent$ApiUsage; HSPLcom/datadog/android/rum/internal/monitor/DatadogRumMonitor;->reportNetworkingLibraryType(Lcom/datadog/android/internal/telemetry/InternalTelemetryEvent$ApiUsage$NetworkInstrumentation$LibraryType;)V HSPLcom/datadog/android/rum/internal/monitor/DatadogRumMonitor;->sendTelemetryEvent(Lcom/datadog/android/internal/telemetry/InternalTelemetryEvent;)V HSPLcom/datadog/android/rum/internal/monitor/DatadogRumMonitor;->start()V +HSPLcom/datadog/android/rum/internal/monitor/DatadogRumMonitor;->startView(Ljava/lang/Object;Ljava/lang/String;Ljava/util/Map;)V HSPLcom/datadog/android/rum/internal/monitor/DatadogRumMonitor;->updateCachedViewUrl(Lcom/datadog/android/rum/internal/domain/RumContext;)V +Lcom/datadog/android/rum/internal/monitor/DatadogRumMonitor$$ExternalSyntheticLambda11; +HSPLcom/datadog/android/rum/internal/monitor/DatadogRumMonitor$$ExternalSyntheticLambda11;->(Lcom/datadog/android/rum/internal/monitor/DatadogRumMonitor;Lcom/datadog/android/rum/internal/domain/scope/RumRawEvent;)V +HSPLcom/datadog/android/rum/internal/monitor/DatadogRumMonitor$$ExternalSyntheticLambda11;->invoke(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object; +Lcom/datadog/android/rum/internal/monitor/DatadogRumMonitor$$ExternalSyntheticLambda2; +HSPLcom/datadog/android/rum/internal/monitor/DatadogRumMonitor$$ExternalSyntheticLambda2;->(Lcom/datadog/android/internal/telemetry/InternalTelemetryEvent$ApiUsage$NetworkInstrumentation$LibraryType;)V +HSPLcom/datadog/android/rum/internal/monitor/DatadogRumMonitor$$ExternalSyntheticLambda2;->invoke()Ljava/lang/Object; +Lcom/datadog/android/rum/internal/monitor/DatadogRumMonitor$$ExternalSyntheticLambda5; +HSPLcom/datadog/android/rum/internal/monitor/DatadogRumMonitor$$ExternalSyntheticLambda5;->(Lcom/datadog/android/rum/internal/monitor/DatadogRumMonitor;Lcom/datadog/android/rum/internal/domain/scope/RumRawEvent;Lcom/datadog/android/api/context/DatadogContext;Lkotlin/jvm/functions/Function1;)V +HSPLcom/datadog/android/rum/internal/monitor/DatadogRumMonitor$$ExternalSyntheticLambda5;->call()Ljava/lang/Object; +Lcom/datadog/android/rum/internal/monitor/DatadogRumMonitor$$ExternalSyntheticLambda6; +HSPLcom/datadog/android/rum/internal/monitor/DatadogRumMonitor$$ExternalSyntheticLambda6;->(Lcom/datadog/android/rum/internal/domain/RumContext;)V +HSPLcom/datadog/android/rum/internal/monitor/DatadogRumMonitor$$ExternalSyntheticLambda6;->invoke(Ljava/lang/Object;)Ljava/lang/Object; +Lcom/datadog/android/rum/internal/monitor/DatadogRumMonitor$$ExternalSyntheticLambda8; +HSPLcom/datadog/android/rum/internal/monitor/DatadogRumMonitor$$ExternalSyntheticLambda8;->(Lcom/datadog/android/rum/internal/monitor/DatadogRumMonitor;Lcom/datadog/android/rum/internal/domain/scope/RumRawEvent;Lcom/datadog/android/api/context/DatadogContext;Lkotlin/jvm/functions/Function1;)V +HSPLcom/datadog/android/rum/internal/monitor/DatadogRumMonitor$$ExternalSyntheticLambda8;->invoke()Ljava/lang/Object; Lcom/datadog/android/rum/internal/monitor/DatadogRumMonitor$Companion; HSPLcom/datadog/android/rum/internal/monitor/DatadogRumMonitor$Companion;->()V HSPLcom/datadog/android/rum/internal/monitor/DatadogRumMonitor$Companion;->(Lkotlin/jvm/internal/DefaultConstructorMarker;)V -Lcom/datadog/android/rum/internal/monitor/DatadogRumMonitor$handleEvent$2; -HSPLcom/datadog/android/rum/internal/monitor/DatadogRumMonitor$handleEvent$2;->(Lcom/datadog/android/rum/internal/monitor/DatadogRumMonitor;Lcom/datadog/android/rum/internal/domain/scope/RumRawEvent;)V -HSPLcom/datadog/android/rum/internal/monitor/DatadogRumMonitor$handleEvent$2;->invoke$lambda$1(Lcom/datadog/android/rum/internal/monitor/DatadogRumMonitor;Lcom/datadog/android/rum/internal/domain/scope/RumRawEvent;Lcom/datadog/android/api/context/DatadogContext;Lkotlin/jvm/functions/Function1;)Lcom/datadog/android/rum/internal/domain/RumContext; -HSPLcom/datadog/android/rum/internal/monitor/DatadogRumMonitor$handleEvent$2;->invoke(Lcom/datadog/android/api/context/DatadogContext;Lkotlin/jvm/functions/Function1;)V -HSPLcom/datadog/android/rum/internal/monitor/DatadogRumMonitor$handleEvent$2;->invoke(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object; -Lcom/datadog/android/rum/internal/monitor/DatadogRumMonitor$handleEvent$2$$ExternalSyntheticLambda0; -HSPLcom/datadog/android/rum/internal/monitor/DatadogRumMonitor$handleEvent$2$$ExternalSyntheticLambda0;->(Lcom/datadog/android/rum/internal/monitor/DatadogRumMonitor;Lcom/datadog/android/rum/internal/domain/scope/RumRawEvent;Lcom/datadog/android/api/context/DatadogContext;Lkotlin/jvm/functions/Function1;)V -HSPLcom/datadog/android/rum/internal/monitor/DatadogRumMonitor$handleEvent$2$$ExternalSyntheticLambda0;->call()Ljava/lang/Object; -Lcom/datadog/android/rum/internal/monitor/DatadogRumMonitor$handleEvent$2$1; -HSPLcom/datadog/android/rum/internal/monitor/DatadogRumMonitor$handleEvent$2$1;->(Lcom/datadog/android/rum/internal/domain/RumContext;)V -HSPLcom/datadog/android/rum/internal/monitor/DatadogRumMonitor$handleEvent$2$1;->invoke(Ljava/lang/Object;)Ljava/lang/Object; -HSPLcom/datadog/android/rum/internal/monitor/DatadogRumMonitor$handleEvent$2$1;->invoke(Ljava/util/Map;)V -Lcom/datadog/android/rum/internal/monitor/DatadogRumMonitor$handleEventWithMethodCallPerf$1; -HSPLcom/datadog/android/rum/internal/monitor/DatadogRumMonitor$handleEventWithMethodCallPerf$1;->(Lcom/datadog/android/rum/internal/monitor/DatadogRumMonitor;Lcom/datadog/android/rum/internal/domain/scope/RumRawEvent;Lcom/datadog/android/api/context/DatadogContext;Lkotlin/jvm/functions/Function1;)V -HSPLcom/datadog/android/rum/internal/monitor/DatadogRumMonitor$handleEventWithMethodCallPerf$1;->invoke()Lcom/datadog/android/rum/internal/domain/scope/RumScope; -HSPLcom/datadog/android/rum/internal/monitor/DatadogRumMonitor$handleEventWithMethodCallPerf$1;->invoke()Ljava/lang/Object; -Lcom/datadog/android/rum/internal/monitor/DatadogRumMonitor$reportNetworkingLibraryType$1; -HSPLcom/datadog/android/rum/internal/monitor/DatadogRumMonitor$reportNetworkingLibraryType$1;->(Lcom/datadog/android/internal/telemetry/InternalTelemetryEvent$ApiUsage$NetworkInstrumentation$LibraryType;)V -HSPLcom/datadog/android/rum/internal/monitor/DatadogRumMonitor$reportNetworkingLibraryType$1;->invoke()Lcom/datadog/android/internal/telemetry/InternalTelemetryEvent$ApiUsage; -HSPLcom/datadog/android/rum/internal/monitor/DatadogRumMonitor$reportNetworkingLibraryType$1;->invoke()Ljava/lang/Object; Lcom/datadog/android/rum/internal/monitor/StorageEvent; HSPLcom/datadog/android/rum/internal/monitor/StorageEvent;->()V HSPLcom/datadog/android/rum/internal/monitor/StorageEvent;->(Lkotlin/jvm/internal/DefaultConstructorMarker;)V @@ -21231,14 +21429,16 @@ Lcom/datadog/android/rum/internal/startup/RumAppStartupDetector$Companion; HSPLcom/datadog/android/rum/internal/startup/RumAppStartupDetector$Companion;->()V HSPLcom/datadog/android/rum/internal/startup/RumAppStartupDetector$Companion;->()V HSPLcom/datadog/android/rum/internal/startup/RumAppStartupDetector$Companion;->create(Landroid/app/Application;Lcom/datadog/android/core/InternalSdkCore;Lcom/datadog/android/rum/internal/startup/RumAppStartupDetector$Listener;Lcom/datadog/android/rum/startup/AppStartupActivityPredicate;)Lcom/datadog/android/rum/internal/startup/RumAppStartupDetector; -Lcom/datadog/android/rum/internal/startup/RumAppStartupDetector$Companion$create$1; -HSPLcom/datadog/android/rum/internal/startup/RumAppStartupDetector$Companion$create$1;->(Lcom/datadog/android/core/InternalSdkCore;)V -Lcom/datadog/android/rum/internal/startup/RumAppStartupDetector$Companion$create$2; -HSPLcom/datadog/android/rum/internal/startup/RumAppStartupDetector$Companion$create$2;->(Lcom/datadog/android/core/InternalSdkCore;)V +Lcom/datadog/android/rum/internal/startup/RumAppStartupDetector$Companion$$ExternalSyntheticLambda0; +HSPLcom/datadog/android/rum/internal/startup/RumAppStartupDetector$Companion$$ExternalSyntheticLambda0;->(Lcom/datadog/android/core/InternalSdkCore;)V +Lcom/datadog/android/rum/internal/startup/RumAppStartupDetector$Companion$$ExternalSyntheticLambda1; +HSPLcom/datadog/android/rum/internal/startup/RumAppStartupDetector$Companion$$ExternalSyntheticLambda1;->(Lcom/datadog/android/core/InternalSdkCore;)V Lcom/datadog/android/rum/internal/startup/RumAppStartupDetector$Listener; Lcom/datadog/android/rum/internal/startup/RumAppStartupDetectorImpl; HSPLcom/datadog/android/rum/internal/startup/RumAppStartupDetectorImpl;->()V HSPLcom/datadog/android/rum/internal/startup/RumAppStartupDetectorImpl;->(Landroid/app/Application;Lcom/datadog/android/internal/system/BuildSdkVersionProvider;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function0;Lcom/datadog/android/rum/internal/startup/RumAppStartupDetector$Listener;Lcom/datadog/android/rum/startup/AppStartupActivityPredicate;Lcom/datadog/android/rum/internal/startup/RumFirstDrawTimeReporter;)V +HSPLcom/datadog/android/rum/internal/startup/RumAppStartupDetectorImpl;->onActivityResumed(Landroid/app/Activity;)V +HSPLcom/datadog/android/rum/internal/startup/RumAppStartupDetectorImpl;->onActivityStarted(Landroid/app/Activity;)V Lcom/datadog/android/rum/internal/startup/RumAppStartupDetectorImpl$Companion; HSPLcom/datadog/android/rum/internal/startup/RumAppStartupDetectorImpl$Companion;->()V HSPLcom/datadog/android/rum/internal/startup/RumAppStartupDetectorImpl$Companion;->(Lkotlin/jvm/internal/DefaultConstructorMarker;)V @@ -21260,8 +21460,8 @@ Lcom/datadog/android/rum/internal/startup/RumFirstDrawTimeReporter$Companion; HSPLcom/datadog/android/rum/internal/startup/RumFirstDrawTimeReporter$Companion;->()V HSPLcom/datadog/android/rum/internal/startup/RumFirstDrawTimeReporter$Companion;->()V HSPLcom/datadog/android/rum/internal/startup/RumFirstDrawTimeReporter$Companion;->create(Lcom/datadog/android/core/InternalSdkCore;)Lcom/datadog/android/rum/internal/startup/RumFirstDrawTimeReporter; -Lcom/datadog/android/rum/internal/startup/RumFirstDrawTimeReporter$Companion$create$1; -HSPLcom/datadog/android/rum/internal/startup/RumFirstDrawTimeReporter$Companion$create$1;->(Lcom/datadog/android/core/InternalSdkCore;)V +Lcom/datadog/android/rum/internal/startup/RumFirstDrawTimeReporter$Companion$$ExternalSyntheticLambda0; +HSPLcom/datadog/android/rum/internal/startup/RumFirstDrawTimeReporter$Companion$$ExternalSyntheticLambda0;->(Lcom/datadog/android/core/InternalSdkCore;)V Lcom/datadog/android/rum/internal/startup/RumFirstDrawTimeReporterImpl; HSPLcom/datadog/android/rum/internal/startup/RumFirstDrawTimeReporterImpl;->(Lcom/datadog/android/api/InternalLogger;Lkotlin/jvm/functions/Function0;Lcom/datadog/android/rum/internal/utils/window/RumWindowCallbacksRegistry;Landroid/os/Handler;)V Lcom/datadog/android/rum/internal/startup/RumSessionScopeStartupManager; @@ -21294,35 +21494,28 @@ HSPLcom/datadog/android/rum/internal/tracking/NoOpInteractionPredicate;->( Lcom/datadog/android/rum/internal/tracking/NoOpUserActionTrackingStrategy; HSPLcom/datadog/android/rum/internal/tracking/NoOpUserActionTrackingStrategy;->()V Lcom/datadog/android/rum/internal/tracking/UserActionTrackingStrategy; -PLcom/datadog/android/rum/internal/utils/NumberExtKt;->percent(F)D Lcom/datadog/android/rum/internal/utils/RuntimeUtilsKt; HSPLcom/datadog/android/rum/internal/utils/RuntimeUtilsKt;->hasUserData(Lcom/datadog/android/api/context/UserInfo;)Z Lcom/datadog/android/rum/internal/utils/SdkCoreExtKt; HSPLcom/datadog/android/rum/internal/utils/SdkCoreExtKt;->newRumEventWriteOperation$default(Lcom/datadog/android/api/feature/FeatureSdkCore;Lcom/datadog/android/api/context/DatadogContext;Lkotlin/jvm/functions/Function1;Lcom/datadog/android/api/storage/DataWriter;Lcom/datadog/android/api/storage/EventType;Lkotlin/jvm/functions/Function0;ILjava/lang/Object;)Lcom/datadog/android/rum/internal/utils/WriteOperation; HSPLcom/datadog/android/rum/internal/utils/SdkCoreExtKt;->newRumEventWriteOperation(Lcom/datadog/android/api/feature/FeatureSdkCore;Lcom/datadog/android/api/context/DatadogContext;Lkotlin/jvm/functions/Function1;Lcom/datadog/android/api/storage/DataWriter;Lcom/datadog/android/api/storage/EventType;Lkotlin/jvm/functions/Function0;)Lcom/datadog/android/rum/internal/utils/WriteOperation; +Lcom/datadog/android/rum/internal/utils/ViewUtilsKt; +HSPLcom/datadog/android/rum/internal/utils/ViewUtilsKt;->resolveViewUrl(Ljava/lang/Object;)Ljava/lang/String; Lcom/datadog/android/rum/internal/utils/WriteOperation; HSPLcom/datadog/android/rum/internal/utils/WriteOperation;->()V HSPLcom/datadog/android/rum/internal/utils/WriteOperation;->(Lcom/datadog/android/api/feature/FeatureSdkCore;Lcom/datadog/android/api/context/DatadogContext;Lkotlin/jvm/functions/Function1;Lcom/datadog/android/api/storage/DataWriter;Lcom/datadog/android/api/storage/EventType;Lkotlin/jvm/functions/Function0;)V -HSPLcom/datadog/android/rum/internal/utils/WriteOperation;->access$getAdvancedRumMonitor$p(Lcom/datadog/android/rum/internal/utils/WriteOperation;)Lcom/datadog/android/rum/internal/monitor/AdvancedRumMonitor; -HSPLcom/datadog/android/rum/internal/utils/WriteOperation;->access$getEventSource$p(Lcom/datadog/android/rum/internal/utils/WriteOperation;)Lkotlin/jvm/functions/Function0; -HSPLcom/datadog/android/rum/internal/utils/WriteOperation;->access$getEventType$p(Lcom/datadog/android/rum/internal/utils/WriteOperation;)Lcom/datadog/android/api/storage/EventType; -HSPLcom/datadog/android/rum/internal/utils/WriteOperation;->access$getOnSuccess$p(Lcom/datadog/android/rum/internal/utils/WriteOperation;)Lkotlin/jvm/functions/Function1; -HSPLcom/datadog/android/rum/internal/utils/WriteOperation;->access$getRumDataWriter$p(Lcom/datadog/android/rum/internal/utils/WriteOperation;)Lcom/datadog/android/api/storage/DataWriter; HSPLcom/datadog/android/rum/internal/utils/WriteOperation;->onError(Lkotlin/jvm/functions/Function1;)Lcom/datadog/android/rum/internal/utils/WriteOperation; HSPLcom/datadog/android/rum/internal/utils/WriteOperation;->onSuccess(Lkotlin/jvm/functions/Function1;)Lcom/datadog/android/rum/internal/utils/WriteOperation; +HSPLcom/datadog/android/rum/internal/utils/WriteOperation;->submit$lambda$3(Lcom/datadog/android/rum/internal/utils/WriteOperation;Lcom/datadog/android/api/storage/EventBatchWriter;)Lkotlin/Unit; HSPLcom/datadog/android/rum/internal/utils/WriteOperation;->submit()V +Lcom/datadog/android/rum/internal/utils/WriteOperation$$ExternalSyntheticLambda1; +HSPLcom/datadog/android/rum/internal/utils/WriteOperation$$ExternalSyntheticLambda1;->()V +Lcom/datadog/android/rum/internal/utils/WriteOperation$$ExternalSyntheticLambda2; +HSPLcom/datadog/android/rum/internal/utils/WriteOperation$$ExternalSyntheticLambda2;->(Lcom/datadog/android/rum/internal/utils/WriteOperation;)V +HSPLcom/datadog/android/rum/internal/utils/WriteOperation$$ExternalSyntheticLambda2;->invoke(Ljava/lang/Object;)Ljava/lang/Object; Lcom/datadog/android/rum/internal/utils/WriteOperation$Companion; HSPLcom/datadog/android/rum/internal/utils/WriteOperation$Companion;->()V HSPLcom/datadog/android/rum/internal/utils/WriteOperation$Companion;->(Lkotlin/jvm/internal/DefaultConstructorMarker;)V -Lcom/datadog/android/rum/internal/utils/WriteOperation$Companion$NO_OP_EVENT_OUTCOME_ACTION$1; -HSPLcom/datadog/android/rum/internal/utils/WriteOperation$Companion$NO_OP_EVENT_OUTCOME_ACTION$1;->()V -HSPLcom/datadog/android/rum/internal/utils/WriteOperation$Companion$NO_OP_EVENT_OUTCOME_ACTION$1;->()V -HSPLcom/datadog/android/rum/internal/utils/WriteOperation$Companion$NO_OP_EVENT_OUTCOME_ACTION$1;->invoke(Lcom/datadog/android/rum/internal/monitor/AdvancedRumMonitor;)V -HSPLcom/datadog/android/rum/internal/utils/WriteOperation$Companion$NO_OP_EVENT_OUTCOME_ACTION$1;->invoke(Ljava/lang/Object;)Ljava/lang/Object; -Lcom/datadog/android/rum/internal/utils/WriteOperation$submit$1; -HSPLcom/datadog/android/rum/internal/utils/WriteOperation$submit$1;->(Lcom/datadog/android/rum/internal/utils/WriteOperation;)V -HSPLcom/datadog/android/rum/internal/utils/WriteOperation$submit$1;->invoke(Lcom/datadog/android/api/storage/EventBatchWriter;)V -HSPLcom/datadog/android/rum/internal/utils/WriteOperation$submit$1;->invoke(Ljava/lang/Object;)Ljava/lang/Object; Lcom/datadog/android/rum/internal/utils/window/RumWindowCallbacksRegistry; Lcom/datadog/android/rum/internal/utils/window/RumWindowCallbacksRegistryImpl; HSPLcom/datadog/android/rum/internal/utils/window/RumWindowCallbacksRegistryImpl;->()V @@ -21332,9 +21525,29 @@ Lcom/datadog/android/rum/internal/vitals/FrameStatesAggregator; HSPLcom/datadog/android/rum/internal/vitals/FrameStatesAggregator;->()V HSPLcom/datadog/android/rum/internal/vitals/FrameStatesAggregator;->(Ljava/util/List;Lcom/datadog/android/api/InternalLogger;Lcom/datadog/android/rum/internal/vitals/JankStatsProvider;Lcom/datadog/android/internal/system/BuildSdkVersionProvider;)V HSPLcom/datadog/android/rum/internal/vitals/FrameStatesAggregator;->(Ljava/util/List;Lcom/datadog/android/api/InternalLogger;Lcom/datadog/android/rum/internal/vitals/JankStatsProvider;Lcom/datadog/android/internal/system/BuildSdkVersionProvider;ILkotlin/jvm/internal/DefaultConstructorMarker;)V +HSPLcom/datadog/android/rum/internal/vitals/FrameStatesAggregator;->access$getFrameMetricsData$p(Lcom/datadog/android/rum/internal/vitals/FrameStatesAggregator;)Lcom/datadog/android/rum/internal/domain/FrameMetricsData; +HSPLcom/datadog/android/rum/internal/vitals/FrameStatesAggregator;->access$update(Lcom/datadog/android/rum/internal/vitals/FrameStatesAggregator;Lcom/datadog/android/rum/internal/domain/FrameMetricsData;Landroid/view/FrameMetrics;I)Lcom/datadog/android/rum/internal/domain/FrameMetricsData; +HSPLcom/datadog/android/rum/internal/vitals/FrameStatesAggregator;->getFrameStateListeners$dd_sdk_android_rum_release()Ljava/util/List; +HSPLcom/datadog/android/rum/internal/vitals/FrameStatesAggregator;->onActivityResumed(Landroid/app/Activity;)V +HSPLcom/datadog/android/rum/internal/vitals/FrameStatesAggregator;->onActivityStarted(Landroid/app/Activity;)V +PLcom/datadog/android/rum/internal/vitals/FrameStatesAggregator;->onFrame(Landroidx/metrics/performance/FrameData;)V +HSPLcom/datadog/android/rum/internal/vitals/FrameStatesAggregator;->registerMetricListener$lambda$15(Landroid/view/View;Lcom/datadog/android/rum/internal/vitals/FrameStatesAggregator;Landroid/view/Window;Landroid/os/Handler;)V +HSPLcom/datadog/android/rum/internal/vitals/FrameStatesAggregator;->registerMetricListener(Landroid/view/Window;)V +HSPLcom/datadog/android/rum/internal/vitals/FrameStatesAggregator;->trackActivity(Landroid/view/Window;Landroid/app/Activity;)V +HSPLcom/datadog/android/rum/internal/vitals/FrameStatesAggregator;->trackWindowJankStats(Landroid/view/Window;)V +HSPLcom/datadog/android/rum/internal/vitals/FrameStatesAggregator;->trackWindowMetrics(ZLandroid/view/Window;Landroid/app/Activity;)V +HSPLcom/datadog/android/rum/internal/vitals/FrameStatesAggregator;->update(Lcom/datadog/android/rum/internal/domain/FrameMetricsData;Landroid/view/FrameMetrics;I)Lcom/datadog/android/rum/internal/domain/FrameMetricsData; +Lcom/datadog/android/rum/internal/vitals/FrameStatesAggregator$$ExternalSyntheticLambda5; +HSPLcom/datadog/android/rum/internal/vitals/FrameStatesAggregator$$ExternalSyntheticLambda5;->(Landroid/view/View;Lcom/datadog/android/rum/internal/vitals/FrameStatesAggregator;Landroid/view/Window;Landroid/os/Handler;)V +HSPLcom/datadog/android/rum/internal/vitals/FrameStatesAggregator$$ExternalSyntheticLambda5;->run()V +Lcom/datadog/android/rum/internal/vitals/FrameStatesAggregator$$ExternalSyntheticLambda7; +HSPLcom/datadog/android/rum/internal/vitals/FrameStatesAggregator$$ExternalSyntheticLambda7;->(Landroid/view/Window;)V Lcom/datadog/android/rum/internal/vitals/FrameStatesAggregator$Companion; HSPLcom/datadog/android/rum/internal/vitals/FrameStatesAggregator$Companion;->()V HSPLcom/datadog/android/rum/internal/vitals/FrameStatesAggregator$Companion;->(Lkotlin/jvm/internal/DefaultConstructorMarker;)V +Lcom/datadog/android/rum/internal/vitals/FrameStatesAggregator$DDFrameMetricsListener; +HSPLcom/datadog/android/rum/internal/vitals/FrameStatesAggregator$DDFrameMetricsListener;->(Lcom/datadog/android/rum/internal/vitals/FrameStatesAggregator;)V +HSPLcom/datadog/android/rum/internal/vitals/FrameStatesAggregator$DDFrameMetricsListener;->onFrameMetricsAvailable(Landroid/view/Window;Landroid/view/FrameMetrics;I)V Lcom/datadog/android/rum/internal/vitals/JankStatsProvider; HSPLcom/datadog/android/rum/internal/vitals/JankStatsProvider;->()V Lcom/datadog/android/rum/internal/vitals/JankStatsProvider$Companion; @@ -21343,9 +21556,11 @@ HSPLcom/datadog/android/rum/internal/vitals/JankStatsProvider$Companion;-> HSPLcom/datadog/android/rum/internal/vitals/JankStatsProvider$Companion;->getDEFAULT()Lcom/datadog/android/rum/internal/vitals/JankStatsProvider; Lcom/datadog/android/rum/internal/vitals/JankStatsProvider$Companion$DEFAULT$1; HSPLcom/datadog/android/rum/internal/vitals/JankStatsProvider$Companion$DEFAULT$1;->()V +HSPLcom/datadog/android/rum/internal/vitals/JankStatsProvider$Companion$DEFAULT$1;->createJankStatsAndTrack(Landroid/view/Window;Landroidx/metrics/performance/JankStats$OnFrameListener;Lcom/datadog/android/api/InternalLogger;)Landroidx/metrics/performance/JankStats; Lcom/datadog/android/rum/internal/vitals/NoOpVitalMonitor; HSPLcom/datadog/android/rum/internal/vitals/NoOpVitalMonitor;->()V HSPLcom/datadog/android/rum/internal/vitals/NoOpVitalMonitor;->register(Lcom/datadog/android/rum/internal/vitals/VitalListener;)V +HSPLcom/datadog/android/rum/internal/vitals/NoOpVitalMonitor;->unregister(Lcom/datadog/android/rum/internal/vitals/VitalListener;)V Lcom/datadog/android/rum/internal/vitals/VitalInfo; Lcom/datadog/android/rum/internal/vitals/VitalListener; Lcom/datadog/android/rum/internal/vitals/VitalMonitor; @@ -21358,7 +21573,6 @@ HSPLcom/datadog/android/rum/metric/interactiontonextview/TimeBasedInteractionIde HSPLcom/datadog/android/rum/metric/interactiontonextview/TimeBasedInteractionIdentifier;->(J)V HSPLcom/datadog/android/rum/metric/interactiontonextview/TimeBasedInteractionIdentifier;->(JILkotlin/jvm/internal/DefaultConstructorMarker;)V HSPLcom/datadog/android/rum/metric/interactiontonextview/TimeBasedInteractionIdentifier;->defaultThresholdUsed$dd_sdk_android_rum_release()Z -PLcom/datadog/android/rum/metric/interactiontonextview/TimeBasedInteractionIdentifier;->getTimeThresholdInMilliseconds$dd_sdk_android_rum_release()J Lcom/datadog/android/rum/metric/interactiontonextview/TimeBasedInteractionIdentifier$Companion; HSPLcom/datadog/android/rum/metric/interactiontonextview/TimeBasedInteractionIdentifier$Companion;->()V HSPLcom/datadog/android/rum/metric/interactiontonextview/TimeBasedInteractionIdentifier$Companion;->(Lkotlin/jvm/internal/DefaultConstructorMarker;)V @@ -21369,7 +21583,7 @@ Lcom/datadog/android/rum/metric/networksettled/TimeBasedInitialResourceIdentifie HSPLcom/datadog/android/rum/metric/networksettled/TimeBasedInitialResourceIdentifier;->()V HSPLcom/datadog/android/rum/metric/networksettled/TimeBasedInitialResourceIdentifier;->(J)V HSPLcom/datadog/android/rum/metric/networksettled/TimeBasedInitialResourceIdentifier;->(JILkotlin/jvm/internal/DefaultConstructorMarker;)V -PLcom/datadog/android/rum/metric/networksettled/TimeBasedInitialResourceIdentifier;->getTimeThresholdInMilliseconds$dd_sdk_android_rum_release()J +HSPLcom/datadog/android/rum/metric/networksettled/TimeBasedInitialResourceIdentifier;->defaultThresholdUsed$dd_sdk_android_rum_release()Z Lcom/datadog/android/rum/metric/networksettled/TimeBasedInitialResourceIdentifier$Companion; HSPLcom/datadog/android/rum/metric/networksettled/TimeBasedInitialResourceIdentifier$Companion;->()V HSPLcom/datadog/android/rum/metric/networksettled/TimeBasedInitialResourceIdentifier$Companion;->(Lkotlin/jvm/internal/DefaultConstructorMarker;)V @@ -21513,8 +21727,8 @@ HSPLcom/datadog/android/rum/model/LongTaskEvent$LongTaskEventSource;->$values()[ HSPLcom/datadog/android/rum/model/LongTaskEvent$LongTaskEventSource;->()V HSPLcom/datadog/android/rum/model/LongTaskEvent$LongTaskEventSource;->(Ljava/lang/String;ILjava/lang/String;)V HSPLcom/datadog/android/rum/model/LongTaskEvent$LongTaskEventSource;->access$getJsonValue$p(Lcom/datadog/android/rum/model/LongTaskEvent$LongTaskEventSource;)Ljava/lang/String; +HSPLcom/datadog/android/rum/model/LongTaskEvent$LongTaskEventSource;->getEntries()Lkotlin/enums/EnumEntries; HSPLcom/datadog/android/rum/model/LongTaskEvent$LongTaskEventSource;->toJson()Lcom/google/gson/JsonElement; -HSPLcom/datadog/android/rum/model/LongTaskEvent$LongTaskEventSource;->values()[Lcom/datadog/android/rum/model/LongTaskEvent$LongTaskEventSource; Lcom/datadog/android/rum/model/LongTaskEvent$LongTaskEventSource$Companion; HSPLcom/datadog/android/rum/model/LongTaskEvent$LongTaskEventSource$Companion;->()V HSPLcom/datadog/android/rum/model/LongTaskEvent$LongTaskEventSource$Companion;->(Lkotlin/jvm/internal/DefaultConstructorMarker;)V @@ -21575,6 +21789,14 @@ HSPLcom/datadog/android/rum/model/ViewEvent;->getUsr()Lcom/datadog/android/rum/m HSPLcom/datadog/android/rum/model/ViewEvent;->getView()Lcom/datadog/android/rum/model/ViewEvent$ViewEventView; HSPLcom/datadog/android/rum/model/ViewEvent;->toJson()Lcom/google/gson/JsonElement; Lcom/datadog/android/rum/model/ViewEvent$Accessibility; +HSPLcom/datadog/android/rum/model/ViewEvent$Accessibility;->()V +HSPLcom/datadog/android/rum/model/ViewEvent$Accessibility;->(Ljava/lang/String;Ljava/lang/Boolean;Ljava/lang/Boolean;Ljava/lang/Boolean;Ljava/lang/Boolean;Ljava/lang/Boolean;Ljava/lang/Boolean;Ljava/lang/Boolean;Ljava/lang/Boolean;Ljava/lang/Boolean;Ljava/lang/Boolean;Ljava/lang/Boolean;Ljava/lang/Boolean;Ljava/lang/Boolean;Ljava/lang/Boolean;Ljava/lang/Boolean;Ljava/lang/Boolean;Ljava/lang/Boolean;Ljava/lang/Boolean;Ljava/lang/Boolean;Ljava/lang/Boolean;Ljava/lang/Boolean;)V +HSPLcom/datadog/android/rum/model/ViewEvent$Accessibility;->(Ljava/lang/String;Ljava/lang/Boolean;Ljava/lang/Boolean;Ljava/lang/Boolean;Ljava/lang/Boolean;Ljava/lang/Boolean;Ljava/lang/Boolean;Ljava/lang/Boolean;Ljava/lang/Boolean;Ljava/lang/Boolean;Ljava/lang/Boolean;Ljava/lang/Boolean;Ljava/lang/Boolean;Ljava/lang/Boolean;Ljava/lang/Boolean;Ljava/lang/Boolean;Ljava/lang/Boolean;Ljava/lang/Boolean;Ljava/lang/Boolean;Ljava/lang/Boolean;Ljava/lang/Boolean;Ljava/lang/Boolean;ILkotlin/jvm/internal/DefaultConstructorMarker;)V +HSPLcom/datadog/android/rum/model/ViewEvent$Accessibility;->equals(Ljava/lang/Object;)Z +HSPLcom/datadog/android/rum/model/ViewEvent$Accessibility;->toJson()Lcom/google/gson/JsonElement; +Lcom/datadog/android/rum/model/ViewEvent$Accessibility$Companion; +HSPLcom/datadog/android/rum/model/ViewEvent$Accessibility$Companion;->()V +HSPLcom/datadog/android/rum/model/ViewEvent$Accessibility$Companion;->(Lkotlin/jvm/internal/DefaultConstructorMarker;)V Lcom/datadog/android/rum/model/ViewEvent$Account; Lcom/datadog/android/rum/model/ViewEvent$Action; HSPLcom/datadog/android/rum/model/ViewEvent$Action;->()V @@ -21597,8 +21819,8 @@ HSPLcom/datadog/android/rum/model/ViewEvent$Companion;->()V HSPLcom/datadog/android/rum/model/ViewEvent$Companion;->(Lkotlin/jvm/internal/DefaultConstructorMarker;)V Lcom/datadog/android/rum/model/ViewEvent$Configuration; HSPLcom/datadog/android/rum/model/ViewEvent$Configuration;->()V -HSPLcom/datadog/android/rum/model/ViewEvent$Configuration;->(Ljava/lang/Number;Ljava/lang/Number;Ljava/lang/Number;Ljava/lang/Number;Ljava/util/List;Ljava/lang/Boolean;)V -HSPLcom/datadog/android/rum/model/ViewEvent$Configuration;->(Ljava/lang/Number;Ljava/lang/Number;Ljava/lang/Number;Ljava/lang/Number;Ljava/util/List;Ljava/lang/Boolean;ILkotlin/jvm/internal/DefaultConstructorMarker;)V +HSPLcom/datadog/android/rum/model/ViewEvent$Configuration;->(Ljava/lang/Number;Ljava/lang/Number;Ljava/lang/Number;Ljava/lang/Number;Ljava/util/List;Ljava/lang/Boolean;Ljava/lang/String;)V +HSPLcom/datadog/android/rum/model/ViewEvent$Configuration;->(Ljava/lang/Number;Ljava/lang/Number;Ljava/lang/Number;Ljava/lang/Number;Ljava/util/List;Ljava/lang/Boolean;Ljava/lang/String;ILkotlin/jvm/internal/DefaultConstructorMarker;)V HSPLcom/datadog/android/rum/model/ViewEvent$Configuration;->toJson()Lcom/google/gson/JsonElement; Lcom/datadog/android/rum/model/ViewEvent$Configuration$Companion; HSPLcom/datadog/android/rum/model/ViewEvent$Configuration$Companion;->()V @@ -21782,8 +22004,8 @@ HSPLcom/datadog/android/rum/model/ViewEvent$ViewEventSource;->$values()[Lcom/dat HSPLcom/datadog/android/rum/model/ViewEvent$ViewEventSource;->()V HSPLcom/datadog/android/rum/model/ViewEvent$ViewEventSource;->(Ljava/lang/String;ILjava/lang/String;)V HSPLcom/datadog/android/rum/model/ViewEvent$ViewEventSource;->access$getJsonValue$p(Lcom/datadog/android/rum/model/ViewEvent$ViewEventSource;)Ljava/lang/String; +HSPLcom/datadog/android/rum/model/ViewEvent$ViewEventSource;->getEntries()Lkotlin/enums/EnumEntries; HSPLcom/datadog/android/rum/model/ViewEvent$ViewEventSource;->toJson()Lcom/google/gson/JsonElement; -HSPLcom/datadog/android/rum/model/ViewEvent$ViewEventSource;->values()[Lcom/datadog/android/rum/model/ViewEvent$ViewEventSource; Lcom/datadog/android/rum/model/ViewEvent$ViewEventSource$Companion; HSPLcom/datadog/android/rum/model/ViewEvent$ViewEventSource$Companion;->()V HSPLcom/datadog/android/rum/model/ViewEvent$ViewEventSource$Companion;->(Lkotlin/jvm/internal/DefaultConstructorMarker;)V @@ -21799,10 +22021,12 @@ HSPLcom/datadog/android/rum/model/ViewEvent$ViewEventView;->getCustomTimings()Lc HSPLcom/datadog/android/rum/model/ViewEvent$ViewEventView;->getId()Ljava/lang/String; HSPLcom/datadog/android/rum/model/ViewEvent$ViewEventView;->getTimeSpent()J HSPLcom/datadog/android/rum/model/ViewEvent$ViewEventView;->getUrl()Ljava/lang/String; +HSPLcom/datadog/android/rum/model/ViewEvent$ViewEventView;->isActive()Ljava/lang/Boolean; HSPLcom/datadog/android/rum/model/ViewEvent$ViewEventView;->toJson()Lcom/google/gson/JsonElement; Lcom/datadog/android/rum/model/ViewEvent$ViewEventView$Companion; HSPLcom/datadog/android/rum/model/ViewEvent$ViewEventView$Companion;->()V HSPLcom/datadog/android/rum/model/ViewEvent$ViewEventView$Companion;->(Lkotlin/jvm/internal/DefaultConstructorMarker;)V +Lcom/datadog/android/rum/model/ViewUpdateEvent; Lcom/datadog/android/rum/operations/FailureReason; HSPLcom/datadog/android/rum/operations/FailureReason;->$values()[Lcom/datadog/android/rum/operations/FailureReason; HSPLcom/datadog/android/rum/operations/FailureReason;->()V @@ -21813,12 +22037,21 @@ Lcom/datadog/android/rum/startup/AppStartupActivityPredicate; Lcom/datadog/android/rum/timeseries/TimeseriesConfiguration; Lcom/datadog/android/rum/tracking/AcceptAllActivities; HSPLcom/datadog/android/rum/tracking/AcceptAllActivities;->()V +HSPLcom/datadog/android/rum/tracking/AcceptAllActivities;->accept(Landroid/app/Activity;)Z +HSPLcom/datadog/android/rum/tracking/AcceptAllActivities;->accept(Ljava/lang/Object;)Z +HSPLcom/datadog/android/rum/tracking/AcceptAllActivities;->getViewName(Landroid/app/Activity;)Ljava/lang/String; +HSPLcom/datadog/android/rum/tracking/AcceptAllActivities;->getViewName(Ljava/lang/Object;)Ljava/lang/String; Lcom/datadog/android/rum/tracking/ActionTrackingStrategy; Lcom/datadog/android/rum/tracking/ActivityLifecycleTrackingStrategy; HSPLcom/datadog/android/rum/tracking/ActivityLifecycleTrackingStrategy;->()V HSPLcom/datadog/android/rum/tracking/ActivityLifecycleTrackingStrategy;->()V +HSPLcom/datadog/android/rum/tracking/ActivityLifecycleTrackingStrategy;->getInternalLogger$dd_sdk_android_rum_release()Lcom/datadog/android/api/InternalLogger; +HSPLcom/datadog/android/rum/tracking/ActivityLifecycleTrackingStrategy;->getSdkCore()Lcom/datadog/android/api/feature/FeatureSdkCore; +HSPLcom/datadog/android/rum/tracking/ActivityLifecycleTrackingStrategy;->onActivityResumed(Landroid/app/Activity;)V +HSPLcom/datadog/android/rum/tracking/ActivityLifecycleTrackingStrategy;->onActivityStarted(Landroid/app/Activity;)V HSPLcom/datadog/android/rum/tracking/ActivityLifecycleTrackingStrategy;->register(Lcom/datadog/android/api/SdkCore;Landroid/content/Context;)V HSPLcom/datadog/android/rum/tracking/ActivityLifecycleTrackingStrategy;->setSdkCore(Lcom/datadog/android/api/feature/FeatureSdkCore;)V +HSPLcom/datadog/android/rum/tracking/ActivityLifecycleTrackingStrategy;->withSdkCore(Lkotlin/jvm/functions/Function1;)Ljava/lang/Object; Lcom/datadog/android/rum/tracking/ActivityLifecycleTrackingStrategy$Companion; HSPLcom/datadog/android/rum/tracking/ActivityLifecycleTrackingStrategy$Companion;->()V HSPLcom/datadog/android/rum/tracking/ActivityLifecycleTrackingStrategy$Companion;->(Lkotlin/jvm/internal/DefaultConstructorMarker;)V @@ -21826,11 +22059,17 @@ Lcom/datadog/android/rum/tracking/ActivityViewTrackingStrategy; HSPLcom/datadog/android/rum/tracking/ActivityViewTrackingStrategy;->()V HSPLcom/datadog/android/rum/tracking/ActivityViewTrackingStrategy;->(ZLcom/datadog/android/rum/tracking/ComponentPredicate;)V HSPLcom/datadog/android/rum/tracking/ActivityViewTrackingStrategy;->(ZLcom/datadog/android/rum/tracking/ComponentPredicate;ILkotlin/jvm/internal/DefaultConstructorMarker;)V +HSPLcom/datadog/android/rum/tracking/ActivityViewTrackingStrategy;->getRumMonitor$lambda$4(Lcom/datadog/android/api/feature/FeatureSdkCore;)Lcom/datadog/android/rum/RumMonitor; +HSPLcom/datadog/android/rum/tracking/ActivityViewTrackingStrategy;->getRumMonitor()Lcom/datadog/android/rum/RumMonitor; +HSPLcom/datadog/android/rum/tracking/ActivityViewTrackingStrategy;->onActivityResumed(Landroid/app/Activity;)V +Lcom/datadog/android/rum/tracking/ActivityViewTrackingStrategy$$ExternalSyntheticLambda0; +HSPLcom/datadog/android/rum/tracking/ActivityViewTrackingStrategy$$ExternalSyntheticLambda0;->()V +HSPLcom/datadog/android/rum/tracking/ActivityViewTrackingStrategy$$ExternalSyntheticLambda0;->invoke(Ljava/lang/Object;)Ljava/lang/Object; +Lcom/datadog/android/rum/tracking/ActivityViewTrackingStrategy$$ExternalSyntheticLambda1; +HSPLcom/datadog/android/rum/tracking/ActivityViewTrackingStrategy$$ExternalSyntheticLambda1;->(Lcom/datadog/android/rum/tracking/ActivityViewTrackingStrategy;)V Lcom/datadog/android/rum/tracking/ActivityViewTrackingStrategy$Companion; HSPLcom/datadog/android/rum/tracking/ActivityViewTrackingStrategy$Companion;->()V HSPLcom/datadog/android/rum/tracking/ActivityViewTrackingStrategy$Companion;->(Lkotlin/jvm/internal/DefaultConstructorMarker;)V -Lcom/datadog/android/rum/tracking/ActivityViewTrackingStrategy$executor$2; -HSPLcom/datadog/android/rum/tracking/ActivityViewTrackingStrategy$executor$2;->(Lcom/datadog/android/rum/tracking/ActivityViewTrackingStrategy;)V Lcom/datadog/android/rum/tracking/ComponentPredicate; Lcom/datadog/android/rum/tracking/InteractionPredicate; Lcom/datadog/android/rum/tracking/NoOpActionTrackingStrategy; @@ -21843,35 +22082,19 @@ Lcom/datadog/android/rum/tracking/TrackingStrategy; Lcom/datadog/android/rum/tracking/ViewAttributesProvider; Lcom/datadog/android/rum/tracking/ViewTrackingStrategy; Lcom/datadog/android/security/Encryption; -PLcom/datadog/android/telemetry/internal/TelemetryEventExtKt;->tryFromSource(Lcom/datadog/android/telemetry/model/TelemetryConfigurationEvent$Source$Companion;Ljava/lang/String;Lcom/datadog/android/api/InternalLogger;)Lcom/datadog/android/telemetry/model/TelemetryConfigurationEvent$Source; Lcom/datadog/android/telemetry/internal/TelemetryEventHandler; HSPLcom/datadog/android/telemetry/internal/TelemetryEventHandler;->()V HSPLcom/datadog/android/telemetry/internal/TelemetryEventHandler;->(Lcom/datadog/android/core/InternalSdkCore;Lcom/datadog/android/core/sampling/Sampler;Lcom/datadog/android/core/sampling/Sampler;Lcom/datadog/android/rum/internal/metric/SessionMetricDispatcher;I)V HSPLcom/datadog/android/telemetry/internal/TelemetryEventHandler;->(Lcom/datadog/android/core/InternalSdkCore;Lcom/datadog/android/core/sampling/Sampler;Lcom/datadog/android/core/sampling/Sampler;Lcom/datadog/android/rum/internal/metric/SessionMetricDispatcher;IILkotlin/jvm/internal/DefaultConstructorMarker;)V -HSPLcom/datadog/android/telemetry/internal/TelemetryEventHandler;->access$setTrackNetworkRequests$p(Lcom/datadog/android/telemetry/internal/TelemetryEventHandler;Z)V HSPLcom/datadog/android/telemetry/internal/TelemetryEventHandler;->canWrite(Lcom/datadog/android/internal/telemetry/InternalTelemetryEvent;)Z -PLcom/datadog/android/telemetry/internal/TelemetryEventHandler;->computeEffectiveSampleRate(Ljava/util/Map;Ljava/lang/Float;)F -PLcom/datadog/android/telemetry/internal/TelemetryEventHandler;->createConfigurationEvent(Lcom/datadog/android/api/context/DatadogContext;JLcom/datadog/android/internal/telemetry/InternalTelemetryEvent$Configuration;F)Lcom/datadog/android/telemetry/model/TelemetryConfigurationEvent; -PLcom/datadog/android/telemetry/internal/TelemetryEventHandler;->getRumConfig()Lcom/datadog/android/rum/internal/RumFeature$Configuration; +HSPLcom/datadog/android/telemetry/internal/TelemetryEventHandler;->handleEvent$lambda$1(Lcom/datadog/android/rum/internal/domain/scope/RumRawEvent$TelemetryEventWrapper;Lcom/datadog/android/internal/telemetry/InternalTelemetryEvent;Lcom/datadog/android/telemetry/internal/TelemetryEventHandler;Lcom/datadog/android/api/storage/DataWriter;Lcom/datadog/android/api/context/DatadogContext;Lkotlin/jvm/functions/Function1;)Lkotlin/Unit; HSPLcom/datadog/android/telemetry/internal/TelemetryEventHandler;->handleEvent(Lcom/datadog/android/rum/internal/domain/scope/RumRawEvent$TelemetryEventWrapper;Lcom/datadog/android/api/storage/DataWriter;)V -HSPLcom/datadog/android/telemetry/internal/TelemetryEventHandler;->isLog(Lcom/datadog/android/internal/telemetry/InternalTelemetryEvent;)Z -PLcom/datadog/android/telemetry/internal/TelemetryEventHandler;->isOpenTelemetryRegistered(Ljava/util/Map;)Z HSPLcom/datadog/android/telemetry/internal/TelemetryEventHandler;->onSessionStarted(Ljava/lang/String;Z)V -PLcom/datadog/android/telemetry/internal/TelemetryEventHandler;->resolveOpenTelemetryApiVersion(Lcom/datadog/android/telemetry/internal/TelemetryEventHandler$TracerApi;Ljava/util/Map;)Ljava/lang/String; -PLcom/datadog/android/telemetry/internal/TelemetryEventHandler;->resolveTracerApi(Ljava/util/Map;)Lcom/datadog/android/telemetry/internal/TelemetryEventHandler$TracerApi; -PLcom/datadog/android/telemetry/internal/TelemetryEventHandler;->rumContext(Lcom/datadog/android/api/context/DatadogContext;)Lcom/datadog/android/rum/internal/domain/RumContext; +Lcom/datadog/android/telemetry/internal/TelemetryEventHandler$$ExternalSyntheticLambda0; +HSPLcom/datadog/android/telemetry/internal/TelemetryEventHandler$$ExternalSyntheticLambda0;->invoke(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object; Lcom/datadog/android/telemetry/internal/TelemetryEventHandler$Companion; HSPLcom/datadog/android/telemetry/internal/TelemetryEventHandler$Companion;->()V HSPLcom/datadog/android/telemetry/internal/TelemetryEventHandler$Companion;->(Lkotlin/jvm/internal/DefaultConstructorMarker;)V -PLcom/datadog/android/telemetry/internal/TelemetryEventHandler$TracerApi;->$values()[Lcom/datadog/android/telemetry/internal/TelemetryEventHandler$TracerApi; -PLcom/datadog/android/telemetry/internal/TelemetryEventHandler$TracerApi;->()V -PLcom/datadog/android/telemetry/internal/TelemetryEventHandler$TracerApi;->(Ljava/lang/String;I)V -Lcom/datadog/android/telemetry/internal/TelemetryEventHandler$handleEvent$1; -HSPLcom/datadog/android/telemetry/internal/TelemetryEventHandler$handleEvent$1;->(Lcom/datadog/android/rum/internal/domain/scope/RumRawEvent$TelemetryEventWrapper;Lcom/datadog/android/internal/telemetry/InternalTelemetryEvent;Lcom/datadog/android/telemetry/internal/TelemetryEventHandler;Lcom/datadog/android/api/storage/DataWriter;)V -HSPLcom/datadog/android/telemetry/internal/TelemetryEventHandler$handleEvent$1;->invoke(Lcom/datadog/android/api/context/DatadogContext;Lkotlin/jvm/functions/Function1;)V -HSPLcom/datadog/android/telemetry/internal/TelemetryEventHandler$handleEvent$1;->invoke(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object; -PLcom/datadog/android/telemetry/internal/TelemetryEventHandler$handleEvent$1$1;->invoke(Lcom/datadog/android/api/storage/EventBatchWriter;)V -PLcom/datadog/android/telemetry/internal/TelemetryEventHandler$handleEvent$1$1;->invoke(Ljava/lang/Object;)Ljava/lang/Object; Lcom/datadog/android/telemetry/internal/TelemetryEventId; HSPLcom/datadog/android/telemetry/internal/TelemetryEventId;->(Lcom/datadog/android/telemetry/internal/TelemetryType;Ljava/lang/String;Ljava/lang/String;)V HSPLcom/datadog/android/telemetry/internal/TelemetryEventId;->hashCode()I @@ -21882,77 +22105,6 @@ Lcom/datadog/android/telemetry/internal/TelemetryType; HSPLcom/datadog/android/telemetry/internal/TelemetryType;->$values()[Lcom/datadog/android/telemetry/internal/TelemetryType; HSPLcom/datadog/android/telemetry/internal/TelemetryType;->()V HSPLcom/datadog/android/telemetry/internal/TelemetryType;->(Ljava/lang/String;I)V -PLcom/datadog/android/telemetry/internal/TracingHeaderTypesSetUtilsKt;->toSelectedTracingPropagator(Lcom/datadog/android/internal/telemetry/TracingHeaderType;)Lcom/datadog/android/telemetry/model/TelemetryConfigurationEvent$SelectedTracingPropagator; -PLcom/datadog/android/telemetry/internal/TracingHeaderTypesSetUtilsKt;->toSelectedTracingPropagators(Lcom/datadog/android/internal/telemetry/TracingHeaderTypesSet;)Ljava/util/List; -PLcom/datadog/android/telemetry/internal/TracingHeaderTypesSetUtilsKt$WhenMappings;->()V -PLcom/datadog/android/telemetry/model/TelemetryConfigurationEvent;->()V -PLcom/datadog/android/telemetry/model/TelemetryConfigurationEvent;->(Lcom/datadog/android/telemetry/model/TelemetryConfigurationEvent$Dd;JLjava/lang/String;Lcom/datadog/android/telemetry/model/TelemetryConfigurationEvent$Source;Ljava/lang/String;Lcom/datadog/android/telemetry/model/TelemetryConfigurationEvent$Application;Lcom/datadog/android/telemetry/model/TelemetryConfigurationEvent$Session;Lcom/datadog/android/telemetry/model/TelemetryConfigurationEvent$View;Lcom/datadog/android/telemetry/model/TelemetryConfigurationEvent$Action;Ljava/lang/Number;Ljava/util/List;Lcom/datadog/android/telemetry/model/TelemetryConfigurationEvent$Telemetry;)V -PLcom/datadog/android/telemetry/model/TelemetryConfigurationEvent;->toJson()Lcom/google/gson/JsonElement; -PLcom/datadog/android/telemetry/model/TelemetryConfigurationEvent$Application;->()V -PLcom/datadog/android/telemetry/model/TelemetryConfigurationEvent$Application;->(Ljava/lang/String;)V -PLcom/datadog/android/telemetry/model/TelemetryConfigurationEvent$Application;->toJson()Lcom/google/gson/JsonElement; -PLcom/datadog/android/telemetry/model/TelemetryConfigurationEvent$Application$Companion;->()V -PLcom/datadog/android/telemetry/model/TelemetryConfigurationEvent$Application$Companion;->(Lkotlin/jvm/internal/DefaultConstructorMarker;)V -PLcom/datadog/android/telemetry/model/TelemetryConfigurationEvent$Companion;->()V -PLcom/datadog/android/telemetry/model/TelemetryConfigurationEvent$Companion;->(Lkotlin/jvm/internal/DefaultConstructorMarker;)V -PLcom/datadog/android/telemetry/model/TelemetryConfigurationEvent$Configuration;->()V -PLcom/datadog/android/telemetry/model/TelemetryConfigurationEvent$Configuration;->(Ljava/lang/Long;Ljava/lang/Long;Ljava/lang/Long;Ljava/lang/Long;Ljava/lang/Long;Lcom/datadog/android/telemetry/model/TelemetryConfigurationEvent$TraceContextInjection;Ljava/lang/Long;Ljava/lang/Long;Ljava/lang/Long;Lcom/datadog/android/telemetry/model/TelemetryConfigurationEvent$TrackingConsent;Ljava/lang/Boolean;Ljava/lang/Boolean;Ljava/lang/Boolean;Ljava/lang/Boolean;Ljava/lang/Boolean;Ljava/lang/Boolean;Ljava/lang/Boolean;Ljava/lang/Boolean;Ljava/lang/Boolean;Ljava/lang/Boolean;Ljava/lang/Boolean;Ljava/lang/Boolean;Ljava/lang/Boolean;Ljava/lang/Boolean;Lcom/datadog/android/telemetry/model/TelemetryConfigurationEvent$SessionPersistence;Ljava/lang/Boolean;Ljava/lang/Boolean;Ljava/lang/String;Ljava/lang/Boolean;Ljava/lang/Boolean;Ljava/lang/Boolean;Ljava/lang/Boolean;Ljava/lang/Boolean;Ljava/util/List;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/Boolean;Ljava/lang/Boolean;Ljava/lang/Boolean;Ljava/lang/Boolean;Ljava/lang/Boolean;Ljava/lang/Boolean;Ljava/lang/Boolean;Ljava/lang/Boolean;Ljava/lang/Boolean;Ljava/lang/Long;Ljava/util/List;Ljava/util/List;Ljava/lang/Boolean;Lcom/datadog/android/telemetry/model/TelemetryConfigurationEvent$ViewTrackingStrategy;Ljava/lang/Boolean;Ljava/lang/Boolean;Ljava/lang/Boolean;Ljava/lang/Long;Ljava/lang/Boolean;Ljava/lang/Boolean;Ljava/lang/Boolean;Ljava/lang/Boolean;Ljava/lang/Boolean;Ljava/lang/Boolean;Ljava/lang/Boolean;Ljava/lang/Boolean;Ljava/lang/String;Ljava/lang/Boolean;Ljava/lang/Long;Ljava/lang/Long;Ljava/lang/Long;Ljava/lang/Boolean;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/Long;Ljava/lang/Boolean;Ljava/lang/String;Ljava/lang/String;Ljava/lang/Boolean;Ljava/util/List;Ljava/lang/Boolean;Ljava/lang/Long;Ljava/lang/Long;Ljava/util/List;Ljava/lang/Boolean;Ljava/lang/Boolean;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/Boolean;Ljava/lang/Number;Ljava/lang/Boolean;Lcom/datadog/android/telemetry/model/TelemetryConfigurationEvent$TrackResourceHeaders;Ljava/lang/Boolean;)V -PLcom/datadog/android/telemetry/model/TelemetryConfigurationEvent$Configuration;->(Ljava/lang/Long;Ljava/lang/Long;Ljava/lang/Long;Ljava/lang/Long;Ljava/lang/Long;Lcom/datadog/android/telemetry/model/TelemetryConfigurationEvent$TraceContextInjection;Ljava/lang/Long;Ljava/lang/Long;Ljava/lang/Long;Lcom/datadog/android/telemetry/model/TelemetryConfigurationEvent$TrackingConsent;Ljava/lang/Boolean;Ljava/lang/Boolean;Ljava/lang/Boolean;Ljava/lang/Boolean;Ljava/lang/Boolean;Ljava/lang/Boolean;Ljava/lang/Boolean;Ljava/lang/Boolean;Ljava/lang/Boolean;Ljava/lang/Boolean;Ljava/lang/Boolean;Ljava/lang/Boolean;Ljava/lang/Boolean;Ljava/lang/Boolean;Lcom/datadog/android/telemetry/model/TelemetryConfigurationEvent$SessionPersistence;Ljava/lang/Boolean;Ljava/lang/Boolean;Ljava/lang/String;Ljava/lang/Boolean;Ljava/lang/Boolean;Ljava/lang/Boolean;Ljava/lang/Boolean;Ljava/lang/Boolean;Ljava/util/List;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/Boolean;Ljava/lang/Boolean;Ljava/lang/Boolean;Ljava/lang/Boolean;Ljava/lang/Boolean;Ljava/lang/Boolean;Ljava/lang/Boolean;Ljava/lang/Boolean;Ljava/lang/Boolean;Ljava/lang/Long;Ljava/util/List;Ljava/util/List;Ljava/lang/Boolean;Lcom/datadog/android/telemetry/model/TelemetryConfigurationEvent$ViewTrackingStrategy;Ljava/lang/Boolean;Ljava/lang/Boolean;Ljava/lang/Boolean;Ljava/lang/Long;Ljava/lang/Boolean;Ljava/lang/Boolean;Ljava/lang/Boolean;Ljava/lang/Boolean;Ljava/lang/Boolean;Ljava/lang/Boolean;Ljava/lang/Boolean;Ljava/lang/Boolean;Ljava/lang/String;Ljava/lang/Boolean;Ljava/lang/Long;Ljava/lang/Long;Ljava/lang/Long;Ljava/lang/Boolean;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/Long;Ljava/lang/Boolean;Ljava/lang/String;Ljava/lang/String;Ljava/lang/Boolean;Ljava/util/List;Ljava/lang/Boolean;Ljava/lang/Long;Ljava/lang/Long;Ljava/util/List;Ljava/lang/Boolean;Ljava/lang/Boolean;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/Boolean;Ljava/lang/Number;Ljava/lang/Boolean;Lcom/datadog/android/telemetry/model/TelemetryConfigurationEvent$TrackResourceHeaders;Ljava/lang/Boolean;IIILkotlin/jvm/internal/DefaultConstructorMarker;)V -PLcom/datadog/android/telemetry/model/TelemetryConfigurationEvent$Configuration;->toJson()Lcom/google/gson/JsonElement; -PLcom/datadog/android/telemetry/model/TelemetryConfigurationEvent$Configuration$Companion;->()V -PLcom/datadog/android/telemetry/model/TelemetryConfigurationEvent$Configuration$Companion;->(Lkotlin/jvm/internal/DefaultConstructorMarker;)V -PLcom/datadog/android/telemetry/model/TelemetryConfigurationEvent$Dd;->()V -PLcom/datadog/android/telemetry/model/TelemetryConfigurationEvent$Dd;->()V -PLcom/datadog/android/telemetry/model/TelemetryConfigurationEvent$Dd;->toJson()Lcom/google/gson/JsonElement; -PLcom/datadog/android/telemetry/model/TelemetryConfigurationEvent$Dd$Companion;->()V -PLcom/datadog/android/telemetry/model/TelemetryConfigurationEvent$Dd$Companion;->(Lkotlin/jvm/internal/DefaultConstructorMarker;)V -PLcom/datadog/android/telemetry/model/TelemetryConfigurationEvent$Device;->()V -PLcom/datadog/android/telemetry/model/TelemetryConfigurationEvent$Device;->(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/Number;Ljava/lang/Number;Ljava/lang/Boolean;)V -PLcom/datadog/android/telemetry/model/TelemetryConfigurationEvent$Device;->toJson()Lcom/google/gson/JsonElement; -PLcom/datadog/android/telemetry/model/TelemetryConfigurationEvent$Device$Companion;->()V -PLcom/datadog/android/telemetry/model/TelemetryConfigurationEvent$Device$Companion;->(Lkotlin/jvm/internal/DefaultConstructorMarker;)V -PLcom/datadog/android/telemetry/model/TelemetryConfigurationEvent$Os;->()V -PLcom/datadog/android/telemetry/model/TelemetryConfigurationEvent$Os;->(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)V -PLcom/datadog/android/telemetry/model/TelemetryConfigurationEvent$Os;->toJson()Lcom/google/gson/JsonElement; -PLcom/datadog/android/telemetry/model/TelemetryConfigurationEvent$Os$Companion;->()V -PLcom/datadog/android/telemetry/model/TelemetryConfigurationEvent$Os$Companion;->(Lkotlin/jvm/internal/DefaultConstructorMarker;)V -PLcom/datadog/android/telemetry/model/TelemetryConfigurationEvent$SelectedTracingPropagator;->$values()[Lcom/datadog/android/telemetry/model/TelemetryConfigurationEvent$SelectedTracingPropagator; -PLcom/datadog/android/telemetry/model/TelemetryConfigurationEvent$SelectedTracingPropagator;->()V -PLcom/datadog/android/telemetry/model/TelemetryConfigurationEvent$SelectedTracingPropagator;->(Ljava/lang/String;ILjava/lang/String;)V -PLcom/datadog/android/telemetry/model/TelemetryConfigurationEvent$SelectedTracingPropagator;->toJson()Lcom/google/gson/JsonElement; -PLcom/datadog/android/telemetry/model/TelemetryConfigurationEvent$SelectedTracingPropagator$Companion;->()V -PLcom/datadog/android/telemetry/model/TelemetryConfigurationEvent$SelectedTracingPropagator$Companion;->(Lkotlin/jvm/internal/DefaultConstructorMarker;)V -PLcom/datadog/android/telemetry/model/TelemetryConfigurationEvent$Session;->()V -PLcom/datadog/android/telemetry/model/TelemetryConfigurationEvent$Session;->(Ljava/lang/String;)V -PLcom/datadog/android/telemetry/model/TelemetryConfigurationEvent$Session;->toJson()Lcom/google/gson/JsonElement; -PLcom/datadog/android/telemetry/model/TelemetryConfigurationEvent$Session$Companion;->()V -PLcom/datadog/android/telemetry/model/TelemetryConfigurationEvent$Session$Companion;->(Lkotlin/jvm/internal/DefaultConstructorMarker;)V -PLcom/datadog/android/telemetry/model/TelemetryConfigurationEvent$Source;->$values()[Lcom/datadog/android/telemetry/model/TelemetryConfigurationEvent$Source; -PLcom/datadog/android/telemetry/model/TelemetryConfigurationEvent$Source;->()V -PLcom/datadog/android/telemetry/model/TelemetryConfigurationEvent$Source;->(Ljava/lang/String;ILjava/lang/String;)V -PLcom/datadog/android/telemetry/model/TelemetryConfigurationEvent$Source;->access$getJsonValue$p(Lcom/datadog/android/telemetry/model/TelemetryConfigurationEvent$Source;)Ljava/lang/String; -PLcom/datadog/android/telemetry/model/TelemetryConfigurationEvent$Source;->toJson()Lcom/google/gson/JsonElement; -PLcom/datadog/android/telemetry/model/TelemetryConfigurationEvent$Source;->values()[Lcom/datadog/android/telemetry/model/TelemetryConfigurationEvent$Source; -PLcom/datadog/android/telemetry/model/TelemetryConfigurationEvent$Source$Companion;->()V -PLcom/datadog/android/telemetry/model/TelemetryConfigurationEvent$Source$Companion;->(Lkotlin/jvm/internal/DefaultConstructorMarker;)V -PLcom/datadog/android/telemetry/model/TelemetryConfigurationEvent$Source$Companion;->fromJson(Ljava/lang/String;)Lcom/datadog/android/telemetry/model/TelemetryConfigurationEvent$Source; -PLcom/datadog/android/telemetry/model/TelemetryConfigurationEvent$Telemetry;->()V -PLcom/datadog/android/telemetry/model/TelemetryConfigurationEvent$Telemetry;->(Lcom/datadog/android/telemetry/model/TelemetryConfigurationEvent$Device;Lcom/datadog/android/telemetry/model/TelemetryConfigurationEvent$Os;Lcom/datadog/android/telemetry/model/TelemetryConfigurationEvent$Configuration;Ljava/util/Map;)V -PLcom/datadog/android/telemetry/model/TelemetryConfigurationEvent$Telemetry;->(Lcom/datadog/android/telemetry/model/TelemetryConfigurationEvent$Device;Lcom/datadog/android/telemetry/model/TelemetryConfigurationEvent$Os;Lcom/datadog/android/telemetry/model/TelemetryConfigurationEvent$Configuration;Ljava/util/Map;ILkotlin/jvm/internal/DefaultConstructorMarker;)V -PLcom/datadog/android/telemetry/model/TelemetryConfigurationEvent$Telemetry;->toJson()Lcom/google/gson/JsonElement; -PLcom/datadog/android/telemetry/model/TelemetryConfigurationEvent$Telemetry$Companion;->()V -PLcom/datadog/android/telemetry/model/TelemetryConfigurationEvent$Telemetry$Companion;->(Lkotlin/jvm/internal/DefaultConstructorMarker;)V -PLcom/datadog/android/telemetry/model/TelemetryConfigurationEvent$View;->()V -PLcom/datadog/android/telemetry/model/TelemetryConfigurationEvent$View;->(Ljava/lang/String;)V -PLcom/datadog/android/telemetry/model/TelemetryConfigurationEvent$View;->toJson()Lcom/google/gson/JsonElement; -PLcom/datadog/android/telemetry/model/TelemetryConfigurationEvent$View$Companion;->()V -PLcom/datadog/android/telemetry/model/TelemetryConfigurationEvent$View$Companion;->(Lkotlin/jvm/internal/DefaultConstructorMarker;)V -PLcom/datadog/android/telemetry/model/TelemetryConfigurationEvent$ViewTrackingStrategy;->$values()[Lcom/datadog/android/telemetry/model/TelemetryConfigurationEvent$ViewTrackingStrategy; -PLcom/datadog/android/telemetry/model/TelemetryConfigurationEvent$ViewTrackingStrategy;->()V -PLcom/datadog/android/telemetry/model/TelemetryConfigurationEvent$ViewTrackingStrategy;->(Ljava/lang/String;ILjava/lang/String;)V -PLcom/datadog/android/telemetry/model/TelemetryConfigurationEvent$ViewTrackingStrategy;->toJson()Lcom/google/gson/JsonElement; -PLcom/datadog/android/telemetry/model/TelemetryConfigurationEvent$ViewTrackingStrategy$Companion;->()V -PLcom/datadog/android/telemetry/model/TelemetryConfigurationEvent$ViewTrackingStrategy$Companion;->(Lkotlin/jvm/internal/DefaultConstructorMarker;)V Lcom/datadog/android/trace/DatadogTracing; HSPLcom/datadog/android/trace/DatadogTracing;->()V HSPLcom/datadog/android/trace/DatadogTracing;->()V @@ -21960,12 +22112,12 @@ HSPLcom/datadog/android/trace/DatadogTracing;->newTracerBuilder(Lcom/datadog/and Lcom/datadog/android/trace/DeterministicTraceSampler; HSPLcom/datadog/android/trace/DeterministicTraceSampler;->(F)V HSPLcom/datadog/android/trace/DeterministicTraceSampler;->(Lkotlin/jvm/functions/Function0;)V +HSPLcom/datadog/android/trace/DeterministicTraceSampler;->_init_$lambda$0(F)F +Lcom/datadog/android/trace/DeterministicTraceSampler$$ExternalSyntheticLambda0; +HSPLcom/datadog/android/trace/DeterministicTraceSampler$$ExternalSyntheticLambda0;->(F)V +HSPLcom/datadog/android/trace/DeterministicTraceSampler$$ExternalSyntheticLambda0;->invoke()Ljava/lang/Object; Lcom/datadog/android/trace/DeterministicTraceSampler$1; HSPLcom/datadog/android/trace/DeterministicTraceSampler$1;->(Ljava/lang/Object;)V -Lcom/datadog/android/trace/DeterministicTraceSampler$2; -HSPLcom/datadog/android/trace/DeterministicTraceSampler$2;->(F)V -HSPLcom/datadog/android/trace/DeterministicTraceSampler$2;->invoke()Ljava/lang/Float; -HSPLcom/datadog/android/trace/DeterministicTraceSampler$2;->invoke()Ljava/lang/Object; Lcom/datadog/android/trace/InternalCoreWriterProvider; Lcom/datadog/android/trace/Trace; HSPLcom/datadog/android/trace/Trace;->()V @@ -21973,10 +22125,11 @@ HSPLcom/datadog/android/trace/Trace;->()V HSPLcom/datadog/android/trace/Trace;->enable$default(Lcom/datadog/android/trace/TraceConfiguration;Lcom/datadog/android/api/SdkCore;ILjava/lang/Object;)V HSPLcom/datadog/android/trace/Trace;->enable(Lcom/datadog/android/trace/TraceConfiguration;Lcom/datadog/android/api/SdkCore;)V Lcom/datadog/android/trace/TraceConfiguration; -HSPLcom/datadog/android/trace/TraceConfiguration;->(Ljava/lang/String;Lcom/datadog/android/trace/event/SpanEventMapper;Z)V +HSPLcom/datadog/android/trace/TraceConfiguration;->(Ljava/lang/String;Lcom/datadog/android/trace/event/SpanEventMapper;ZZLjava/lang/String;)V HSPLcom/datadog/android/trace/TraceConfiguration;->getCustomEndpointUrl$dd_sdk_android_trace_release()Ljava/lang/String; HSPLcom/datadog/android/trace/TraceConfiguration;->getEventMapper$dd_sdk_android_trace_release()Lcom/datadog/android/trace/event/SpanEventMapper; HSPLcom/datadog/android/trace/TraceConfiguration;->getNetworkInfoEnabled$dd_sdk_android_trace_release()Z +HSPLcom/datadog/android/trace/TraceConfiguration;->getStatsComputationEnabled$dd_sdk_android_trace_release()Z Lcom/datadog/android/trace/TraceConfiguration$Builder; HSPLcom/datadog/android/trace/TraceConfiguration$Builder;->()V HSPLcom/datadog/android/trace/TraceConfiguration$Builder;->build()Lcom/datadog/android/trace/TraceConfiguration; @@ -22020,13 +22173,14 @@ Lcom/datadog/android/trace/internal/DatadogTracerAdapter; HSPLcom/datadog/android/trace/internal/DatadogTracerAdapter;->(Lcom/datadog/android/api/feature/FeatureSdkCore;Lcom/datadog/trace/bootstrap/instrumentation/api/AgentTracer$TracerAPI;ZLcom/datadog/android/trace/internal/DatadogSpanLogger;Lcom/datadog/android/trace/internal/RumContextPropagator;)V HSPLcom/datadog/android/trace/internal/DatadogTracerAdapter;->(Lcom/datadog/android/api/feature/FeatureSdkCore;Lcom/datadog/trace/bootstrap/instrumentation/api/AgentTracer$TracerAPI;ZLcom/datadog/android/trace/internal/DatadogSpanLogger;Lcom/datadog/android/trace/internal/RumContextPropagator;ILkotlin/jvm/internal/DefaultConstructorMarker;)V HSPLcom/datadog/android/trace/internal/DatadogTracerAdapter;->addScopeListener(Lcom/datadog/android/trace/api/scope/DatadogScopeListener;)V -Lcom/datadog/android/trace/internal/DatadogTracerAdapter$1; -HSPLcom/datadog/android/trace/internal/DatadogTracerAdapter$1;->(Lcom/datadog/android/api/feature/FeatureSdkCore;)V +Lcom/datadog/android/trace/internal/DatadogTracerAdapter$$ExternalSyntheticLambda0; +HSPLcom/datadog/android/trace/internal/DatadogTracerAdapter$$ExternalSyntheticLambda0;->(Lcom/datadog/android/api/feature/FeatureSdkCore;)V Lcom/datadog/android/trace/internal/DatadogTracerBuilderAdapter; HSPLcom/datadog/android/trace/internal/DatadogTracerBuilderAdapter;->()V HSPLcom/datadog/android/trace/internal/DatadogTracerBuilderAdapter;->(Lcom/datadog/android/api/feature/FeatureSdkCore;Ljava/lang/String;Lcom/datadog/trace/core/CoreTracer$CoreTracerBuilder;)V HSPLcom/datadog/android/trace/internal/DatadogTracerBuilderAdapter;->build()Lcom/datadog/android/trace/api/tracer/DatadogTracer; HSPLcom/datadog/android/trace/internal/DatadogTracerBuilderAdapter;->properties$dd_sdk_android_trace_release()Ljava/util/Properties; +HSPLcom/datadog/android/trace/internal/DatadogTracerBuilderAdapter;->properties$lambda$8(Lcom/datadog/android/trace/TracingHeaderType;)Ljava/lang/CharSequence; HSPLcom/datadog/android/trace/internal/DatadogTracerBuilderAdapter;->setCustomIdGenerationStrategy$dd_sdk_android_trace_release(Lcom/datadog/trace/api/IdGenerationStrategy;)Lcom/datadog/android/trace/internal/DatadogTracerBuilderAdapter; HSPLcom/datadog/android/trace/internal/DatadogTracerBuilderAdapter;->setSdkV2Compatible$dd_sdk_android_trace_release()V HSPLcom/datadog/android/trace/internal/DatadogTracerBuilderAdapter;->setTraceId128BitGenerationEnabled$dd_sdk_android_trace_release(Z)Lcom/datadog/android/trace/internal/DatadogTracerBuilderAdapter; @@ -22036,17 +22190,15 @@ HSPLcom/datadog/android/trace/internal/DatadogTracerBuilderAdapter;->withService HSPLcom/datadog/android/trace/internal/DatadogTracerBuilderAdapter;->withServiceName(Ljava/lang/String;)Lcom/datadog/android/trace/internal/DatadogTracerBuilderAdapter; HSPLcom/datadog/android/trace/internal/DatadogTracerBuilderAdapter;->withTracingHeadersTypes(Ljava/util/Set;)Lcom/datadog/android/trace/api/tracer/DatadogTracerBuilder; HSPLcom/datadog/android/trace/internal/DatadogTracerBuilderAdapter;->withTracingHeadersTypes(Ljava/util/Set;)Lcom/datadog/android/trace/internal/DatadogTracerBuilderAdapter; +Lcom/datadog/android/trace/internal/DatadogTracerBuilderAdapter$$ExternalSyntheticLambda0; +HSPLcom/datadog/android/trace/internal/DatadogTracerBuilderAdapter$$ExternalSyntheticLambda0;->()V +HSPLcom/datadog/android/trace/internal/DatadogTracerBuilderAdapter$$ExternalSyntheticLambda0;->invoke(Ljava/lang/Object;)Ljava/lang/Object; Lcom/datadog/android/trace/internal/DatadogTracerBuilderAdapter$Companion; HSPLcom/datadog/android/trace/internal/DatadogTracerBuilderAdapter$Companion;->()V HSPLcom/datadog/android/trace/internal/DatadogTracerBuilderAdapter$Companion;->(Lkotlin/jvm/internal/DefaultConstructorMarker;)V HSPLcom/datadog/android/trace/internal/DatadogTracerBuilderAdapter$Companion;->toPropagationStyle$dd_sdk_android_trace_release(Lcom/datadog/android/trace/TracingHeaderType;)Ljava/lang/String; Lcom/datadog/android/trace/internal/DatadogTracerBuilderAdapter$Companion$WhenMappings; HSPLcom/datadog/android/trace/internal/DatadogTracerBuilderAdapter$Companion$WhenMappings;->()V -Lcom/datadog/android/trace/internal/DatadogTracerBuilderAdapter$properties$propagationStyles$1; -HSPLcom/datadog/android/trace/internal/DatadogTracerBuilderAdapter$properties$propagationStyles$1;->()V -HSPLcom/datadog/android/trace/internal/DatadogTracerBuilderAdapter$properties$propagationStyles$1;->()V -HSPLcom/datadog/android/trace/internal/DatadogTracerBuilderAdapter$properties$propagationStyles$1;->invoke(Lcom/datadog/android/trace/TracingHeaderType;)Ljava/lang/CharSequence; -HSPLcom/datadog/android/trace/internal/DatadogTracerBuilderAdapter$properties$propagationStyles$1;->invoke(Ljava/lang/Object;)Ljava/lang/Object; Lcom/datadog/android/trace/internal/RumContextPropagator; HSPLcom/datadog/android/trace/internal/RumContextPropagator;->()V HSPLcom/datadog/android/trace/internal/RumContextPropagator;->(Lkotlin/jvm/functions/Function0;)V @@ -22056,8 +22208,7 @@ HSPLcom/datadog/android/trace/internal/RumContextPropagator$Companion;->(L Lcom/datadog/android/trace/internal/TracePropagationScopeListener; HSPLcom/datadog/android/trace/internal/TracePropagationScopeListener;->(Lcom/datadog/android/api/feature/FeatureSdkCore;Lcom/datadog/android/trace/api/tracer/DatadogTracer;)V Lcom/datadog/android/trace/internal/TracingFeature; -HSPLcom/datadog/android/trace/internal/TracingFeature;->(Lcom/datadog/android/api/feature/FeatureSdkCore;Ljava/lang/String;Lcom/datadog/android/trace/event/SpanEventMapper;Z)V -HSPLcom/datadog/android/trace/internal/TracingFeature;->access$getSdkCore$p(Lcom/datadog/android/trace/internal/TracingFeature;)Lcom/datadog/android/api/feature/FeatureSdkCore; +HSPLcom/datadog/android/trace/internal/TracingFeature;->(Lcom/datadog/android/api/feature/FeatureSdkCore;Ljava/lang/String;Lcom/datadog/android/trace/event/SpanEventMapper;ZZ)V HSPLcom/datadog/android/trace/internal/TracingFeature;->createDataWriter(Lcom/datadog/android/api/feature/FeatureSdkCore;)Lcom/datadog/trace/common/writer/Writer; HSPLcom/datadog/android/trace/internal/TracingFeature;->getCoreTracerWriter()Lcom/datadog/android/trace/api/span/DatadogSpanWriter; HSPLcom/datadog/android/trace/internal/TracingFeature;->getCoreTracerWriter()Lcom/datadog/android/trace/internal/DatadogSpanWriterWrapper; @@ -22065,10 +22216,10 @@ HSPLcom/datadog/android/trace/internal/TracingFeature;->getName()Ljava/lang/Stri HSPLcom/datadog/android/trace/internal/TracingFeature;->getRequestFactory()Lcom/datadog/android/api/net/RequestFactory; HSPLcom/datadog/android/trace/internal/TracingFeature;->getStorageConfiguration()Lcom/datadog/android/api/storage/FeatureStorageConfiguration; HSPLcom/datadog/android/trace/internal/TracingFeature;->onInitialize(Landroid/content/Context;)V -Lcom/datadog/android/trace/internal/TracingFeature$requestFactory$2; -HSPLcom/datadog/android/trace/internal/TracingFeature$requestFactory$2;->(Ljava/lang/String;Lcom/datadog/android/trace/internal/TracingFeature;)V -HSPLcom/datadog/android/trace/internal/TracingFeature$requestFactory$2;->invoke()Lcom/datadog/android/trace/internal/net/TracesRequestFactory; -HSPLcom/datadog/android/trace/internal/TracingFeature$requestFactory$2;->invoke()Ljava/lang/Object; +HSPLcom/datadog/android/trace/internal/TracingFeature;->requestFactory_delegate$lambda$0(Ljava/lang/String;Lcom/datadog/android/trace/internal/TracingFeature;)Lcom/datadog/android/trace/internal/net/TracesRequestFactory; +Lcom/datadog/android/trace/internal/TracingFeature$$ExternalSyntheticLambda0; +HSPLcom/datadog/android/trace/internal/TracingFeature$$ExternalSyntheticLambda0;->(Ljava/lang/String;Lcom/datadog/android/trace/internal/TracingFeature;)V +HSPLcom/datadog/android/trace/internal/TracingFeature$$ExternalSyntheticLambda0;->invoke()Ljava/lang/Object; Lcom/datadog/android/trace/internal/_TraceInternalProxy; HSPLcom/datadog/android/trace/internal/_TraceInternalProxy;->()V HSPLcom/datadog/android/trace/internal/_TraceInternalProxy;->()V @@ -22083,8 +22234,8 @@ HSPLcom/datadog/android/trace/internal/data/CoreTraceWriter;->()V HSPLcom/datadog/android/trace/internal/data/CoreTraceWriter;->(Lcom/datadog/android/api/feature/FeatureSdkCore;Lcom/datadog/android/trace/internal/domain/event/ContextAwareMapper;Lcom/datadog/android/event/EventMapper;Lcom/datadog/android/trace/internal/storage/ContextAwareSerializer;Lcom/datadog/android/api/InternalLogger;Lcom/datadog/android/trace/internal/RumContextPropagator;)V HSPLcom/datadog/android/trace/internal/data/CoreTraceWriter;->(Lcom/datadog/android/api/feature/FeatureSdkCore;Lcom/datadog/android/trace/internal/domain/event/ContextAwareMapper;Lcom/datadog/android/event/EventMapper;Lcom/datadog/android/trace/internal/storage/ContextAwareSerializer;Lcom/datadog/android/api/InternalLogger;Lcom/datadog/android/trace/internal/RumContextPropagator;ILkotlin/jvm/internal/DefaultConstructorMarker;)V HSPLcom/datadog/android/trace/internal/data/CoreTraceWriter;->start()V -Lcom/datadog/android/trace/internal/data/CoreTraceWriter$1; -HSPLcom/datadog/android/trace/internal/data/CoreTraceWriter$1;->(Lcom/datadog/android/api/feature/FeatureSdkCore;)V +Lcom/datadog/android/trace/internal/data/CoreTraceWriter$$ExternalSyntheticLambda0; +HSPLcom/datadog/android/trace/internal/data/CoreTraceWriter$$ExternalSyntheticLambda0;->(Lcom/datadog/android/api/feature/FeatureSdkCore;)V Lcom/datadog/android/trace/internal/data/CoreTraceWriter$Companion; HSPLcom/datadog/android/trace/internal/data/CoreTraceWriter$Companion;->()V HSPLcom/datadog/android/trace/internal/data/CoreTraceWriter$Companion;->(Lkotlin/jvm/internal/DefaultConstructorMarker;)V @@ -22093,7 +22244,7 @@ HSPLcom/datadog/android/trace/internal/domain/event/BaseSpanEventMapper;-> Lcom/datadog/android/trace/internal/domain/event/ContextAwareMapper; Lcom/datadog/android/trace/internal/domain/event/CoreTracerSpanToSpanEventMapper; HSPLcom/datadog/android/trace/internal/domain/event/CoreTracerSpanToSpanEventMapper;->()V -HSPLcom/datadog/android/trace/internal/domain/event/CoreTracerSpanToSpanEventMapper;->(Z)V +HSPLcom/datadog/android/trace/internal/domain/event/CoreTracerSpanToSpanEventMapper;->(ZZ)V Lcom/datadog/android/trace/internal/domain/event/CoreTracerSpanToSpanEventMapper$Companion; HSPLcom/datadog/android/trace/internal/domain/event/CoreTracerSpanToSpanEventMapper$Companion;->()V HSPLcom/datadog/android/trace/internal/domain/event/CoreTracerSpanToSpanEventMapper$Companion;->(Lkotlin/jvm/internal/DefaultConstructorMarker;)V @@ -22129,33 +22280,29 @@ HSPLcom/datadog/android/trace/opentelemetry/DatadogOpenTelemetry;->(Ljava/ Lcom/datadog/android/trace/opentelemetry/OtelTracerProvider; HSPLcom/datadog/android/trace/opentelemetry/OtelTracerProvider;->()V HSPLcom/datadog/android/trace/opentelemetry/OtelTracerProvider;->(Lcom/datadog/android/trace/api/tracer/DatadogTracer;Lcom/datadog/android/api/InternalLogger;)V +HSPLcom/datadog/android/trace/opentelemetry/OtelTracerProvider;->_init_$lambda$3()Lkotlin/Unit; +Lcom/datadog/android/trace/opentelemetry/OtelTracerProvider$$ExternalSyntheticLambda0; +HSPLcom/datadog/android/trace/opentelemetry/OtelTracerProvider$$ExternalSyntheticLambda0;->()V +HSPLcom/datadog/android/trace/opentelemetry/OtelTracerProvider$$ExternalSyntheticLambda0;->invoke()Ljava/lang/Object; Lcom/datadog/android/trace/opentelemetry/OtelTracerProvider$Builder; HSPLcom/datadog/android/trace/opentelemetry/OtelTracerProvider$Builder;->(Lcom/datadog/android/api/SdkCore;)V HSPLcom/datadog/android/trace/opentelemetry/OtelTracerProvider$Builder;->(Lcom/datadog/android/api/SdkCore;ILkotlin/jvm/internal/DefaultConstructorMarker;)V HSPLcom/datadog/android/trace/opentelemetry/OtelTracerProvider$Builder;->(Lcom/datadog/android/api/feature/FeatureSdkCore;)V -HSPLcom/datadog/android/trace/opentelemetry/OtelTracerProvider$Builder;->access$createDatadogTracer(Lcom/datadog/android/trace/opentelemetry/OtelTracerProvider$Builder;)Lcom/datadog/android/trace/api/tracer/DatadogTracer; -HSPLcom/datadog/android/trace/opentelemetry/OtelTracerProvider$Builder;->access$getSdkCore$p(Lcom/datadog/android/trace/opentelemetry/OtelTracerProvider$Builder;)Lcom/datadog/android/api/feature/FeatureSdkCore; +HSPLcom/datadog/android/trace/opentelemetry/OtelTracerProvider$Builder;->build$lambda$4$lambda$3(Ljava/util/Map;)Lkotlin/Unit; +HSPLcom/datadog/android/trace/opentelemetry/OtelTracerProvider$Builder;->build$lambda$4(Lcom/datadog/android/trace/opentelemetry/OtelTracerProvider$Builder;)Lio/opentelemetry/api/trace/TracerProvider; HSPLcom/datadog/android/trace/opentelemetry/OtelTracerProvider$Builder;->build()Lio/opentelemetry/api/trace/TracerProvider; HSPLcom/datadog/android/trace/opentelemetry/OtelTracerProvider$Builder;->createDatadogTracer()Lcom/datadog/android/trace/api/tracer/DatadogTracer; HSPLcom/datadog/android/trace/opentelemetry/OtelTracerProvider$Builder;->getServiceName()Ljava/lang/String; HSPLcom/datadog/android/trace/opentelemetry/OtelTracerProvider$Builder;->setService(Ljava/lang/String;)Lcom/datadog/android/trace/opentelemetry/OtelTracerProvider$Builder; -Lcom/datadog/android/trace/opentelemetry/OtelTracerProvider$Builder$build$1; -HSPLcom/datadog/android/trace/opentelemetry/OtelTracerProvider$Builder$build$1;->(Lcom/datadog/android/trace/opentelemetry/OtelTracerProvider$Builder;)V -HSPLcom/datadog/android/trace/opentelemetry/OtelTracerProvider$Builder$build$1;->invoke()Lio/opentelemetry/api/trace/TracerProvider; -HSPLcom/datadog/android/trace/opentelemetry/OtelTracerProvider$Builder$build$1;->invoke()Ljava/lang/Object; -Lcom/datadog/android/trace/opentelemetry/OtelTracerProvider$Builder$build$1$1; -HSPLcom/datadog/android/trace/opentelemetry/OtelTracerProvider$Builder$build$1$1;->()V -HSPLcom/datadog/android/trace/opentelemetry/OtelTracerProvider$Builder$build$1$1;->()V -HSPLcom/datadog/android/trace/opentelemetry/OtelTracerProvider$Builder$build$1$1;->invoke(Ljava/lang/Object;)Ljava/lang/Object; -HSPLcom/datadog/android/trace/opentelemetry/OtelTracerProvider$Builder$build$1$1;->invoke(Ljava/util/Map;)V +Lcom/datadog/android/trace/opentelemetry/OtelTracerProvider$Builder$$ExternalSyntheticLambda0; +HSPLcom/datadog/android/trace/opentelemetry/OtelTracerProvider$Builder$$ExternalSyntheticLambda0;->(Lcom/datadog/android/trace/opentelemetry/OtelTracerProvider$Builder;)V +HSPLcom/datadog/android/trace/opentelemetry/OtelTracerProvider$Builder$$ExternalSyntheticLambda0;->invoke()Ljava/lang/Object; +Lcom/datadog/android/trace/opentelemetry/OtelTracerProvider$Builder$$ExternalSyntheticLambda1; +HSPLcom/datadog/android/trace/opentelemetry/OtelTracerProvider$Builder$$ExternalSyntheticLambda1;->()V +HSPLcom/datadog/android/trace/opentelemetry/OtelTracerProvider$Builder$$ExternalSyntheticLambda1;->invoke(Ljava/lang/Object;)Ljava/lang/Object; Lcom/datadog/android/trace/opentelemetry/OtelTracerProvider$Companion; HSPLcom/datadog/android/trace/opentelemetry/OtelTracerProvider$Companion;->()V HSPLcom/datadog/android/trace/opentelemetry/OtelTracerProvider$Companion;->(Lkotlin/jvm/internal/DefaultConstructorMarker;)V -Lcom/datadog/android/trace/opentelemetry/OtelTracerProvider$Companion$1; -HSPLcom/datadog/android/trace/opentelemetry/OtelTracerProvider$Companion$1;->()V -HSPLcom/datadog/android/trace/opentelemetry/OtelTracerProvider$Companion$1;->()V -HSPLcom/datadog/android/trace/opentelemetry/OtelTracerProvider$Companion$1;->invoke()Ljava/lang/Object; -HSPLcom/datadog/android/trace/opentelemetry/OtelTracerProvider$Companion$1;->invoke()V Lcom/datadog/android/trace/opentelemetry/internal/DatadogContextStorageWrapper; HSPLcom/datadog/android/trace/opentelemetry/internal/DatadogContextStorageWrapper;->()V Lcom/datadog/android/trace/opentelemetry/internal/MiscUtilsKt; @@ -22529,8 +22676,8 @@ Lcom/datadog/trace/common/writer/Writer; Lcom/datadog/trace/core/CoreTracer; HSPLcom/datadog/trace/core/CoreTracer;->-$$Nest$sminvertMap(Ljava/util/Map;)Ljava/util/Map; HSPLcom/datadog/trace/core/CoreTracer;->()V -HSPLcom/datadog/trace/core/CoreTracer;->(Lcom/datadog/trace/api/Config;Ljava/lang/String;Lcom/datadog/trace/common/writer/Writer;Lcom/datadog/trace/api/IdGenerationStrategy;Lcom/datadog/trace/common/sampling/Sampler;Lcom/datadog/trace/core/propagation/HttpCodec$Injector;Lcom/datadog/trace/core/propagation/HttpCodec$Extractor;Lcom/datadog/trace/bootstrap/instrumentation/api/AgentScopeManager;Ljava/util/Map;Ljava/util/Map;Ljava/util/Map;Ljava/util/Map;Ljava/util/Map;ILcom/datadog/trace/core/taginterceptor/TagInterceptor;ZLcom/datadog/trace/api/time/TimeSource;Lcom/datadog/trace/bootstrap/instrumentation/api/ProfilingContextIntegration;ZLcom/datadog/android/api/InternalLogger;)V -HSPLcom/datadog/trace/core/CoreTracer;->(Lcom/datadog/trace/api/Config;Ljava/lang/String;Lcom/datadog/trace/common/writer/Writer;Lcom/datadog/trace/api/IdGenerationStrategy;Lcom/datadog/trace/common/sampling/Sampler;Lcom/datadog/trace/core/propagation/HttpCodec$Injector;Lcom/datadog/trace/core/propagation/HttpCodec$Extractor;Lcom/datadog/trace/bootstrap/instrumentation/api/AgentScopeManager;Ljava/util/Map;Ljava/util/Map;Ljava/util/Map;Ljava/util/Map;Ljava/util/Map;ILcom/datadog/trace/core/taginterceptor/TagInterceptor;ZLcom/datadog/trace/api/time/TimeSource;Lcom/datadog/trace/bootstrap/instrumentation/api/ProfilingContextIntegration;ZLcom/datadog/android/api/InternalLogger;Lcom/datadog/trace/core/CoreTracer-IA;)V +HSPLcom/datadog/trace/core/CoreTracer;->(Lcom/datadog/trace/api/Config;Ljava/lang/String;Lcom/datadog/trace/common/writer/Writer;Lcom/datadog/trace/common/metrics/MetricsAggregator;Lcom/datadog/trace/api/IdGenerationStrategy;Lcom/datadog/trace/common/sampling/Sampler;Lcom/datadog/trace/core/propagation/HttpCodec$Injector;Lcom/datadog/trace/core/propagation/HttpCodec$Extractor;Lcom/datadog/trace/bootstrap/instrumentation/api/AgentScopeManager;Ljava/util/Map;Ljava/util/Map;Ljava/util/Map;Ljava/util/Map;Ljava/util/Map;ILcom/datadog/trace/core/taginterceptor/TagInterceptor;ZLcom/datadog/trace/api/time/TimeSource;Lcom/datadog/trace/bootstrap/instrumentation/api/ProfilingContextIntegration;ZLcom/datadog/android/api/InternalLogger;)V +HSPLcom/datadog/trace/core/CoreTracer;->(Lcom/datadog/trace/api/Config;Ljava/lang/String;Lcom/datadog/trace/common/writer/Writer;Lcom/datadog/trace/common/metrics/MetricsAggregator;Lcom/datadog/trace/api/IdGenerationStrategy;Lcom/datadog/trace/common/sampling/Sampler;Lcom/datadog/trace/core/propagation/HttpCodec$Injector;Lcom/datadog/trace/core/propagation/HttpCodec$Extractor;Lcom/datadog/trace/bootstrap/instrumentation/api/AgentScopeManager;Ljava/util/Map;Ljava/util/Map;Ljava/util/Map;Ljava/util/Map;Ljava/util/Map;ILcom/datadog/trace/core/taginterceptor/TagInterceptor;ZLcom/datadog/trace/api/time/TimeSource;Lcom/datadog/trace/bootstrap/instrumentation/api/ProfilingContextIntegration;ZLcom/datadog/android/api/InternalLogger;Lcom/datadog/trace/core/CoreTracer-IA;)V HSPLcom/datadog/trace/core/CoreTracer;->addScopeListener(Lcom/datadog/trace/api/scopemanager/ScopeListener;)V HSPLcom/datadog/trace/core/CoreTracer;->invertMap(Ljava/util/Map;)Ljava/util/Map; HSPLcom/datadog/trace/core/CoreTracer;->lambda$new$0$com-datadog-trace-core-CoreTracer(Lcom/datadog/trace/api/DynamicConfig$Builder;Lcom/datadog/trace/core/CoreTracer$ConfigSnapshot;)Lcom/datadog/trace/core/CoreTracer$ConfigSnapshot; @@ -22551,6 +22698,7 @@ HSPLcom/datadog/trace/core/CoreTracer$CoreTracerBuilder;->idGenerationStrategy(L HSPLcom/datadog/trace/core/CoreTracer$CoreTracerBuilder;->injectBaggageAsTags(Z)Lcom/datadog/trace/core/CoreTracer$CoreTracerBuilder; HSPLcom/datadog/trace/core/CoreTracer$CoreTracerBuilder;->injector(Lcom/datadog/trace/core/propagation/HttpCodec$Injector;)Lcom/datadog/trace/core/CoreTracer$CoreTracerBuilder; HSPLcom/datadog/trace/core/CoreTracer$CoreTracerBuilder;->localRootSpanTags(Ljava/util/Map;)Lcom/datadog/trace/core/CoreTracer$CoreTracerBuilder; +HSPLcom/datadog/trace/core/CoreTracer$CoreTracerBuilder;->metricsAggregator(Lcom/datadog/trace/common/metrics/MetricsAggregator;)Lcom/datadog/trace/core/CoreTracer$CoreTracerBuilder; HSPLcom/datadog/trace/core/CoreTracer$CoreTracerBuilder;->partialFlushMinSpans(I)Lcom/datadog/trace/core/CoreTracer$CoreTracerBuilder; HSPLcom/datadog/trace/core/CoreTracer$CoreTracerBuilder;->sampler(Lcom/datadog/trace/common/sampling/Sampler;)Lcom/datadog/trace/core/CoreTracer$CoreTracerBuilder; HSPLcom/datadog/trace/core/CoreTracer$CoreTracerBuilder;->serviceName(Ljava/lang/String;)Lcom/datadog/trace/core/CoreTracer$CoreTracerBuilder; @@ -22765,27 +22913,32 @@ HSPLcom/datadog/trace/util/PidHelper;->getPidAsLong()J HSPLcom/datadog/trace/util/PidHelper;->parsePid()J Lcom/datadog/trace/util/Strings; HSPLcom/datadog/trace/util/Strings;->trim(Ljava/lang/String;)Ljava/lang/String; -PLcom/google/android/datatransport/AutoValue_Event;->(Ljava/lang/Integer;Ljava/lang/Object;Lcom/google/android/datatransport/Priority;Lcom/google/android/datatransport/ProductData;Lcom/google/android/datatransport/EventContext;)V -PLcom/google/android/datatransport/AutoValue_Event;->getCode()Ljava/lang/Integer; -PLcom/google/android/datatransport/AutoValue_Event;->getEventContext()Lcom/google/android/datatransport/EventContext; -PLcom/google/android/datatransport/AutoValue_Event;->getPayload()Ljava/lang/Object; -PLcom/google/android/datatransport/AutoValue_Event;->getPriority()Lcom/google/android/datatransport/Priority; -PLcom/google/android/datatransport/AutoValue_Event;->getProductData()Lcom/google/android/datatransport/ProductData; +Lcom/google/android/datatransport/AutoValue_Event; +HSPLcom/google/android/datatransport/AutoValue_Event;->(Ljava/lang/Integer;Ljava/lang/Object;Lcom/google/android/datatransport/Priority;Lcom/google/android/datatransport/ProductData;Lcom/google/android/datatransport/EventContext;)V +HSPLcom/google/android/datatransport/AutoValue_Event;->getCode()Ljava/lang/Integer; +HSPLcom/google/android/datatransport/AutoValue_Event;->getEventContext()Lcom/google/android/datatransport/EventContext; +HSPLcom/google/android/datatransport/AutoValue_Event;->getPayload()Ljava/lang/Object; +HSPLcom/google/android/datatransport/AutoValue_Event;->getPriority()Lcom/google/android/datatransport/Priority; +HSPLcom/google/android/datatransport/AutoValue_Event;->getProductData()Lcom/google/android/datatransport/ProductData; Lcom/google/android/datatransport/Encoding; HSPLcom/google/android/datatransport/Encoding;->(Ljava/lang/String;)V HSPLcom/google/android/datatransport/Encoding;->equals(Ljava/lang/Object;)Z -PLcom/google/android/datatransport/Encoding;->getName()Ljava/lang/String; +HSPLcom/google/android/datatransport/Encoding;->getName()Ljava/lang/String; HSPLcom/google/android/datatransport/Encoding;->hashCode()I HSPLcom/google/android/datatransport/Encoding;->of(Ljava/lang/String;)Lcom/google/android/datatransport/Encoding; -PLcom/google/android/datatransport/Event;->()V -PLcom/google/android/datatransport/Event;->ofData(Ljava/lang/Object;)Lcom/google/android/datatransport/Event; +Lcom/google/android/datatransport/Event; +HSPLcom/google/android/datatransport/Event;->()V +HSPLcom/google/android/datatransport/Event;->ofData(Ljava/lang/Object;)Lcom/google/android/datatransport/Event; +Lcom/google/android/datatransport/EventContext; Lcom/google/android/datatransport/Priority; HSPLcom/google/android/datatransport/Priority;->()V HSPLcom/google/android/datatransport/Priority;->(Ljava/lang/String;I)V HSPLcom/google/android/datatransport/Priority;->values()[Lcom/google/android/datatransport/Priority; +Lcom/google/android/datatransport/ProductData; Lcom/google/android/datatransport/Transformer; Lcom/google/android/datatransport/Transport; Lcom/google/android/datatransport/TransportFactory; +Lcom/google/android/datatransport/TransportScheduleCallback; Lcom/google/android/datatransport/cct/CCTDestination; HSPLcom/google/android/datatransport/cct/CCTDestination;->()V HSPLcom/google/android/datatransport/cct/CCTDestination;->(Ljava/lang/String;Ljava/lang/String;)V @@ -22793,86 +22946,124 @@ HSPLcom/google/android/datatransport/cct/CCTDestination;->asByteArray()[B HSPLcom/google/android/datatransport/cct/CCTDestination;->getExtras()[B HSPLcom/google/android/datatransport/cct/CCTDestination;->getName()Ljava/lang/String; HSPLcom/google/android/datatransport/cct/CCTDestination;->getSupportedEncodings()Ljava/util/Set; -PLcom/google/android/datatransport/cct/CctBackendFactory;->()V -PLcom/google/android/datatransport/cct/CctBackendFactory;->create(Lcom/google/android/datatransport/runtime/backends/CreationContext;)Lcom/google/android/datatransport/runtime/backends/TransportBackend; -PLcom/google/android/datatransport/cct/CctTransportBackend;->(Landroid/content/Context;Lcom/google/android/datatransport/runtime/time/Clock;Lcom/google/android/datatransport/runtime/time/Clock;)V -PLcom/google/android/datatransport/cct/CctTransportBackend;->(Landroid/content/Context;Lcom/google/android/datatransport/runtime/time/Clock;Lcom/google/android/datatransport/runtime/time/Clock;I)V -PLcom/google/android/datatransport/cct/CctTransportBackend;->decorate(Lcom/google/android/datatransport/runtime/EventInternal;)Lcom/google/android/datatransport/runtime/EventInternal; -PLcom/google/android/datatransport/cct/CctTransportBackend;->getMccMncOrEmpty(Landroid/content/Context;)Ljava/lang/String; -PLcom/google/android/datatransport/cct/CctTransportBackend;->getNetSubtypeValue(Landroid/net/NetworkInfo;)I -PLcom/google/android/datatransport/cct/CctTransportBackend;->getNetTypeValue(Landroid/net/NetworkInfo;)I -PLcom/google/android/datatransport/cct/CctTransportBackend;->getPackageVersionCode(Landroid/content/Context;)I -PLcom/google/android/datatransport/cct/CctTransportBackend;->getTelephonyManager(Landroid/content/Context;)Landroid/telephony/TelephonyManager; -PLcom/google/android/datatransport/cct/CctTransportBackend;->getTzOffset()J -PLcom/google/android/datatransport/cct/CctTransportBackend;->parseUrlOrThrow(Ljava/lang/String;)Ljava/net/URL; +Lcom/google/android/datatransport/cct/CctBackendFactory; +HSPLcom/google/android/datatransport/cct/CctBackendFactory;->()V +HSPLcom/google/android/datatransport/cct/CctBackendFactory;->create(Lcom/google/android/datatransport/runtime/backends/CreationContext;)Lcom/google/android/datatransport/runtime/backends/TransportBackend; +Lcom/google/android/datatransport/cct/CctTransportBackend; +HSPLcom/google/android/datatransport/cct/CctTransportBackend;->(Landroid/content/Context;Lcom/google/android/datatransport/runtime/time/Clock;Lcom/google/android/datatransport/runtime/time/Clock;)V +HSPLcom/google/android/datatransport/cct/CctTransportBackend;->(Landroid/content/Context;Lcom/google/android/datatransport/runtime/time/Clock;Lcom/google/android/datatransport/runtime/time/Clock;I)V +HSPLcom/google/android/datatransport/cct/CctTransportBackend;->decorate(Lcom/google/android/datatransport/runtime/EventInternal;)Lcom/google/android/datatransport/runtime/EventInternal; +HSPLcom/google/android/datatransport/cct/CctTransportBackend;->getMccMncOrEmpty(Landroid/content/Context;)Ljava/lang/String; +HSPLcom/google/android/datatransport/cct/CctTransportBackend;->getNetSubtypeValue(Landroid/net/NetworkInfo;)I +HSPLcom/google/android/datatransport/cct/CctTransportBackend;->getNetTypeValue(Landroid/net/NetworkInfo;)I +HSPLcom/google/android/datatransport/cct/CctTransportBackend;->getPackageVersionCode(Landroid/content/Context;)I +HSPLcom/google/android/datatransport/cct/CctTransportBackend;->getTelephonyManager(Landroid/content/Context;)Landroid/telephony/TelephonyManager; +HSPLcom/google/android/datatransport/cct/CctTransportBackend;->getTzOffset()J +HSPLcom/google/android/datatransport/cct/CctTransportBackend;->parseUrlOrThrow(Ljava/lang/String;)Ljava/net/URL; Lcom/google/android/datatransport/cct/StringMerger; HSPLcom/google/android/datatransport/cct/StringMerger;->mergeStrings(Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String; -PLcom/google/android/datatransport/cct/internal/AutoBatchedLogRequestEncoder;->()V -PLcom/google/android/datatransport/cct/internal/AutoBatchedLogRequestEncoder;->()V -PLcom/google/android/datatransport/cct/internal/AutoBatchedLogRequestEncoder;->configure(Lcom/google/firebase/encoders/config/EncoderConfig;)V -PLcom/google/android/datatransport/cct/internal/AutoBatchedLogRequestEncoder$AndroidClientInfoEncoder;->()V -PLcom/google/android/datatransport/cct/internal/AutoBatchedLogRequestEncoder$AndroidClientInfoEncoder;->()V -PLcom/google/android/datatransport/cct/internal/AutoBatchedLogRequestEncoder$BatchedLogRequestEncoder;->()V -PLcom/google/android/datatransport/cct/internal/AutoBatchedLogRequestEncoder$BatchedLogRequestEncoder;->()V -PLcom/google/android/datatransport/cct/internal/AutoBatchedLogRequestEncoder$ClientInfoEncoder;->()V -PLcom/google/android/datatransport/cct/internal/AutoBatchedLogRequestEncoder$ClientInfoEncoder;->()V -PLcom/google/android/datatransport/cct/internal/AutoBatchedLogRequestEncoder$ComplianceDataEncoder;->()V -PLcom/google/android/datatransport/cct/internal/AutoBatchedLogRequestEncoder$ComplianceDataEncoder;->()V -PLcom/google/android/datatransport/cct/internal/AutoBatchedLogRequestEncoder$ExperimentIdsEncoder;->()V -PLcom/google/android/datatransport/cct/internal/AutoBatchedLogRequestEncoder$ExperimentIdsEncoder;->()V -PLcom/google/android/datatransport/cct/internal/AutoBatchedLogRequestEncoder$ExternalPRequestContextEncoder;->()V -PLcom/google/android/datatransport/cct/internal/AutoBatchedLogRequestEncoder$ExternalPRequestContextEncoder;->()V -PLcom/google/android/datatransport/cct/internal/AutoBatchedLogRequestEncoder$ExternalPrivacyContextEncoder;->()V -PLcom/google/android/datatransport/cct/internal/AutoBatchedLogRequestEncoder$ExternalPrivacyContextEncoder;->()V -PLcom/google/android/datatransport/cct/internal/AutoBatchedLogRequestEncoder$LogEventEncoder;->()V -PLcom/google/android/datatransport/cct/internal/AutoBatchedLogRequestEncoder$LogEventEncoder;->()V -PLcom/google/android/datatransport/cct/internal/AutoBatchedLogRequestEncoder$LogRequestEncoder;->()V -PLcom/google/android/datatransport/cct/internal/AutoBatchedLogRequestEncoder$LogRequestEncoder;->()V -PLcom/google/android/datatransport/cct/internal/AutoBatchedLogRequestEncoder$NetworkConnectionInfoEncoder;->()V -PLcom/google/android/datatransport/cct/internal/AutoBatchedLogRequestEncoder$NetworkConnectionInfoEncoder;->()V -PLcom/google/android/datatransport/cct/internal/BatchedLogRequest;->createDataEncoder()Lcom/google/firebase/encoders/DataEncoder; -PLcom/google/android/datatransport/cct/internal/NetworkConnectionInfo$MobileSubtype;->()V -PLcom/google/android/datatransport/cct/internal/NetworkConnectionInfo$MobileSubtype;->(Ljava/lang/String;II)V -PLcom/google/android/datatransport/cct/internal/NetworkConnectionInfo$MobileSubtype;->getValue()I -PLcom/google/android/datatransport/runtime/AutoValue_EventInternal;->(Ljava/lang/String;Ljava/lang/Integer;Lcom/google/android/datatransport/runtime/EncodedPayload;JJLjava/util/Map;Ljava/lang/Integer;Ljava/lang/String;[B[B)V -PLcom/google/android/datatransport/runtime/AutoValue_EventInternal;->(Ljava/lang/String;Ljava/lang/Integer;Lcom/google/android/datatransport/runtime/EncodedPayload;JJLjava/util/Map;Ljava/lang/Integer;Ljava/lang/String;[B[BLcom/google/android/datatransport/runtime/AutoValue_EventInternal$1;)V -PLcom/google/android/datatransport/runtime/AutoValue_EventInternal;->getAutoMetadata()Ljava/util/Map; -PLcom/google/android/datatransport/runtime/AutoValue_EventInternal;->getCode()Ljava/lang/Integer; -PLcom/google/android/datatransport/runtime/AutoValue_EventInternal;->getEncodedPayload()Lcom/google/android/datatransport/runtime/EncodedPayload; -PLcom/google/android/datatransport/runtime/AutoValue_EventInternal;->getEventMillis()J -PLcom/google/android/datatransport/runtime/AutoValue_EventInternal;->getExperimentIdsClear()[B -PLcom/google/android/datatransport/runtime/AutoValue_EventInternal;->getExperimentIdsEncrypted()[B -PLcom/google/android/datatransport/runtime/AutoValue_EventInternal;->getProductId()Ljava/lang/Integer; -PLcom/google/android/datatransport/runtime/AutoValue_EventInternal;->getPseudonymousId()Ljava/lang/String; -PLcom/google/android/datatransport/runtime/AutoValue_EventInternal;->getTransportName()Ljava/lang/String; -PLcom/google/android/datatransport/runtime/AutoValue_EventInternal;->getUptimeMillis()J -PLcom/google/android/datatransport/runtime/AutoValue_EventInternal$Builder;->()V -PLcom/google/android/datatransport/runtime/AutoValue_EventInternal$Builder;->build()Lcom/google/android/datatransport/runtime/EventInternal; -PLcom/google/android/datatransport/runtime/AutoValue_EventInternal$Builder;->getAutoMetadata()Ljava/util/Map; -PLcom/google/android/datatransport/runtime/AutoValue_EventInternal$Builder;->setAutoMetadata(Ljava/util/Map;)Lcom/google/android/datatransport/runtime/EventInternal$Builder; -PLcom/google/android/datatransport/runtime/AutoValue_EventInternal$Builder;->setCode(Ljava/lang/Integer;)Lcom/google/android/datatransport/runtime/EventInternal$Builder; -PLcom/google/android/datatransport/runtime/AutoValue_EventInternal$Builder;->setEncodedPayload(Lcom/google/android/datatransport/runtime/EncodedPayload;)Lcom/google/android/datatransport/runtime/EventInternal$Builder; -PLcom/google/android/datatransport/runtime/AutoValue_EventInternal$Builder;->setEventMillis(J)Lcom/google/android/datatransport/runtime/EventInternal$Builder; -PLcom/google/android/datatransport/runtime/AutoValue_EventInternal$Builder;->setExperimentIdsClear([B)Lcom/google/android/datatransport/runtime/EventInternal$Builder; -PLcom/google/android/datatransport/runtime/AutoValue_EventInternal$Builder;->setExperimentIdsEncrypted([B)Lcom/google/android/datatransport/runtime/EventInternal$Builder; -PLcom/google/android/datatransport/runtime/AutoValue_EventInternal$Builder;->setProductId(Ljava/lang/Integer;)Lcom/google/android/datatransport/runtime/EventInternal$Builder; -PLcom/google/android/datatransport/runtime/AutoValue_EventInternal$Builder;->setPseudonymousId(Ljava/lang/String;)Lcom/google/android/datatransport/runtime/EventInternal$Builder; -PLcom/google/android/datatransport/runtime/AutoValue_EventInternal$Builder;->setTransportName(Ljava/lang/String;)Lcom/google/android/datatransport/runtime/EventInternal$Builder; -PLcom/google/android/datatransport/runtime/AutoValue_EventInternal$Builder;->setUptimeMillis(J)Lcom/google/android/datatransport/runtime/EventInternal$Builder; -PLcom/google/android/datatransport/runtime/AutoValue_SendRequest;->(Lcom/google/android/datatransport/runtime/TransportContext;Ljava/lang/String;Lcom/google/android/datatransport/Event;Lcom/google/android/datatransport/Transformer;Lcom/google/android/datatransport/Encoding;)V -PLcom/google/android/datatransport/runtime/AutoValue_SendRequest;->(Lcom/google/android/datatransport/runtime/TransportContext;Ljava/lang/String;Lcom/google/android/datatransport/Event;Lcom/google/android/datatransport/Transformer;Lcom/google/android/datatransport/Encoding;Lcom/google/android/datatransport/runtime/AutoValue_SendRequest$1;)V -PLcom/google/android/datatransport/runtime/AutoValue_SendRequest;->getEncoding()Lcom/google/android/datatransport/Encoding; -PLcom/google/android/datatransport/runtime/AutoValue_SendRequest;->getEvent()Lcom/google/android/datatransport/Event; -PLcom/google/android/datatransport/runtime/AutoValue_SendRequest;->getTransformer()Lcom/google/android/datatransport/Transformer; -PLcom/google/android/datatransport/runtime/AutoValue_SendRequest;->getTransportContext()Lcom/google/android/datatransport/runtime/TransportContext; -PLcom/google/android/datatransport/runtime/AutoValue_SendRequest;->getTransportName()Ljava/lang/String; -PLcom/google/android/datatransport/runtime/AutoValue_SendRequest$Builder;->()V -PLcom/google/android/datatransport/runtime/AutoValue_SendRequest$Builder;->build()Lcom/google/android/datatransport/runtime/SendRequest; -PLcom/google/android/datatransport/runtime/AutoValue_SendRequest$Builder;->setEncoding(Lcom/google/android/datatransport/Encoding;)Lcom/google/android/datatransport/runtime/SendRequest$Builder; -PLcom/google/android/datatransport/runtime/AutoValue_SendRequest$Builder;->setEvent(Lcom/google/android/datatransport/Event;)Lcom/google/android/datatransport/runtime/SendRequest$Builder; -PLcom/google/android/datatransport/runtime/AutoValue_SendRequest$Builder;->setTransformer(Lcom/google/android/datatransport/Transformer;)Lcom/google/android/datatransport/runtime/SendRequest$Builder; -PLcom/google/android/datatransport/runtime/AutoValue_SendRequest$Builder;->setTransportContext(Lcom/google/android/datatransport/runtime/TransportContext;)Lcom/google/android/datatransport/runtime/SendRequest$Builder; -PLcom/google/android/datatransport/runtime/AutoValue_SendRequest$Builder;->setTransportName(Ljava/lang/String;)Lcom/google/android/datatransport/runtime/SendRequest$Builder; +Lcom/google/android/datatransport/cct/internal/AndroidClientInfo; +Lcom/google/android/datatransport/cct/internal/AutoBatchedLogRequestEncoder; +HSPLcom/google/android/datatransport/cct/internal/AutoBatchedLogRequestEncoder;->()V +HSPLcom/google/android/datatransport/cct/internal/AutoBatchedLogRequestEncoder;->()V +HSPLcom/google/android/datatransport/cct/internal/AutoBatchedLogRequestEncoder;->configure(Lcom/google/firebase/encoders/config/EncoderConfig;)V +Lcom/google/android/datatransport/cct/internal/AutoBatchedLogRequestEncoder$AndroidClientInfoEncoder; +HSPLcom/google/android/datatransport/cct/internal/AutoBatchedLogRequestEncoder$AndroidClientInfoEncoder;->()V +HSPLcom/google/android/datatransport/cct/internal/AutoBatchedLogRequestEncoder$AndroidClientInfoEncoder;->()V +Lcom/google/android/datatransport/cct/internal/AutoBatchedLogRequestEncoder$BatchedLogRequestEncoder; +HSPLcom/google/android/datatransport/cct/internal/AutoBatchedLogRequestEncoder$BatchedLogRequestEncoder;->()V +HSPLcom/google/android/datatransport/cct/internal/AutoBatchedLogRequestEncoder$BatchedLogRequestEncoder;->()V +Lcom/google/android/datatransport/cct/internal/AutoBatchedLogRequestEncoder$ClientInfoEncoder; +HSPLcom/google/android/datatransport/cct/internal/AutoBatchedLogRequestEncoder$ClientInfoEncoder;->()V +HSPLcom/google/android/datatransport/cct/internal/AutoBatchedLogRequestEncoder$ClientInfoEncoder;->()V +Lcom/google/android/datatransport/cct/internal/AutoBatchedLogRequestEncoder$ComplianceDataEncoder; +HSPLcom/google/android/datatransport/cct/internal/AutoBatchedLogRequestEncoder$ComplianceDataEncoder;->()V +HSPLcom/google/android/datatransport/cct/internal/AutoBatchedLogRequestEncoder$ComplianceDataEncoder;->()V +Lcom/google/android/datatransport/cct/internal/AutoBatchedLogRequestEncoder$ExperimentIdsEncoder; +HSPLcom/google/android/datatransport/cct/internal/AutoBatchedLogRequestEncoder$ExperimentIdsEncoder;->()V +HSPLcom/google/android/datatransport/cct/internal/AutoBatchedLogRequestEncoder$ExperimentIdsEncoder;->()V +Lcom/google/android/datatransport/cct/internal/AutoBatchedLogRequestEncoder$ExternalPRequestContextEncoder; +HSPLcom/google/android/datatransport/cct/internal/AutoBatchedLogRequestEncoder$ExternalPRequestContextEncoder;->()V +HSPLcom/google/android/datatransport/cct/internal/AutoBatchedLogRequestEncoder$ExternalPRequestContextEncoder;->()V +Lcom/google/android/datatransport/cct/internal/AutoBatchedLogRequestEncoder$ExternalPrivacyContextEncoder; +HSPLcom/google/android/datatransport/cct/internal/AutoBatchedLogRequestEncoder$ExternalPrivacyContextEncoder;->()V +HSPLcom/google/android/datatransport/cct/internal/AutoBatchedLogRequestEncoder$ExternalPrivacyContextEncoder;->()V +Lcom/google/android/datatransport/cct/internal/AutoBatchedLogRequestEncoder$LogEventEncoder; +HSPLcom/google/android/datatransport/cct/internal/AutoBatchedLogRequestEncoder$LogEventEncoder;->()V +HSPLcom/google/android/datatransport/cct/internal/AutoBatchedLogRequestEncoder$LogEventEncoder;->()V +Lcom/google/android/datatransport/cct/internal/AutoBatchedLogRequestEncoder$LogRequestEncoder; +HSPLcom/google/android/datatransport/cct/internal/AutoBatchedLogRequestEncoder$LogRequestEncoder;->()V +HSPLcom/google/android/datatransport/cct/internal/AutoBatchedLogRequestEncoder$LogRequestEncoder;->()V +Lcom/google/android/datatransport/cct/internal/AutoBatchedLogRequestEncoder$NetworkConnectionInfoEncoder; +HSPLcom/google/android/datatransport/cct/internal/AutoBatchedLogRequestEncoder$NetworkConnectionInfoEncoder;->()V +HSPLcom/google/android/datatransport/cct/internal/AutoBatchedLogRequestEncoder$NetworkConnectionInfoEncoder;->()V +Lcom/google/android/datatransport/cct/internal/AutoValue_AndroidClientInfo; +Lcom/google/android/datatransport/cct/internal/AutoValue_BatchedLogRequest; +Lcom/google/android/datatransport/cct/internal/AutoValue_ClientInfo; +Lcom/google/android/datatransport/cct/internal/AutoValue_ComplianceData; +Lcom/google/android/datatransport/cct/internal/AutoValue_ExperimentIds; +Lcom/google/android/datatransport/cct/internal/AutoValue_ExternalPRequestContext; +Lcom/google/android/datatransport/cct/internal/AutoValue_ExternalPrivacyContext; +Lcom/google/android/datatransport/cct/internal/AutoValue_LogEvent; +Lcom/google/android/datatransport/cct/internal/AutoValue_LogRequest; +Lcom/google/android/datatransport/cct/internal/AutoValue_NetworkConnectionInfo; +Lcom/google/android/datatransport/cct/internal/BatchedLogRequest; +HSPLcom/google/android/datatransport/cct/internal/BatchedLogRequest;->createDataEncoder()Lcom/google/firebase/encoders/DataEncoder; +Lcom/google/android/datatransport/cct/internal/ClientInfo; +Lcom/google/android/datatransport/cct/internal/ComplianceData; +Lcom/google/android/datatransport/cct/internal/ExperimentIds; +Lcom/google/android/datatransport/cct/internal/ExternalPRequestContext; +Lcom/google/android/datatransport/cct/internal/ExternalPrivacyContext; +Lcom/google/android/datatransport/cct/internal/LogEvent; +Lcom/google/android/datatransport/cct/internal/LogRequest; +Lcom/google/android/datatransport/cct/internal/NetworkConnectionInfo; +Lcom/google/android/datatransport/cct/internal/NetworkConnectionInfo$MobileSubtype; +HSPLcom/google/android/datatransport/cct/internal/NetworkConnectionInfo$MobileSubtype;->()V +HSPLcom/google/android/datatransport/cct/internal/NetworkConnectionInfo$MobileSubtype;->(Ljava/lang/String;II)V +HSPLcom/google/android/datatransport/cct/internal/NetworkConnectionInfo$MobileSubtype;->getValue()I +Lcom/google/android/datatransport/runtime/AutoValue_EventInternal; +HSPLcom/google/android/datatransport/runtime/AutoValue_EventInternal;->(Ljava/lang/String;Ljava/lang/Integer;Lcom/google/android/datatransport/runtime/EncodedPayload;JJLjava/util/Map;Ljava/lang/Integer;Ljava/lang/String;[B[B)V +HSPLcom/google/android/datatransport/runtime/AutoValue_EventInternal;->(Ljava/lang/String;Ljava/lang/Integer;Lcom/google/android/datatransport/runtime/EncodedPayload;JJLjava/util/Map;Ljava/lang/Integer;Ljava/lang/String;[B[BLcom/google/android/datatransport/runtime/AutoValue_EventInternal$1;)V +HSPLcom/google/android/datatransport/runtime/AutoValue_EventInternal;->getAutoMetadata()Ljava/util/Map; +HSPLcom/google/android/datatransport/runtime/AutoValue_EventInternal;->getCode()Ljava/lang/Integer; +HSPLcom/google/android/datatransport/runtime/AutoValue_EventInternal;->getEncodedPayload()Lcom/google/android/datatransport/runtime/EncodedPayload; +HSPLcom/google/android/datatransport/runtime/AutoValue_EventInternal;->getEventMillis()J +HSPLcom/google/android/datatransport/runtime/AutoValue_EventInternal;->getExperimentIdsClear()[B +HSPLcom/google/android/datatransport/runtime/AutoValue_EventInternal;->getExperimentIdsEncrypted()[B +HSPLcom/google/android/datatransport/runtime/AutoValue_EventInternal;->getProductId()Ljava/lang/Integer; +HSPLcom/google/android/datatransport/runtime/AutoValue_EventInternal;->getPseudonymousId()Ljava/lang/String; +HSPLcom/google/android/datatransport/runtime/AutoValue_EventInternal;->getTransportName()Ljava/lang/String; +HSPLcom/google/android/datatransport/runtime/AutoValue_EventInternal;->getUptimeMillis()J +Lcom/google/android/datatransport/runtime/AutoValue_EventInternal$Builder; +HSPLcom/google/android/datatransport/runtime/AutoValue_EventInternal$Builder;->()V +HSPLcom/google/android/datatransport/runtime/AutoValue_EventInternal$Builder;->build()Lcom/google/android/datatransport/runtime/EventInternal; +HSPLcom/google/android/datatransport/runtime/AutoValue_EventInternal$Builder;->getAutoMetadata()Ljava/util/Map; +HSPLcom/google/android/datatransport/runtime/AutoValue_EventInternal$Builder;->setAutoMetadata(Ljava/util/Map;)Lcom/google/android/datatransport/runtime/EventInternal$Builder; +HSPLcom/google/android/datatransport/runtime/AutoValue_EventInternal$Builder;->setCode(Ljava/lang/Integer;)Lcom/google/android/datatransport/runtime/EventInternal$Builder; +HSPLcom/google/android/datatransport/runtime/AutoValue_EventInternal$Builder;->setEncodedPayload(Lcom/google/android/datatransport/runtime/EncodedPayload;)Lcom/google/android/datatransport/runtime/EventInternal$Builder; +HSPLcom/google/android/datatransport/runtime/AutoValue_EventInternal$Builder;->setEventMillis(J)Lcom/google/android/datatransport/runtime/EventInternal$Builder; +HSPLcom/google/android/datatransport/runtime/AutoValue_EventInternal$Builder;->setExperimentIdsClear([B)Lcom/google/android/datatransport/runtime/EventInternal$Builder; +HSPLcom/google/android/datatransport/runtime/AutoValue_EventInternal$Builder;->setExperimentIdsEncrypted([B)Lcom/google/android/datatransport/runtime/EventInternal$Builder; +HSPLcom/google/android/datatransport/runtime/AutoValue_EventInternal$Builder;->setProductId(Ljava/lang/Integer;)Lcom/google/android/datatransport/runtime/EventInternal$Builder; +HSPLcom/google/android/datatransport/runtime/AutoValue_EventInternal$Builder;->setPseudonymousId(Ljava/lang/String;)Lcom/google/android/datatransport/runtime/EventInternal$Builder; +HSPLcom/google/android/datatransport/runtime/AutoValue_EventInternal$Builder;->setTransportName(Ljava/lang/String;)Lcom/google/android/datatransport/runtime/EventInternal$Builder; +HSPLcom/google/android/datatransport/runtime/AutoValue_EventInternal$Builder;->setUptimeMillis(J)Lcom/google/android/datatransport/runtime/EventInternal$Builder; +Lcom/google/android/datatransport/runtime/AutoValue_SendRequest; +HSPLcom/google/android/datatransport/runtime/AutoValue_SendRequest;->(Lcom/google/android/datatransport/runtime/TransportContext;Ljava/lang/String;Lcom/google/android/datatransport/Event;Lcom/google/android/datatransport/Transformer;Lcom/google/android/datatransport/Encoding;)V +HSPLcom/google/android/datatransport/runtime/AutoValue_SendRequest;->(Lcom/google/android/datatransport/runtime/TransportContext;Ljava/lang/String;Lcom/google/android/datatransport/Event;Lcom/google/android/datatransport/Transformer;Lcom/google/android/datatransport/Encoding;Lcom/google/android/datatransport/runtime/AutoValue_SendRequest$1;)V +HSPLcom/google/android/datatransport/runtime/AutoValue_SendRequest;->getEncoding()Lcom/google/android/datatransport/Encoding; +HSPLcom/google/android/datatransport/runtime/AutoValue_SendRequest;->getEvent()Lcom/google/android/datatransport/Event; +HSPLcom/google/android/datatransport/runtime/AutoValue_SendRequest;->getTransformer()Lcom/google/android/datatransport/Transformer; +HSPLcom/google/android/datatransport/runtime/AutoValue_SendRequest;->getTransportContext()Lcom/google/android/datatransport/runtime/TransportContext; +HSPLcom/google/android/datatransport/runtime/AutoValue_SendRequest;->getTransportName()Ljava/lang/String; +Lcom/google/android/datatransport/runtime/AutoValue_SendRequest$Builder; +HSPLcom/google/android/datatransport/runtime/AutoValue_SendRequest$Builder;->()V +HSPLcom/google/android/datatransport/runtime/AutoValue_SendRequest$Builder;->build()Lcom/google/android/datatransport/runtime/SendRequest; +HSPLcom/google/android/datatransport/runtime/AutoValue_SendRequest$Builder;->setEncoding(Lcom/google/android/datatransport/Encoding;)Lcom/google/android/datatransport/runtime/SendRequest$Builder; +HSPLcom/google/android/datatransport/runtime/AutoValue_SendRequest$Builder;->setEvent(Lcom/google/android/datatransport/Event;)Lcom/google/android/datatransport/runtime/SendRequest$Builder; +HSPLcom/google/android/datatransport/runtime/AutoValue_SendRequest$Builder;->setTransformer(Lcom/google/android/datatransport/Transformer;)Lcom/google/android/datatransport/runtime/SendRequest$Builder; +HSPLcom/google/android/datatransport/runtime/AutoValue_SendRequest$Builder;->setTransportContext(Lcom/google/android/datatransport/runtime/TransportContext;)Lcom/google/android/datatransport/runtime/SendRequest$Builder; +HSPLcom/google/android/datatransport/runtime/AutoValue_SendRequest$Builder;->setTransportName(Ljava/lang/String;)Lcom/google/android/datatransport/runtime/SendRequest$Builder; Lcom/google/android/datatransport/runtime/AutoValue_TransportContext; HSPLcom/google/android/datatransport/runtime/AutoValue_TransportContext;->(Ljava/lang/String;[BLcom/google/android/datatransport/Priority;)V HSPLcom/google/android/datatransport/runtime/AutoValue_TransportContext;->(Ljava/lang/String;[BLcom/google/android/datatransport/Priority;Lcom/google/android/datatransport/runtime/AutoValue_TransportContext$1;)V @@ -22900,17 +23091,20 @@ HSPLcom/google/android/datatransport/runtime/DaggerTransportRuntimeComponent$Tra HSPLcom/google/android/datatransport/runtime/DaggerTransportRuntimeComponent$TransportRuntimeComponentImpl;->initialize(Landroid/content/Context;)V Lcom/google/android/datatransport/runtime/Destination; Lcom/google/android/datatransport/runtime/EncodedDestination; -PLcom/google/android/datatransport/runtime/EncodedPayload;->(Lcom/google/android/datatransport/Encoding;[B)V -PLcom/google/android/datatransport/runtime/EncodedPayload;->getBytes()[B -PLcom/google/android/datatransport/runtime/EncodedPayload;->getEncoding()Lcom/google/android/datatransport/Encoding; -PLcom/google/android/datatransport/runtime/EventInternal;->()V -PLcom/google/android/datatransport/runtime/EventInternal;->builder()Lcom/google/android/datatransport/runtime/EventInternal$Builder; -PLcom/google/android/datatransport/runtime/EventInternal;->getMetadata()Ljava/util/Map; -PLcom/google/android/datatransport/runtime/EventInternal;->toBuilder()Lcom/google/android/datatransport/runtime/EventInternal$Builder; -PLcom/google/android/datatransport/runtime/EventInternal$Builder;->()V -PLcom/google/android/datatransport/runtime/EventInternal$Builder;->addMetadata(Ljava/lang/String;I)Lcom/google/android/datatransport/runtime/EventInternal$Builder; -PLcom/google/android/datatransport/runtime/EventInternal$Builder;->addMetadata(Ljava/lang/String;J)Lcom/google/android/datatransport/runtime/EventInternal$Builder; -PLcom/google/android/datatransport/runtime/EventInternal$Builder;->addMetadata(Ljava/lang/String;Ljava/lang/String;)Lcom/google/android/datatransport/runtime/EventInternal$Builder; +Lcom/google/android/datatransport/runtime/EncodedPayload; +HSPLcom/google/android/datatransport/runtime/EncodedPayload;->(Lcom/google/android/datatransport/Encoding;[B)V +HSPLcom/google/android/datatransport/runtime/EncodedPayload;->getBytes()[B +HSPLcom/google/android/datatransport/runtime/EncodedPayload;->getEncoding()Lcom/google/android/datatransport/Encoding; +Lcom/google/android/datatransport/runtime/EventInternal; +HSPLcom/google/android/datatransport/runtime/EventInternal;->()V +HSPLcom/google/android/datatransport/runtime/EventInternal;->builder()Lcom/google/android/datatransport/runtime/EventInternal$Builder; +HSPLcom/google/android/datatransport/runtime/EventInternal;->getMetadata()Ljava/util/Map; +HSPLcom/google/android/datatransport/runtime/EventInternal;->toBuilder()Lcom/google/android/datatransport/runtime/EventInternal$Builder; +Lcom/google/android/datatransport/runtime/EventInternal$Builder; +HSPLcom/google/android/datatransport/runtime/EventInternal$Builder;->()V +HSPLcom/google/android/datatransport/runtime/EventInternal$Builder;->addMetadata(Ljava/lang/String;I)Lcom/google/android/datatransport/runtime/EventInternal$Builder; +HSPLcom/google/android/datatransport/runtime/EventInternal$Builder;->addMetadata(Ljava/lang/String;J)Lcom/google/android/datatransport/runtime/EventInternal$Builder; +HSPLcom/google/android/datatransport/runtime/EventInternal$Builder;->addMetadata(Ljava/lang/String;Ljava/lang/String;)Lcom/google/android/datatransport/runtime/EventInternal$Builder; Lcom/google/android/datatransport/runtime/ExecutionModule; HSPLcom/google/android/datatransport/runtime/ExecutionModule;->executor()Ljava/util/concurrent/Executor; Lcom/google/android/datatransport/runtime/ExecutionModule_ExecutorFactory; @@ -22928,14 +23122,16 @@ HSPLcom/google/android/datatransport/runtime/SafeLoggingExecutor;->execute(Ljava Lcom/google/android/datatransport/runtime/SafeLoggingExecutor$SafeLoggingRunnable; HSPLcom/google/android/datatransport/runtime/SafeLoggingExecutor$SafeLoggingRunnable;->(Ljava/lang/Runnable;)V HSPLcom/google/android/datatransport/runtime/SafeLoggingExecutor$SafeLoggingRunnable;->run()V -PLcom/google/android/datatransport/runtime/SendRequest;->()V -PLcom/google/android/datatransport/runtime/SendRequest;->builder()Lcom/google/android/datatransport/runtime/SendRequest$Builder; -PLcom/google/android/datatransport/runtime/SendRequest;->getPayload()[B -PLcom/google/android/datatransport/runtime/SendRequest$Builder;->()V +Lcom/google/android/datatransport/runtime/SendRequest; +HSPLcom/google/android/datatransport/runtime/SendRequest;->()V +HSPLcom/google/android/datatransport/runtime/SendRequest;->builder()Lcom/google/android/datatransport/runtime/SendRequest$Builder; +HSPLcom/google/android/datatransport/runtime/SendRequest;->getPayload()[B +Lcom/google/android/datatransport/runtime/SendRequest$Builder; +HSPLcom/google/android/datatransport/runtime/SendRequest$Builder;->()V Lcom/google/android/datatransport/runtime/TransportContext; HSPLcom/google/android/datatransport/runtime/TransportContext;->()V HSPLcom/google/android/datatransport/runtime/TransportContext;->builder()Lcom/google/android/datatransport/runtime/TransportContext$Builder; -PLcom/google/android/datatransport/runtime/TransportContext;->withPriority(Lcom/google/android/datatransport/Priority;)Lcom/google/android/datatransport/runtime/TransportContext; +HSPLcom/google/android/datatransport/runtime/TransportContext;->withPriority(Lcom/google/android/datatransport/Priority;)Lcom/google/android/datatransport/runtime/TransportContext; Lcom/google/android/datatransport/runtime/TransportContext$Builder; HSPLcom/google/android/datatransport/runtime/TransportContext$Builder;->()V Lcom/google/android/datatransport/runtime/TransportFactoryImpl; @@ -22943,21 +23139,22 @@ HSPLcom/google/android/datatransport/runtime/TransportFactoryImpl;->(Ljava HSPLcom/google/android/datatransport/runtime/TransportFactoryImpl;->getTransport(Ljava/lang/String;Ljava/lang/Class;Lcom/google/android/datatransport/Encoding;Lcom/google/android/datatransport/Transformer;)Lcom/google/android/datatransport/Transport; Lcom/google/android/datatransport/runtime/TransportImpl; HSPLcom/google/android/datatransport/runtime/TransportImpl;->(Lcom/google/android/datatransport/runtime/TransportContext;Ljava/lang/String;Lcom/google/android/datatransport/Encoding;Lcom/google/android/datatransport/Transformer;Lcom/google/android/datatransport/runtime/TransportInternal;)V -PLcom/google/android/datatransport/runtime/TransportImpl;->lambda$send$0(Ljava/lang/Exception;)V -PLcom/google/android/datatransport/runtime/TransportImpl;->schedule(Lcom/google/android/datatransport/Event;Lcom/google/android/datatransport/TransportScheduleCallback;)V -PLcom/google/android/datatransport/runtime/TransportImpl;->send(Lcom/google/android/datatransport/Event;)V -PLcom/google/android/datatransport/runtime/TransportImpl$$ExternalSyntheticLambda0;->()V -PLcom/google/android/datatransport/runtime/TransportImpl$$ExternalSyntheticLambda0;->onSchedule(Ljava/lang/Exception;)V +HSPLcom/google/android/datatransport/runtime/TransportImpl;->lambda$send$0(Ljava/lang/Exception;)V +HSPLcom/google/android/datatransport/runtime/TransportImpl;->schedule(Lcom/google/android/datatransport/Event;Lcom/google/android/datatransport/TransportScheduleCallback;)V +HSPLcom/google/android/datatransport/runtime/TransportImpl;->send(Lcom/google/android/datatransport/Event;)V +Lcom/google/android/datatransport/runtime/TransportImpl$$ExternalSyntheticLambda0; +HSPLcom/google/android/datatransport/runtime/TransportImpl$$ExternalSyntheticLambda0;->()V +HSPLcom/google/android/datatransport/runtime/TransportImpl$$ExternalSyntheticLambda0;->onSchedule(Ljava/lang/Exception;)V Lcom/google/android/datatransport/runtime/TransportInternal; Lcom/google/android/datatransport/runtime/TransportRuntime; HSPLcom/google/android/datatransport/runtime/TransportRuntime;->()V HSPLcom/google/android/datatransport/runtime/TransportRuntime;->(Lcom/google/android/datatransport/runtime/time/Clock;Lcom/google/android/datatransport/runtime/time/Clock;Lcom/google/android/datatransport/runtime/scheduling/Scheduler;Lcom/google/android/datatransport/runtime/scheduling/jobscheduling/Uploader;Lcom/google/android/datatransport/runtime/scheduling/jobscheduling/WorkInitializer;)V -PLcom/google/android/datatransport/runtime/TransportRuntime;->convert(Lcom/google/android/datatransport/runtime/SendRequest;)Lcom/google/android/datatransport/runtime/EventInternal; +HSPLcom/google/android/datatransport/runtime/TransportRuntime;->convert(Lcom/google/android/datatransport/runtime/SendRequest;)Lcom/google/android/datatransport/runtime/EventInternal; HSPLcom/google/android/datatransport/runtime/TransportRuntime;->getInstance()Lcom/google/android/datatransport/runtime/TransportRuntime; HSPLcom/google/android/datatransport/runtime/TransportRuntime;->getSupportedEncodings(Lcom/google/android/datatransport/runtime/Destination;)Ljava/util/Set; HSPLcom/google/android/datatransport/runtime/TransportRuntime;->initialize(Landroid/content/Context;)V HSPLcom/google/android/datatransport/runtime/TransportRuntime;->newFactory(Lcom/google/android/datatransport/runtime/Destination;)Lcom/google/android/datatransport/TransportFactory; -PLcom/google/android/datatransport/runtime/TransportRuntime;->send(Lcom/google/android/datatransport/runtime/SendRequest;Lcom/google/android/datatransport/TransportScheduleCallback;)V +HSPLcom/google/android/datatransport/runtime/TransportRuntime;->send(Lcom/google/android/datatransport/runtime/SendRequest;Lcom/google/android/datatransport/TransportScheduleCallback;)V Lcom/google/android/datatransport/runtime/TransportRuntimeComponent; HSPLcom/google/android/datatransport/runtime/TransportRuntimeComponent;->()V Lcom/google/android/datatransport/runtime/TransportRuntimeComponent$Builder; @@ -22967,16 +23164,19 @@ HSPLcom/google/android/datatransport/runtime/TransportRuntime_Factory;->create(L HSPLcom/google/android/datatransport/runtime/TransportRuntime_Factory;->get()Lcom/google/android/datatransport/runtime/TransportRuntime; HSPLcom/google/android/datatransport/runtime/TransportRuntime_Factory;->get()Ljava/lang/Object; HSPLcom/google/android/datatransport/runtime/TransportRuntime_Factory;->newInstance(Lcom/google/android/datatransport/runtime/time/Clock;Lcom/google/android/datatransport/runtime/time/Clock;Lcom/google/android/datatransport/runtime/scheduling/Scheduler;Lcom/google/android/datatransport/runtime/scheduling/jobscheduling/Uploader;Lcom/google/android/datatransport/runtime/scheduling/jobscheduling/WorkInitializer;)Lcom/google/android/datatransport/runtime/TransportRuntime; -PLcom/google/android/datatransport/runtime/backends/AutoValue_CreationContext;->(Landroid/content/Context;Lcom/google/android/datatransport/runtime/time/Clock;Lcom/google/android/datatransport/runtime/time/Clock;Ljava/lang/String;)V -PLcom/google/android/datatransport/runtime/backends/AutoValue_CreationContext;->getApplicationContext()Landroid/content/Context; -PLcom/google/android/datatransport/runtime/backends/AutoValue_CreationContext;->getMonotonicClock()Lcom/google/android/datatransport/runtime/time/Clock; -PLcom/google/android/datatransport/runtime/backends/AutoValue_CreationContext;->getWallClock()Lcom/google/android/datatransport/runtime/time/Clock; +Lcom/google/android/datatransport/runtime/backends/AutoValue_CreationContext; +HSPLcom/google/android/datatransport/runtime/backends/AutoValue_CreationContext;->(Landroid/content/Context;Lcom/google/android/datatransport/runtime/time/Clock;Lcom/google/android/datatransport/runtime/time/Clock;Ljava/lang/String;)V +HSPLcom/google/android/datatransport/runtime/backends/AutoValue_CreationContext;->getApplicationContext()Landroid/content/Context; +HSPLcom/google/android/datatransport/runtime/backends/AutoValue_CreationContext;->getMonotonicClock()Lcom/google/android/datatransport/runtime/time/Clock; +HSPLcom/google/android/datatransport/runtime/backends/AutoValue_CreationContext;->getWallClock()Lcom/google/android/datatransport/runtime/time/Clock; +Lcom/google/android/datatransport/runtime/backends/BackendFactory; Lcom/google/android/datatransport/runtime/backends/BackendRegistry; -PLcom/google/android/datatransport/runtime/backends/CreationContext;->()V -PLcom/google/android/datatransport/runtime/backends/CreationContext;->create(Landroid/content/Context;Lcom/google/android/datatransport/runtime/time/Clock;Lcom/google/android/datatransport/runtime/time/Clock;Ljava/lang/String;)Lcom/google/android/datatransport/runtime/backends/CreationContext; +Lcom/google/android/datatransport/runtime/backends/CreationContext; +HSPLcom/google/android/datatransport/runtime/backends/CreationContext;->()V +HSPLcom/google/android/datatransport/runtime/backends/CreationContext;->create(Landroid/content/Context;Lcom/google/android/datatransport/runtime/time/Clock;Lcom/google/android/datatransport/runtime/time/Clock;Ljava/lang/String;)Lcom/google/android/datatransport/runtime/backends/CreationContext; Lcom/google/android/datatransport/runtime/backends/CreationContextFactory; HSPLcom/google/android/datatransport/runtime/backends/CreationContextFactory;->(Landroid/content/Context;Lcom/google/android/datatransport/runtime/time/Clock;Lcom/google/android/datatransport/runtime/time/Clock;)V -PLcom/google/android/datatransport/runtime/backends/CreationContextFactory;->create(Ljava/lang/String;)Lcom/google/android/datatransport/runtime/backends/CreationContext; +HSPLcom/google/android/datatransport/runtime/backends/CreationContextFactory;->create(Ljava/lang/String;)Lcom/google/android/datatransport/runtime/backends/CreationContext; Lcom/google/android/datatransport/runtime/backends/CreationContextFactory_Factory; HSPLcom/google/android/datatransport/runtime/backends/CreationContextFactory_Factory;->(Ljavax/inject/Provider;Ljavax/inject/Provider;Ljavax/inject/Provider;)V HSPLcom/google/android/datatransport/runtime/backends/CreationContextFactory_Factory;->create(Ljavax/inject/Provider;Ljavax/inject/Provider;Ljavax/inject/Provider;)Lcom/google/android/datatransport/runtime/backends/CreationContextFactory_Factory; @@ -22986,19 +23186,21 @@ HSPLcom/google/android/datatransport/runtime/backends/CreationContextFactory_Fac Lcom/google/android/datatransport/runtime/backends/MetadataBackendRegistry; HSPLcom/google/android/datatransport/runtime/backends/MetadataBackendRegistry;->(Landroid/content/Context;Lcom/google/android/datatransport/runtime/backends/CreationContextFactory;)V HSPLcom/google/android/datatransport/runtime/backends/MetadataBackendRegistry;->(Lcom/google/android/datatransport/runtime/backends/MetadataBackendRegistry$BackendFactoryProvider;Lcom/google/android/datatransport/runtime/backends/CreationContextFactory;)V -PLcom/google/android/datatransport/runtime/backends/MetadataBackendRegistry;->get(Ljava/lang/String;)Lcom/google/android/datatransport/runtime/backends/TransportBackend; +HSPLcom/google/android/datatransport/runtime/backends/MetadataBackendRegistry;->get(Ljava/lang/String;)Lcom/google/android/datatransport/runtime/backends/TransportBackend; Lcom/google/android/datatransport/runtime/backends/MetadataBackendRegistry$BackendFactoryProvider; HSPLcom/google/android/datatransport/runtime/backends/MetadataBackendRegistry$BackendFactoryProvider;->(Landroid/content/Context;)V -PLcom/google/android/datatransport/runtime/backends/MetadataBackendRegistry$BackendFactoryProvider;->discover(Landroid/content/Context;)Ljava/util/Map; -PLcom/google/android/datatransport/runtime/backends/MetadataBackendRegistry$BackendFactoryProvider;->get(Ljava/lang/String;)Lcom/google/android/datatransport/runtime/backends/BackendFactory; -PLcom/google/android/datatransport/runtime/backends/MetadataBackendRegistry$BackendFactoryProvider;->getBackendProviders()Ljava/util/Map; -PLcom/google/android/datatransport/runtime/backends/MetadataBackendRegistry$BackendFactoryProvider;->getMetadata(Landroid/content/Context;)Landroid/os/Bundle; +HSPLcom/google/android/datatransport/runtime/backends/MetadataBackendRegistry$BackendFactoryProvider;->discover(Landroid/content/Context;)Ljava/util/Map; +HSPLcom/google/android/datatransport/runtime/backends/MetadataBackendRegistry$BackendFactoryProvider;->get(Ljava/lang/String;)Lcom/google/android/datatransport/runtime/backends/BackendFactory; +HSPLcom/google/android/datatransport/runtime/backends/MetadataBackendRegistry$BackendFactoryProvider;->getBackendProviders()Ljava/util/Map; +HSPLcom/google/android/datatransport/runtime/backends/MetadataBackendRegistry$BackendFactoryProvider;->getMetadata(Landroid/content/Context;)Landroid/os/Bundle; Lcom/google/android/datatransport/runtime/backends/MetadataBackendRegistry_Factory; HSPLcom/google/android/datatransport/runtime/backends/MetadataBackendRegistry_Factory;->(Ljavax/inject/Provider;Ljavax/inject/Provider;)V HSPLcom/google/android/datatransport/runtime/backends/MetadataBackendRegistry_Factory;->create(Ljavax/inject/Provider;Ljavax/inject/Provider;)Lcom/google/android/datatransport/runtime/backends/MetadataBackendRegistry_Factory; HSPLcom/google/android/datatransport/runtime/backends/MetadataBackendRegistry_Factory;->get()Lcom/google/android/datatransport/runtime/backends/MetadataBackendRegistry; HSPLcom/google/android/datatransport/runtime/backends/MetadataBackendRegistry_Factory;->get()Ljava/lang/Object; HSPLcom/google/android/datatransport/runtime/backends/MetadataBackendRegistry_Factory;->newInstance(Landroid/content/Context;Ljava/lang/Object;)Lcom/google/android/datatransport/runtime/backends/MetadataBackendRegistry; +Lcom/google/android/datatransport/runtime/backends/TransportBackend; +Lcom/google/android/datatransport/runtime/backends/TransportBackendDiscovery; Lcom/google/android/datatransport/runtime/dagger/Lazy; Lcom/google/android/datatransport/runtime/dagger/internal/DoubleCheck; HSPLcom/google/android/datatransport/runtime/dagger/internal/DoubleCheck;->()V @@ -23018,19 +23220,21 @@ HSPLcom/google/android/datatransport/runtime/dagger/internal/Preconditions;->che HSPLcom/google/android/datatransport/runtime/dagger/internal/Preconditions;->checkNotNull(Ljava/lang/Object;Ljava/lang/String;)Ljava/lang/Object; HSPLcom/google/android/datatransport/runtime/dagger/internal/Preconditions;->checkNotNullFromProvides(Ljava/lang/Object;)Ljava/lang/Object; Lcom/google/android/datatransport/runtime/logging/Logging; -PLcom/google/android/datatransport/runtime/logging/Logging;->d(Ljava/lang/String;Ljava/lang/String;Ljava/lang/Object;)V +HSPLcom/google/android/datatransport/runtime/logging/Logging;->d(Ljava/lang/String;Ljava/lang/String;Ljava/lang/Object;)V HSPLcom/google/android/datatransport/runtime/logging/Logging;->d(Ljava/lang/String;Ljava/lang/String;[Ljava/lang/Object;)V HSPLcom/google/android/datatransport/runtime/logging/Logging;->getTag(Ljava/lang/String;)Ljava/lang/String; Lcom/google/android/datatransport/runtime/scheduling/DefaultScheduler; HSPLcom/google/android/datatransport/runtime/scheduling/DefaultScheduler;->()V HSPLcom/google/android/datatransport/runtime/scheduling/DefaultScheduler;->(Ljava/util/concurrent/Executor;Lcom/google/android/datatransport/runtime/backends/BackendRegistry;Lcom/google/android/datatransport/runtime/scheduling/jobscheduling/WorkScheduler;Lcom/google/android/datatransport/runtime/scheduling/persistence/EventStore;Lcom/google/android/datatransport/runtime/synchronization/SynchronizationGuard;)V -PLcom/google/android/datatransport/runtime/scheduling/DefaultScheduler;->lambda$schedule$0$com-google-android-datatransport-runtime-scheduling-DefaultScheduler(Lcom/google/android/datatransport/runtime/TransportContext;Lcom/google/android/datatransport/runtime/EventInternal;)Ljava/lang/Object; -PLcom/google/android/datatransport/runtime/scheduling/DefaultScheduler;->lambda$schedule$1$com-google-android-datatransport-runtime-scheduling-DefaultScheduler(Lcom/google/android/datatransport/runtime/TransportContext;Lcom/google/android/datatransport/TransportScheduleCallback;Lcom/google/android/datatransport/runtime/EventInternal;)V -PLcom/google/android/datatransport/runtime/scheduling/DefaultScheduler;->schedule(Lcom/google/android/datatransport/runtime/TransportContext;Lcom/google/android/datatransport/runtime/EventInternal;Lcom/google/android/datatransport/TransportScheduleCallback;)V -PLcom/google/android/datatransport/runtime/scheduling/DefaultScheduler$$ExternalSyntheticLambda0;->(Lcom/google/android/datatransport/runtime/scheduling/DefaultScheduler;Lcom/google/android/datatransport/runtime/TransportContext;Lcom/google/android/datatransport/TransportScheduleCallback;Lcom/google/android/datatransport/runtime/EventInternal;)V -PLcom/google/android/datatransport/runtime/scheduling/DefaultScheduler$$ExternalSyntheticLambda0;->run()V -PLcom/google/android/datatransport/runtime/scheduling/DefaultScheduler$$ExternalSyntheticLambda1;->(Lcom/google/android/datatransport/runtime/scheduling/DefaultScheduler;Lcom/google/android/datatransport/runtime/TransportContext;Lcom/google/android/datatransport/runtime/EventInternal;)V -PLcom/google/android/datatransport/runtime/scheduling/DefaultScheduler$$ExternalSyntheticLambda1;->execute()Ljava/lang/Object; +HSPLcom/google/android/datatransport/runtime/scheduling/DefaultScheduler;->lambda$schedule$0$com-google-android-datatransport-runtime-scheduling-DefaultScheduler(Lcom/google/android/datatransport/runtime/TransportContext;Lcom/google/android/datatransport/runtime/EventInternal;)Ljava/lang/Object; +HSPLcom/google/android/datatransport/runtime/scheduling/DefaultScheduler;->lambda$schedule$1$com-google-android-datatransport-runtime-scheduling-DefaultScheduler(Lcom/google/android/datatransport/runtime/TransportContext;Lcom/google/android/datatransport/TransportScheduleCallback;Lcom/google/android/datatransport/runtime/EventInternal;)V +HSPLcom/google/android/datatransport/runtime/scheduling/DefaultScheduler;->schedule(Lcom/google/android/datatransport/runtime/TransportContext;Lcom/google/android/datatransport/runtime/EventInternal;Lcom/google/android/datatransport/TransportScheduleCallback;)V +Lcom/google/android/datatransport/runtime/scheduling/DefaultScheduler$$ExternalSyntheticLambda0; +HSPLcom/google/android/datatransport/runtime/scheduling/DefaultScheduler$$ExternalSyntheticLambda0;->(Lcom/google/android/datatransport/runtime/scheduling/DefaultScheduler;Lcom/google/android/datatransport/runtime/TransportContext;Lcom/google/android/datatransport/TransportScheduleCallback;Lcom/google/android/datatransport/runtime/EventInternal;)V +HSPLcom/google/android/datatransport/runtime/scheduling/DefaultScheduler$$ExternalSyntheticLambda0;->run()V +Lcom/google/android/datatransport/runtime/scheduling/DefaultScheduler$$ExternalSyntheticLambda1; +HSPLcom/google/android/datatransport/runtime/scheduling/DefaultScheduler$$ExternalSyntheticLambda1;->(Lcom/google/android/datatransport/runtime/scheduling/DefaultScheduler;Lcom/google/android/datatransport/runtime/TransportContext;Lcom/google/android/datatransport/runtime/EventInternal;)V +HSPLcom/google/android/datatransport/runtime/scheduling/DefaultScheduler$$ExternalSyntheticLambda1;->execute()Ljava/lang/Object; Lcom/google/android/datatransport/runtime/scheduling/DefaultScheduler_Factory; HSPLcom/google/android/datatransport/runtime/scheduling/DefaultScheduler_Factory;->(Ljavax/inject/Provider;Ljavax/inject/Provider;Ljavax/inject/Provider;Ljavax/inject/Provider;Ljavax/inject/Provider;)V HSPLcom/google/android/datatransport/runtime/scheduling/DefaultScheduler_Factory;->create(Ljavax/inject/Provider;Ljavax/inject/Provider;Ljavax/inject/Provider;Ljavax/inject/Provider;Ljavax/inject/Provider;)Lcom/google/android/datatransport/runtime/scheduling/DefaultScheduler_Factory; @@ -23129,8 +23333,8 @@ Lcom/google/android/datatransport/runtime/scheduling/jobscheduling/WorkScheduler Lcom/google/android/datatransport/runtime/scheduling/persistence/AutoValue_EventStoreConfig; HSPLcom/google/android/datatransport/runtime/scheduling/persistence/AutoValue_EventStoreConfig;->(JIIJI)V HSPLcom/google/android/datatransport/runtime/scheduling/persistence/AutoValue_EventStoreConfig;->(JIIJILcom/google/android/datatransport/runtime/scheduling/persistence/AutoValue_EventStoreConfig$1;)V -PLcom/google/android/datatransport/runtime/scheduling/persistence/AutoValue_EventStoreConfig;->getMaxBlobByteSizePerRow()I -PLcom/google/android/datatransport/runtime/scheduling/persistence/AutoValue_EventStoreConfig;->getMaxStorageSizeInBytes()J +HSPLcom/google/android/datatransport/runtime/scheduling/persistence/AutoValue_EventStoreConfig;->getMaxBlobByteSizePerRow()I +HSPLcom/google/android/datatransport/runtime/scheduling/persistence/AutoValue_EventStoreConfig;->getMaxStorageSizeInBytes()J Lcom/google/android/datatransport/runtime/scheduling/persistence/AutoValue_EventStoreConfig$Builder; HSPLcom/google/android/datatransport/runtime/scheduling/persistence/AutoValue_EventStoreConfig$Builder;->()V HSPLcom/google/android/datatransport/runtime/scheduling/persistence/AutoValue_EventStoreConfig$Builder;->build()Lcom/google/android/datatransport/runtime/scheduling/persistence/EventStoreConfig; @@ -23139,7 +23343,8 @@ HSPLcom/google/android/datatransport/runtime/scheduling/persistence/AutoValue_Ev HSPLcom/google/android/datatransport/runtime/scheduling/persistence/AutoValue_EventStoreConfig$Builder;->setLoadBatchSize(I)Lcom/google/android/datatransport/runtime/scheduling/persistence/EventStoreConfig$Builder; HSPLcom/google/android/datatransport/runtime/scheduling/persistence/AutoValue_EventStoreConfig$Builder;->setMaxBlobByteSizePerRow(I)Lcom/google/android/datatransport/runtime/scheduling/persistence/EventStoreConfig$Builder; HSPLcom/google/android/datatransport/runtime/scheduling/persistence/AutoValue_EventStoreConfig$Builder;->setMaxStorageSizeInBytes(J)Lcom/google/android/datatransport/runtime/scheduling/persistence/EventStoreConfig$Builder; -PLcom/google/android/datatransport/runtime/scheduling/persistence/AutoValue_PersistedEvent;->(JLcom/google/android/datatransport/runtime/TransportContext;Lcom/google/android/datatransport/runtime/EventInternal;)V +Lcom/google/android/datatransport/runtime/scheduling/persistence/AutoValue_PersistedEvent; +HSPLcom/google/android/datatransport/runtime/scheduling/persistence/AutoValue_PersistedEvent;->(JLcom/google/android/datatransport/runtime/TransportContext;Lcom/google/android/datatransport/runtime/EventInternal;)V Lcom/google/android/datatransport/runtime/scheduling/persistence/ClientHealthMetricsStore; Lcom/google/android/datatransport/runtime/scheduling/persistence/EventStore; Lcom/google/android/datatransport/runtime/scheduling/persistence/EventStoreConfig; @@ -23182,29 +23387,30 @@ HSPLcom/google/android/datatransport/runtime/scheduling/persistence/EventStoreMo Lcom/google/android/datatransport/runtime/scheduling/persistence/EventStoreModule_StoreConfigFactory$InstanceHolder; HSPLcom/google/android/datatransport/runtime/scheduling/persistence/EventStoreModule_StoreConfigFactory$InstanceHolder;->()V HSPLcom/google/android/datatransport/runtime/scheduling/persistence/EventStoreModule_StoreConfigFactory$InstanceHolder;->access$000()Lcom/google/android/datatransport/runtime/scheduling/persistence/EventStoreModule_StoreConfigFactory; -PLcom/google/android/datatransport/runtime/scheduling/persistence/PersistedEvent;->()V -PLcom/google/android/datatransport/runtime/scheduling/persistence/PersistedEvent;->create(JLcom/google/android/datatransport/runtime/TransportContext;Lcom/google/android/datatransport/runtime/EventInternal;)Lcom/google/android/datatransport/runtime/scheduling/persistence/PersistedEvent; +Lcom/google/android/datatransport/runtime/scheduling/persistence/PersistedEvent; +HSPLcom/google/android/datatransport/runtime/scheduling/persistence/PersistedEvent;->()V +HSPLcom/google/android/datatransport/runtime/scheduling/persistence/PersistedEvent;->create(JLcom/google/android/datatransport/runtime/TransportContext;Lcom/google/android/datatransport/runtime/EventInternal;)Lcom/google/android/datatransport/runtime/scheduling/persistence/PersistedEvent; Lcom/google/android/datatransport/runtime/scheduling/persistence/SQLiteEventStore; HSPLcom/google/android/datatransport/runtime/scheduling/persistence/SQLiteEventStore;->$r8$lambda$r0gdOZu0LN8RFPU64_YFxoLiMZU(Lcom/google/android/datatransport/runtime/scheduling/persistence/SchemaManager;)Landroid/database/sqlite/SQLiteDatabase; HSPLcom/google/android/datatransport/runtime/scheduling/persistence/SQLiteEventStore;->()V HSPLcom/google/android/datatransport/runtime/scheduling/persistence/SQLiteEventStore;->(Lcom/google/android/datatransport/runtime/time/Clock;Lcom/google/android/datatransport/runtime/time/Clock;Lcom/google/android/datatransport/runtime/scheduling/persistence/EventStoreConfig;Lcom/google/android/datatransport/runtime/scheduling/persistence/SchemaManager;Ljavax/inject/Provider;)V HSPLcom/google/android/datatransport/runtime/scheduling/persistence/SQLiteEventStore;->ensureBeginTransaction(Landroid/database/sqlite/SQLiteDatabase;)V -PLcom/google/android/datatransport/runtime/scheduling/persistence/SQLiteEventStore;->ensureTransportContext(Landroid/database/sqlite/SQLiteDatabase;Lcom/google/android/datatransport/runtime/TransportContext;)J +HSPLcom/google/android/datatransport/runtime/scheduling/persistence/SQLiteEventStore;->ensureTransportContext(Landroid/database/sqlite/SQLiteDatabase;Lcom/google/android/datatransport/runtime/TransportContext;)J HSPLcom/google/android/datatransport/runtime/scheduling/persistence/SQLiteEventStore;->getDb()Landroid/database/sqlite/SQLiteDatabase; HSPLcom/google/android/datatransport/runtime/scheduling/persistence/SQLiteEventStore;->getNextCallTime(Lcom/google/android/datatransport/runtime/TransportContext;)J -PLcom/google/android/datatransport/runtime/scheduling/persistence/SQLiteEventStore;->getPageCount()J -PLcom/google/android/datatransport/runtime/scheduling/persistence/SQLiteEventStore;->getPageSize()J -PLcom/google/android/datatransport/runtime/scheduling/persistence/SQLiteEventStore;->getTransportContextId(Landroid/database/sqlite/SQLiteDatabase;Lcom/google/android/datatransport/runtime/TransportContext;)Ljava/lang/Long; +HSPLcom/google/android/datatransport/runtime/scheduling/persistence/SQLiteEventStore;->getPageCount()J +HSPLcom/google/android/datatransport/runtime/scheduling/persistence/SQLiteEventStore;->getPageSize()J +HSPLcom/google/android/datatransport/runtime/scheduling/persistence/SQLiteEventStore;->getTransportContextId(Landroid/database/sqlite/SQLiteDatabase;Lcom/google/android/datatransport/runtime/TransportContext;)Ljava/lang/Long; HSPLcom/google/android/datatransport/runtime/scheduling/persistence/SQLiteEventStore;->inTransaction(Lcom/google/android/datatransport/runtime/scheduling/persistence/SQLiteEventStore$Function;)Ljava/lang/Object; -PLcom/google/android/datatransport/runtime/scheduling/persistence/SQLiteEventStore;->isStorageAtLimit()Z +HSPLcom/google/android/datatransport/runtime/scheduling/persistence/SQLiteEventStore;->isStorageAtLimit()Z HSPLcom/google/android/datatransport/runtime/scheduling/persistence/SQLiteEventStore;->lambda$ensureBeginTransaction$24(Landroid/database/sqlite/SQLiteDatabase;)Ljava/lang/Object; HSPLcom/google/android/datatransport/runtime/scheduling/persistence/SQLiteEventStore;->lambda$getNextCallTime$5(Landroid/database/Cursor;)Ljava/lang/Long; -PLcom/google/android/datatransport/runtime/scheduling/persistence/SQLiteEventStore;->lambda$getTransportContextId$2(Landroid/database/Cursor;)Ljava/lang/Long; +HSPLcom/google/android/datatransport/runtime/scheduling/persistence/SQLiteEventStore;->lambda$getTransportContextId$2(Landroid/database/Cursor;)Ljava/lang/Long; HSPLcom/google/android/datatransport/runtime/scheduling/persistence/SQLiteEventStore;->lambda$loadActiveContexts$10(Landroid/database/sqlite/SQLiteDatabase;)Ljava/util/List; HSPLcom/google/android/datatransport/runtime/scheduling/persistence/SQLiteEventStore;->lambda$loadActiveContexts$9(Landroid/database/Cursor;)Ljava/util/List; -PLcom/google/android/datatransport/runtime/scheduling/persistence/SQLiteEventStore;->lambda$persist$1$com-google-android-datatransport-runtime-scheduling-persistence-SQLiteEventStore(Lcom/google/android/datatransport/runtime/EventInternal;Lcom/google/android/datatransport/runtime/TransportContext;Landroid/database/sqlite/SQLiteDatabase;)Ljava/lang/Long; +HSPLcom/google/android/datatransport/runtime/scheduling/persistence/SQLiteEventStore;->lambda$persist$1$com-google-android-datatransport-runtime-scheduling-persistence-SQLiteEventStore(Lcom/google/android/datatransport/runtime/EventInternal;Lcom/google/android/datatransport/runtime/TransportContext;Landroid/database/sqlite/SQLiteDatabase;)Ljava/lang/Long; HSPLcom/google/android/datatransport/runtime/scheduling/persistence/SQLiteEventStore;->loadActiveContexts()Ljava/lang/Iterable; -PLcom/google/android/datatransport/runtime/scheduling/persistence/SQLiteEventStore;->persist(Lcom/google/android/datatransport/runtime/TransportContext;Lcom/google/android/datatransport/runtime/EventInternal;)Lcom/google/android/datatransport/runtime/scheduling/persistence/PersistedEvent; +HSPLcom/google/android/datatransport/runtime/scheduling/persistence/SQLiteEventStore;->persist(Lcom/google/android/datatransport/runtime/TransportContext;Lcom/google/android/datatransport/runtime/EventInternal;)Lcom/google/android/datatransport/runtime/scheduling/persistence/PersistedEvent; HSPLcom/google/android/datatransport/runtime/scheduling/persistence/SQLiteEventStore;->retryIfDbLocked(Lcom/google/android/datatransport/runtime/scheduling/persistence/SQLiteEventStore$Producer;Lcom/google/android/datatransport/runtime/scheduling/persistence/SQLiteEventStore$Function;)Ljava/lang/Object; HSPLcom/google/android/datatransport/runtime/scheduling/persistence/SQLiteEventStore;->runCriticalSection(Lcom/google/android/datatransport/runtime/synchronization/SynchronizationGuard$CriticalSection;)Ljava/lang/Object; HSPLcom/google/android/datatransport/runtime/scheduling/persistence/SQLiteEventStore;->tryWithCursor(Landroid/database/Cursor;Lcom/google/android/datatransport/runtime/scheduling/persistence/SQLiteEventStore$Function;)Ljava/lang/Object; @@ -23217,8 +23423,9 @@ HSPLcom/google/android/datatransport/runtime/scheduling/persistence/SQLiteEventS Lcom/google/android/datatransport/runtime/scheduling/persistence/SQLiteEventStore$$ExternalSyntheticLambda17; HSPLcom/google/android/datatransport/runtime/scheduling/persistence/SQLiteEventStore$$ExternalSyntheticLambda17;->()V HSPLcom/google/android/datatransport/runtime/scheduling/persistence/SQLiteEventStore$$ExternalSyntheticLambda17;->apply(Ljava/lang/Object;)Ljava/lang/Object; -PLcom/google/android/datatransport/runtime/scheduling/persistence/SQLiteEventStore$$ExternalSyntheticLambda2;->()V -PLcom/google/android/datatransport/runtime/scheduling/persistence/SQLiteEventStore$$ExternalSyntheticLambda2;->apply(Ljava/lang/Object;)Ljava/lang/Object; +Lcom/google/android/datatransport/runtime/scheduling/persistence/SQLiteEventStore$$ExternalSyntheticLambda2; +HSPLcom/google/android/datatransport/runtime/scheduling/persistence/SQLiteEventStore$$ExternalSyntheticLambda2;->()V +HSPLcom/google/android/datatransport/runtime/scheduling/persistence/SQLiteEventStore$$ExternalSyntheticLambda2;->apply(Ljava/lang/Object;)Ljava/lang/Object; Lcom/google/android/datatransport/runtime/scheduling/persistence/SQLiteEventStore$$ExternalSyntheticLambda20; HSPLcom/google/android/datatransport/runtime/scheduling/persistence/SQLiteEventStore$$ExternalSyntheticLambda20;->(Landroid/database/sqlite/SQLiteDatabase;)V HSPLcom/google/android/datatransport/runtime/scheduling/persistence/SQLiteEventStore$$ExternalSyntheticLambda20;->produce()Ljava/lang/Object; @@ -23229,8 +23436,9 @@ HSPLcom/google/android/datatransport/runtime/scheduling/persistence/SQLiteEventS HSPLcom/google/android/datatransport/runtime/scheduling/persistence/SQLiteEventStore$$ExternalSyntheticLambda25;->produce()Ljava/lang/Object; Lcom/google/android/datatransport/runtime/scheduling/persistence/SQLiteEventStore$$ExternalSyntheticLambda26; HSPLcom/google/android/datatransport/runtime/scheduling/persistence/SQLiteEventStore$$ExternalSyntheticLambda26;->()V -PLcom/google/android/datatransport/runtime/scheduling/persistence/SQLiteEventStore$$ExternalSyntheticLambda4;->(Lcom/google/android/datatransport/runtime/scheduling/persistence/SQLiteEventStore;Lcom/google/android/datatransport/runtime/EventInternal;Lcom/google/android/datatransport/runtime/TransportContext;)V -PLcom/google/android/datatransport/runtime/scheduling/persistence/SQLiteEventStore$$ExternalSyntheticLambda4;->apply(Ljava/lang/Object;)Ljava/lang/Object; +Lcom/google/android/datatransport/runtime/scheduling/persistence/SQLiteEventStore$$ExternalSyntheticLambda4; +HSPLcom/google/android/datatransport/runtime/scheduling/persistence/SQLiteEventStore$$ExternalSyntheticLambda4;->(Lcom/google/android/datatransport/runtime/scheduling/persistence/SQLiteEventStore;Lcom/google/android/datatransport/runtime/EventInternal;Lcom/google/android/datatransport/runtime/TransportContext;)V +HSPLcom/google/android/datatransport/runtime/scheduling/persistence/SQLiteEventStore$$ExternalSyntheticLambda4;->apply(Ljava/lang/Object;)Ljava/lang/Object; Lcom/google/android/datatransport/runtime/scheduling/persistence/SQLiteEventStore$Function; Lcom/google/android/datatransport/runtime/scheduling/persistence/SQLiteEventStore$Producer; Lcom/google/android/datatransport/runtime/scheduling/persistence/SQLiteEventStore_Factory; @@ -23572,6 +23780,7 @@ HSPLcom/google/android/gms/common/internal/GmsClientSupervisor;->()V HSPLcom/google/android/gms/common/internal/GmsClientSupervisor;->()V HSPLcom/google/android/gms/common/internal/GmsClientSupervisor;->getInstance(Landroid/content/Context;)Lcom/google/android/gms/common/internal/GmsClientSupervisor; HSPLcom/google/android/gms/common/internal/GmsClientSupervisor;->getOrStartHandlerThread()Landroid/os/HandlerThread; +PLcom/google/android/gms/common/internal/GmsClientSupervisor;->zzb(Ljava/lang/String;Ljava/lang/String;ILandroid/content/ServiceConnection;Ljava/lang/String;Z)V Lcom/google/android/gms/common/internal/GmsLogger; HSPLcom/google/android/gms/common/internal/GmsLogger;->(Ljava/lang/String;Ljava/lang/String;)V Lcom/google/android/gms/common/internal/IAccountAccessor; @@ -24437,7 +24646,7 @@ HSPLcom/google/android/gms/internal/measurement/zzags;->()V HSPLcom/google/android/gms/internal/measurement/zzags;->()V HSPLcom/google/android/gms/internal/measurement/zzags;->zzC()J HSPLcom/google/android/gms/internal/measurement/zzags;->zzF()Ljava/lang/String; -HSPLcom/google/android/gms/internal/measurement/zzags;->zzI()J +PLcom/google/android/gms/internal/measurement/zzags;->zzI()J HSPLcom/google/android/gms/internal/measurement/zzags;->zzaz()Lcom/google/android/gms/internal/measurement/zzagt; HSPLcom/google/android/gms/internal/measurement/zzags;->zzs()J Lcom/google/android/gms/internal/measurement/zzagt; @@ -24446,7 +24655,7 @@ HSPLcom/google/android/gms/internal/measurement/zzagu;->()V HSPLcom/google/android/gms/internal/measurement/zzagu;->()V HSPLcom/google/android/gms/internal/measurement/zzagu;->zzC()J HSPLcom/google/android/gms/internal/measurement/zzagu;->zzF()Ljava/lang/String; -HSPLcom/google/android/gms/internal/measurement/zzagu;->zzI()J +PLcom/google/android/gms/internal/measurement/zzagu;->zzI()J HSPLcom/google/android/gms/internal/measurement/zzagu;->zzs()J Lcom/google/android/gms/internal/measurement/zzagv; HSPLcom/google/android/gms/internal/measurement/zzagv;->()V @@ -24556,7 +24765,7 @@ HSPLcom/google/android/gms/internal/measurement/zzbm;->onTransact(ILandroid/os/P Lcom/google/android/gms/internal/measurement/zzbn; HSPLcom/google/android/gms/internal/measurement/zzbn;->()V HSPLcom/google/android/gms/internal/measurement/zzbn;->zzb(Landroid/os/Parcel;Landroid/os/Parcelable$Creator;)Landroid/os/Parcelable; -HSPLcom/google/android/gms/internal/measurement/zzbn;->zzc(Landroid/os/Parcel;Landroid/os/Parcelable;)V +PLcom/google/android/gms/internal/measurement/zzbn;->zzc(Landroid/os/Parcel;Landroid/os/Parcelable;)V HSPLcom/google/android/gms/internal/measurement/zzbn;->zzd(Landroid/os/Parcel;Landroid/os/IInterface;)V HSPLcom/google/android/gms/internal/measurement/zzbn;->zzf(Landroid/os/Parcel;)V Lcom/google/android/gms/internal/measurement/zzbw; @@ -24624,6 +24833,7 @@ Lcom/google/android/gms/internal/measurement/zzeb; HSPLcom/google/android/gms/internal/measurement/zzeb;->(Lcom/google/android/gms/internal/measurement/zzez;)V HSPLcom/google/android/gms/internal/measurement/zzeb;->newThread(Ljava/lang/Runnable;)Ljava/lang/Thread; Lcom/google/android/gms/internal/measurement/zzel; +HSPLcom/google/android/gms/internal/measurement/zzel;->(Lcom/google/android/gms/internal/measurement/zzez;Lcom/google/android/gms/internal/measurement/zzeq;)V HSPLcom/google/android/gms/internal/measurement/zzel;->zza()V Lcom/google/android/gms/internal/measurement/zzeo; HSPLcom/google/android/gms/internal/measurement/zzeo;->(Lcom/google/android/gms/internal/measurement/zzez;Z)V @@ -25821,7 +26031,7 @@ HSPLcom/google/android/gms/measurement/internal/zzdh;->()V Lcom/google/android/gms/measurement/internal/zzdi; HSPLcom/google/android/gms/measurement/internal/zzdi;->()V HSPLcom/google/android/gms/measurement/internal/zzdi;->()V -HSPLcom/google/android/gms/measurement/internal/zzdi;->zza()Ljava/lang/Object; +PLcom/google/android/gms/measurement/internal/zzdi;->zza()Ljava/lang/Object; Lcom/google/android/gms/measurement/internal/zzdj; HSPLcom/google/android/gms/measurement/internal/zzdj;->()V HSPLcom/google/android/gms/measurement/internal/zzdj;->()V @@ -26334,7 +26544,7 @@ HSPLcom/google/android/gms/measurement/internal/zzmr;->(Lcom/google/androi PLcom/google/android/gms/measurement/internal/zzmr;->run()V Lcom/google/android/gms/measurement/internal/zzms; HSPLcom/google/android/gms/measurement/internal/zzms;->(Lcom/google/android/gms/measurement/internal/zznl;Lcom/google/android/gms/measurement/internal/zzr;)V -HSPLcom/google/android/gms/measurement/internal/zzms;->run()V +PLcom/google/android/gms/measurement/internal/zzms;->run()V PLcom/google/android/gms/measurement/internal/zzna;->(Lcom/google/android/gms/measurement/internal/zznf;Lcom/google/android/gms/measurement/internal/zzgb;)V PLcom/google/android/gms/measurement/internal/zzna;->run()V Lcom/google/android/gms/measurement/internal/zznf; @@ -26342,22 +26552,22 @@ HSPLcom/google/android/gms/measurement/internal/zznf;->(Lcom/google/androi HSPLcom/google/android/gms/measurement/internal/zznf;->onConnected(Landroid/os/Bundle;)V PLcom/google/android/gms/measurement/internal/zznf;->zzb()V HSPLcom/google/android/gms/measurement/internal/zznf;->zzc()V -HSPLcom/google/android/gms/measurement/internal/zznf;->zzd(Z)V +PLcom/google/android/gms/measurement/internal/zznf;->zzd(Z)V Lcom/google/android/gms/measurement/internal/zznl; HSPLcom/google/android/gms/measurement/internal/zznl;->(Lcom/google/android/gms/measurement/internal/zzic;)V HSPLcom/google/android/gms/measurement/internal/zznl;->zzC(Ljava/util/concurrent/atomic/AtomicReference;)V HSPLcom/google/android/gms/measurement/internal/zznl;->zzI()V HSPLcom/google/android/gms/measurement/internal/zznl;->zzK()Z -HSPLcom/google/android/gms/measurement/internal/zznl;->zzL(Lcom/google/android/gms/measurement/internal/zzgb;)V +PLcom/google/android/gms/measurement/internal/zznl;->zzL(Lcom/google/android/gms/measurement/internal/zzgb;)V PLcom/google/android/gms/measurement/internal/zznl;->zzM()V HSPLcom/google/android/gms/measurement/internal/zznl;->zzO()Z HSPLcom/google/android/gms/measurement/internal/zznl;->zzP()Z -HSPLcom/google/android/gms/measurement/internal/zznl;->zzV()V -HSPLcom/google/android/gms/measurement/internal/zznl;->zzZ()Lcom/google/android/gms/measurement/internal/zzgb; +PLcom/google/android/gms/measurement/internal/zznl;->zzV()V +PLcom/google/android/gms/measurement/internal/zznl;->zzZ()Lcom/google/android/gms/measurement/internal/zzgb; PLcom/google/android/gms/measurement/internal/zznl;->zzab()Ljava/util/concurrent/ScheduledExecutorService; -HSPLcom/google/android/gms/measurement/internal/zznl;->zzae()V +PLcom/google/android/gms/measurement/internal/zznl;->zzae()V HSPLcom/google/android/gms/measurement/internal/zznl;->zzaf(Ljava/lang/Runnable;)V -HSPLcom/google/android/gms/measurement/internal/zznl;->zzag()V +PLcom/google/android/gms/measurement/internal/zznl;->zzag()V HSPLcom/google/android/gms/measurement/internal/zznl;->zzah(Z)Lcom/google/android/gms/measurement/internal/zzr; HSPLcom/google/android/gms/measurement/internal/zznl;->zze()Z HSPLcom/google/android/gms/measurement/internal/zznl;->zzh()Z @@ -26399,7 +26609,7 @@ HSPLcom/google/android/gms/measurement/internal/zzof;->zzb(Landroid/content/Shar HSPLcom/google/android/gms/measurement/internal/zzof;->zzd(Lcom/google/common/collect/ImmutableMap;Lcom/google/common/collect/ImmutableMap;Lcom/google/common/collect/ImmutableSet;[CIIIIILjava/lang/String;Ljava/lang/String;Ljava/lang/String;ZZZ)Ljava/util/Map; Lcom/google/android/gms/measurement/internal/zzog; HSPLcom/google/android/gms/measurement/internal/zzog;->(Lcom/google/android/gms/common/util/Clock;)V -HSPLcom/google/android/gms/measurement/internal/zzog;->zza()V +PLcom/google/android/gms/measurement/internal/zzog;->zza()V Lcom/google/android/gms/measurement/internal/zzpo; Lcom/google/android/gms/measurement/internal/zzpp; HSPLcom/google/android/gms/measurement/internal/zzpp;->()V @@ -26441,7 +26651,7 @@ HSPLcom/google/android/gms/measurement/internal/zzq;->(Lcom/google/android Lcom/google/android/gms/measurement/internal/zzr; HSPLcom/google/android/gms/measurement/internal/zzr;->()V HSPLcom/google/android/gms/measurement/internal/zzr;->(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;JLjava/lang/String;JJLjava/lang/String;ZZLjava/lang/String;JIZZLjava/lang/Boolean;JLjava/util/List;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;ZJILjava/lang/String;IJLjava/lang/String;Ljava/lang/String;JIJ)V -HSPLcom/google/android/gms/measurement/internal/zzr;->writeToParcel(Landroid/os/Parcel;I)V +PLcom/google/android/gms/measurement/internal/zzr;->writeToParcel(Landroid/os/Parcel;I)V Lcom/google/android/gms/measurement/internal/zzs; HSPLcom/google/android/gms/measurement/internal/zzs;->()V Lcom/google/android/gms/measurement/internal/zzx; @@ -27001,7 +27211,7 @@ Lcom/google/firebase/FirebaseCommonRegistrar$$ExternalSyntheticLambda3; HSPLcom/google/firebase/FirebaseCommonRegistrar$$ExternalSyntheticLambda3;->()V HSPLcom/google/firebase/FirebaseCommonRegistrar$$ExternalSyntheticLambda3;->extract(Ljava/lang/Object;)Ljava/lang/String; Lcom/google/firebase/FirebaseException; -PLcom/google/firebase/FirebaseException;->()V +HSPLcom/google/firebase/FirebaseException;->()V HSPLcom/google/firebase/FirebaseException;->(Ljava/lang/String;)V Lcom/google/firebase/FirebaseKt; HSPLcom/google/firebase/FirebaseKt;->getApp(Lcom/google/firebase/Firebase;)Lcom/google/firebase/FirebaseApp; @@ -28340,10 +28550,10 @@ Lcom/google/firebase/datatransport/TransportBackend; Lcom/google/firebase/datatransport/TransportRegistrar; HSPLcom/google/firebase/datatransport/TransportRegistrar;->()V HSPLcom/google/firebase/datatransport/TransportRegistrar;->getComponents()Ljava/util/List; -PLcom/google/firebase/datatransport/TransportRegistrar;->lambda$getComponents$0(Lcom/google/firebase/components/ComponentContainer;)Lcom/google/android/datatransport/TransportFactory; +HSPLcom/google/firebase/datatransport/TransportRegistrar;->lambda$getComponents$0(Lcom/google/firebase/components/ComponentContainer;)Lcom/google/android/datatransport/TransportFactory; Lcom/google/firebase/datatransport/TransportRegistrar$$ExternalSyntheticLambda0; HSPLcom/google/firebase/datatransport/TransportRegistrar$$ExternalSyntheticLambda0;->()V -PLcom/google/firebase/datatransport/TransportRegistrar$$ExternalSyntheticLambda0;->create(Lcom/google/firebase/components/ComponentContainer;)Ljava/lang/Object; +HSPLcom/google/firebase/datatransport/TransportRegistrar$$ExternalSyntheticLambda0;->create(Lcom/google/firebase/components/ComponentContainer;)Ljava/lang/Object; Lcom/google/firebase/datatransport/TransportRegistrar$$ExternalSyntheticLambda1; HSPLcom/google/firebase/datatransport/TransportRegistrar$$ExternalSyntheticLambda1;->()V Lcom/google/firebase/datatransport/TransportRegistrar$$ExternalSyntheticLambda2; @@ -28351,6 +28561,7 @@ HSPLcom/google/firebase/datatransport/TransportRegistrar$$ExternalSyntheticLambd Lcom/google/firebase/dynamicloading/ComponentLoader; Lcom/google/firebase/encoders/DataEncoder; Lcom/google/firebase/encoders/Encoder; +Lcom/google/firebase/encoders/EncodingException; Lcom/google/firebase/encoders/FieldDescriptor; HSPLcom/google/firebase/encoders/FieldDescriptor;->(Ljava/lang/String;Ljava/util/Map;)V HSPLcom/google/firebase/encoders/FieldDescriptor;->getName()Ljava/lang/String; @@ -28392,10 +28603,10 @@ HSPLcom/google/firebase/encoders/json/JsonDataEncoderBuilder$TimestampEncoder;-> HSPLcom/google/firebase/encoders/json/JsonDataEncoderBuilder$TimestampEncoder;->(Lcom/google/firebase/encoders/json/JsonDataEncoderBuilder$1;)V Lcom/google/firebase/encoders/json/JsonValueObjectEncoderContext; HSPLcom/google/firebase/encoders/json/JsonValueObjectEncoderContext;->(Ljava/io/Writer;Ljava/util/Map;Ljava/util/Map;Lcom/google/firebase/encoders/ObjectEncoder;Z)V -PLcom/google/firebase/encoders/json/JsonValueObjectEncoderContext;->add(D)Lcom/google/firebase/encoders/json/JsonValueObjectEncoderContext; +HSPLcom/google/firebase/encoders/json/JsonValueObjectEncoderContext;->add(D)Lcom/google/firebase/encoders/json/JsonValueObjectEncoderContext; HSPLcom/google/firebase/encoders/json/JsonValueObjectEncoderContext;->add(I)Lcom/google/firebase/encoders/json/JsonValueObjectEncoderContext; HSPLcom/google/firebase/encoders/json/JsonValueObjectEncoderContext;->add(J)Lcom/google/firebase/encoders/json/JsonValueObjectEncoderContext; -PLcom/google/firebase/encoders/json/JsonValueObjectEncoderContext;->add(Lcom/google/firebase/encoders/FieldDescriptor;D)Lcom/google/firebase/encoders/ObjectEncoderContext; +HSPLcom/google/firebase/encoders/json/JsonValueObjectEncoderContext;->add(Lcom/google/firebase/encoders/FieldDescriptor;D)Lcom/google/firebase/encoders/ObjectEncoderContext; HSPLcom/google/firebase/encoders/json/JsonValueObjectEncoderContext;->add(Lcom/google/firebase/encoders/FieldDescriptor;I)Lcom/google/firebase/encoders/ObjectEncoderContext; HSPLcom/google/firebase/encoders/json/JsonValueObjectEncoderContext;->add(Lcom/google/firebase/encoders/FieldDescriptor;J)Lcom/google/firebase/encoders/ObjectEncoderContext; HSPLcom/google/firebase/encoders/json/JsonValueObjectEncoderContext;->add(Lcom/google/firebase/encoders/FieldDescriptor;Ljava/lang/Object;)Lcom/google/firebase/encoders/ObjectEncoderContext; @@ -28403,7 +28614,7 @@ HSPLcom/google/firebase/encoders/json/JsonValueObjectEncoderContext;->add(Lcom/g HSPLcom/google/firebase/encoders/json/JsonValueObjectEncoderContext;->add(Ljava/lang/Object;Z)Lcom/google/firebase/encoders/json/JsonValueObjectEncoderContext; HSPLcom/google/firebase/encoders/json/JsonValueObjectEncoderContext;->add(Ljava/lang/String;)Lcom/google/firebase/encoders/ValueEncoderContext; HSPLcom/google/firebase/encoders/json/JsonValueObjectEncoderContext;->add(Ljava/lang/String;)Lcom/google/firebase/encoders/json/JsonValueObjectEncoderContext; -PLcom/google/firebase/encoders/json/JsonValueObjectEncoderContext;->add(Ljava/lang/String;D)Lcom/google/firebase/encoders/json/JsonValueObjectEncoderContext; +HSPLcom/google/firebase/encoders/json/JsonValueObjectEncoderContext;->add(Ljava/lang/String;D)Lcom/google/firebase/encoders/json/JsonValueObjectEncoderContext; HSPLcom/google/firebase/encoders/json/JsonValueObjectEncoderContext;->add(Ljava/lang/String;I)Lcom/google/firebase/encoders/json/JsonValueObjectEncoderContext; HSPLcom/google/firebase/encoders/json/JsonValueObjectEncoderContext;->add(Ljava/lang/String;J)Lcom/google/firebase/encoders/json/JsonValueObjectEncoderContext; HSPLcom/google/firebase/encoders/json/JsonValueObjectEncoderContext;->add(Ljava/lang/String;Ljava/lang/Object;)Lcom/google/firebase/encoders/json/JsonValueObjectEncoderContext; @@ -28525,7 +28736,7 @@ Lcom/google/firebase/installations/FirebaseInstallations$3; HSPLcom/google/firebase/installations/FirebaseInstallations$3;->()V Lcom/google/firebase/installations/FirebaseInstallationsApi; Lcom/google/firebase/installations/FirebaseInstallationsException; -PLcom/google/firebase/installations/FirebaseInstallationsException;->(Lcom/google/firebase/installations/FirebaseInstallationsException$Status;)V +HSPLcom/google/firebase/installations/FirebaseInstallationsException;->(Lcom/google/firebase/installations/FirebaseInstallationsException$Status;)V HSPLcom/google/firebase/installations/FirebaseInstallationsException;->(Ljava/lang/String;Lcom/google/firebase/installations/FirebaseInstallationsException$Status;)V Lcom/google/firebase/installations/FirebaseInstallationsException$Status; HSPLcom/google/firebase/installations/FirebaseInstallationsException$Status;->$values()[Lcom/google/firebase/installations/FirebaseInstallationsException$Status; @@ -28619,13 +28830,13 @@ HSPLcom/google/firebase/installations/local/PersistedInstallationEntry;->withUnr Lcom/google/firebase/installations/local/PersistedInstallationEntry$Builder; HSPLcom/google/firebase/installations/local/PersistedInstallationEntry$Builder;->()V Lcom/google/firebase/installations/remote/AutoValue_InstallationResponse; -PLcom/google/firebase/installations/remote/AutoValue_InstallationResponse;->(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Lcom/google/firebase/installations/remote/TokenResult;Lcom/google/firebase/installations/remote/InstallationResponse$ResponseCode;)V -PLcom/google/firebase/installations/remote/AutoValue_InstallationResponse;->(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Lcom/google/firebase/installations/remote/TokenResult;Lcom/google/firebase/installations/remote/InstallationResponse$ResponseCode;Lcom/google/firebase/installations/remote/AutoValue_InstallationResponse$1;)V +HSPLcom/google/firebase/installations/remote/AutoValue_InstallationResponse;->(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Lcom/google/firebase/installations/remote/TokenResult;Lcom/google/firebase/installations/remote/InstallationResponse$ResponseCode;)V +HSPLcom/google/firebase/installations/remote/AutoValue_InstallationResponse;->(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Lcom/google/firebase/installations/remote/TokenResult;Lcom/google/firebase/installations/remote/InstallationResponse$ResponseCode;Lcom/google/firebase/installations/remote/AutoValue_InstallationResponse$1;)V HSPLcom/google/firebase/installations/remote/AutoValue_InstallationResponse;->getResponseCode()Lcom/google/firebase/installations/remote/InstallationResponse$ResponseCode; Lcom/google/firebase/installations/remote/AutoValue_InstallationResponse$Builder; -PLcom/google/firebase/installations/remote/AutoValue_InstallationResponse$Builder;->()V -PLcom/google/firebase/installations/remote/AutoValue_InstallationResponse$Builder;->build()Lcom/google/firebase/installations/remote/InstallationResponse; -PLcom/google/firebase/installations/remote/AutoValue_InstallationResponse$Builder;->setResponseCode(Lcom/google/firebase/installations/remote/InstallationResponse$ResponseCode;)Lcom/google/firebase/installations/remote/InstallationResponse$Builder; +HSPLcom/google/firebase/installations/remote/AutoValue_InstallationResponse$Builder;->()V +HSPLcom/google/firebase/installations/remote/AutoValue_InstallationResponse$Builder;->build()Lcom/google/firebase/installations/remote/InstallationResponse; +HSPLcom/google/firebase/installations/remote/AutoValue_InstallationResponse$Builder;->setResponseCode(Lcom/google/firebase/installations/remote/InstallationResponse$ResponseCode;)Lcom/google/firebase/installations/remote/InstallationResponse$Builder; Lcom/google/firebase/installations/remote/FirebaseInstallationServiceClient; HSPLcom/google/firebase/installations/remote/FirebaseInstallationServiceClient;->()V HSPLcom/google/firebase/installations/remote/FirebaseInstallationServiceClient;->(Landroid/content/Context;Lcom/google/firebase/inject/Provider;)V @@ -28635,18 +28846,18 @@ HSPLcom/google/firebase/installations/remote/FirebaseInstallationServiceClient;- HSPLcom/google/firebase/installations/remote/FirebaseInstallationServiceClient;->getFingerprintHashForPackage()Ljava/lang/String; HSPLcom/google/firebase/installations/remote/FirebaseInstallationServiceClient;->getFullyQualifiedRequestUri(Ljava/lang/String;)Ljava/net/URL; HSPLcom/google/firebase/installations/remote/FirebaseInstallationServiceClient;->getJsonBytes(Lorg/json/JSONObject;)[B -PLcom/google/firebase/installations/remote/FirebaseInstallationServiceClient;->isSuccessfulResponseCode(I)Z -PLcom/google/firebase/installations/remote/FirebaseInstallationServiceClient;->logBadConfigError()V -PLcom/google/firebase/installations/remote/FirebaseInstallationServiceClient;->logFisCommunicationError(Ljava/net/HttpURLConnection;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)V +HSPLcom/google/firebase/installations/remote/FirebaseInstallationServiceClient;->isSuccessfulResponseCode(I)Z +HSPLcom/google/firebase/installations/remote/FirebaseInstallationServiceClient;->logBadConfigError()V +HSPLcom/google/firebase/installations/remote/FirebaseInstallationServiceClient;->logFisCommunicationError(Ljava/net/HttpURLConnection;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)V HSPLcom/google/firebase/installations/remote/FirebaseInstallationServiceClient;->openHttpURLConnection(Ljava/net/URL;Ljava/lang/String;)Ljava/net/HttpURLConnection; HSPLcom/google/firebase/installations/remote/FirebaseInstallationServiceClient;->readErrorResponse(Ljava/net/HttpURLConnection;)Ljava/lang/String; HSPLcom/google/firebase/installations/remote/FirebaseInstallationServiceClient;->writeFIDCreateRequestBodyToOutputStream(Ljava/net/HttpURLConnection;Ljava/lang/String;Ljava/lang/String;)V HSPLcom/google/firebase/installations/remote/FirebaseInstallationServiceClient;->writeRequestBodyToOutputStream(Ljava/net/URLConnection;[B)V Lcom/google/firebase/installations/remote/InstallationResponse; HSPLcom/google/firebase/installations/remote/InstallationResponse;->()V -PLcom/google/firebase/installations/remote/InstallationResponse;->builder()Lcom/google/firebase/installations/remote/InstallationResponse$Builder; +HSPLcom/google/firebase/installations/remote/InstallationResponse;->builder()Lcom/google/firebase/installations/remote/InstallationResponse$Builder; Lcom/google/firebase/installations/remote/InstallationResponse$Builder; -PLcom/google/firebase/installations/remote/InstallationResponse$Builder;->()V +HSPLcom/google/firebase/installations/remote/InstallationResponse$Builder;->()V Lcom/google/firebase/installations/remote/InstallationResponse$ResponseCode; HSPLcom/google/firebase/installations/remote/InstallationResponse$ResponseCode;->$values()[Lcom/google/firebase/installations/remote/InstallationResponse$ResponseCode; HSPLcom/google/firebase/installations/remote/InstallationResponse$ResponseCode;->()V @@ -28660,6 +28871,7 @@ HSPLcom/google/firebase/installations/remote/RequestLimiter;->isRequestAllowed() HSPLcom/google/firebase/installations/remote/RequestLimiter;->isRetryableError(I)Z HSPLcom/google/firebase/installations/remote/RequestLimiter;->isSuccessfulOrRequiresNewFidCreation(I)Z HSPLcom/google/firebase/installations/remote/RequestLimiter;->setNextRequestTime(I)V +Lcom/google/firebase/installations/remote/TokenResult; Lcom/google/firebase/installations/remote/TokenResult$ResponseCode; HSPLcom/google/firebase/installations/remote/TokenResult$ResponseCode;->$values()[Lcom/google/firebase/installations/remote/TokenResult$ResponseCode; HSPLcom/google/firebase/installations/remote/TokenResult$ResponseCode;->()V @@ -28722,18 +28934,18 @@ Lcom/google/firebase/remoteconfig/interop/rollouts/RolloutsStateSubscriber; Lcom/google/firebase/sessions/AndroidApplicationInfo; HSPLcom/google/firebase/sessions/AndroidApplicationInfo;->(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Lcom/google/firebase/sessions/ProcessDetails;Ljava/util/List;)V HSPLcom/google/firebase/sessions/AndroidApplicationInfo;->getAppBuildVersion()Ljava/lang/String; -PLcom/google/firebase/sessions/AndroidApplicationInfo;->getAppProcessDetails()Ljava/util/List; -PLcom/google/firebase/sessions/AndroidApplicationInfo;->getCurrentProcessDetails()Lcom/google/firebase/sessions/ProcessDetails; -PLcom/google/firebase/sessions/AndroidApplicationInfo;->getDeviceManufacturer()Ljava/lang/String; -PLcom/google/firebase/sessions/AndroidApplicationInfo;->getPackageName()Ljava/lang/String; +HSPLcom/google/firebase/sessions/AndroidApplicationInfo;->getAppProcessDetails()Ljava/util/List; +HSPLcom/google/firebase/sessions/AndroidApplicationInfo;->getCurrentProcessDetails()Lcom/google/firebase/sessions/ProcessDetails; +HSPLcom/google/firebase/sessions/AndroidApplicationInfo;->getDeviceManufacturer()Ljava/lang/String; +HSPLcom/google/firebase/sessions/AndroidApplicationInfo;->getPackageName()Ljava/lang/String; HSPLcom/google/firebase/sessions/AndroidApplicationInfo;->getVersionName()Ljava/lang/String; Lcom/google/firebase/sessions/ApplicationInfo; HSPLcom/google/firebase/sessions/ApplicationInfo;->(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Lcom/google/firebase/sessions/LogEnvironment;Lcom/google/firebase/sessions/AndroidApplicationInfo;)V HSPLcom/google/firebase/sessions/ApplicationInfo;->getAndroidAppInfo()Lcom/google/firebase/sessions/AndroidApplicationInfo; HSPLcom/google/firebase/sessions/ApplicationInfo;->getAppId()Ljava/lang/String; -PLcom/google/firebase/sessions/ApplicationInfo;->getDeviceModel()Ljava/lang/String; -PLcom/google/firebase/sessions/ApplicationInfo;->getLogEnvironment()Lcom/google/firebase/sessions/LogEnvironment; -PLcom/google/firebase/sessions/ApplicationInfo;->getOsVersion()Ljava/lang/String; +HSPLcom/google/firebase/sessions/ApplicationInfo;->getDeviceModel()Ljava/lang/String; +HSPLcom/google/firebase/sessions/ApplicationInfo;->getLogEnvironment()Lcom/google/firebase/sessions/LogEnvironment; +HSPLcom/google/firebase/sessions/ApplicationInfo;->getOsVersion()Ljava/lang/String; HSPLcom/google/firebase/sessions/ApplicationInfo;->getSessionSdkVersion()Ljava/lang/String; Lcom/google/firebase/sessions/AutoSessionEventEncoder; HSPLcom/google/firebase/sessions/AutoSessionEventEncoder;->()V @@ -28742,33 +28954,33 @@ HSPLcom/google/firebase/sessions/AutoSessionEventEncoder;->configure(Lcom/google Lcom/google/firebase/sessions/AutoSessionEventEncoder$AndroidApplicationInfoEncoder; HSPLcom/google/firebase/sessions/AutoSessionEventEncoder$AndroidApplicationInfoEncoder;->()V HSPLcom/google/firebase/sessions/AutoSessionEventEncoder$AndroidApplicationInfoEncoder;->()V -PLcom/google/firebase/sessions/AutoSessionEventEncoder$AndroidApplicationInfoEncoder;->encode(Lcom/google/firebase/sessions/AndroidApplicationInfo;Lcom/google/firebase/encoders/ObjectEncoderContext;)V -PLcom/google/firebase/sessions/AutoSessionEventEncoder$AndroidApplicationInfoEncoder;->encode(Ljava/lang/Object;Ljava/lang/Object;)V +HSPLcom/google/firebase/sessions/AutoSessionEventEncoder$AndroidApplicationInfoEncoder;->encode(Lcom/google/firebase/sessions/AndroidApplicationInfo;Lcom/google/firebase/encoders/ObjectEncoderContext;)V +HSPLcom/google/firebase/sessions/AutoSessionEventEncoder$AndroidApplicationInfoEncoder;->encode(Ljava/lang/Object;Ljava/lang/Object;)V Lcom/google/firebase/sessions/AutoSessionEventEncoder$ApplicationInfoEncoder; HSPLcom/google/firebase/sessions/AutoSessionEventEncoder$ApplicationInfoEncoder;->()V HSPLcom/google/firebase/sessions/AutoSessionEventEncoder$ApplicationInfoEncoder;->()V -PLcom/google/firebase/sessions/AutoSessionEventEncoder$ApplicationInfoEncoder;->encode(Lcom/google/firebase/sessions/ApplicationInfo;Lcom/google/firebase/encoders/ObjectEncoderContext;)V -PLcom/google/firebase/sessions/AutoSessionEventEncoder$ApplicationInfoEncoder;->encode(Ljava/lang/Object;Ljava/lang/Object;)V +HSPLcom/google/firebase/sessions/AutoSessionEventEncoder$ApplicationInfoEncoder;->encode(Lcom/google/firebase/sessions/ApplicationInfo;Lcom/google/firebase/encoders/ObjectEncoderContext;)V +HSPLcom/google/firebase/sessions/AutoSessionEventEncoder$ApplicationInfoEncoder;->encode(Ljava/lang/Object;Ljava/lang/Object;)V Lcom/google/firebase/sessions/AutoSessionEventEncoder$DataCollectionStatusEncoder; HSPLcom/google/firebase/sessions/AutoSessionEventEncoder$DataCollectionStatusEncoder;->()V HSPLcom/google/firebase/sessions/AutoSessionEventEncoder$DataCollectionStatusEncoder;->()V -PLcom/google/firebase/sessions/AutoSessionEventEncoder$DataCollectionStatusEncoder;->encode(Lcom/google/firebase/sessions/DataCollectionStatus;Lcom/google/firebase/encoders/ObjectEncoderContext;)V -PLcom/google/firebase/sessions/AutoSessionEventEncoder$DataCollectionStatusEncoder;->encode(Ljava/lang/Object;Ljava/lang/Object;)V +HSPLcom/google/firebase/sessions/AutoSessionEventEncoder$DataCollectionStatusEncoder;->encode(Lcom/google/firebase/sessions/DataCollectionStatus;Lcom/google/firebase/encoders/ObjectEncoderContext;)V +HSPLcom/google/firebase/sessions/AutoSessionEventEncoder$DataCollectionStatusEncoder;->encode(Ljava/lang/Object;Ljava/lang/Object;)V Lcom/google/firebase/sessions/AutoSessionEventEncoder$ProcessDetailsEncoder; HSPLcom/google/firebase/sessions/AutoSessionEventEncoder$ProcessDetailsEncoder;->()V HSPLcom/google/firebase/sessions/AutoSessionEventEncoder$ProcessDetailsEncoder;->()V -PLcom/google/firebase/sessions/AutoSessionEventEncoder$ProcessDetailsEncoder;->encode(Lcom/google/firebase/sessions/ProcessDetails;Lcom/google/firebase/encoders/ObjectEncoderContext;)V -PLcom/google/firebase/sessions/AutoSessionEventEncoder$ProcessDetailsEncoder;->encode(Ljava/lang/Object;Ljava/lang/Object;)V +HSPLcom/google/firebase/sessions/AutoSessionEventEncoder$ProcessDetailsEncoder;->encode(Lcom/google/firebase/sessions/ProcessDetails;Lcom/google/firebase/encoders/ObjectEncoderContext;)V +HSPLcom/google/firebase/sessions/AutoSessionEventEncoder$ProcessDetailsEncoder;->encode(Ljava/lang/Object;Ljava/lang/Object;)V Lcom/google/firebase/sessions/AutoSessionEventEncoder$SessionEventEncoder; HSPLcom/google/firebase/sessions/AutoSessionEventEncoder$SessionEventEncoder;->()V HSPLcom/google/firebase/sessions/AutoSessionEventEncoder$SessionEventEncoder;->()V -PLcom/google/firebase/sessions/AutoSessionEventEncoder$SessionEventEncoder;->encode(Lcom/google/firebase/sessions/SessionEvent;Lcom/google/firebase/encoders/ObjectEncoderContext;)V -PLcom/google/firebase/sessions/AutoSessionEventEncoder$SessionEventEncoder;->encode(Ljava/lang/Object;Ljava/lang/Object;)V +HSPLcom/google/firebase/sessions/AutoSessionEventEncoder$SessionEventEncoder;->encode(Lcom/google/firebase/sessions/SessionEvent;Lcom/google/firebase/encoders/ObjectEncoderContext;)V +HSPLcom/google/firebase/sessions/AutoSessionEventEncoder$SessionEventEncoder;->encode(Ljava/lang/Object;Ljava/lang/Object;)V Lcom/google/firebase/sessions/AutoSessionEventEncoder$SessionInfoEncoder; HSPLcom/google/firebase/sessions/AutoSessionEventEncoder$SessionInfoEncoder;->()V HSPLcom/google/firebase/sessions/AutoSessionEventEncoder$SessionInfoEncoder;->()V -PLcom/google/firebase/sessions/AutoSessionEventEncoder$SessionInfoEncoder;->encode(Lcom/google/firebase/sessions/SessionInfo;Lcom/google/firebase/encoders/ObjectEncoderContext;)V -PLcom/google/firebase/sessions/AutoSessionEventEncoder$SessionInfoEncoder;->encode(Ljava/lang/Object;Ljava/lang/Object;)V +HSPLcom/google/firebase/sessions/AutoSessionEventEncoder$SessionInfoEncoder;->encode(Lcom/google/firebase/sessions/SessionInfo;Lcom/google/firebase/encoders/ObjectEncoderContext;)V +HSPLcom/google/firebase/sessions/AutoSessionEventEncoder$SessionInfoEncoder;->encode(Ljava/lang/Object;Ljava/lang/Object;)V Lcom/google/firebase/sessions/DaggerFirebaseSessionsComponent; HSPLcom/google/firebase/sessions/DaggerFirebaseSessionsComponent;->builder()Lcom/google/firebase/sessions/FirebaseSessionsComponent$Builder; Lcom/google/firebase/sessions/DaggerFirebaseSessionsComponent$Builder; @@ -28791,23 +29003,25 @@ Lcom/google/firebase/sessions/DaggerFirebaseSessionsComponent$FirebaseSessionsCo HSPLcom/google/firebase/sessions/DaggerFirebaseSessionsComponent$FirebaseSessionsComponentImpl;->(Landroid/content/Context;Lkotlin/coroutines/CoroutineContext;Lkotlin/coroutines/CoroutineContext;Lcom/google/firebase/FirebaseApp;Lcom/google/firebase/installations/FirebaseInstallationsApi;Lcom/google/firebase/inject/Provider;)V HSPLcom/google/firebase/sessions/DaggerFirebaseSessionsComponent$FirebaseSessionsComponentImpl;->getFirebaseSessions()Lcom/google/firebase/sessions/FirebaseSessions; HSPLcom/google/firebase/sessions/DaggerFirebaseSessionsComponent$FirebaseSessionsComponentImpl;->initialize(Landroid/content/Context;Lkotlin/coroutines/CoroutineContext;Lkotlin/coroutines/CoroutineContext;Lcom/google/firebase/FirebaseApp;Lcom/google/firebase/installations/FirebaseInstallationsApi;Lcom/google/firebase/inject/Provider;)V -PLcom/google/firebase/sessions/DataCollectionState;->$values()[Lcom/google/firebase/sessions/DataCollectionState; -PLcom/google/firebase/sessions/DataCollectionState;->()V -PLcom/google/firebase/sessions/DataCollectionState;->(Ljava/lang/String;II)V -PLcom/google/firebase/sessions/DataCollectionState;->getNumber()I +Lcom/google/firebase/sessions/DataCollectionState; +HSPLcom/google/firebase/sessions/DataCollectionState;->$values()[Lcom/google/firebase/sessions/DataCollectionState; +HSPLcom/google/firebase/sessions/DataCollectionState;->()V +HSPLcom/google/firebase/sessions/DataCollectionState;->(Ljava/lang/String;II)V +HSPLcom/google/firebase/sessions/DataCollectionState;->getNumber()I Lcom/google/firebase/sessions/DataCollectionStatus; -PLcom/google/firebase/sessions/DataCollectionStatus;->(Lcom/google/firebase/sessions/DataCollectionState;Lcom/google/firebase/sessions/DataCollectionState;D)V -PLcom/google/firebase/sessions/DataCollectionStatus;->getCrashlytics()Lcom/google/firebase/sessions/DataCollectionState; -PLcom/google/firebase/sessions/DataCollectionStatus;->getPerformance()Lcom/google/firebase/sessions/DataCollectionState; -PLcom/google/firebase/sessions/DataCollectionStatus;->getSessionSamplingRate()D +HSPLcom/google/firebase/sessions/DataCollectionStatus;->(Lcom/google/firebase/sessions/DataCollectionState;Lcom/google/firebase/sessions/DataCollectionState;D)V +HSPLcom/google/firebase/sessions/DataCollectionStatus;->getCrashlytics()Lcom/google/firebase/sessions/DataCollectionState; +HSPLcom/google/firebase/sessions/DataCollectionStatus;->getPerformance()Lcom/google/firebase/sessions/DataCollectionState; +HSPLcom/google/firebase/sessions/DataCollectionStatus;->getSessionSamplingRate()D Lcom/google/firebase/sessions/EventGDTLogger; -PLcom/google/firebase/sessions/EventGDTLogger;->$r8$lambda$W13Olas0VV9EajY9qS9yOYVUJPs(Lcom/google/firebase/sessions/EventGDTLogger;Lcom/google/firebase/sessions/SessionEvent;)[B +HSPLcom/google/firebase/sessions/EventGDTLogger;->$r8$lambda$W13Olas0VV9EajY9qS9yOYVUJPs(Lcom/google/firebase/sessions/EventGDTLogger;Lcom/google/firebase/sessions/SessionEvent;)[B HSPLcom/google/firebase/sessions/EventGDTLogger;->()V HSPLcom/google/firebase/sessions/EventGDTLogger;->(Lcom/google/firebase/inject/Provider;)V -PLcom/google/firebase/sessions/EventGDTLogger;->encode(Lcom/google/firebase/sessions/SessionEvent;)[B -PLcom/google/firebase/sessions/EventGDTLogger;->log(Lcom/google/firebase/sessions/SessionEvent;)V -PLcom/google/firebase/sessions/EventGDTLogger$$ExternalSyntheticLambda0;->(Lcom/google/firebase/sessions/EventGDTLogger;)V -PLcom/google/firebase/sessions/EventGDTLogger$$ExternalSyntheticLambda0;->apply(Ljava/lang/Object;)Ljava/lang/Object; +HSPLcom/google/firebase/sessions/EventGDTLogger;->encode(Lcom/google/firebase/sessions/SessionEvent;)[B +HSPLcom/google/firebase/sessions/EventGDTLogger;->log(Lcom/google/firebase/sessions/SessionEvent;)V +Lcom/google/firebase/sessions/EventGDTLogger$$ExternalSyntheticLambda0; +HSPLcom/google/firebase/sessions/EventGDTLogger$$ExternalSyntheticLambda0;->(Lcom/google/firebase/sessions/EventGDTLogger;)V +HSPLcom/google/firebase/sessions/EventGDTLogger$$ExternalSyntheticLambda0;->apply(Ljava/lang/Object;)Ljava/lang/Object; Lcom/google/firebase/sessions/EventGDTLogger$Companion; HSPLcom/google/firebase/sessions/EventGDTLogger$Companion;->()V HSPLcom/google/firebase/sessions/EventGDTLogger$Companion;->(Lkotlin/jvm/internal/DefaultConstructorMarker;)V @@ -28818,10 +29032,11 @@ HSPLcom/google/firebase/sessions/EventGDTLogger_Factory;->create(Lcom/google/fir HSPLcom/google/firebase/sessions/EventGDTLogger_Factory;->get()Lcom/google/firebase/sessions/EventGDTLogger; HSPLcom/google/firebase/sessions/EventGDTLogger_Factory;->get()Ljava/lang/Object; HSPLcom/google/firebase/sessions/EventGDTLogger_Factory;->newInstance(Lcom/google/firebase/inject/Provider;)Lcom/google/firebase/sessions/EventGDTLogger; -PLcom/google/firebase/sessions/EventType;->$values()[Lcom/google/firebase/sessions/EventType; -PLcom/google/firebase/sessions/EventType;->()V -PLcom/google/firebase/sessions/EventType;->(Ljava/lang/String;II)V -PLcom/google/firebase/sessions/EventType;->getNumber()I +Lcom/google/firebase/sessions/EventType; +HSPLcom/google/firebase/sessions/EventType;->$values()[Lcom/google/firebase/sessions/EventType; +HSPLcom/google/firebase/sessions/EventType;->()V +HSPLcom/google/firebase/sessions/EventType;->(Ljava/lang/String;II)V +HSPLcom/google/firebase/sessions/EventType;->getNumber()I Lcom/google/firebase/sessions/FirebaseSessions; HSPLcom/google/firebase/sessions/FirebaseSessions;->()V HSPLcom/google/firebase/sessions/FirebaseSessions;->(Lcom/google/firebase/FirebaseApp;Lcom/google/firebase/sessions/settings/SessionsSettings;Lkotlin/coroutines/CoroutineContext;Lcom/google/firebase/sessions/SessionsActivityLifecycleCallbacks;)V @@ -28932,7 +29147,7 @@ Lcom/google/firebase/sessions/LogEnvironment; HSPLcom/google/firebase/sessions/LogEnvironment;->$values()[Lcom/google/firebase/sessions/LogEnvironment; HSPLcom/google/firebase/sessions/LogEnvironment;->()V HSPLcom/google/firebase/sessions/LogEnvironment;->(Ljava/lang/String;II)V -PLcom/google/firebase/sessions/LogEnvironment;->getNumber()I +HSPLcom/google/firebase/sessions/LogEnvironment;->getNumber()I Lcom/google/firebase/sessions/ProcessData; HSPLcom/google/firebase/sessions/ProcessData;->()V HSPLcom/google/firebase/sessions/ProcessData;->(IILjava/lang/String;Lkotlinx/serialization/internal/SerializationConstructorMarker;)V @@ -28987,10 +29202,10 @@ HSPLcom/google/firebase/sessions/ProcessDataManagerImpl_Factory;->get()Ljava/lan HSPLcom/google/firebase/sessions/ProcessDataManagerImpl_Factory;->newInstance(Landroid/content/Context;Lcom/google/firebase/sessions/UuidGenerator;)Lcom/google/firebase/sessions/ProcessDataManagerImpl; Lcom/google/firebase/sessions/ProcessDetails; HSPLcom/google/firebase/sessions/ProcessDetails;->(Ljava/lang/String;IIZ)V -PLcom/google/firebase/sessions/ProcessDetails;->getImportance()I +HSPLcom/google/firebase/sessions/ProcessDetails;->getImportance()I HSPLcom/google/firebase/sessions/ProcessDetails;->getPid()I HSPLcom/google/firebase/sessions/ProcessDetails;->getProcessName()Ljava/lang/String; -PLcom/google/firebase/sessions/ProcessDetails;->isDefaultProcess()Z +HSPLcom/google/firebase/sessions/ProcessDetails;->isDefaultProcess()Z Lcom/google/firebase/sessions/ProcessDetailsProvider; HSPLcom/google/firebase/sessions/ProcessDetailsProvider;->()V HSPLcom/google/firebase/sessions/ProcessDetailsProvider;->()V @@ -29044,10 +29259,10 @@ HSPLcom/google/firebase/sessions/SessionDetails;->()V HSPLcom/google/firebase/sessions/SessionDetails;->(ILjava/lang/String;Ljava/lang/String;IJLkotlinx/serialization/internal/SerializationConstructorMarker;)V HSPLcom/google/firebase/sessions/SessionDetails;->(Ljava/lang/String;Ljava/lang/String;IJ)V HSPLcom/google/firebase/sessions/SessionDetails;->equals(Ljava/lang/Object;)Z -PLcom/google/firebase/sessions/SessionDetails;->getFirstSessionId()Ljava/lang/String; +HSPLcom/google/firebase/sessions/SessionDetails;->getFirstSessionId()Ljava/lang/String; HSPLcom/google/firebase/sessions/SessionDetails;->getSessionId()Ljava/lang/String; -PLcom/google/firebase/sessions/SessionDetails;->getSessionIndex()I -PLcom/google/firebase/sessions/SessionDetails;->getSessionStartTimestampUs()J +HSPLcom/google/firebase/sessions/SessionDetails;->getSessionIndex()I +HSPLcom/google/firebase/sessions/SessionDetails;->getSessionStartTimestampUs()J HSPLcom/google/firebase/sessions/SessionDetails;->hashCode()I HSPLcom/google/firebase/sessions/SessionDetails;->write$Self$com_google_firebase_firebase_sessions(Lcom/google/firebase/sessions/SessionDetails;Lkotlinx/serialization/encoding/CompositeEncoder;Lkotlinx/serialization/descriptors/SerialDescriptor;)V Lcom/google/firebase/sessions/SessionDetails$$serializer; @@ -29061,17 +29276,17 @@ Lcom/google/firebase/sessions/SessionDetails$Companion; HSPLcom/google/firebase/sessions/SessionDetails$Companion;->()V HSPLcom/google/firebase/sessions/SessionDetails$Companion;->(Lkotlin/jvm/internal/DefaultConstructorMarker;)V Lcom/google/firebase/sessions/SessionEvent; -PLcom/google/firebase/sessions/SessionEvent;->(Lcom/google/firebase/sessions/EventType;Lcom/google/firebase/sessions/SessionInfo;Lcom/google/firebase/sessions/ApplicationInfo;)V -PLcom/google/firebase/sessions/SessionEvent;->getApplicationInfo()Lcom/google/firebase/sessions/ApplicationInfo; -PLcom/google/firebase/sessions/SessionEvent;->getEventType()Lcom/google/firebase/sessions/EventType; -PLcom/google/firebase/sessions/SessionEvent;->getSessionData()Lcom/google/firebase/sessions/SessionInfo; +HSPLcom/google/firebase/sessions/SessionEvent;->(Lcom/google/firebase/sessions/EventType;Lcom/google/firebase/sessions/SessionInfo;Lcom/google/firebase/sessions/ApplicationInfo;)V +HSPLcom/google/firebase/sessions/SessionEvent;->getApplicationInfo()Lcom/google/firebase/sessions/ApplicationInfo; +HSPLcom/google/firebase/sessions/SessionEvent;->getEventType()Lcom/google/firebase/sessions/EventType; +HSPLcom/google/firebase/sessions/SessionEvent;->getSessionData()Lcom/google/firebase/sessions/SessionInfo; Lcom/google/firebase/sessions/SessionEvents; HSPLcom/google/firebase/sessions/SessionEvents;->()V HSPLcom/google/firebase/sessions/SessionEvents;->()V -PLcom/google/firebase/sessions/SessionEvents;->buildSession(Lcom/google/firebase/FirebaseApp;Lcom/google/firebase/sessions/SessionDetails;Lcom/google/firebase/sessions/settings/SessionsSettings;Ljava/util/Map;Ljava/lang/String;Ljava/lang/String;)Lcom/google/firebase/sessions/SessionEvent; +HSPLcom/google/firebase/sessions/SessionEvents;->buildSession(Lcom/google/firebase/FirebaseApp;Lcom/google/firebase/sessions/SessionDetails;Lcom/google/firebase/sessions/settings/SessionsSettings;Ljava/util/Map;Ljava/lang/String;Ljava/lang/String;)Lcom/google/firebase/sessions/SessionEvent; HSPLcom/google/firebase/sessions/SessionEvents;->getApplicationInfo(Lcom/google/firebase/FirebaseApp;)Lcom/google/firebase/sessions/ApplicationInfo; -PLcom/google/firebase/sessions/SessionEvents;->getSESSION_EVENT_ENCODER$com_google_firebase_firebase_sessions()Lcom/google/firebase/encoders/DataEncoder; -PLcom/google/firebase/sessions/SessionEvents;->toDataCollectionState(Lcom/google/firebase/sessions/api/SessionSubscriber;)Lcom/google/firebase/sessions/DataCollectionState; +HSPLcom/google/firebase/sessions/SessionEvents;->getSESSION_EVENT_ENCODER$com_google_firebase_firebase_sessions()Lcom/google/firebase/encoders/DataEncoder; +HSPLcom/google/firebase/sessions/SessionEvents;->toDataCollectionState(Lcom/google/firebase/sessions/api/SessionSubscriber;)Lcom/google/firebase/sessions/DataCollectionState; Lcom/google/firebase/sessions/SessionFirelogPublisher; Lcom/google/firebase/sessions/SessionFirelogPublisherImpl; HSPLcom/google/firebase/sessions/SessionFirelogPublisherImpl;->()V @@ -29088,7 +29303,7 @@ HSPLcom/google/firebase/sessions/SessionFirelogPublisherImpl$mayLogSession$1;->c HSPLcom/google/firebase/sessions/SessionFirelogPublisherImpl$mayLogSession$1;->invokeSuspend(Ljava/lang/Object;)Ljava/lang/Object; Lcom/google/firebase/sessions/SessionFirelogPublisherImpl$shouldLogSession$1; HSPLcom/google/firebase/sessions/SessionFirelogPublisherImpl$shouldLogSession$1;->(Lcom/google/firebase/sessions/SessionFirelogPublisherImpl;Lkotlin/coroutines/Continuation;)V -PLcom/google/firebase/sessions/SessionFirelogPublisherImpl$shouldLogSession$1;->invokeSuspend(Ljava/lang/Object;)Ljava/lang/Object; +HSPLcom/google/firebase/sessions/SessionFirelogPublisherImpl$shouldLogSession$1;->invokeSuspend(Ljava/lang/Object;)Ljava/lang/Object; Lcom/google/firebase/sessions/SessionFirelogPublisherImpl_Factory; HSPLcom/google/firebase/sessions/SessionFirelogPublisherImpl_Factory;->(Lcom/google/firebase/sessions/dagger/internal/Provider;Lcom/google/firebase/sessions/dagger/internal/Provider;Lcom/google/firebase/sessions/dagger/internal/Provider;Lcom/google/firebase/sessions/dagger/internal/Provider;Lcom/google/firebase/sessions/dagger/internal/Provider;)V HSPLcom/google/firebase/sessions/SessionFirelogPublisherImpl_Factory;->create(Lcom/google/firebase/sessions/dagger/internal/Provider;Lcom/google/firebase/sessions/dagger/internal/Provider;Lcom/google/firebase/sessions/dagger/internal/Provider;Lcom/google/firebase/sessions/dagger/internal/Provider;Lcom/google/firebase/sessions/dagger/internal/Provider;)Lcom/google/firebase/sessions/SessionFirelogPublisherImpl_Factory; @@ -29106,14 +29321,14 @@ HSPLcom/google/firebase/sessions/SessionGenerator_Factory;->get()Lcom/google/fir HSPLcom/google/firebase/sessions/SessionGenerator_Factory;->get()Ljava/lang/Object; HSPLcom/google/firebase/sessions/SessionGenerator_Factory;->newInstance(Lcom/google/firebase/sessions/TimeProvider;Lcom/google/firebase/sessions/UuidGenerator;)Lcom/google/firebase/sessions/SessionGenerator; Lcom/google/firebase/sessions/SessionInfo; -PLcom/google/firebase/sessions/SessionInfo;->(Ljava/lang/String;Ljava/lang/String;IJLcom/google/firebase/sessions/DataCollectionStatus;Ljava/lang/String;Ljava/lang/String;)V -PLcom/google/firebase/sessions/SessionInfo;->getDataCollectionStatus()Lcom/google/firebase/sessions/DataCollectionStatus; -PLcom/google/firebase/sessions/SessionInfo;->getEventTimestampUs()J -PLcom/google/firebase/sessions/SessionInfo;->getFirebaseAuthenticationToken()Ljava/lang/String; -PLcom/google/firebase/sessions/SessionInfo;->getFirebaseInstallationId()Ljava/lang/String; -PLcom/google/firebase/sessions/SessionInfo;->getFirstSessionId()Ljava/lang/String; -PLcom/google/firebase/sessions/SessionInfo;->getSessionId()Ljava/lang/String; -PLcom/google/firebase/sessions/SessionInfo;->getSessionIndex()I +HSPLcom/google/firebase/sessions/SessionInfo;->(Ljava/lang/String;Ljava/lang/String;IJLcom/google/firebase/sessions/DataCollectionStatus;Ljava/lang/String;Ljava/lang/String;)V +HSPLcom/google/firebase/sessions/SessionInfo;->getDataCollectionStatus()Lcom/google/firebase/sessions/DataCollectionStatus; +HSPLcom/google/firebase/sessions/SessionInfo;->getEventTimestampUs()J +HSPLcom/google/firebase/sessions/SessionInfo;->getFirebaseAuthenticationToken()Ljava/lang/String; +HSPLcom/google/firebase/sessions/SessionInfo;->getFirebaseInstallationId()Ljava/lang/String; +HSPLcom/google/firebase/sessions/SessionInfo;->getFirstSessionId()Ljava/lang/String; +HSPLcom/google/firebase/sessions/SessionInfo;->getSessionId()Ljava/lang/String; +HSPLcom/google/firebase/sessions/SessionInfo;->getSessionIndex()I Lcom/google/firebase/sessions/SessionsActivityLifecycleCallbacks; HSPLcom/google/firebase/sessions/SessionsActivityLifecycleCallbacks;->(Lcom/google/firebase/sessions/SharedSessionRepository;)V HSPLcom/google/firebase/sessions/SessionsActivityLifecycleCallbacks;->onActivityCreated(Landroid/app/Activity;Landroid/os/Bundle;)V @@ -29250,7 +29465,7 @@ Lcom/google/firebase/sessions/settings/CrashlyticsSettingsFetcher; Lcom/google/firebase/sessions/settings/LocalOverrideSettings; HSPLcom/google/firebase/sessions/settings/LocalOverrideSettings;->()V HSPLcom/google/firebase/sessions/settings/LocalOverrideSettings;->(Landroid/content/Context;)V -PLcom/google/firebase/sessions/settings/LocalOverrideSettings;->getSamplingRate()Ljava/lang/Double; +HSPLcom/google/firebase/sessions/settings/LocalOverrideSettings;->getSamplingRate()Ljava/lang/Double; HSPLcom/google/firebase/sessions/settings/LocalOverrideSettings;->getSessionEnabled()Ljava/lang/Boolean; HSPLcom/google/firebase/sessions/settings/LocalOverrideSettings;->updateSettings(Lkotlin/coroutines/Continuation;)Ljava/lang/Object; Lcom/google/firebase/sessions/settings/LocalOverrideSettings$Companion; @@ -29265,7 +29480,7 @@ HSPLcom/google/firebase/sessions/settings/LocalOverrideSettings_Factory;->newIns Lcom/google/firebase/sessions/settings/RemoteSettings; HSPLcom/google/firebase/sessions/settings/RemoteSettings;->()V HSPLcom/google/firebase/sessions/settings/RemoteSettings;->(Lcom/google/firebase/sessions/TimeProvider;Lcom/google/firebase/installations/FirebaseInstallationsApi;Lcom/google/firebase/sessions/ApplicationInfo;Lcom/google/firebase/sessions/settings/CrashlyticsSettingsFetcher;Lcom/google/firebase/sessions/settings/SettingsCache;)V -PLcom/google/firebase/sessions/settings/RemoteSettings;->getSamplingRate()Ljava/lang/Double; +HSPLcom/google/firebase/sessions/settings/RemoteSettings;->getSamplingRate()Ljava/lang/Double; HSPLcom/google/firebase/sessions/settings/RemoteSettings;->getSessionEnabled()Ljava/lang/Boolean; HSPLcom/google/firebase/sessions/settings/RemoteSettings;->sanitize(Ljava/lang/String;)Ljava/lang/String; HSPLcom/google/firebase/sessions/settings/RemoteSettings;->updateSettings(Lkotlin/coroutines/Continuation;)Ljava/lang/Object; @@ -29314,7 +29529,7 @@ HSPLcom/google/firebase/sessions/settings/SessionConfigs;->(Ljava/lang/Boo HSPLcom/google/firebase/sessions/settings/SessionConfigs;->equals(Ljava/lang/Object;)Z HSPLcom/google/firebase/sessions/settings/SessionConfigs;->getCacheDurationSeconds()Ljava/lang/Integer; HSPLcom/google/firebase/sessions/settings/SessionConfigs;->getCacheUpdatedTimeSeconds()Ljava/lang/Long; -PLcom/google/firebase/sessions/settings/SessionConfigs;->getSessionSamplingRate()Ljava/lang/Double; +HSPLcom/google/firebase/sessions/settings/SessionConfigs;->getSessionSamplingRate()Ljava/lang/Double; HSPLcom/google/firebase/sessions/settings/SessionConfigs;->getSessionsEnabled()Ljava/lang/Boolean; HSPLcom/google/firebase/sessions/settings/SessionConfigs;->hashCode()I Lcom/google/firebase/sessions/settings/SessionConfigs$Companion; @@ -29328,7 +29543,7 @@ HSPLcom/google/firebase/sessions/settings/SessionConfigsSerializer;->getDefaultV Lcom/google/firebase/sessions/settings/SessionsSettings; HSPLcom/google/firebase/sessions/settings/SessionsSettings;->()V HSPLcom/google/firebase/sessions/settings/SessionsSettings;->(Lcom/google/firebase/sessions/settings/SettingsProvider;Lcom/google/firebase/sessions/settings/SettingsProvider;)V -PLcom/google/firebase/sessions/settings/SessionsSettings;->getSamplingRate()D +HSPLcom/google/firebase/sessions/settings/SessionsSettings;->getSamplingRate()D HSPLcom/google/firebase/sessions/settings/SessionsSettings;->updateSettings(Lkotlin/coroutines/Continuation;)Ljava/lang/Object; Lcom/google/firebase/sessions/settings/SessionsSettings$Companion; HSPLcom/google/firebase/sessions/settings/SessionsSettings$Companion;->()V @@ -29349,7 +29564,7 @@ HSPLcom/google/firebase/sessions/settings/SettingsCacheImpl;->access$getSessionC HSPLcom/google/firebase/sessions/settings/SettingsCacheImpl;->access$getSessionConfigsDataStore$p(Lcom/google/firebase/sessions/settings/SettingsCacheImpl;)Landroidx/datastore/core/DataStore; HSPLcom/google/firebase/sessions/settings/SettingsCacheImpl;->getSessionConfigs()Lcom/google/firebase/sessions/settings/SessionConfigs; HSPLcom/google/firebase/sessions/settings/SettingsCacheImpl;->hasCacheExpired()Z -PLcom/google/firebase/sessions/settings/SettingsCacheImpl;->sessionSamplingRate()Ljava/lang/Double; +HSPLcom/google/firebase/sessions/settings/SettingsCacheImpl;->sessionSamplingRate()Ljava/lang/Double; HSPLcom/google/firebase/sessions/settings/SettingsCacheImpl;->sessionsEnabled()Ljava/lang/Boolean; Lcom/google/firebase/sessions/settings/SettingsCacheImpl$1; HSPLcom/google/firebase/sessions/settings/SettingsCacheImpl$1;->(Lcom/google/firebase/sessions/settings/SettingsCacheImpl;Lkotlin/coroutines/Continuation;)V @@ -29917,6 +30132,7 @@ HSPLcom/squareup/wire/FieldEncoding;->(Ljava/lang/String;II)V Lcom/squareup/wire/FieldEncoding$Companion; HSPLcom/squareup/wire/FieldEncoding$Companion;->()V HSPLcom/squareup/wire/FieldEncoding$Companion;->(Lkotlin/jvm/internal/DefaultConstructorMarker;)V +Lcom/squareup/wire/FieldMask; Lcom/squareup/wire/FloatArrayProtoAdapter; HSPLcom/squareup/wire/FloatArrayProtoAdapter;->(Lcom/squareup/wire/ProtoAdapter;)V Lcom/squareup/wire/FloatProtoAdapter; @@ -29929,6 +30145,9 @@ Lcom/squareup/wire/Message; HSPLcom/squareup/wire/Message;->()V HSPLcom/squareup/wire/Message;->(Lcom/squareup/wire/ProtoAdapter;Lokio/ByteString;)V HSPLcom/squareup/wire/Message;->unknownFields()Lokio/ByteString; +Lcom/squareup/wire/Message$Builder; +HSPLcom/squareup/wire/Message$Builder;->()V +HSPLcom/squareup/wire/Message$Builder;->buildUnknownFields()Lokio/ByteString; Lcom/squareup/wire/Message$Companion; HSPLcom/squareup/wire/Message$Companion;->()V HSPLcom/squareup/wire/Message$Companion;->(Lkotlin/jvm/internal/DefaultConstructorMarker;)V @@ -29953,6 +30172,7 @@ HSPLcom/squareup/wire/ProtoAdapterKt;->commonBytes()Lcom/squareup/wire/ProtoAdap HSPLcom/squareup/wire/ProtoAdapterKt;->commonDouble()Lcom/squareup/wire/DoubleProtoAdapter; HSPLcom/squareup/wire/ProtoAdapterKt;->commonDuration()Lcom/squareup/wire/ProtoAdapter; HSPLcom/squareup/wire/ProtoAdapterKt;->commonEmpty()Lcom/squareup/wire/ProtoAdapter; +HSPLcom/squareup/wire/ProtoAdapterKt;->commonFieldMask()Lcom/squareup/wire/ProtoAdapter; HSPLcom/squareup/wire/ProtoAdapterKt;->commonFixed32()Lcom/squareup/wire/ProtoAdapter; HSPLcom/squareup/wire/ProtoAdapterKt;->commonFixed64()Lcom/squareup/wire/ProtoAdapter; HSPLcom/squareup/wire/ProtoAdapterKt;->commonFloat()Lcom/squareup/wire/FloatProtoAdapter; @@ -29979,6 +30199,8 @@ Lcom/squareup/wire/ProtoAdapterKt$commonDuration$1; HSPLcom/squareup/wire/ProtoAdapterKt$commonDuration$1;->(Lcom/squareup/wire/FieldEncoding;Lkotlin/reflect/KClass;Lcom/squareup/wire/Syntax;)V Lcom/squareup/wire/ProtoAdapterKt$commonEmpty$1; HSPLcom/squareup/wire/ProtoAdapterKt$commonEmpty$1;->(Lcom/squareup/wire/FieldEncoding;Lkotlin/reflect/KClass;Lcom/squareup/wire/Syntax;)V +Lcom/squareup/wire/ProtoAdapterKt$commonFieldMask$1; +HSPLcom/squareup/wire/ProtoAdapterKt$commonFieldMask$1;->(Lcom/squareup/wire/FieldEncoding;Lkotlin/reflect/KClass;Lcom/squareup/wire/Syntax;)V Lcom/squareup/wire/ProtoAdapterKt$commonFixed32$1; HSPLcom/squareup/wire/ProtoAdapterKt$commonFixed32$1;->(Lcom/squareup/wire/FieldEncoding;Lkotlin/reflect/KClass;Lcom/squareup/wire/Syntax;)V Lcom/squareup/wire/ProtoAdapterKt$commonFixed64$1; @@ -30019,6 +30241,11 @@ Lcom/squareup/wire/Syntax$Companion; HSPLcom/squareup/wire/Syntax$Companion;->()V HSPLcom/squareup/wire/Syntax$Companion;->(Lkotlin/jvm/internal/DefaultConstructorMarker;)V Lcom/squareup/wire/WireEnum; +Lcom/squareup/wire/internal/Internal; +HSPLcom/squareup/wire/internal/Internal;->immutableCopyOf(Ljava/lang/String;Ljava/util/List;)Ljava/util/List; +Lcom/squareup/wire/internal/Internal__InternalKt; +HSPLcom/squareup/wire/internal/Internal__InternalKt;->immutableCopyOf(Ljava/lang/String;Ljava/util/List;)Ljava/util/List; +Lcom/squareup/wire/internal/MutableOnWriteList; Lio/ktor/client/HttpClient; HSPLio/ktor/client/HttpClient;->()V HSPLio/ktor/client/HttpClient;->(Lio/ktor/client/engine/HttpClientEngine;Lio/ktor/client/HttpClientConfig;)V @@ -30128,8 +30355,8 @@ HSPLio/ktor/client/engine/okhttp/OkHttpEngine;->getConfig()Lio/ktor/client/engin HSPLio/ktor/client/engine/okhttp/OkHttpEngine;->getCoroutineContext()Lkotlin/coroutines/CoroutineContext; Lio/ktor/client/engine/okhttp/OkHttpEngine$$ExternalSyntheticLambda0; HSPLio/ktor/client/engine/okhttp/OkHttpEngine$$ExternalSyntheticLambda0;->()V -Lio/ktor/client/engine/okhttp/OkHttpEngine$$ExternalSyntheticLambda2; -HSPLio/ktor/client/engine/okhttp/OkHttpEngine$$ExternalSyntheticLambda2;->()V +Lio/ktor/client/engine/okhttp/OkHttpEngine$$ExternalSyntheticLambda1; +HSPLio/ktor/client/engine/okhttp/OkHttpEngine$$ExternalSyntheticLambda1;->()V Lio/ktor/client/engine/okhttp/OkHttpEngine$1; HSPLio/ktor/client/engine/okhttp/OkHttpEngine$1;->(Lio/ktor/client/engine/okhttp/OkHttpEngine;Lkotlin/coroutines/Continuation;)V HSPLio/ktor/client/engine/okhttp/OkHttpEngine$1;->create(Ljava/lang/Object;Lkotlin/coroutines/Continuation;)Lkotlin/coroutines/Continuation; @@ -30533,9 +30760,19 @@ HSPLio/ktor/client/plugins/contentnegotiation/ContentNegotiationKt$ContentNegoti HSPLio/ktor/client/plugins/contentnegotiation/ContentNegotiationKt$ContentNegotiation$1;->invoke()Lio/ktor/client/plugins/contentnegotiation/ContentNegotiationConfig; HSPLio/ktor/client/plugins/contentnegotiation/ContentNegotiationKt$ContentNegotiation$1;->invoke()Ljava/lang/Object; Lio/ktor/client/plugins/contentnegotiation/ContentNegotiationKt$ContentNegotiation$2$1; -HSPLio/ktor/client/plugins/contentnegotiation/ContentNegotiationKt$ContentNegotiation$2$1;->(Ljava/util/List;Ljava/util/Set;Lio/ktor/client/plugins/api/ClientPluginBuilder;Lkotlin/coroutines/Continuation;)V +HSPLio/ktor/client/plugins/contentnegotiation/ContentNegotiationKt$ContentNegotiation$2$1;->(Ljava/util/List;Lio/ktor/client/plugins/api/ClientPluginBuilder;Ljava/util/Set;Lkotlin/coroutines/Continuation;)V Lio/ktor/client/plugins/contentnegotiation/ContentNegotiationKt$ContentNegotiation$2$2; HSPLio/ktor/client/plugins/contentnegotiation/ContentNegotiationKt$ContentNegotiation$2$2;->(Ljava/util/Set;Ljava/util/List;Lio/ktor/client/plugins/api/ClientPluginBuilder;Lkotlin/coroutines/Continuation;)V +Lio/ktor/client/plugins/contentnegotiation/ContentTypeMergeStrategy; +HSPLio/ktor/client/plugins/contentnegotiation/ContentTypeMergeStrategy;->()V +Lio/ktor/client/plugins/contentnegotiation/ContentTypeMergeStrategy$Companion; +HSPLio/ktor/client/plugins/contentnegotiation/ContentTypeMergeStrategy$Companion;->()V +HSPLio/ktor/client/plugins/contentnegotiation/ContentTypeMergeStrategy$Companion;->()V +HSPLio/ktor/client/plugins/contentnegotiation/ContentTypeMergeStrategy$Companion;->getDefault()Lio/ktor/client/plugins/contentnegotiation/ContentTypeMergeStrategy; +Lio/ktor/client/plugins/contentnegotiation/ContentTypeMergeStrategy$Companion$$ExternalSyntheticLambda2; +HSPLio/ktor/client/plugins/contentnegotiation/ContentTypeMergeStrategy$Companion$$ExternalSyntheticLambda2;->()V +Lio/ktor/client/plugins/contentnegotiation/ContentTypeMergeStrategy$Companion$$ExternalSyntheticLambda3; +HSPLio/ktor/client/plugins/contentnegotiation/ContentTypeMergeStrategy$Companion$$ExternalSyntheticLambda3;->()V Lio/ktor/client/plugins/contentnegotiation/DefaultIgnoredTypesJvmKt; HSPLio/ktor/client/plugins/contentnegotiation/DefaultIgnoredTypesJvmKt;->()V HSPLio/ktor/client/plugins/contentnegotiation/DefaultIgnoredTypesJvmKt;->getDefaultIgnoredTypes()Ljava/util/Set; @@ -30610,6 +30847,7 @@ Lio/ktor/events/EventDefinition; HSPLio/ktor/events/EventDefinition;->()V Lio/ktor/events/Events; HSPLio/ktor/events/Events;->()V +Lio/ktor/http/BadContentTypeFormatException; Lio/ktor/http/ContentType; HSPLio/ktor/http/ContentType;->()V HSPLio/ktor/http/ContentType;->(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/util/List;)V @@ -30644,6 +30882,19 @@ HSPLio/ktor/http/HttpMethod$Companion;->()V HSPLio/ktor/http/HttpMethod$Companion;->(Lkotlin/jvm/internal/DefaultConstructorMarker;)V Lio/ktor/http/HttpStatusCode; Lio/ktor/http/content/OutgoingContent; +Lio/ktor/openapi/JsonSchemaInference; +Lio/ktor/openapi/JsonSchemaInferenceKt; +HSPLio/ktor/openapi/JsonSchemaInferenceKt;->()V +HSPLio/ktor/openapi/JsonSchemaInferenceKt;->getKotlinxSerializerDefaultFormats()Lkotlin/jvm/functions/Function1; +Lio/ktor/openapi/JsonSchemaInferenceKt$$ExternalSyntheticLambda0; +HSPLio/ktor/openapi/JsonSchemaInferenceKt$$ExternalSyntheticLambda0;->()V +Lio/ktor/openapi/KotlinxSerializerJsonSchemaInference; +HSPLio/ktor/openapi/KotlinxSerializerJsonSchemaInference;->()V +HSPLio/ktor/openapi/KotlinxSerializerJsonSchemaInference;->(Lkotlinx/serialization/modules/SerializersModule;Lkotlin/jvm/functions/Function1;)V +HSPLio/ktor/openapi/KotlinxSerializerJsonSchemaInference;->(Lkotlinx/serialization/modules/SerializersModule;Lkotlin/jvm/functions/Function1;ILkotlin/jvm/internal/DefaultConstructorMarker;)V +Lio/ktor/openapi/KotlinxSerializerJsonSchemaInference$Companion; +HSPLio/ktor/openapi/KotlinxSerializerJsonSchemaInference$Companion;->()V +HSPLio/ktor/openapi/KotlinxSerializerJsonSchemaInference$Companion;->(Lkotlin/jvm/internal/DefaultConstructorMarker;)V Lio/ktor/serialization/Configuration; HSPLio/ktor/serialization/Configuration;->register$default(Lio/ktor/serialization/Configuration;Lio/ktor/http/ContentType;Lio/ktor/serialization/ContentConverter;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)V HSPLio/ktor/serialization/Configuration;->register$lambda$0(Lio/ktor/serialization/ContentConverter;)Lkotlin/Unit; @@ -30722,7 +30973,12 @@ Lio/ktor/util/collections/CopyOnWriteHashMap; HSPLio/ktor/util/collections/CopyOnWriteHashMap;->()V HSPLio/ktor/util/collections/CopyOnWriteHashMap;->()V Lio/ktor/util/logging/KtorSimpleLoggerJvmKt; +HSPLio/ktor/util/logging/KtorSimpleLoggerJvmKt;->()V HSPLio/ktor/util/logging/KtorSimpleLoggerJvmKt;->KtorSimpleLogger(Ljava/lang/String;)Lorg/slf4j/Logger; +Lio/ktor/util/logging/KtorSimpleLoggerJvmKt$KtorSimpleLogger$1; +HSPLio/ktor/util/logging/KtorSimpleLoggerJvmKt$KtorSimpleLogger$1;->(Ljava/lang/String;)V +Lio/ktor/util/logging/KtorSimpleLoggerJvmKt$KtorSimpleLogger$1$$ExternalSyntheticLambda0; +HSPLio/ktor/util/logging/KtorSimpleLoggerJvmKt$KtorSimpleLogger$1$$ExternalSyntheticLambda0;->(Ljava/lang/String;)V Lio/ktor/util/pipeline/PhaseContent; HSPLio/ktor/util/pipeline/PhaseContent;->()V HSPLio/ktor/util/pipeline/PhaseContent;->(Lio/ktor/util/pipeline/PipelinePhase;Lio/ktor/util/pipeline/PipelinePhaseRelation;)V @@ -30760,7 +31016,7 @@ Lio/ktor/util/pipeline/PipelinePhaseRelation$Last; HSPLio/ktor/util/pipeline/PipelinePhaseRelation$Last;->()V HSPLio/ktor/util/pipeline/PipelinePhaseRelation$Last;->()V Lio/ktor/util/reflect/TypeInfo; -HSPLio/ktor/util/reflect/TypeInfo;->(Lkotlin/reflect/KClass;Lkotlin/reflect/KType;)V +HSPLio/ktor/util/reflect/TypeInfo;->(Lkotlin/reflect/KClass;ZLkotlin/reflect/KType;)V HSPLio/ktor/util/reflect/TypeInfo;->hashCode()I Lio/ktor/utils/io/ByteReadChannel; Lio/ktor/utils/io/IODispatcher_jvmKt; @@ -30785,6 +31041,7 @@ HSPLio/opentelemetry/context/ContextStorageWrappers;->()V HSPLio/opentelemetry/context/ContextStorageWrappers;->addWrapper(Ljava/util/function/Function;)V Ljavax/inject/Provider; Lkotlin/Function; +Lkotlin/KotlinNothingValueException; Lkotlin/KotlinVersion; HSPLkotlin/KotlinVersion;->()V HSPLkotlin/KotlinVersion;->(III)V @@ -31362,7 +31619,7 @@ Lkotlin/coroutines/CombinedContext; HSPLkotlin/coroutines/CombinedContext;->(Lkotlin/coroutines/CoroutineContext;Lkotlin/coroutines/CoroutineContext$Element;)V HSPLkotlin/coroutines/CombinedContext;->fold(Ljava/lang/Object;Lkotlin/jvm/functions/Function2;)Ljava/lang/Object; HSPLkotlin/coroutines/CombinedContext;->get(Lkotlin/coroutines/CoroutineContext$Key;)Lkotlin/coroutines/CoroutineContext$Element; -HPLkotlin/coroutines/CombinedContext;->minusKey(Lkotlin/coroutines/CoroutineContext$Key;)Lkotlin/coroutines/CoroutineContext; +HSPLkotlin/coroutines/CombinedContext;->minusKey(Lkotlin/coroutines/CoroutineContext$Key;)Lkotlin/coroutines/CoroutineContext; HSPLkotlin/coroutines/CombinedContext;->plus(Lkotlin/coroutines/CoroutineContext;)Lkotlin/coroutines/CoroutineContext; Lkotlin/coroutines/Continuation; Lkotlin/coroutines/ContinuationInterceptor; @@ -31561,7 +31818,7 @@ Lkotlin/jdk7/AutoCloseableKt; HSPLkotlin/jdk7/AutoCloseableKt;->closeFinally(Ljava/lang/AutoCloseable;Ljava/lang/Throwable;)V Lkotlin/jvm/JvmClassMappingKt; HSPLkotlin/jvm/JvmClassMappingKt;->getJavaClass(Lkotlin/reflect/KClass;)Ljava/lang/Class; -HPLkotlin/jvm/JvmClassMappingKt;->getJavaObjectType(Lkotlin/reflect/KClass;)Ljava/lang/Class; +HSPLkotlin/jvm/JvmClassMappingKt;->getJavaObjectType(Lkotlin/reflect/KClass;)Ljava/lang/Class; HSPLkotlin/jvm/JvmClassMappingKt;->getKotlinClass(Ljava/lang/Class;)Lkotlin/reflect/KClass; Lkotlin/jvm/functions/Function0; Lkotlin/jvm/functions/Function1; @@ -31742,6 +31999,7 @@ HSPLkotlin/jvm/internal/TypeReference;->(Lkotlin/reflect/KClassifier;Ljava HSPLkotlin/jvm/internal/TypeReference;->getArguments()Ljava/util/List; HSPLkotlin/jvm/internal/TypeReference;->getClassifier()Lkotlin/reflect/KClassifier; HSPLkotlin/jvm/internal/TypeReference;->hashCode()I +HSPLkotlin/jvm/internal/TypeReference;->isMarkedNullable()Z Lkotlin/jvm/internal/TypeReference$Companion; HSPLkotlin/jvm/internal/TypeReference$Companion;->()V HSPLkotlin/jvm/internal/TypeReference$Companion;->(Lkotlin/jvm/internal/DefaultConstructorMarker;)V @@ -32108,8 +32366,8 @@ HSPLkotlin/time/Clock$System;->now()Lkotlin/time/Instant; Lkotlin/time/ComparableTimeMark; Lkotlin/time/Duration; HSPLkotlin/time/Duration;->()V -HSPLkotlin/time/Duration;->(J)V -HSPLkotlin/time/Duration;->box-impl(J)Lkotlin/time/Duration; +PLkotlin/time/Duration;->(J)V +PLkotlin/time/Duration;->box-impl(J)Lkotlin/time/Duration; HSPLkotlin/time/Duration;->constructor-impl(J)J HSPLkotlin/time/Duration;->getInWholeMicroseconds-impl(J)J HSPLkotlin/time/Duration;->getInWholeMilliseconds-impl(J)J @@ -32124,7 +32382,7 @@ HSPLkotlin/time/Duration;->isInfinite-impl(J)Z HSPLkotlin/time/Duration;->isPositive-impl(J)Z HSPLkotlin/time/Duration;->plus-LRDsOJo(JJ)J HSPLkotlin/time/Duration;->toLong-impl(JLkotlin/time/DurationUnit;)J -HSPLkotlin/time/Duration;->unbox-impl()J +PLkotlin/time/Duration;->unbox-impl()J Lkotlin/time/Duration$Companion; HSPLkotlin/time/Duration$Companion;->()V HSPLkotlin/time/Duration$Companion;->(Lkotlin/jvm/internal/DefaultConstructorMarker;)V @@ -32351,7 +32609,7 @@ Lkotlinx/coroutines/CancelHandler$UserSupplied; HSPLkotlinx/coroutines/CancelHandler$UserSupplied;->(Lkotlin/jvm/functions/Function1;)V HSPLkotlinx/coroutines/CancelHandler$UserSupplied;->invoke(Ljava/lang/Throwable;)V Lkotlinx/coroutines/CancellableContinuation; -HSPLkotlinx/coroutines/CancellableContinuation;->cancel$default(Lkotlinx/coroutines/CancellableContinuation;Ljava/lang/Throwable;ILjava/lang/Object;)Z +PLkotlinx/coroutines/CancellableContinuation;->cancel$default(Lkotlinx/coroutines/CancellableContinuation;Ljava/lang/Throwable;ILjava/lang/Object;)Z PLkotlinx/coroutines/CancellableContinuation$DefaultImpls;->cancel$default(Lkotlinx/coroutines/CancellableContinuation;Ljava/lang/Throwable;ILjava/lang/Object;)Z Lkotlinx/coroutines/CancellableContinuationImpl; HSPLkotlinx/coroutines/CancellableContinuationImpl;->()V @@ -32359,6 +32617,7 @@ HSPLkotlinx/coroutines/CancellableContinuationImpl;->(Lkotlin/coroutines/C HSPLkotlinx/coroutines/CancellableContinuationImpl;->callCancelHandler(Lkotlinx/coroutines/CancelHandler;Ljava/lang/Throwable;)V PLkotlinx/coroutines/CancellableContinuationImpl;->callSegmentOnCancellation(Lkotlinx/coroutines/internal/Segment;Ljava/lang/Throwable;)V HSPLkotlinx/coroutines/CancellableContinuationImpl;->cancel(Ljava/lang/Throwable;)Z +HSPLkotlinx/coroutines/CancellableContinuationImpl;->cancelCompletedResult$kotlinx_coroutines_core(Ljava/lang/Object;Ljava/lang/Throwable;)V HSPLkotlinx/coroutines/CancellableContinuationImpl;->cancelLater(Ljava/lang/Throwable;)Z HSPLkotlinx/coroutines/CancellableContinuationImpl;->completeResume(Ljava/lang/Object;)V HSPLkotlinx/coroutines/CancellableContinuationImpl;->detachChild$kotlinx_coroutines_core()V @@ -32389,6 +32648,7 @@ HSPLkotlinx/coroutines/CancellableContinuationImpl;->parentCancelled$kotlinx_cor HSPLkotlinx/coroutines/CancellableContinuationImpl;->releaseClaimedReusableContinuation$kotlinx_coroutines_core()V HSPLkotlinx/coroutines/CancellableContinuationImpl;->resetStateReusable()Z HSPLkotlinx/coroutines/CancellableContinuationImpl;->resume(Ljava/lang/Object;Lkotlin/jvm/functions/Function1;)V +HSPLkotlinx/coroutines/CancellableContinuationImpl;->resume(Ljava/lang/Object;Lkotlin/jvm/functions/Function3;)V HSPLkotlinx/coroutines/CancellableContinuationImpl;->resumeImpl$kotlinx_coroutines_core$default(Lkotlinx/coroutines/CancellableContinuationImpl;Ljava/lang/Object;ILkotlin/jvm/functions/Function3;ILjava/lang/Object;)V HSPLkotlinx/coroutines/CancellableContinuationImpl;->resumeImpl$kotlinx_coroutines_core(Ljava/lang/Object;ILkotlin/jvm/functions/Function3;)V PLkotlinx/coroutines/CancellableContinuationImpl;->resumeUndispatched(Lkotlinx/coroutines/CoroutineDispatcher;Ljava/lang/Object;)V @@ -32419,7 +32679,7 @@ Lkotlinx/coroutines/ChildHandle; Lkotlinx/coroutines/ChildHandleNode; HSPLkotlinx/coroutines/ChildHandleNode;->(Lkotlinx/coroutines/ChildJob;)V HSPLkotlinx/coroutines/ChildHandleNode;->childCancelled(Ljava/lang/Throwable;)Z -HSPLkotlinx/coroutines/ChildHandleNode;->getOnCancelling()Z +PLkotlinx/coroutines/ChildHandleNode;->getOnCancelling()Z HSPLkotlinx/coroutines/ChildHandleNode;->getParent()Lkotlinx/coroutines/Job; HSPLkotlinx/coroutines/ChildHandleNode;->invoke(Ljava/lang/Throwable;)V Lkotlinx/coroutines/ChildJob; @@ -32437,6 +32697,10 @@ Lkotlinx/coroutines/CompletableJob; Lkotlinx/coroutines/CompletedContinuation; HSPLkotlinx/coroutines/CompletedContinuation;->(Ljava/lang/Object;Lkotlinx/coroutines/CancelHandler;Lkotlin/jvm/functions/Function3;Ljava/lang/Object;Ljava/lang/Throwable;)V HSPLkotlinx/coroutines/CompletedContinuation;->(Ljava/lang/Object;Lkotlinx/coroutines/CancelHandler;Lkotlin/jvm/functions/Function3;Ljava/lang/Object;Ljava/lang/Throwable;ILkotlin/jvm/internal/DefaultConstructorMarker;)V +HSPLkotlinx/coroutines/CompletedContinuation;->copy$default(Lkotlinx/coroutines/CompletedContinuation;Ljava/lang/Object;Lkotlinx/coroutines/CancelHandler;Lkotlin/jvm/functions/Function3;Ljava/lang/Object;Ljava/lang/Throwable;ILjava/lang/Object;)Lkotlinx/coroutines/CompletedContinuation; +HSPLkotlinx/coroutines/CompletedContinuation;->copy(Ljava/lang/Object;Lkotlinx/coroutines/CancelHandler;Lkotlin/jvm/functions/Function3;Ljava/lang/Object;Ljava/lang/Throwable;)Lkotlinx/coroutines/CompletedContinuation; +HSPLkotlinx/coroutines/CompletedContinuation;->getCancelled()Z +HSPLkotlinx/coroutines/CompletedContinuation;->invokeHandlers(Lkotlinx/coroutines/CancellableContinuationImpl;Ljava/lang/Throwable;)V Lkotlinx/coroutines/CompletedExceptionally; HSPLkotlinx/coroutines/CompletedExceptionally;->()V HSPLkotlinx/coroutines/CompletedExceptionally;->(Ljava/lang/Throwable;Z)V @@ -32580,7 +32844,7 @@ HSPLkotlinx/coroutines/DisposeOnCancel;->(Lkotlinx/coroutines/DisposableHa HSPLkotlinx/coroutines/DisposeOnCancel;->invoke(Ljava/lang/Throwable;)V Lkotlinx/coroutines/DisposeOnCompletion; HSPLkotlinx/coroutines/DisposeOnCompletion;->(Lkotlinx/coroutines/DisposableHandle;)V -HSPLkotlinx/coroutines/DisposeOnCompletion;->getOnCancelling()Z +PLkotlinx/coroutines/DisposeOnCompletion;->getOnCancelling()Z HSPLkotlinx/coroutines/DisposeOnCompletion;->invoke(Ljava/lang/Throwable;)V Lkotlinx/coroutines/Empty; HSPLkotlinx/coroutines/Empty;->(Z)V @@ -32662,7 +32926,7 @@ HSPLkotlinx/coroutines/ExecutorCoroutineDispatcherImpl;->(Ljava/util/concu HSPLkotlinx/coroutines/ExecutorCoroutineDispatcherImpl;->dispatch(Lkotlin/coroutines/CoroutineContext;Ljava/lang/Runnable;)V HSPLkotlinx/coroutines/ExecutorCoroutineDispatcherImpl;->equals(Ljava/lang/Object;)Z HSPLkotlinx/coroutines/ExecutorCoroutineDispatcherImpl;->getExecutor()Ljava/util/concurrent/Executor; -HSPLkotlinx/coroutines/ExecutorCoroutineDispatcherImpl;->toString()Ljava/lang/String; +PLkotlinx/coroutines/ExecutorCoroutineDispatcherImpl;->toString()Ljava/lang/String; Lkotlinx/coroutines/ExecutorsKt; HSPLkotlinx/coroutines/ExecutorsKt;->asExecutor(Lkotlinx/coroutines/CoroutineDispatcher;)Ljava/util/concurrent/Executor; HSPLkotlinx/coroutines/ExecutorsKt;->from(Ljava/util/concurrent/Executor;)Lkotlinx/coroutines/CoroutineDispatcher; @@ -32694,7 +32958,7 @@ Lkotlinx/coroutines/Job; HSPLkotlinx/coroutines/Job;->()V HSPLkotlinx/coroutines/Job;->cancel$default(Lkotlinx/coroutines/Job;Ljava/util/concurrent/CancellationException;ILjava/lang/Object;)V Lkotlinx/coroutines/Job$DefaultImpls; -HSPLkotlinx/coroutines/Job$DefaultImpls;->cancel$default(Lkotlinx/coroutines/Job;Ljava/util/concurrent/CancellationException;ILjava/lang/Object;)V +PLkotlinx/coroutines/Job$DefaultImpls;->cancel$default(Lkotlinx/coroutines/Job;Ljava/util/concurrent/CancellationException;ILjava/lang/Object;)V HSPLkotlinx/coroutines/Job$DefaultImpls;->fold(Lkotlinx/coroutines/Job;Ljava/lang/Object;Lkotlin/jvm/functions/Function2;)Ljava/lang/Object; HSPLkotlinx/coroutines/Job$DefaultImpls;->get(Lkotlinx/coroutines/Job;Lkotlin/coroutines/CoroutineContext$Key;)Lkotlin/coroutines/CoroutineContext$Element; HSPLkotlinx/coroutines/Job$DefaultImpls;->minusKey(Lkotlinx/coroutines/Job;Lkotlin/coroutines/CoroutineContext$Key;)Lkotlin/coroutines/CoroutineContext; @@ -32709,7 +32973,7 @@ HSPLkotlinx/coroutines/JobCancellationException;->fillInStackTrace()Ljava/lang/T Lkotlinx/coroutines/JobImpl; HSPLkotlinx/coroutines/JobImpl;->(Lkotlinx/coroutines/Job;)V HSPLkotlinx/coroutines/JobImpl;->getHandlesException$kotlinx_coroutines_core()Z -HSPLkotlinx/coroutines/JobImpl;->getOnCancelComplete$kotlinx_coroutines_core()Z +PLkotlinx/coroutines/JobImpl;->getOnCancelComplete$kotlinx_coroutines_core()Z HSPLkotlinx/coroutines/JobImpl;->handlesException()Z Lkotlinx/coroutines/JobKt; HSPLkotlinx/coroutines/JobKt;->Job$default(Lkotlinx/coroutines/Job;ILjava/lang/Object;)Lkotlinx/coroutines/CompletableJob; @@ -32740,7 +33004,7 @@ Lkotlinx/coroutines/JobSupport; HSPLkotlinx/coroutines/JobSupport;->()V HSPLkotlinx/coroutines/JobSupport;->(Z)V HSPLkotlinx/coroutines/JobSupport;->access$cancellationExceptionMessage(Lkotlinx/coroutines/JobSupport;)Ljava/lang/String; -HSPLkotlinx/coroutines/JobSupport;->access$continueCompleting(Lkotlinx/coroutines/JobSupport;Lkotlinx/coroutines/JobSupport$Finishing;Lkotlinx/coroutines/ChildHandleNode;Ljava/lang/Object;)V +PLkotlinx/coroutines/JobSupport;->access$continueCompleting(Lkotlinx/coroutines/JobSupport;Lkotlinx/coroutines/JobSupport$Finishing;Lkotlinx/coroutines/ChildHandleNode;Ljava/lang/Object;)V HSPLkotlinx/coroutines/JobSupport;->addSuppressedExceptions(Ljava/lang/Throwable;Ljava/util/List;)V HSPLkotlinx/coroutines/JobSupport;->afterCompletion(Ljava/lang/Object;)V HSPLkotlinx/coroutines/JobSupport;->attachChild(Lkotlinx/coroutines/ChildJob;)Lkotlinx/coroutines/ChildHandle; @@ -32750,18 +33014,18 @@ HSPLkotlinx/coroutines/JobSupport;->cancel(Ljava/util/concurrent/CancellationExc HSPLkotlinx/coroutines/JobSupport;->cancelCoroutine(Ljava/lang/Throwable;)Z HSPLkotlinx/coroutines/JobSupport;->cancelImpl$kotlinx_coroutines_core(Ljava/lang/Object;)Z HSPLkotlinx/coroutines/JobSupport;->cancelInternal(Ljava/lang/Throwable;)V -HSPLkotlinx/coroutines/JobSupport;->cancelMakeCompleting(Ljava/lang/Object;)Ljava/lang/Object; +PLkotlinx/coroutines/JobSupport;->cancelMakeCompleting(Ljava/lang/Object;)Ljava/lang/Object; HSPLkotlinx/coroutines/JobSupport;->cancelParent(Ljava/lang/Throwable;)Z -HSPLkotlinx/coroutines/JobSupport;->cancellationExceptionMessage()Ljava/lang/String; +PLkotlinx/coroutines/JobSupport;->cancellationExceptionMessage()Ljava/lang/String; HSPLkotlinx/coroutines/JobSupport;->childCancelled(Ljava/lang/Throwable;)Z HSPLkotlinx/coroutines/JobSupport;->completeStateFinalization(Lkotlinx/coroutines/Incomplete;Ljava/lang/Object;)V -HSPLkotlinx/coroutines/JobSupport;->continueCompleting(Lkotlinx/coroutines/JobSupport$Finishing;Lkotlinx/coroutines/ChildHandleNode;Ljava/lang/Object;)V +PLkotlinx/coroutines/JobSupport;->continueCompleting(Lkotlinx/coroutines/JobSupport$Finishing;Lkotlinx/coroutines/ChildHandleNode;Ljava/lang/Object;)V HSPLkotlinx/coroutines/JobSupport;->createCauseException(Ljava/lang/Object;)Ljava/lang/Throwable; HSPLkotlinx/coroutines/JobSupport;->finalizeFinishingState(Lkotlinx/coroutines/JobSupport$Finishing;Ljava/lang/Object;)Ljava/lang/Object; HSPLkotlinx/coroutines/JobSupport;->fold(Ljava/lang/Object;Lkotlin/jvm/functions/Function2;)Ljava/lang/Object; HSPLkotlinx/coroutines/JobSupport;->get(Lkotlin/coroutines/CoroutineContext$Key;)Lkotlin/coroutines/CoroutineContext$Element; HSPLkotlinx/coroutines/JobSupport;->getCancellationException()Ljava/util/concurrent/CancellationException; -HSPLkotlinx/coroutines/JobSupport;->getChildJobCancellationCause()Ljava/util/concurrent/CancellationException; +PLkotlinx/coroutines/JobSupport;->getChildJobCancellationCause()Ljava/util/concurrent/CancellationException; HSPLkotlinx/coroutines/JobSupport;->getCompletedInternal$kotlinx_coroutines_core()Ljava/lang/Object; HSPLkotlinx/coroutines/JobSupport;->getCompletionExceptionOrNull()Ljava/lang/Throwable; HSPLkotlinx/coroutines/JobSupport;->getExceptionOrNull(Ljava/lang/Object;)Ljava/lang/Throwable; @@ -32816,7 +33080,7 @@ Lkotlinx/coroutines/JobSupport$AwaitContinuation; HSPLkotlinx/coroutines/JobSupport$AwaitContinuation;->(Lkotlin/coroutines/Continuation;Lkotlinx/coroutines/JobSupport;)V HSPLkotlinx/coroutines/JobSupport$AwaitContinuation;->getContinuationCancellationCause(Lkotlinx/coroutines/Job;)Ljava/lang/Throwable; Lkotlinx/coroutines/JobSupport$ChildCompletion; -PLkotlinx/coroutines/JobSupport$ChildCompletion;->(Lkotlinx/coroutines/JobSupport;Lkotlinx/coroutines/JobSupport$Finishing;Lkotlinx/coroutines/ChildHandleNode;Ljava/lang/Object;)V +HSPLkotlinx/coroutines/JobSupport$ChildCompletion;->(Lkotlinx/coroutines/JobSupport;Lkotlinx/coroutines/JobSupport$Finishing;Lkotlinx/coroutines/ChildHandleNode;Ljava/lang/Object;)V PLkotlinx/coroutines/JobSupport$ChildCompletion;->getOnCancelling()Z PLkotlinx/coroutines/JobSupport$ChildCompletion;->invoke(Ljava/lang/Throwable;)V Lkotlinx/coroutines/JobSupport$Finishing; @@ -32837,7 +33101,7 @@ PLkotlinx/coroutines/JobSupport$Finishing;->isSealed()Z HSPLkotlinx/coroutines/JobSupport$Finishing;->sealLocked(Ljava/lang/Throwable;)Ljava/util/List; HSPLkotlinx/coroutines/JobSupport$Finishing;->setCompleting(Z)V HSPLkotlinx/coroutines/JobSupport$Finishing;->setExceptionsHolder(Ljava/lang/Object;)V -HSPLkotlinx/coroutines/JobSupport$Finishing;->setRootCause(Ljava/lang/Throwable;)V +PLkotlinx/coroutines/JobSupport$Finishing;->setRootCause(Ljava/lang/Throwable;)V Lkotlinx/coroutines/JobSupportKt; HSPLkotlinx/coroutines/JobSupportKt;->()V HSPLkotlinx/coroutines/JobSupportKt;->access$getCOMPLETING_ALREADY$p()Lkotlinx/coroutines/internal/Symbol; @@ -33077,6 +33341,7 @@ HSPLkotlinx/coroutines/channels/BufferedChannel$BufferedChannelIterator;->hasNex HSPLkotlinx/coroutines/channels/BufferedChannel$BufferedChannelIterator;->hasNextOnNoWaiterSuspend(Lkotlinx/coroutines/channels/ChannelSegment;IJLkotlin/coroutines/Continuation;)Ljava/lang/Object; HSPLkotlinx/coroutines/channels/BufferedChannel$BufferedChannelIterator;->invokeOnCancellation(Lkotlinx/coroutines/internal/Segment;I)V HSPLkotlinx/coroutines/channels/BufferedChannel$BufferedChannelIterator;->next()Ljava/lang/Object; +HSPLkotlinx/coroutines/channels/BufferedChannel$BufferedChannelIterator;->onClosedHasNextNoWaiterSuspend()V HSPLkotlinx/coroutines/channels/BufferedChannel$BufferedChannelIterator;->tryResumeHasNext(Ljava/lang/Object;)Z HSPLkotlinx/coroutines/channels/BufferedChannel$BufferedChannelIterator;->tryResumeHasNextOnClosedChannel()V Lkotlinx/coroutines/channels/BufferedChannel$SendBroadcast; @@ -33395,7 +33660,7 @@ HSPLkotlinx/coroutines/flow/FlowKt__EmittersKt$onStart$$inlined$unsafeFlow$1;->< HSPLkotlinx/coroutines/flow/FlowKt__EmittersKt$onStart$$inlined$unsafeFlow$1;->collect(Lkotlinx/coroutines/flow/FlowCollector;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; Lkotlinx/coroutines/flow/FlowKt__EmittersKt$onStart$$inlined$unsafeFlow$1$1; HSPLkotlinx/coroutines/flow/FlowKt__EmittersKt$onStart$$inlined$unsafeFlow$1$1;->(Lkotlinx/coroutines/flow/FlowKt__EmittersKt$onStart$$inlined$unsafeFlow$1;Lkotlin/coroutines/Continuation;)V -PLkotlinx/coroutines/flow/FlowKt__EmittersKt$onStart$$inlined$unsafeFlow$1$1;->invokeSuspend(Ljava/lang/Object;)Ljava/lang/Object; +HSPLkotlinx/coroutines/flow/FlowKt__EmittersKt$onStart$$inlined$unsafeFlow$1$1;->invokeSuspend(Ljava/lang/Object;)Ljava/lang/Object; Lkotlinx/coroutines/flow/FlowKt__ErrorsKt; HSPLkotlinx/coroutines/flow/FlowKt__ErrorsKt;->catch(Lkotlinx/coroutines/flow/Flow;Lkotlin/jvm/functions/Function3;)Lkotlinx/coroutines/flow/Flow; HSPLkotlinx/coroutines/flow/FlowKt__ErrorsKt;->catchImpl(Lkotlinx/coroutines/flow/Flow;Lkotlinx/coroutines/flow/FlowCollector;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; @@ -33591,6 +33856,7 @@ HSPLkotlinx/coroutines/flow/SharedFlowImpl;->getHead()J HSPLkotlinx/coroutines/flow/SharedFlowImpl;->getLastReplayedLocked()Ljava/lang/Object; HSPLkotlinx/coroutines/flow/SharedFlowImpl;->getPeekedValueLockedAt(J)Ljava/lang/Object; HSPLkotlinx/coroutines/flow/SharedFlowImpl;->getQueueEndIndex()J +HSPLkotlinx/coroutines/flow/SharedFlowImpl;->getReplayCache()Ljava/util/List; HSPLkotlinx/coroutines/flow/SharedFlowImpl;->getReplaySize()I HSPLkotlinx/coroutines/flow/SharedFlowImpl;->getTotalSize()I HSPLkotlinx/coroutines/flow/SharedFlowImpl;->growBuffer([Ljava/lang/Object;II)[Ljava/lang/Object; @@ -33878,7 +34144,7 @@ HSPLkotlinx/coroutines/internal/ContextScope;->getCoroutineContext()Lkotlin/coro Lkotlinx/coroutines/internal/DispatchedContinuation; HSPLkotlinx/coroutines/internal/DispatchedContinuation;->()V HSPLkotlinx/coroutines/internal/DispatchedContinuation;->(Lkotlinx/coroutines/CoroutineDispatcher;Lkotlin/coroutines/Continuation;)V -HPLkotlinx/coroutines/internal/DispatchedContinuation;->awaitReusability$kotlinx_coroutines_core()V +HSPLkotlinx/coroutines/internal/DispatchedContinuation;->awaitReusability$kotlinx_coroutines_core()V HSPLkotlinx/coroutines/internal/DispatchedContinuation;->claimReusableCancellableContinuation$kotlinx_coroutines_core()Lkotlinx/coroutines/CancellableContinuationImpl; HSPLkotlinx/coroutines/internal/DispatchedContinuation;->dispatchYield$kotlinx_coroutines_core(Lkotlin/coroutines/CoroutineContext;Ljava/lang/Object;)V HSPLkotlinx/coroutines/internal/DispatchedContinuation;->getContext()Lkotlin/coroutines/CoroutineContext; @@ -33937,7 +34203,7 @@ HSPLkotlinx/coroutines/internal/LockFreeLinkedListNode;->addNext(Lkotlinx/corout HSPLkotlinx/coroutines/internal/LockFreeLinkedListNode;->addOneIfEmpty(Lkotlinx/coroutines/internal/LockFreeLinkedListNode;)Z HSPLkotlinx/coroutines/internal/LockFreeLinkedListNode;->close(I)V HSPLkotlinx/coroutines/internal/LockFreeLinkedListNode;->correctPrev()Lkotlinx/coroutines/internal/LockFreeLinkedListNode; -HSPLkotlinx/coroutines/internal/LockFreeLinkedListNode;->findPrevNonRemoved(Lkotlinx/coroutines/internal/LockFreeLinkedListNode;)Lkotlinx/coroutines/internal/LockFreeLinkedListNode; +PLkotlinx/coroutines/internal/LockFreeLinkedListNode;->findPrevNonRemoved(Lkotlinx/coroutines/internal/LockFreeLinkedListNode;)Lkotlinx/coroutines/internal/LockFreeLinkedListNode; HSPLkotlinx/coroutines/internal/LockFreeLinkedListNode;->finishAdd(Lkotlinx/coroutines/internal/LockFreeLinkedListNode;)V HSPLkotlinx/coroutines/internal/LockFreeLinkedListNode;->getNext()Ljava/lang/Object; HSPLkotlinx/coroutines/internal/LockFreeLinkedListNode;->getNextNode()Lkotlinx/coroutines/internal/LockFreeLinkedListNode; @@ -33995,7 +34261,7 @@ HSPLkotlinx/coroutines/internal/ScopeCoroutine;->(Lkotlin/coroutines/Corou PLkotlinx/coroutines/internal/ScopeCoroutine;->afterCompletion(Ljava/lang/Object;)V HSPLkotlinx/coroutines/internal/ScopeCoroutine;->afterCompletionUndispatched()V HSPLkotlinx/coroutines/internal/ScopeCoroutine;->afterResume(Ljava/lang/Object;)V -HSPLkotlinx/coroutines/internal/ScopeCoroutine;->isScopedCoroutine()Z +PLkotlinx/coroutines/internal/ScopeCoroutine;->isScopedCoroutine()Z Lkotlinx/coroutines/internal/Segment; HSPLkotlinx/coroutines/internal/Segment;->()V HSPLkotlinx/coroutines/internal/Segment;->(JLkotlinx/coroutines/internal/Segment;I)V @@ -34195,7 +34461,7 @@ HSPLkotlinx/coroutines/scheduling/WorkQueue;->pollWithExclusiveMode(Z)Lkotlinx/c HSPLkotlinx/coroutines/scheduling/WorkQueue;->stealWithExclusiveMode(Z)Lkotlinx/coroutines/scheduling/Task; HSPLkotlinx/coroutines/scheduling/WorkQueue;->tryExtractFromTheMiddle(IZ)Lkotlinx/coroutines/scheduling/Task; HSPLkotlinx/coroutines/scheduling/WorkQueue;->trySteal(ILkotlin/jvm/internal/Ref$ObjectRef;)J -HSPLkotlinx/coroutines/scheduling/WorkQueue;->tryStealLastScheduled(ILkotlin/jvm/internal/Ref$ObjectRef;)J +HPLkotlinx/coroutines/scheduling/WorkQueue;->tryStealLastScheduled(ILkotlin/jvm/internal/Ref$ObjectRef;)J Lkotlinx/coroutines/selects/OnTimeout; HSPLkotlinx/coroutines/selects/OnTimeout;->(J)V HSPLkotlinx/coroutines/selects/OnTimeout;->access$register(Lkotlinx/coroutines/selects/OnTimeout;Lkotlinx/coroutines/selects/SelectInstance;Ljava/lang/Object;)V @@ -34305,7 +34571,6 @@ Lkotlinx/coroutines/sync/MutexImpl$CancellableContinuationWithOwner; HSPLkotlinx/coroutines/sync/MutexImpl$CancellableContinuationWithOwner;->(Lkotlinx/coroutines/sync/MutexImpl;Lkotlinx/coroutines/CancellableContinuationImpl;Ljava/lang/Object;)V HSPLkotlinx/coroutines/sync/MutexImpl$CancellableContinuationWithOwner;->completeResume(Ljava/lang/Object;)V HSPLkotlinx/coroutines/sync/MutexImpl$CancellableContinuationWithOwner;->invokeOnCancellation(Lkotlinx/coroutines/internal/Segment;I)V -HSPLkotlinx/coroutines/sync/MutexImpl$CancellableContinuationWithOwner;->resume(Ljava/lang/Object;Lkotlin/jvm/functions/Function3;)V HSPLkotlinx/coroutines/sync/MutexImpl$CancellableContinuationWithOwner;->resume(Lkotlin/Unit;Lkotlin/jvm/functions/Function3;)V HSPLkotlinx/coroutines/sync/MutexImpl$CancellableContinuationWithOwner;->tryResume(Ljava/lang/Object;Ljava/lang/Object;Lkotlin/jvm/functions/Function3;)Ljava/lang/Object; HSPLkotlinx/coroutines/sync/MutexImpl$CancellableContinuationWithOwner;->tryResume(Lkotlin/Unit;Ljava/lang/Object;Lkotlin/jvm/functions/Function3;)Ljava/lang/Object; @@ -35147,7 +35412,7 @@ HSPLokhttp3/Address;->(Ljava/lang/String;ILokhttp3/Dns;Ljavax/net/SocketFa HSPLokhttp3/Address;->certificatePinner()Lokhttp3/CertificatePinner; HSPLokhttp3/Address;->connectionSpecs()Ljava/util/List; HSPLokhttp3/Address;->dns()Lokhttp3/Dns; -PLokhttp3/Address;->equalsNonHost$okhttp(Lokhttp3/Address;)Z +HSPLokhttp3/Address;->equalsNonHost$okhttp(Lokhttp3/Address;)Z HSPLokhttp3/Address;->hashCode()I HSPLokhttp3/Address;->hostnameVerifier()Ljavax/net/ssl/HostnameVerifier; HSPLokhttp3/Address;->protocols()Ljava/util/List; @@ -35195,7 +35460,7 @@ HSPLokhttp3/CertificatePinner;->()V HSPLokhttp3/CertificatePinner;->(Ljava/util/Set;Lokhttp3/internal/tls/CertificateChainCleaner;)V HSPLokhttp3/CertificatePinner;->(Ljava/util/Set;Lokhttp3/internal/tls/CertificateChainCleaner;ILkotlin/jvm/internal/DefaultConstructorMarker;)V HSPLokhttp3/CertificatePinner;->check$okhttp(Ljava/lang/String;Lkotlin/jvm/functions/Function0;)V -PLokhttp3/CertificatePinner;->equals(Ljava/lang/Object;)Z +HSPLokhttp3/CertificatePinner;->equals(Ljava/lang/Object;)Z HSPLokhttp3/CertificatePinner;->findMatchingPins(Ljava/lang/String;)Ljava/util/List; HSPLokhttp3/CertificatePinner;->hashCode()I HSPLokhttp3/CertificatePinner;->withCertificateChainCleaner$okhttp(Lokhttp3/internal/tls/CertificateChainCleaner;)Lokhttp3/CertificatePinner; @@ -35277,12 +35542,17 @@ HSPLokhttp3/Dispatcher$promoteAndExecute$Effects;->getCallsToExecute()Ljava/util HSPLokhttp3/Dispatcher$promoteAndExecute$Effects;->getIdleCallbackToRun()Ljava/lang/Runnable; Lokhttp3/Dns; HSPLokhttp3/Dns;->()V +HSPLokhttp3/Dns;->newCall(Lokhttp3/Dns$Request;)Lokhttp3/Dns$Call; +Lokhttp3/Dns$Call; Lokhttp3/Dns$Companion; HSPLokhttp3/Dns$Companion;->()V HSPLokhttp3/Dns$Companion;->()V -Lokhttp3/Dns$Companion$DnsSystem; -HSPLokhttp3/Dns$Companion$DnsSystem;->()V -HSPLokhttp3/Dns$Companion$DnsSystem;->lookup(Ljava/lang/String;)Ljava/util/List; +Lokhttp3/Dns$Companion$SYSTEM$1; +HSPLokhttp3/Dns$Companion$SYSTEM$1;->()V +HSPLokhttp3/Dns$Companion$SYSTEM$1;->lookup(Ljava/lang/String;)Ljava/util/List; +Lokhttp3/Dns$Request; +HSPLokhttp3/Dns$Request;->(Ljava/lang/String;I)V +HSPLokhttp3/Dns$Request;->hostname()Ljava/lang/String; Lokhttp3/EventListener; HSPLokhttp3/EventListener;->()V HSPLokhttp3/EventListener;->()V @@ -35400,7 +35670,6 @@ HSPLokhttp3/HttpUrl$Builder;->username(Ljava/lang/String;)Lokhttp3/HttpUrl$Build Lokhttp3/HttpUrl$Companion; HSPLokhttp3/HttpUrl$Companion;->()V HSPLokhttp3/HttpUrl$Companion;->(Lkotlin/jvm/internal/DefaultConstructorMarker;)V -HSPLokhttp3/HttpUrl$Companion;->access$toQueryString(Lokhttp3/HttpUrl$Companion;Ljava/util/List;Ljava/lang/StringBuilder;)V HSPLokhttp3/HttpUrl$Companion;->defaultPort(Ljava/lang/String;)I HSPLokhttp3/HttpUrl$Companion;->get(Ljava/lang/String;)Lokhttp3/HttpUrl; HSPLokhttp3/HttpUrl$Companion;->toQueryString(Ljava/util/List;Ljava/lang/StringBuilder;)V @@ -35423,8 +35692,6 @@ HSPLokhttp3/OkHttpClient;->()V HSPLokhttp3/OkHttpClient;->(Lokhttp3/OkHttpClient$Builder;)V HSPLokhttp3/OkHttpClient;->access$getDEFAULT_CONNECTION_SPECS$cp()Ljava/util/List; HSPLokhttp3/OkHttpClient;->access$getDEFAULT_PROTOCOLS$cp()Ljava/util/List; -HSPLokhttp3/OkHttpClient;->access$getSslSocketFactoryOrNull$p(Lokhttp3/OkHttpClient;)Ljavax/net/ssl/SSLSocketFactory; -HSPLokhttp3/OkHttpClient;->address(Lokhttp3/HttpUrl;)Lokhttp3/Address; HSPLokhttp3/OkHttpClient;->authenticator()Lokhttp3/Authenticator; HSPLokhttp3/OkHttpClient;->cache()Lokhttp3/Cache; HSPLokhttp3/OkHttpClient;->callTimeoutMillis()I @@ -35441,6 +35708,7 @@ HSPLokhttp3/OkHttpClient;->fastFallback()Z HSPLokhttp3/OkHttpClient;->followRedirects()Z HSPLokhttp3/OkHttpClient;->followSslRedirects()Z HSPLokhttp3/OkHttpClient;->getRouteDatabase$okhttp()Lokhttp3/internal/connection/RouteDatabase; +HSPLokhttp3/OkHttpClient;->getSslSocketFactoryOrNull$okhttp()Ljavax/net/ssl/SSLSocketFactory; HSPLokhttp3/OkHttpClient;->getTaskRunner$okhttp()Lokhttp3/internal/concurrent/TaskRunner; HSPLokhttp3/OkHttpClient;->hostnameVerifier()Ljavax/net/ssl/HostnameVerifier; HSPLokhttp3/OkHttpClient;->interceptors()Ljava/util/List; @@ -35456,7 +35724,6 @@ HSPLokhttp3/OkHttpClient;->proxySelector()Ljava/net/ProxySelector; HSPLokhttp3/OkHttpClient;->readTimeoutMillis()I HSPLokhttp3/OkHttpClient;->retryOnConnectionFailure()Z HSPLokhttp3/OkHttpClient;->socketFactory()Ljavax/net/SocketFactory; -HSPLokhttp3/OkHttpClient;->sslSocketFactory()Ljavax/net/ssl/SSLSocketFactory; HSPLokhttp3/OkHttpClient;->verifyClientState()V HSPLokhttp3/OkHttpClient;->webSocketCloseTimeout()I HSPLokhttp3/OkHttpClient;->writeTimeoutMillis()I @@ -35620,7 +35887,9 @@ Lokhttp3/ResponseBody$Companion$asResponseBody$1; HSPLokhttp3/ResponseBody$Companion$asResponseBody$1;->(Lokhttp3/MediaType;JLokio/BufferedSource;)V Lokhttp3/Route; HSPLokhttp3/Route;->(Lokhttp3/Address;Ljava/net/Proxy;Ljava/net/InetSocketAddress;)V +HSPLokhttp3/Route;->(Lokhttp3/Address;Ljava/net/Proxy;Ljava/net/InetSocketAddress;Lokio/ByteString;)V HSPLokhttp3/Route;->address()Lokhttp3/Address; +HSPLokhttp3/Route;->echConfigList()Lokio/ByteString; HSPLokhttp3/Route;->hashCode()I HSPLokhttp3/Route;->proxy()Ljava/net/Proxy; HSPLokhttp3/Route;->requiresTunnel()Z @@ -35732,7 +36001,7 @@ HSPLokhttp3/internal/authenticator/JavaNetAuthenticator;->(Lokhttp3/Dns;)V HSPLokhttp3/internal/authenticator/JavaNetAuthenticator;->(Lokhttp3/Dns;ILkotlin/jvm/internal/DefaultConstructorMarker;)V Lokhttp3/internal/cache/CacheInterceptor; HSPLokhttp3/internal/cache/CacheInterceptor;->()V -HSPLokhttp3/internal/cache/CacheInterceptor;->(Lokhttp3/Cache;)V +HSPLokhttp3/internal/cache/CacheInterceptor;->()V HSPLokhttp3/internal/cache/CacheInterceptor;->intercept(Lokhttp3/Interceptor$Chain;)Lokhttp3/Response; Lokhttp3/internal/cache/CacheInterceptor$Companion; HSPLokhttp3/internal/cache/CacheInterceptor$Companion;->()V @@ -35817,19 +36086,22 @@ HSPLokhttp3/internal/connection/ConnectInterceptor;->()V HSPLokhttp3/internal/connection/ConnectInterceptor;->intercept(Lokhttp3/Interceptor$Chain;)Lokhttp3/Response; Lokhttp3/internal/connection/ConnectPlan; HSPLokhttp3/internal/connection/ConnectPlan;->()V -HSPLokhttp3/internal/connection/ConnectPlan;->(Lokhttp3/internal/concurrent/TaskRunner;Lokhttp3/internal/connection/RealConnectionPool;IIIIIZLokhttp3/internal/connection/RealCall;Lokhttp3/internal/connection/RealRoutePlanner;Lokhttp3/Route;Ljava/util/List;ILokhttp3/Request;IZ)V +HSPLokhttp3/internal/connection/ConnectPlan;->(Lokhttp3/internal/concurrent/TaskRunner;Lokhttp3/internal/connection/RealConnectionPool;IIIIIZLokhttp3/internal/connection/RealCall;Lokhttp3/internal/connection/RealRoutePlanner;Lokhttp3/Route;Ljava/util/List;ILokhttp3/Request;IZLokhttp3/internal/ech/EchRetryPlan;)V +HSPLokhttp3/internal/connection/ConnectPlan;->(Lokhttp3/internal/concurrent/TaskRunner;Lokhttp3/internal/connection/RealConnectionPool;IIIIIZLokhttp3/internal/connection/RealCall;Lokhttp3/internal/connection/RealRoutePlanner;Lokhttp3/Route;Ljava/util/List;ILokhttp3/Request;IZLokhttp3/internal/ech/EchRetryPlan;ILkotlin/jvm/internal/DefaultConstructorMarker;)V +PLokhttp3/internal/connection/ConnectPlan;->cancel()V HSPLokhttp3/internal/connection/ConnectPlan;->connectSocket()V HSPLokhttp3/internal/connection/ConnectPlan;->connectTcp()Lokhttp3/internal/connection/RoutePlanner$ConnectResult; HSPLokhttp3/internal/connection/ConnectPlan;->connectTls(Ljavax/net/ssl/SSLSocket;Lokhttp3/ConnectionSpec;)V HSPLokhttp3/internal/connection/ConnectPlan;->connectTlsEtc()Lokhttp3/internal/connection/RoutePlanner$ConnectResult; -HSPLokhttp3/internal/connection/ConnectPlan;->copy$default(Lokhttp3/internal/connection/ConnectPlan;ILokhttp3/Request;IZILjava/lang/Object;)Lokhttp3/internal/connection/ConnectPlan; -HSPLokhttp3/internal/connection/ConnectPlan;->copy(ILokhttp3/Request;IZ)Lokhttp3/internal/connection/ConnectPlan; +HSPLokhttp3/internal/connection/ConnectPlan;->copy$default(Lokhttp3/internal/connection/ConnectPlan;Lokhttp3/Route;ILokhttp3/Request;IZLokhttp3/internal/ech/EchRetryPlan;ILjava/lang/Object;)Lokhttp3/internal/connection/ConnectPlan; +HSPLokhttp3/internal/connection/ConnectPlan;->copy(Lokhttp3/Route;ILokhttp3/Request;IZLokhttp3/internal/ech/EchRetryPlan;)Lokhttp3/internal/connection/ConnectPlan; HSPLokhttp3/internal/connection/ConnectPlan;->getRoute()Lokhttp3/Route; HSPLokhttp3/internal/connection/ConnectPlan;->getRoutes$okhttp()Ljava/util/List; HSPLokhttp3/internal/connection/ConnectPlan;->handleSuccess()Lokhttp3/internal/connection/RealConnection; HSPLokhttp3/internal/connection/ConnectPlan;->isReady()Z -HSPLokhttp3/internal/connection/ConnectPlan;->nextConnectionSpec$okhttp(Ljava/util/List;Ljavax/net/ssl/SSLSocket;)Lokhttp3/internal/connection/ConnectPlan; +HSPLokhttp3/internal/connection/ConnectPlan;->nextCompatibleConnectionSpec(Ljava/util/List;Ljavax/net/ssl/SSLSocket;)Lokhttp3/internal/connection/ConnectPlan; HSPLokhttp3/internal/connection/ConnectPlan;->planWithCurrentOrInitialConnectionSpec$okhttp(Ljava/util/List;Ljavax/net/ssl/SSLSocket;)Lokhttp3/internal/connection/ConnectPlan; +PLokhttp3/internal/connection/ConnectPlan;->retry()Lokhttp3/internal/connection/RoutePlanner$Plan; Lokhttp3/internal/connection/ConnectPlan$$ExternalSyntheticLambda0; HSPLokhttp3/internal/connection/ConnectPlan$$ExternalSyntheticLambda0;->(Lokhttp3/CertificatePinner;Lokhttp3/Handshake;Lokhttp3/Address;)V Lokhttp3/internal/connection/ConnectPlan$$ExternalSyntheticLambda1; @@ -35855,14 +36127,13 @@ HSPLokhttp3/internal/connection/ConnectionListener$Companion;->getNONE()Lokhttp3 Lokhttp3/internal/connection/ConnectionListener$Companion$NONE$1; HSPLokhttp3/internal/connection/ConnectionListener$Companion$NONE$1;->()V Lokhttp3/internal/connection/Exchange; -HSPLokhttp3/internal/connection/Exchange;->(Lokhttp3/internal/connection/RealCall;Lokhttp3/EventListener;Lokhttp3/internal/connection/ExchangeFinder;Lokhttp3/internal/http/ExchangeCodec;)V +HSPLokhttp3/internal/connection/Exchange;->(Lokhttp3/internal/connection/RealCall;Lokhttp3/internal/connection/ExchangeFinder;Lokhttp3/internal/http/ExchangeCodec;)V HSPLokhttp3/internal/connection/Exchange;->bodyComplete$default(Lokhttp3/internal/connection/Exchange;JZZZLjava/io/IOException;ILjava/lang/Object;)Ljava/io/IOException; HSPLokhttp3/internal/connection/Exchange;->bodyComplete(JZZZLjava/io/IOException;)Ljava/io/IOException; HSPLokhttp3/internal/connection/Exchange;->createRequestBody(Lokhttp3/Request;Z)Lokio/Sink; HSPLokhttp3/internal/connection/Exchange;->finishRequest()V HSPLokhttp3/internal/connection/Exchange;->getCall$okhttp()Lokhttp3/internal/connection/RealCall; HSPLokhttp3/internal/connection/Exchange;->getConnection$okhttp()Lokhttp3/internal/connection/RealConnection; -HSPLokhttp3/internal/connection/Exchange;->getEventListener$okhttp()Lokhttp3/EventListener; HSPLokhttp3/internal/connection/Exchange;->getFinder$okhttp()Lokhttp3/internal/connection/ExchangeFinder; HSPLokhttp3/internal/connection/Exchange;->isDuplex$okhttp()Z HSPLokhttp3/internal/connection/Exchange;->openResponseBody(Lokhttp3/Response;)Lokhttp3/ResponseBody; @@ -35896,6 +36167,7 @@ HSPLokhttp3/internal/connection/FastFallbackExchangeFinder$launchTcpConnect$1;-> Lokhttp3/internal/connection/InetAddressOrderKt; HSPLokhttp3/internal/connection/InetAddressOrderKt;->reorderForHappyEyeballs(Ljava/util/List;)Ljava/util/List; Lokhttp3/internal/connection/RealCall; +HSPLokhttp3/internal/connection/RealCall;->()V HSPLokhttp3/internal/connection/RealCall;->(Lokhttp3/OkHttpClient;Lokhttp3/Request;Z)V HSPLokhttp3/internal/connection/RealCall;->acquireConnectionNoEvents(Lokhttp3/internal/connection/RealConnection;)V HSPLokhttp3/internal/connection/RealCall;->callDone(Ljava/io/IOException;)Ljava/io/IOException; @@ -35917,6 +36189,9 @@ HSPLokhttp3/internal/connection/RealCall;->releaseConnectionNoEvents$okhttp()Lja HSPLokhttp3/internal/connection/RealCall;->timeoutExit(Ljava/io/IOException;)Ljava/io/IOException; Lokhttp3/internal/connection/RealCall$CallReference; HSPLokhttp3/internal/connection/RealCall$CallReference;->(Lokhttp3/internal/connection/RealCall;Ljava/lang/Object;)V +Lokhttp3/internal/connection/RealCall$Companion; +HSPLokhttp3/internal/connection/RealCall$Companion;->()V +HSPLokhttp3/internal/connection/RealCall$Companion;->(Lkotlin/jvm/internal/DefaultConstructorMarker;)V Lokhttp3/internal/connection/RealCall$timeout$1; HSPLokhttp3/internal/connection/RealCall$timeout$1;->(Lokhttp3/internal/connection/RealCall;)V Lokhttp3/internal/connection/RealConnection; @@ -35929,8 +36204,8 @@ HSPLokhttp3/internal/connection/RealConnection;->getNoNewExchanges()Z HSPLokhttp3/internal/connection/RealConnection;->getRoute()Lokhttp3/Route; HSPLokhttp3/internal/connection/RealConnection;->handshake()Lokhttp3/Handshake; HSPLokhttp3/internal/connection/RealConnection;->incrementSuccessCount$okhttp()V -PLokhttp3/internal/connection/RealConnection;->isEligible$okhttp(Lokhttp3/Address;Ljava/util/List;)Z -PLokhttp3/internal/connection/RealConnection;->isHealthy(Z)Z +HSPLokhttp3/internal/connection/RealConnection;->isEligible$okhttp(Lokhttp3/Address;Ljava/util/List;)Z +HSPLokhttp3/internal/connection/RealConnection;->isHealthy(Z)Z HSPLokhttp3/internal/connection/RealConnection;->newCodec$okhttp(Lokhttp3/OkHttpClient;Lokhttp3/internal/http/RealInterceptorChain;)Lokhttp3/internal/http/ExchangeCodec; HSPLokhttp3/internal/connection/RealConnection;->onSettings(Lokhttp3/internal/http2/Http2Connection;Lokhttp3/internal/http2/Settings;)V HSPLokhttp3/internal/connection/RealConnection;->route()Lokhttp3/Route; @@ -35970,9 +36245,12 @@ HSPLokhttp3/internal/connection/RealRoutePlanner;->planReuseCallConnection()Lokh HSPLokhttp3/internal/connection/RealRoutePlanner;->planReusePooledConnection$okhttp$default(Lokhttp3/internal/connection/RealRoutePlanner;Lokhttp3/internal/connection/ConnectPlan;Ljava/util/List;ILjava/lang/Object;)Lokhttp3/internal/connection/ReusePlan; HSPLokhttp3/internal/connection/RealRoutePlanner;->planReusePooledConnection$okhttp(Lokhttp3/internal/connection/ConnectPlan;Ljava/util/List;)Lokhttp3/internal/connection/ReusePlan; HSPLokhttp3/internal/connection/RealRoutePlanner;->sameHostAndPort(Lokhttp3/HttpUrl;)Z -PLokhttp3/internal/connection/ReusePlan;->(Lokhttp3/internal/connection/RealConnection;)V -PLokhttp3/internal/connection/ReusePlan;->handleSuccess()Lokhttp3/internal/connection/RealConnection; -PLokhttp3/internal/connection/ReusePlan;->isReady()Z +Lokhttp3/internal/connection/RetryTlsHandshakeKt; +HSPLokhttp3/internal/connection/RetryTlsHandshakeKt;->attemptAnotherConnection(Ljava/io/IOException;)Z +Lokhttp3/internal/connection/ReusePlan; +HSPLokhttp3/internal/connection/ReusePlan;->(Lokhttp3/internal/connection/RealConnection;)V +HSPLokhttp3/internal/connection/ReusePlan;->handleSuccess()Lokhttp3/internal/connection/RealConnection; +HSPLokhttp3/internal/connection/ReusePlan;->isReady()Z Lokhttp3/internal/connection/RouteDatabase; HSPLokhttp3/internal/connection/RouteDatabase;->()V HSPLokhttp3/internal/connection/RouteDatabase;->connected(Lokhttp3/Route;)V @@ -35990,11 +36268,12 @@ Lokhttp3/internal/connection/RoutePlanner$Plan; Lokhttp3/internal/connection/RouteSelector; HSPLokhttp3/internal/connection/RouteSelector;->()V HSPLokhttp3/internal/connection/RouteSelector;->(Lokhttp3/Address;Lokhttp3/internal/connection/RouteDatabase;Lokhttp3/internal/connection/RealCall;Z)V +HSPLokhttp3/internal/connection/RouteSelector;->dnsLookup(Ljava/net/Proxy;Ljava/lang/String;I)Ljava/util/List; HSPLokhttp3/internal/connection/RouteSelector;->hasNext()Z HSPLokhttp3/internal/connection/RouteSelector;->hasNextProxy()Z HSPLokhttp3/internal/connection/RouteSelector;->next()Lokhttp3/internal/connection/RouteSelector$Selection; -HSPLokhttp3/internal/connection/RouteSelector;->nextProxy()Ljava/net/Proxy; -HSPLokhttp3/internal/connection/RouteSelector;->resetNextInetSocketAddress(Ljava/net/Proxy;)V +HSPLokhttp3/internal/connection/RouteSelector;->nextProxy()Ljava/util/List; +HSPLokhttp3/internal/connection/RouteSelector;->nextRoutes(Ljava/net/Proxy;)Ljava/util/List; HSPLokhttp3/internal/connection/RouteSelector;->resetNextProxy$selectProxies(Ljava/net/Proxy;Lokhttp3/HttpUrl;Lokhttp3/internal/connection/RouteSelector;)Ljava/util/List; HSPLokhttp3/internal/connection/RouteSelector;->resetNextProxy(Lokhttp3/HttpUrl;Ljava/net/Proxy;)V Lokhttp3/internal/connection/RouteSelector$Companion; @@ -36005,8 +36284,20 @@ HSPLokhttp3/internal/connection/RouteSelector$Selection;->(Ljava/util/List HSPLokhttp3/internal/connection/RouteSelector$Selection;->getRoutes()Ljava/util/List; HSPLokhttp3/internal/connection/RouteSelector$Selection;->hasNext()Z HSPLokhttp3/internal/connection/RouteSelector$Selection;->next()Lokhttp3/Route; +Lokhttp3/internal/dns/InetAddressDns; +HSPLokhttp3/internal/dns/InetAddressDns;->()V +HSPLokhttp3/internal/dns/InetAddressDns;->()V +HSPLokhttp3/internal/dns/InetAddressDns;->lookup(Ljava/lang/String;)Ljava/util/List; +Lokhttp3/internal/dns/LookupDnsCall; +HSPLokhttp3/internal/dns/LookupDnsCall;->(Lokhttp3/internal/concurrent/TaskRunner;Lokhttp3/Dns;Lokhttp3/Dns$Request;)V +Lokhttp3/internal/dns/LookupDnsCall$State; +Lokhttp3/internal/dns/LookupDnsCall$State$Idle; +HSPLokhttp3/internal/dns/LookupDnsCall$State$Idle;->()V +HSPLokhttp3/internal/dns/LookupDnsCall$State$Idle;->()V +Lokhttp3/internal/ech/EchRetryPlan; +Lokhttp3/internal/ech/EchUntrustedException; Lokhttp3/internal/http/BridgeInterceptor; -HSPLokhttp3/internal/http/BridgeInterceptor;->(Lokhttp3/CookieJar;)V +HSPLokhttp3/internal/http/BridgeInterceptor;->()V HSPLokhttp3/internal/http/BridgeInterceptor;->intercept(Lokhttp3/Interceptor$Chain;)Lokhttp3/Response; Lokhttp3/internal/http/CallServerInterceptor; HSPLokhttp3/internal/http/CallServerInterceptor;->()V @@ -36026,16 +36317,33 @@ HSPLokhttp3/internal/http/HttpMethod;->()V HSPLokhttp3/internal/http/HttpMethod;->()V HSPLokhttp3/internal/http/HttpMethod;->permitsRequestBody(Ljava/lang/String;)Z Lokhttp3/internal/http/RealInterceptorChain; -HSPLokhttp3/internal/http/RealInterceptorChain;->(Lokhttp3/internal/connection/RealCall;Ljava/util/List;ILokhttp3/internal/connection/Exchange;Lokhttp3/Request;III)V +HSPLokhttp3/internal/http/RealInterceptorChain;->(Lokhttp3/internal/connection/RealCall;Ljava/util/List;ILjava/lang/Void;Lokhttp3/Request;Lokhttp3/OkHttpClient;)V +HSPLokhttp3/internal/http/RealInterceptorChain;->(Lokhttp3/internal/connection/RealCall;Ljava/util/List;ILjava/lang/Void;Lokhttp3/Request;Lokhttp3/OkHttpClient;ILkotlin/jvm/internal/DefaultConstructorMarker;)V +HSPLokhttp3/internal/http/RealInterceptorChain;->(Lokhttp3/internal/connection/RealCall;Ljava/util/List;ILokhttp3/internal/connection/Exchange;Lokhttp3/Request;IIILokhttp3/Authenticator;Lokhttp3/Cache;Lokhttp3/CertificatePinner;Lokhttp3/ConnectionPool;Lokhttp3/CookieJar;Lokhttp3/Dns;Ljavax/net/ssl/HostnameVerifier;Ljava/net/Proxy;Lokhttp3/Authenticator;Ljava/net/ProxySelector;ZLjavax/net/SocketFactory;Ljavax/net/ssl/SSLSocketFactory;Ljavax/net/ssl/X509TrustManager;Lokhttp3/internal/tls/CertificateChainCleaner;)V +HSPLokhttp3/internal/http/RealInterceptorChain;->address(Lokhttp3/HttpUrl;)Lokhttp3/Address; HSPLokhttp3/internal/http/RealInterceptorChain;->call()Lokhttp3/Call; -HSPLokhttp3/internal/http/RealInterceptorChain;->copy$okhttp$default(Lokhttp3/internal/http/RealInterceptorChain;ILokhttp3/internal/connection/Exchange;Lokhttp3/Request;IIIILjava/lang/Object;)Lokhttp3/internal/http/RealInterceptorChain; -HSPLokhttp3/internal/http/RealInterceptorChain;->copy$okhttp(ILokhttp3/internal/connection/Exchange;Lokhttp3/Request;III)Lokhttp3/internal/http/RealInterceptorChain; +HSPLokhttp3/internal/http/RealInterceptorChain;->copy$okhttp$default(Lokhttp3/internal/http/RealInterceptorChain;ILokhttp3/internal/connection/Exchange;Lokhttp3/Request;IIILokhttp3/Authenticator;Lokhttp3/Cache;Lokhttp3/CertificatePinner;Lokhttp3/ConnectionPool;Lokhttp3/CookieJar;Lokhttp3/Dns;Ljavax/net/ssl/HostnameVerifier;Ljava/net/Proxy;Lokhttp3/Authenticator;Ljava/net/ProxySelector;ZLjavax/net/SocketFactory;Ljavax/net/ssl/SSLSocketFactory;Ljavax/net/ssl/X509TrustManager;Lokhttp3/internal/tls/CertificateChainCleaner;ILjava/lang/Object;)Lokhttp3/internal/http/RealInterceptorChain; +HSPLokhttp3/internal/http/RealInterceptorChain;->copy$okhttp(ILokhttp3/internal/connection/Exchange;Lokhttp3/Request;IIILokhttp3/Authenticator;Lokhttp3/Cache;Lokhttp3/CertificatePinner;Lokhttp3/ConnectionPool;Lokhttp3/CookieJar;Lokhttp3/Dns;Ljavax/net/ssl/HostnameVerifier;Ljava/net/Proxy;Lokhttp3/Authenticator;Ljava/net/ProxySelector;ZLjavax/net/SocketFactory;Ljavax/net/ssl/SSLSocketFactory;Ljavax/net/ssl/X509TrustManager;Lokhttp3/internal/tls/CertificateChainCleaner;)Lokhttp3/internal/http/RealInterceptorChain; +HSPLokhttp3/internal/http/RealInterceptorChain;->getAuthenticator()Lokhttp3/Authenticator; +HSPLokhttp3/internal/http/RealInterceptorChain;->getCache()Lokhttp3/Cache; HSPLokhttp3/internal/http/RealInterceptorChain;->getCall$okhttp()Lokhttp3/internal/connection/RealCall; +HSPLokhttp3/internal/http/RealInterceptorChain;->getCertificatePinner()Lokhttp3/CertificatePinner; HSPLokhttp3/internal/http/RealInterceptorChain;->getConnectTimeoutMillis$okhttp()I +HSPLokhttp3/internal/http/RealInterceptorChain;->getConnectionPool()Lokhttp3/ConnectionPool; +HSPLokhttp3/internal/http/RealInterceptorChain;->getCookieJar()Lokhttp3/CookieJar; +HSPLokhttp3/internal/http/RealInterceptorChain;->getDns()Lokhttp3/Dns; HSPLokhttp3/internal/http/RealInterceptorChain;->getExchange$okhttp()Lokhttp3/internal/connection/Exchange; +HSPLokhttp3/internal/http/RealInterceptorChain;->getHostnameVerifier()Ljavax/net/ssl/HostnameVerifier; +HSPLokhttp3/internal/http/RealInterceptorChain;->getProxy()Ljava/net/Proxy; +HSPLokhttp3/internal/http/RealInterceptorChain;->getProxyAuthenticator()Lokhttp3/Authenticator; +HSPLokhttp3/internal/http/RealInterceptorChain;->getProxySelector()Ljava/net/ProxySelector; HSPLokhttp3/internal/http/RealInterceptorChain;->getReadTimeoutMillis$okhttp()I HSPLokhttp3/internal/http/RealInterceptorChain;->getRequest$okhttp()Lokhttp3/Request; +HSPLokhttp3/internal/http/RealInterceptorChain;->getRetryOnConnectionFailure()Z +HSPLokhttp3/internal/http/RealInterceptorChain;->getSocketFactory()Ljavax/net/SocketFactory; +HSPLokhttp3/internal/http/RealInterceptorChain;->getSslSocketFactoryOrNull()Ljavax/net/ssl/SSLSocketFactory; HSPLokhttp3/internal/http/RealInterceptorChain;->getWriteTimeoutMillis$okhttp()I +HSPLokhttp3/internal/http/RealInterceptorChain;->getX509TrustManagerOrNull()Ljavax/net/ssl/X509TrustManager; HSPLokhttp3/internal/http/RealInterceptorChain;->proceed(Lokhttp3/Request;)Lokhttp3/Response; HSPLokhttp3/internal/http/RealInterceptorChain;->request()Lokhttp3/Request; Lokhttp3/internal/http/RealResponseBody; @@ -36049,8 +36357,8 @@ HSPLokhttp3/internal/http/RequestLine;->()V HSPLokhttp3/internal/http/RequestLine;->requestPath(Lokhttp3/HttpUrl;)Ljava/lang/String; Lokhttp3/internal/http/RetryAndFollowUpInterceptor; HSPLokhttp3/internal/http/RetryAndFollowUpInterceptor;->()V -HSPLokhttp3/internal/http/RetryAndFollowUpInterceptor;->(Lokhttp3/OkHttpClient;)V -HSPLokhttp3/internal/http/RetryAndFollowUpInterceptor;->followUpRequest(Lokhttp3/Response;Lokhttp3/internal/connection/Exchange;)Lokhttp3/Request; +HSPLokhttp3/internal/http/RetryAndFollowUpInterceptor;->()V +HSPLokhttp3/internal/http/RetryAndFollowUpInterceptor;->followUpRequest(Lokhttp3/Response;Lokhttp3/internal/connection/Exchange;Lokhttp3/Interceptor$Chain;)Lokhttp3/Request; HSPLokhttp3/internal/http/RetryAndFollowUpInterceptor;->intercept(Lokhttp3/Interceptor$Chain;)Lokhttp3/Response; Lokhttp3/internal/http/RetryAndFollowUpInterceptor$Companion; HSPLokhttp3/internal/http/RetryAndFollowUpInterceptor$Companion;->()V @@ -36066,6 +36374,7 @@ Lokhttp3/internal/http2/ErrorCode; HSPLokhttp3/internal/http2/ErrorCode;->$values()[Lokhttp3/internal/http2/ErrorCode; HSPLokhttp3/internal/http2/ErrorCode;->()V HSPLokhttp3/internal/http2/ErrorCode;->(Ljava/lang/String;II)V +PLokhttp3/internal/http2/ErrorCode;->getHttpCode()I Lokhttp3/internal/http2/ErrorCode$Companion; HSPLokhttp3/internal/http2/ErrorCode$Companion;->()V HSPLokhttp3/internal/http2/ErrorCode$Companion;->(Lkotlin/jvm/internal/DefaultConstructorMarker;)V @@ -36095,7 +36404,8 @@ HSPLokhttp3/internal/http2/Hpack;->nameToFirstIndex()Ljava/util/Map; Lokhttp3/internal/http2/Hpack$Reader; HSPLokhttp3/internal/http2/Hpack$Reader;->(Lokio/Source;II)V HSPLokhttp3/internal/http2/Hpack$Reader;->(Lokio/Source;IIILkotlin/jvm/internal/DefaultConstructorMarker;)V -PLokhttp3/internal/http2/Hpack$Reader;->dynamicTableIndex(I)I +HSPLokhttp3/internal/http2/Hpack$Reader;->addHeader(Lokhttp3/internal/http2/Header;)V +HSPLokhttp3/internal/http2/Hpack$Reader;->dynamicTableIndex(I)I HSPLokhttp3/internal/http2/Hpack$Reader;->evictToRecoverBytes(I)I HSPLokhttp3/internal/http2/Hpack$Reader;->getAndResetHeaderList()Ljava/util/List; HSPLokhttp3/internal/http2/Hpack$Reader;->getName(I)Lokio/ByteString; @@ -36104,7 +36414,7 @@ HSPLokhttp3/internal/http2/Hpack$Reader;->isStaticHeader(I)Z HSPLokhttp3/internal/http2/Hpack$Reader;->readByte()I HSPLokhttp3/internal/http2/Hpack$Reader;->readByteString()Lokio/ByteString; HSPLokhttp3/internal/http2/Hpack$Reader;->readHeaders()V -PLokhttp3/internal/http2/Hpack$Reader;->readIndexedHeader(I)V +HSPLokhttp3/internal/http2/Hpack$Reader;->readIndexedHeader(I)V HSPLokhttp3/internal/http2/Hpack$Reader;->readInt(II)I HSPLokhttp3/internal/http2/Hpack$Reader;->readLiteralHeaderWithIncrementalIndexingIndexedName(I)V HSPLokhttp3/internal/http2/Hpack$Reader;->readLiteralHeaderWithIncrementalIndexingNewName()V @@ -36139,7 +36449,7 @@ HSPLokhttp3/internal/http2/Http2Connection;->getStream(I)Lokhttp3/internal/http2 HSPLokhttp3/internal/http2/Http2Connection;->getStreams$okhttp()Ljava/util/Map; HSPLokhttp3/internal/http2/Http2Connection;->getWriteBytesMaximum()J HSPLokhttp3/internal/http2/Http2Connection;->getWriter()Lokhttp3/internal/http2/Http2Writer; -PLokhttp3/internal/http2/Http2Connection;->isHealthy(J)Z +HSPLokhttp3/internal/http2/Http2Connection;->isHealthy(J)Z HSPLokhttp3/internal/http2/Http2Connection;->newStream(ILjava/util/List;Z)Lokhttp3/internal/http2/Http2Stream; HSPLokhttp3/internal/http2/Http2Connection;->newStream(Ljava/util/List;Z)Lokhttp3/internal/http2/Http2Stream; HSPLokhttp3/internal/http2/Http2Connection;->pushedStream$okhttp(I)Z @@ -36149,6 +36459,7 @@ HSPLokhttp3/internal/http2/Http2Connection;->start$default(Lokhttp3/internal/htt HSPLokhttp3/internal/http2/Http2Connection;->start(Z)V HSPLokhttp3/internal/http2/Http2Connection;->updateConnectionFlowControl$okhttp(J)V HSPLokhttp3/internal/http2/Http2Connection;->writeData(IZLokio/Buffer;J)V +PLokhttp3/internal/http2/Http2Connection;->writeSynReset$okhttp(ILokhttp3/internal/http2/ErrorCode;)V Lokhttp3/internal/http2/Http2Connection$Builder; HSPLokhttp3/internal/http2/Http2Connection$Builder;->(ZLokhttp3/internal/concurrent/TaskRunner;)V HSPLokhttp3/internal/http2/Http2Connection$Builder;->build()Lokhttp3/internal/http2/Http2Connection; @@ -36230,11 +36541,9 @@ HSPLokhttp3/internal/http2/Http2Reader$Companion;->(Lkotlin/jvm/internal/D HSPLokhttp3/internal/http2/Http2Reader$Companion;->lengthWithoutPadding(III)I Lokhttp3/internal/http2/Http2Reader$ContinuationSource; HSPLokhttp3/internal/http2/Http2Reader$ContinuationSource;->(Lokio/BufferedSource;)V -HSPLokhttp3/internal/http2/Http2Reader$ContinuationSource;->getLeft()I HSPLokhttp3/internal/http2/Http2Reader$ContinuationSource;->read(Lokio/Buffer;J)J HSPLokhttp3/internal/http2/Http2Reader$ContinuationSource;->setFlags(I)V HSPLokhttp3/internal/http2/Http2Reader$ContinuationSource;->setLeft(I)V -HSPLokhttp3/internal/http2/Http2Reader$ContinuationSource;->setLength(I)V HSPLokhttp3/internal/http2/Http2Reader$ContinuationSource;->setPadding(I)V HSPLokhttp3/internal/http2/Http2Reader$ContinuationSource;->setStreamId(I)V Lokhttp3/internal/http2/Http2Reader$Handler; @@ -36244,6 +36553,8 @@ HSPLokhttp3/internal/http2/Http2Stream;->(ILokhttp3/internal/http2/Http2Co HSPLokhttp3/internal/http2/Http2Stream;->addBytesToWriteWindow(J)V HSPLokhttp3/internal/http2/Http2Stream;->cancelStreamIfNecessary$okhttp()V HSPLokhttp3/internal/http2/Http2Stream;->checkOutNotClosed$okhttp()V +PLokhttp3/internal/http2/Http2Stream;->close(Lokhttp3/internal/http2/ErrorCode;Ljava/io/IOException;)V +PLokhttp3/internal/http2/Http2Stream;->closeInternal(Lokhttp3/internal/http2/ErrorCode;Ljava/io/IOException;)Z HSPLokhttp3/internal/http2/Http2Stream;->doReadTimeout()Z HSPLokhttp3/internal/http2/Http2Stream;->getConnection()Lokhttp3/internal/http2/Http2Connection; HSPLokhttp3/internal/http2/Http2Stream;->getErrorCode$okhttp()Lokhttp3/internal/http2/ErrorCode; @@ -36296,6 +36607,7 @@ HSPLokhttp3/internal/http2/Http2Writer;->flush()V HSPLokhttp3/internal/http2/Http2Writer;->frameHeader(IIII)V HSPLokhttp3/internal/http2/Http2Writer;->headers(ZILjava/util/List;)V HSPLokhttp3/internal/http2/Http2Writer;->maxDataLength()I +PLokhttp3/internal/http2/Http2Writer;->rstStream(ILokhttp3/internal/http2/ErrorCode;)V HSPLokhttp3/internal/http2/Http2Writer;->settings(Lokhttp3/internal/http2/Settings;)V HSPLokhttp3/internal/http2/Http2Writer;->windowUpdate(IJ)V Lokhttp3/internal/http2/Http2Writer$Companion; @@ -36364,7 +36676,7 @@ HSPLokhttp3/internal/platform/Android10Platform;->()V HSPLokhttp3/internal/platform/Android10Platform;->()V HSPLokhttp3/internal/platform/Android10Platform;->access$isSupported$cp()Z HSPLokhttp3/internal/platform/Android10Platform;->buildCertificateChainCleaner(Ljavax/net/ssl/X509TrustManager;)Lokhttp3/internal/tls/CertificateChainCleaner; -HSPLokhttp3/internal/platform/Android10Platform;->configureTlsExtensions(Ljavax/net/ssl/SSLSocket;Ljava/lang/String;Ljava/util/List;)V +HSPLokhttp3/internal/platform/Android10Platform;->configureTlsExtensions(Ljavax/net/ssl/SSLSocket;Ljava/lang/String;Ljava/util/List;Lokio/ByteString;)V HSPLokhttp3/internal/platform/Android10Platform;->getSelectedProtocol(Ljavax/net/ssl/SSLSocket;)Ljava/lang/String; HSPLokhttp3/internal/platform/Android10Platform;->getStackTraceForCloseable(Ljava/lang/String;)Ljava/lang/Object; HSPLokhttp3/internal/platform/Android10Platform;->newSSLContext()Ljavax/net/ssl/SSLContext; @@ -36381,6 +36693,8 @@ HSPLokhttp3/internal/platform/Platform;->()V HSPLokhttp3/internal/platform/Platform;->access$getPlatform$cp()Lokhttp3/internal/platform/Platform; HSPLokhttp3/internal/platform/Platform;->afterHandshake(Ljavax/net/ssl/SSLSocket;)V HSPLokhttp3/internal/platform/Platform;->connectSocket(Ljava/net/Socket;Ljava/net/InetSocketAddress;I)V +HSPLokhttp3/internal/platform/Platform;->getSocketFactory()Ljavax/net/SocketFactory; +HSPLokhttp3/internal/platform/Platform;->getSystemDns()Lokhttp3/Dns; HSPLokhttp3/internal/platform/Platform;->newSSLContext()Ljavax/net/ssl/SSLContext; HSPLokhttp3/internal/platform/Platform;->newSslSocketFactory(Ljavax/net/ssl/X509TrustManager;)Ljavax/net/ssl/SSLSocketFactory; HSPLokhttp3/internal/platform/Platform;->platformTrustManager()Ljavax/net/ssl/X509TrustManager; @@ -36406,7 +36720,7 @@ HSPLokhttp3/internal/platform/PlatformRegistry;->setApplicationContext(Landroid/ Lokhttp3/internal/platform/android/Android10SocketAdapter; HSPLokhttp3/internal/platform/android/Android10SocketAdapter;->()V HSPLokhttp3/internal/platform/android/Android10SocketAdapter;->()V -HSPLokhttp3/internal/platform/android/Android10SocketAdapter;->configureTlsExtensions(Ljavax/net/ssl/SSLSocket;Ljava/lang/String;Ljava/util/List;)V +HSPLokhttp3/internal/platform/android/Android10SocketAdapter;->configureTlsExtensions(Ljavax/net/ssl/SSLSocket;Ljava/lang/String;Ljava/util/List;Lokio/ByteString;)V HSPLokhttp3/internal/platform/android/Android10SocketAdapter;->getSelectedProtocol(Ljavax/net/ssl/SSLSocket;)Ljava/lang/String; HSPLokhttp3/internal/platform/android/Android10SocketAdapter;->isSupported()Z HSPLokhttp3/internal/platform/android/Android10SocketAdapter;->matchesSocket(Ljavax/net/ssl/SSLSocket;)Z @@ -36415,6 +36729,13 @@ HSPLokhttp3/internal/platform/android/Android10SocketAdapter$Companion;->( HSPLokhttp3/internal/platform/android/Android10SocketAdapter$Companion;->(Lkotlin/jvm/internal/DefaultConstructorMarker;)V HSPLokhttp3/internal/platform/android/Android10SocketAdapter$Companion;->buildIfSupported()Lokhttp3/internal/platform/android/SocketAdapter; HSPLokhttp3/internal/platform/android/Android10SocketAdapter$Companion;->isSupported()Z +Lokhttp3/internal/platform/android/Android17SocketAdapter; +HSPLokhttp3/internal/platform/android/Android17SocketAdapter;->()V +Lokhttp3/internal/platform/android/Android17SocketAdapter$Companion; +HSPLokhttp3/internal/platform/android/Android17SocketAdapter$Companion;->()V +HSPLokhttp3/internal/platform/android/Android17SocketAdapter$Companion;->(Lkotlin/jvm/internal/DefaultConstructorMarker;)V +HSPLokhttp3/internal/platform/android/Android17SocketAdapter$Companion;->buildIfSupported()Lokhttp3/internal/platform/android/SocketAdapter; +HSPLokhttp3/internal/platform/android/Android17SocketAdapter$Companion;->isSupported()Z Lokhttp3/internal/platform/android/AndroidCertificateChainCleaner; HSPLokhttp3/internal/platform/android/AndroidCertificateChainCleaner;->()V HSPLokhttp3/internal/platform/android/AndroidCertificateChainCleaner;->(Ljavax/net/ssl/X509TrustManager;Landroid/net/http/X509TrustManagerExtensions;)V @@ -36673,9 +36994,7 @@ HSPLokio/Path;->isAbsolute()Z HSPLokio/Path;->name()Ljava/lang/String; HSPLokio/Path;->nameBytes()Lokio/ByteString; HSPLokio/Path;->normalized()Lokio/Path; -HSPLokio/Path;->resolve$default(Lokio/Path;Ljava/lang/String;ZILjava/lang/Object;)Lokio/Path; HSPLokio/Path;->resolve(Ljava/lang/String;)Lokio/Path; -HSPLokio/Path;->resolve(Ljava/lang/String;Z)Lokio/Path; HSPLokio/Path;->toFile()Ljava/io/File; HSPLokio/Path;->toNioPath()Ljava/nio/file/Path; HSPLokio/Path;->toString()Ljava/lang/String; @@ -37430,17 +37749,17 @@ HSPLorg/koin/viewmodel/factory/KoinViewModelFactory;->create(Lkotlin/reflect/KCl Lorg/meshtastic/app/ComposableSingletons$MainActivityKt; HSPLorg/meshtastic/app/ComposableSingletons$MainActivityKt;->()V HSPLorg/meshtastic/app/ComposableSingletons$MainActivityKt;->()V +HSPLorg/meshtastic/app/ComposableSingletons$MainActivityKt;->getLambda$-1483891377$androidApp()Lkotlin/jvm/functions/Function4; HSPLorg/meshtastic/app/ComposableSingletons$MainActivityKt;->getLambda$-1568620155$androidApp()Lkotlin/jvm/functions/Function4; -HSPLorg/meshtastic/app/ComposableSingletons$MainActivityKt;->getLambda$-1828719539$androidApp()Lkotlin/jvm/functions/Function4; -HSPLorg/meshtastic/app/ComposableSingletons$MainActivityKt;->getLambda$-604181803$androidApp()Lkotlin/jvm/functions/Function6; -HSPLorg/meshtastic/app/ComposableSingletons$MainActivityKt;->getLambda$-945529377$androidApp()Lkotlin/jvm/functions/Function6; -HSPLorg/meshtastic/app/ComposableSingletons$MainActivityKt;->getLambda$14580037$androidApp()Lkotlin/jvm/functions/Function7; -HSPLorg/meshtastic/app/ComposableSingletons$MainActivityKt;->getLambda$1497467864$androidApp()Lkotlin/jvm/functions/Function2; -HSPLorg/meshtastic/app/ComposableSingletons$MainActivityKt;->getLambda$1502840620$androidApp()Lkotlin/jvm/functions/Function4; +HSPLorg/meshtastic/app/ComposableSingletons$MainActivityKt;->getLambda$-1824458015$androidApp()Lkotlin/jvm/functions/Function2; +HSPLorg/meshtastic/app/ComposableSingletons$MainActivityKt;->getLambda$-259353641$androidApp()Lkotlin/jvm/functions/Function6; +HSPLorg/meshtastic/app/ComposableSingletons$MainActivityKt;->getLambda$-600701215$androidApp()Lkotlin/jvm/functions/Function6; +HSPLorg/meshtastic/app/ComposableSingletons$MainActivityKt;->getLambda$1529737142$androidApp()Lkotlin/jvm/functions/Function6; HSPLorg/meshtastic/app/ComposableSingletons$MainActivityKt;->getLambda$1770700875$androidApp()Lkotlin/jvm/functions/Function5; -HSPLorg/meshtastic/app/ComposableSingletons$MainActivityKt;->getLambda$1859180575$androidApp()Lkotlin/jvm/functions/Function6; -HSPLorg/meshtastic/app/ComposableSingletons$MainActivityKt;->getLambda$502894782$androidApp()Lkotlin/jvm/functions/Function6; -HSPLorg/meshtastic/app/ComposableSingletons$MainActivityKt;->lambda_1497467864$lambda$0(Landroidx/compose/runtime/Composer;I)Lkotlin/Unit; +HSPLorg/meshtastic/app/ComposableSingletons$MainActivityKt;->getLambda$1847668782$androidApp()Lkotlin/jvm/functions/Function4; +HSPLorg/meshtastic/app/ComposableSingletons$MainActivityKt;->getLambda$359408199$androidApp()Lkotlin/jvm/functions/Function7; +HSPLorg/meshtastic/app/ComposableSingletons$MainActivityKt;->getLambda$847722944$androidApp()Lkotlin/jvm/functions/Function6; +HSPLorg/meshtastic/app/ComposableSingletons$MainActivityKt;->lambda__1824458015$lambda$0(Landroidx/compose/runtime/Composer;I)Lkotlin/Unit; Lorg/meshtastic/app/ComposableSingletons$MainActivityKt$$ExternalSyntheticLambda1; HSPLorg/meshtastic/app/ComposableSingletons$MainActivityKt$$ExternalSyntheticLambda1;->()V Lorg/meshtastic/app/ComposableSingletons$MainActivityKt$$ExternalSyntheticLambda10; @@ -37503,31 +37822,33 @@ Lorg/meshtastic/app/MainActivity$$ExternalSyntheticLambda0; HSPLorg/meshtastic/app/MainActivity$$ExternalSyntheticLambda0;->(Z)V HSPLorg/meshtastic/app/MainActivity$$ExternalSyntheticLambda0;->invoke(Ljava/lang/Object;)Ljava/lang/Object; Lorg/meshtastic/app/MainActivity$$ExternalSyntheticLambda1; -HSPLorg/meshtastic/app/MainActivity$$ExternalSyntheticLambda1;->(Lorg/meshtastic/app/MainActivity;)V -HSPLorg/meshtastic/app/MainActivity$$ExternalSyntheticLambda1;->invoke(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object; +HSPLorg/meshtastic/app/MainActivity$$ExternalSyntheticLambda1;->(Lorg/meshtastic/app/MainActivity;Lkotlin/jvm/functions/Function2;I)V Lorg/meshtastic/app/MainActivity$$ExternalSyntheticLambda10; -HSPLorg/meshtastic/app/MainActivity$$ExternalSyntheticLambda10;->()V +HSPLorg/meshtastic/app/MainActivity$$ExternalSyntheticLambda10;->(ZZLorg/meshtastic/app/MainActivity;)V +HSPLorg/meshtastic/app/MainActivity$$ExternalSyntheticLambda10;->invoke(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object; Lorg/meshtastic/app/MainActivity$$ExternalSyntheticLambda11; -HSPLorg/meshtastic/app/MainActivity$$ExternalSyntheticLambda11;->(Lorg/meshtastic/app/MainActivity;Lkotlin/jvm/functions/Function2;I)V -Lorg/meshtastic/app/MainActivity$$ExternalSyntheticLambda3; -HSPLorg/meshtastic/app/MainActivity$$ExternalSyntheticLambda3;->(Z)V -HSPLorg/meshtastic/app/MainActivity$$ExternalSyntheticLambda3;->invoke(Ljava/lang/Object;)Ljava/lang/Object; +HSPLorg/meshtastic/app/MainActivity$$ExternalSyntheticLambda11;->()V +Lorg/meshtastic/app/MainActivity$$ExternalSyntheticLambda12; +HSPLorg/meshtastic/app/MainActivity$$ExternalSyntheticLambda12;->(Lorg/meshtastic/app/MainActivity;)V +Lorg/meshtastic/app/MainActivity$$ExternalSyntheticLambda2; +HSPLorg/meshtastic/app/MainActivity$$ExternalSyntheticLambda2;->(Lorg/meshtastic/app/MainActivity;)V +HSPLorg/meshtastic/app/MainActivity$$ExternalSyntheticLambda2;->invoke(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object; Lorg/meshtastic/app/MainActivity$$ExternalSyntheticLambda4; -HSPLorg/meshtastic/app/MainActivity$$ExternalSyntheticLambda4;->(Lorg/meshtastic/app/MainActivity;)V -HSPLorg/meshtastic/app/MainActivity$$ExternalSyntheticLambda4;->invoke(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object; +HSPLorg/meshtastic/app/MainActivity$$ExternalSyntheticLambda4;->(Z)V +HSPLorg/meshtastic/app/MainActivity$$ExternalSyntheticLambda4;->invoke(Ljava/lang/Object;)Ljava/lang/Object; Lorg/meshtastic/app/MainActivity$$ExternalSyntheticLambda5; HSPLorg/meshtastic/app/MainActivity$$ExternalSyntheticLambda5;->(Lorg/meshtastic/app/MainActivity;)V +HSPLorg/meshtastic/app/MainActivity$$ExternalSyntheticLambda5;->invoke(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object; Lorg/meshtastic/app/MainActivity$$ExternalSyntheticLambda6; -HSPLorg/meshtastic/app/MainActivity$$ExternalSyntheticLambda6;->()V -HSPLorg/meshtastic/app/MainActivity$$ExternalSyntheticLambda6;->invoke()Ljava/lang/Object; +HSPLorg/meshtastic/app/MainActivity$$ExternalSyntheticLambda6;->(Lorg/meshtastic/app/MainActivity;)V Lorg/meshtastic/app/MainActivity$$ExternalSyntheticLambda7; -HSPLorg/meshtastic/app/MainActivity$$ExternalSyntheticLambda7;->(Lorg/meshtastic/app/MainActivity;)V +HSPLorg/meshtastic/app/MainActivity$$ExternalSyntheticLambda7;->()V +HSPLorg/meshtastic/app/MainActivity$$ExternalSyntheticLambda7;->invoke()Ljava/lang/Object; Lorg/meshtastic/app/MainActivity$$ExternalSyntheticLambda8; -HSPLorg/meshtastic/app/MainActivity$$ExternalSyntheticLambda8;->(Lorg/meshtastic/app/MainActivity;Z)V -HSPLorg/meshtastic/app/MainActivity$$ExternalSyntheticLambda8;->invoke()Ljava/lang/Object; +HSPLorg/meshtastic/app/MainActivity$$ExternalSyntheticLambda8;->(Lorg/meshtastic/app/MainActivity;)V Lorg/meshtastic/app/MainActivity$$ExternalSyntheticLambda9; -HSPLorg/meshtastic/app/MainActivity$$ExternalSyntheticLambda9;->(ZZLorg/meshtastic/app/MainActivity;)V -HSPLorg/meshtastic/app/MainActivity$$ExternalSyntheticLambda9;->invoke(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object; +HSPLorg/meshtastic/app/MainActivity$$ExternalSyntheticLambda9;->(Lorg/meshtastic/app/MainActivity;Z)V +HSPLorg/meshtastic/app/MainActivity$$ExternalSyntheticLambda9;->invoke()Ljava/lang/Object; Lorg/meshtastic/app/MainActivity$AppCompositionLocals$1$1; HSPLorg/meshtastic/app/MainActivity$AppCompositionLocals$1$1;->(Ljava/lang/Object;)V Lorg/meshtastic/app/MainActivity$Companion; @@ -37599,6 +37920,8 @@ Lorg/meshtastic/app/OrgMeshtasticAppMainKoinModuleModuleKt$$ExternalSyntheticLam HSPLorg/meshtastic/app/OrgMeshtasticAppMainKoinModuleModuleKt$$ExternalSyntheticLambda5;->()V HSPLorg/meshtastic/app/OrgMeshtasticAppMainKoinModuleModuleKt$$ExternalSyntheticLambda5;->invoke(Ljava/lang/Object;)Ljava/lang/Object; Lorg/meshtastic/app/R$array; +Lorg/meshtastic/app/ai/GeminiNanoDocAssistant$isSupported$4$$ExternalSyntheticBackport0; +HSPLorg/meshtastic/app/ai/GeminiNanoDocAssistant$isSupported$4$$ExternalSyntheticBackport0;->m(II)I Lorg/meshtastic/app/ai/appfunctions/AppFunctionStateSync; HSPLorg/meshtastic/app/ai/appfunctions/AppFunctionStateSync;->()V HSPLorg/meshtastic/app/ai/appfunctions/AppFunctionStateSync;->(Landroid/content/Context;Lorg/meshtastic/core/repository/AppFunctionsPrefs;Lorg/meshtastic/core/di/CoroutineDispatchers;)V @@ -38014,12 +38337,12 @@ HSPLorg/meshtastic/core/common/di/ApplicationCoroutineScopeImpl;->()V Lorg/meshtastic/core/common/di/CoreCommonModule; HSPLorg/meshtastic/core/common/di/CoreCommonModule;->()V Lorg/meshtastic/core/common/di/OrgMeshtasticCoreCommonDiCoreCommonModuleModuleKt; -HSPLorg/meshtastic/core/common/di/OrgMeshtasticCoreCommonDiCoreCommonModuleModuleKt;->$r8$lambda$I-DGDXtqd874HfFM-Np3E4PIzKA(Lorg/koin/core/scope/Scope;Lorg/koin/core/parameter/ParametersHolder;)Lorg/meshtastic/core/common/state/HiddenFeaturesUnlock; HSPLorg/meshtastic/core/common/di/OrgMeshtasticCoreCommonDiCoreCommonModuleModuleKt;->$r8$lambda$K7WC1tfKRtoYO3seEKXDs4MV_Yg(Lorg/koin/core/scope/Scope;Lorg/koin/core/parameter/ParametersHolder;)Lorg/meshtastic/core/common/di/ApplicationCoroutineScopeImpl; -HSPLorg/meshtastic/core/common/di/OrgMeshtasticCoreCommonDiCoreCommonModuleModuleKt;->$r8$lambda$nIdr_JMJBZNDHGj2KXiSaUAHV2c(Lorg/koin/core/scope/Scope;Lorg/koin/core/parameter/ParametersHolder;)Lorg/meshtastic/core/common/state/FirmwareMaintenanceLock; +HSPLorg/meshtastic/core/common/di/OrgMeshtasticCoreCommonDiCoreCommonModuleModuleKt;->$r8$lambda$M6QacCv6WohZ21c25lQMM2z6gGk(Lorg/koin/core/scope/Scope;Lorg/koin/core/parameter/ParametersHolder;)Lorg/meshtastic/core/common/state/RadioOperationLock; +HSPLorg/meshtastic/core/common/di/OrgMeshtasticCoreCommonDiCoreCommonModuleModuleKt;->$r8$lambda$qCq5yc3dzvILOC-TT4uNIwDh2jw(Lorg/koin/core/scope/Scope;Lorg/koin/core/parameter/ParametersHolder;)Lorg/meshtastic/core/common/state/HiddenFeaturesUnlock; HSPLorg/meshtastic/core/common/di/OrgMeshtasticCoreCommonDiCoreCommonModuleModuleKt;->module$lambda$0$3(Lorg/koin/core/scope/Scope;Lorg/koin/core/parameter/ParametersHolder;)Lorg/meshtastic/core/common/di/ApplicationCoroutineScopeImpl; -HSPLorg/meshtastic/core/common/di/OrgMeshtasticCoreCommonDiCoreCommonModuleModuleKt;->module$lambda$0$4(Lorg/koin/core/scope/Scope;Lorg/koin/core/parameter/ParametersHolder;)Lorg/meshtastic/core/common/state/FirmwareMaintenanceLock; -HSPLorg/meshtastic/core/common/di/OrgMeshtasticCoreCommonDiCoreCommonModuleModuleKt;->module$lambda$0$5(Lorg/koin/core/scope/Scope;Lorg/koin/core/parameter/ParametersHolder;)Lorg/meshtastic/core/common/state/HiddenFeaturesUnlock; +HSPLorg/meshtastic/core/common/di/OrgMeshtasticCoreCommonDiCoreCommonModuleModuleKt;->module$lambda$0$4(Lorg/koin/core/scope/Scope;Lorg/koin/core/parameter/ParametersHolder;)Lorg/meshtastic/core/common/state/HiddenFeaturesUnlock; +HSPLorg/meshtastic/core/common/di/OrgMeshtasticCoreCommonDiCoreCommonModuleModuleKt;->module$lambda$0$5(Lorg/koin/core/scope/Scope;Lorg/koin/core/parameter/ParametersHolder;)Lorg/meshtastic/core/common/state/RadioOperationLock; HSPLorg/meshtastic/core/common/di/OrgMeshtasticCoreCommonDiCoreCommonModuleModuleKt;->module$lambda$0(Lorg/koin/core/module/Module;)Lkotlin/Unit; HSPLorg/meshtastic/core/common/di/OrgMeshtasticCoreCommonDiCoreCommonModuleModuleKt;->module(Lorg/meshtastic/core/common/di/CoreCommonModule;)Lorg/koin/core/module/Module; Lorg/meshtastic/core/common/di/OrgMeshtasticCoreCommonDiCoreCommonModuleModuleKt$$ExternalSyntheticLambda0; @@ -38050,11 +38373,14 @@ Lorg/meshtastic/core/common/log/ExpectedCondition; Lorg/meshtastic/core/common/log/ExpectedConditionKt; HSPLorg/meshtastic/core/common/log/ExpectedConditionKt;->shouldDowngradeForDatadog(Lco/touchlab/kermit/Severity;Ljava/lang/Throwable;)Z HSPLorg/meshtastic/core/common/log/ExpectedConditionKt;->shouldReportAsException(Lco/touchlab/kermit/Severity;Ljava/lang/Throwable;)Z -Lorg/meshtastic/core/common/state/FirmwareMaintenanceLock; -HSPLorg/meshtastic/core/common/state/FirmwareMaintenanceLock;->()V Lorg/meshtastic/core/common/state/HiddenFeaturesUnlock; HSPLorg/meshtastic/core/common/state/HiddenFeaturesUnlock;->()V HSPLorg/meshtastic/core/common/state/HiddenFeaturesUnlock;->getUnlocked()Lkotlinx/coroutines/flow/StateFlow; +Lorg/meshtastic/core/common/state/RadioOperationLock; +HSPLorg/meshtastic/core/common/state/RadioOperationLock;->()V +HSPLorg/meshtastic/core/common/state/RadioOperationLock;->getActiveOperations()Ljava/util/Set; +HSPLorg/meshtastic/core/common/state/RadioOperationLock;->getHolders()Lkotlinx/coroutines/flow/StateFlow; +PLorg/meshtastic/core/common/state/RadioOperationLock;->isActive()Z Lorg/meshtastic/core/common/util/AddressUtilsKt; HSPLorg/meshtastic/core/common/util/AddressUtilsKt;->isNoDeviceSentinel(Ljava/lang/String;)Z HSPLorg/meshtastic/core/common/util/AddressUtilsKt;->isValidDeviceAddress(Ljava/lang/String;)Z @@ -38605,6 +38931,8 @@ HSPLorg/meshtastic/core/data/manager/MeshMessageProcessorImpl$Companion;-> Lorg/meshtastic/core/data/manager/MessageFilterImpl; Lorg/meshtastic/core/data/manager/MqttManagerImpl; HSPLorg/meshtastic/core/data/manager/MqttManagerImpl;->(Lorg/meshtastic/core/network/repository/MQTTRepository;Lorg/meshtastic/core/repository/PacketHandler;Lorg/meshtastic/core/repository/ServiceStateWriter;Lorg/meshtastic/core/repository/NodeRepository;Lorg/meshtastic/core/common/di/ServiceScope;)V +Lorg/meshtastic/core/data/manager/MqttManagerImpl$$ExternalSyntheticTypeSwitchClass1; +HSPLorg/meshtastic/core/data/manager/MqttManagerImpl$$ExternalSyntheticTypeSwitchClass1;->switchDispatch$1(Ljava/lang/Object;I)I Lorg/meshtastic/core/data/manager/MqttManagerImpl$mqttConnectionState$1; HSPLorg/meshtastic/core/data/manager/MqttManagerImpl$mqttConnectionState$1;->(Lorg/meshtastic/core/data/manager/MqttManagerImpl;Lkotlin/coroutines/Continuation;)V HSPLorg/meshtastic/core/data/manager/MqttManagerImpl$mqttConnectionState$1;->invoke(Ljava/lang/Object;Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object; @@ -38697,9 +39025,9 @@ Lorg/meshtastic/core/data/repository/MaintenanceUf2RepositoryImpl; Lorg/meshtastic/core/data/repository/MeshLogRepositoryImpl; HSPLorg/meshtastic/core/data/repository/MeshLogRepositoryImpl;->()V HSPLorg/meshtastic/core/data/repository/MeshLogRepositoryImpl;->(Lorg/meshtastic/core/database/DatabaseProvider;Lorg/meshtastic/core/di/CoroutineDispatchers;Lorg/meshtastic/core/repository/MeshLogPrefs;Lorg/meshtastic/core/data/datasource/NodeInfoReadDataSource;)V -HSPLorg/meshtastic/core/data/repository/MeshLogRepositoryImpl;->access$getDbManager$p(Lorg/meshtastic/core/data/repository/MeshLogRepositoryImpl;)Lorg/meshtastic/core/database/DatabaseProvider; -HSPLorg/meshtastic/core/data/repository/MeshLogRepositoryImpl;->deleteLogsOlderThan$suspendImpl(Lorg/meshtastic/core/data/repository/MeshLogRepositoryImpl;ILkotlin/coroutines/Continuation;)Ljava/lang/Object; -HSPLorg/meshtastic/core/data/repository/MeshLogRepositoryImpl;->deleteLogsOlderThan(ILkotlin/coroutines/Continuation;)Ljava/lang/Object; +PLorg/meshtastic/core/data/repository/MeshLogRepositoryImpl;->access$getDbManager$p(Lorg/meshtastic/core/data/repository/MeshLogRepositoryImpl;)Lorg/meshtastic/core/database/DatabaseProvider; +PLorg/meshtastic/core/data/repository/MeshLogRepositoryImpl;->deleteLogsOlderThan$suspendImpl(Lorg/meshtastic/core/data/repository/MeshLogRepositoryImpl;ILkotlin/coroutines/Continuation;)Ljava/lang/Object; +PLorg/meshtastic/core/data/repository/MeshLogRepositoryImpl;->deleteLogsOlderThan(ILkotlin/coroutines/Continuation;)Ljava/lang/Object; HSPLorg/meshtastic/core/data/repository/MeshLogRepositoryImpl;->getMyNodeInfo$lambda$0(Lorg/meshtastic/core/database/MeshtasticDatabase;)Lkotlinx/coroutines/flow/Flow; HSPLorg/meshtastic/core/data/repository/MeshLogRepositoryImpl;->getMyNodeInfo()Lkotlinx/coroutines/flow/Flow; Lorg/meshtastic/core/data/repository/MeshLogRepositoryImpl$$ExternalSyntheticLambda2; @@ -38870,7 +39198,6 @@ HSPLorg/meshtastic/core/database/DatabaseManager;->(Lorg/meshtastic/core/d HSPLorg/meshtastic/core/database/DatabaseManager;->access$checkOpen(Lorg/meshtastic/core/database/DatabaseManager;)V HSPLorg/meshtastic/core/database/DatabaseManager;->access$cleanupLegacyDbIfNeeded(Lorg/meshtastic/core/database/DatabaseManager;Ljava/lang/String;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; HSPLorg/meshtastic/core/database/DatabaseManager;->access$cleanupPersistedRetirementsAtStartup(Lorg/meshtastic/core/database/DatabaseManager;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; -HSPLorg/meshtastic/core/database/DatabaseManager;->access$dbFileExists(Lorg/meshtastic/core/database/DatabaseManager;Ljava/lang/String;)Z HSPLorg/meshtastic/core/database/DatabaseManager;->access$deviceDbNamesLocked(Lorg/meshtastic/core/database/DatabaseManager;)Ljava/util/List; HSPLorg/meshtastic/core/database/DatabaseManager;->access$endWrite(Lorg/meshtastic/core/database/DatabaseManager;Lorg/meshtastic/core/database/MeshtasticDatabase;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; HSPLorg/meshtastic/core/database/DatabaseManager;->access$enforceCacheLimit(Lorg/meshtastic/core/database/DatabaseManager;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; @@ -38901,7 +39228,6 @@ HSPLorg/meshtastic/core/database/DatabaseManager;->checkOpen()V HSPLorg/meshtastic/core/database/DatabaseManager;->cleanupLegacyDbIfNeeded(Ljava/lang/String;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; HSPLorg/meshtastic/core/database/DatabaseManager;->cleanupPersistedRetirementsAtStartup(Lkotlin/coroutines/Continuation;)Ljava/lang/Object; HSPLorg/meshtastic/core/database/DatabaseManager;->createPoolLane()Lkotlinx/coroutines/CoroutineDispatcher; -HSPLorg/meshtastic/core/database/DatabaseManager;->dbFileExists(Ljava/lang/String;)Z HSPLorg/meshtastic/core/database/DatabaseManager;->deviceDbNamesLocked()Ljava/util/List; HSPLorg/meshtastic/core/database/DatabaseManager;->endWrite(Lorg/meshtastic/core/database/MeshtasticDatabase;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; HSPLorg/meshtastic/core/database/DatabaseManager;->enforceCacheLimit(Lkotlin/coroutines/Continuation;)Ljava/lang/Object; @@ -38995,12 +39321,6 @@ HSPLorg/meshtastic/core/database/DatabaseManager$cleanupLegacyDbIfNeeded$2;->cre HSPLorg/meshtastic/core/database/DatabaseManager$cleanupLegacyDbIfNeeded$2;->invoke(Ljava/lang/Object;)Ljava/lang/Object; HSPLorg/meshtastic/core/database/DatabaseManager$cleanupLegacyDbIfNeeded$2;->invoke(Lkotlin/coroutines/Continuation;)Ljava/lang/Object; HSPLorg/meshtastic/core/database/DatabaseManager$cleanupLegacyDbIfNeeded$2;->invokeSuspend(Ljava/lang/Object;)Ljava/lang/Object; -Lorg/meshtastic/core/database/DatabaseManager$cleanupLegacyDbIfNeeded$2$1$4; -HSPLorg/meshtastic/core/database/DatabaseManager$cleanupLegacyDbIfNeeded$2$1$4;->(Lorg/meshtastic/core/database/DatabaseManager;Lkotlin/coroutines/Continuation;)V -HSPLorg/meshtastic/core/database/DatabaseManager$cleanupLegacyDbIfNeeded$2$1$4;->create(Ljava/lang/Object;Lkotlin/coroutines/Continuation;)Lkotlin/coroutines/Continuation; -HSPLorg/meshtastic/core/database/DatabaseManager$cleanupLegacyDbIfNeeded$2$1$4;->invoke(Landroidx/datastore/preferences/core/MutablePreferences;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; -HSPLorg/meshtastic/core/database/DatabaseManager$cleanupLegacyDbIfNeeded$2$1$4;->invoke(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object; -HSPLorg/meshtastic/core/database/DatabaseManager$cleanupLegacyDbIfNeeded$2$1$4;->invokeSuspend(Ljava/lang/Object;)Ljava/lang/Object; Lorg/meshtastic/core/database/DatabaseManager$cleanupPersistedRetirementsAtStartup$1; HSPLorg/meshtastic/core/database/DatabaseManager$cleanupPersistedRetirementsAtStartup$1;->(Lorg/meshtastic/core/database/DatabaseManager;Lkotlin/coroutines/Continuation;)V HSPLorg/meshtastic/core/database/DatabaseManager$cleanupPersistedRetirementsAtStartup$1;->invokeSuspend(Ljava/lang/Object;)Ljava/lang/Object; @@ -39282,6 +39602,10 @@ Lorg/meshtastic/core/database/MeshtasticDatabase_AutoMigration_56_57_Impl; HSPLorg/meshtastic/core/database/MeshtasticDatabase_AutoMigration_56_57_Impl;->()V Lorg/meshtastic/core/database/MeshtasticDatabase_AutoMigration_57_58_Impl; HSPLorg/meshtastic/core/database/MeshtasticDatabase_AutoMigration_57_58_Impl;->()V +Lorg/meshtastic/core/database/MeshtasticDatabase_AutoMigration_58_59_Impl; +HSPLorg/meshtastic/core/database/MeshtasticDatabase_AutoMigration_58_59_Impl;->()V +Lorg/meshtastic/core/database/MeshtasticDatabase_AutoMigration_59_60_Impl; +HSPLorg/meshtastic/core/database/MeshtasticDatabase_AutoMigration_59_60_Impl;->()V Lorg/meshtastic/core/database/MeshtasticDatabase_AutoMigration_5_6_Impl; HSPLorg/meshtastic/core/database/MeshtasticDatabase_AutoMigration_5_6_Impl;->()V Lorg/meshtastic/core/database/MeshtasticDatabase_AutoMigration_6_7_Impl; @@ -39437,7 +39761,7 @@ HSPLorg/meshtastic/core/database/dao/MeshLogDao$Companion;->()V Lorg/meshtastic/core/database/dao/MeshLogDao_Impl; HSPLorg/meshtastic/core/database/dao/MeshLogDao_Impl;->()V HSPLorg/meshtastic/core/database/dao/MeshLogDao_Impl;->(Landroidx/room3/RoomDatabase;)V -HSPLorg/meshtastic/core/database/dao/MeshLogDao_Impl;->deleteOlderThan(JLkotlin/coroutines/Continuation;)Ljava/lang/Object; +PLorg/meshtastic/core/database/dao/MeshLogDao_Impl;->deleteOlderThan(JLkotlin/coroutines/Continuation;)Ljava/lang/Object; HSPLorg/meshtastic/core/database/dao/MeshLogDao_Impl;->getLogsFrom(III)Lkotlinx/coroutines/flow/Flow; Lorg/meshtastic/core/database/dao/MeshLogDao_Impl$1; HSPLorg/meshtastic/core/database/dao/MeshLogDao_Impl$1;->(Lorg/meshtastic/core/database/dao/MeshLogDao_Impl;)V @@ -40129,9 +40453,9 @@ Lorg/meshtastic/core/network/repository/AndroidSerialDevicePresence$special$$inl HSPLorg/meshtastic/core/network/repository/AndroidSerialDevicePresence$special$$inlined$map$1;->(Lkotlinx/coroutines/flow/Flow;)V HSPLorg/meshtastic/core/network/repository/AndroidSerialDevicePresence$special$$inlined$map$1;->collect(Lkotlinx/coroutines/flow/FlowCollector;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; Lorg/meshtastic/core/network/repository/AndroidSerialDevicePresence$special$$inlined$map$1$1; -PLorg/meshtastic/core/network/repository/AndroidSerialDevicePresence$special$$inlined$map$1$1;->(Lorg/meshtastic/core/network/repository/AndroidSerialDevicePresence$special$$inlined$map$1;Lkotlin/coroutines/Continuation;)V +HSPLorg/meshtastic/core/network/repository/AndroidSerialDevicePresence$special$$inlined$map$1$1;->(Lorg/meshtastic/core/network/repository/AndroidSerialDevicePresence$special$$inlined$map$1;Lkotlin/coroutines/Continuation;)V Lorg/meshtastic/core/network/repository/AndroidSerialDevicePresence$special$$inlined$map$1$2; -PLorg/meshtastic/core/network/repository/AndroidSerialDevicePresence$special$$inlined$map$1$2;->(Lkotlinx/coroutines/flow/FlowCollector;)V +HSPLorg/meshtastic/core/network/repository/AndroidSerialDevicePresence$special$$inlined$map$1$2;->(Lkotlinx/coroutines/flow/FlowCollector;)V HSPLorg/meshtastic/core/network/repository/AndroidSerialDevicePresence$special$$inlined$map$1$2;->emit(Ljava/lang/Object;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; Lorg/meshtastic/core/network/repository/AndroidSerialDevicePresence$special$$inlined$map$1$2$1; HSPLorg/meshtastic/core/network/repository/AndroidSerialDevicePresence$special$$inlined$map$1$2$1;->(Lorg/meshtastic/core/network/repository/AndroidSerialDevicePresence$special$$inlined$map$1$2;Lkotlin/coroutines/Continuation;)V @@ -40494,7 +40818,7 @@ HSPLorg/meshtastic/core/prefs/meshlog/MeshLogPrefsImpl;->()V HSPLorg/meshtastic/core/prefs/meshlog/MeshLogPrefsImpl;->(Lorg/meshtastic/core/prefs/di/MeshLogDataStore;Lorg/meshtastic/core/di/CoroutineDispatchers;)V HSPLorg/meshtastic/core/prefs/meshlog/MeshLogPrefsImpl;->access$getKEY_LOGGING_ENABLED_PREF$cp()Landroidx/datastore/preferences/core/Preferences$Key; HSPLorg/meshtastic/core/prefs/meshlog/MeshLogPrefsImpl;->access$getKEY_RETENTION_DAYS_PREF$cp()Landroidx/datastore/preferences/core/Preferences$Key; -HSPLorg/meshtastic/core/prefs/meshlog/MeshLogPrefsImpl;->awaitCleanupPolicy(Lkotlin/coroutines/Continuation;)Ljava/lang/Object; +PLorg/meshtastic/core/prefs/meshlog/MeshLogPrefsImpl;->awaitCleanupPolicy(Lkotlin/coroutines/Continuation;)Ljava/lang/Object; Lorg/meshtastic/core/prefs/meshlog/MeshLogPrefsImpl$Companion; HSPLorg/meshtastic/core/prefs/meshlog/MeshLogPrefsImpl$Companion;->()V HSPLorg/meshtastic/core/prefs/meshlog/MeshLogPrefsImpl$Companion;->(Lkotlin/jvm/internal/DefaultConstructorMarker;)V @@ -40660,10 +40984,34 @@ Lorg/meshtastic/core/prefs/ui/UiPrefsImpl$special$$inlined$map$1$2$1; HSPLorg/meshtastic/core/prefs/ui/UiPrefsImpl$special$$inlined$map$1$2$1;->(Lorg/meshtastic/core/prefs/ui/UiPrefsImpl$special$$inlined$map$1$2;Lkotlin/coroutines/Continuation;)V Lorg/meshtastic/core/prefs/ui/UiPrefsImpl$special$$inlined$map$10; HSPLorg/meshtastic/core/prefs/ui/UiPrefsImpl$special$$inlined$map$10;->(Lkotlinx/coroutines/flow/Flow;)V +HSPLorg/meshtastic/core/prefs/ui/UiPrefsImpl$special$$inlined$map$10;->collect(Lkotlinx/coroutines/flow/FlowCollector;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; +Lorg/meshtastic/core/prefs/ui/UiPrefsImpl$special$$inlined$map$10$1; +HSPLorg/meshtastic/core/prefs/ui/UiPrefsImpl$special$$inlined$map$10$1;->(Lorg/meshtastic/core/prefs/ui/UiPrefsImpl$special$$inlined$map$10;Lkotlin/coroutines/Continuation;)V +Lorg/meshtastic/core/prefs/ui/UiPrefsImpl$special$$inlined$map$10$2; +HSPLorg/meshtastic/core/prefs/ui/UiPrefsImpl$special$$inlined$map$10$2;->(Lkotlinx/coroutines/flow/FlowCollector;)V +HSPLorg/meshtastic/core/prefs/ui/UiPrefsImpl$special$$inlined$map$10$2;->emit(Ljava/lang/Object;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; +Lorg/meshtastic/core/prefs/ui/UiPrefsImpl$special$$inlined$map$10$2$1; +HSPLorg/meshtastic/core/prefs/ui/UiPrefsImpl$special$$inlined$map$10$2$1;->(Lorg/meshtastic/core/prefs/ui/UiPrefsImpl$special$$inlined$map$10$2;Lkotlin/coroutines/Continuation;)V Lorg/meshtastic/core/prefs/ui/UiPrefsImpl$special$$inlined$map$11; HSPLorg/meshtastic/core/prefs/ui/UiPrefsImpl$special$$inlined$map$11;->(Lkotlinx/coroutines/flow/Flow;)V +HSPLorg/meshtastic/core/prefs/ui/UiPrefsImpl$special$$inlined$map$11;->collect(Lkotlinx/coroutines/flow/FlowCollector;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; +Lorg/meshtastic/core/prefs/ui/UiPrefsImpl$special$$inlined$map$11$1; +HSPLorg/meshtastic/core/prefs/ui/UiPrefsImpl$special$$inlined$map$11$1;->(Lorg/meshtastic/core/prefs/ui/UiPrefsImpl$special$$inlined$map$11;Lkotlin/coroutines/Continuation;)V +Lorg/meshtastic/core/prefs/ui/UiPrefsImpl$special$$inlined$map$11$2; +HSPLorg/meshtastic/core/prefs/ui/UiPrefsImpl$special$$inlined$map$11$2;->(Lkotlinx/coroutines/flow/FlowCollector;)V +HSPLorg/meshtastic/core/prefs/ui/UiPrefsImpl$special$$inlined$map$11$2;->emit(Ljava/lang/Object;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; +Lorg/meshtastic/core/prefs/ui/UiPrefsImpl$special$$inlined$map$11$2$1; +HSPLorg/meshtastic/core/prefs/ui/UiPrefsImpl$special$$inlined$map$11$2$1;->(Lorg/meshtastic/core/prefs/ui/UiPrefsImpl$special$$inlined$map$11$2;Lkotlin/coroutines/Continuation;)V Lorg/meshtastic/core/prefs/ui/UiPrefsImpl$special$$inlined$map$12; HSPLorg/meshtastic/core/prefs/ui/UiPrefsImpl$special$$inlined$map$12;->(Lkotlinx/coroutines/flow/Flow;)V +HSPLorg/meshtastic/core/prefs/ui/UiPrefsImpl$special$$inlined$map$12;->collect(Lkotlinx/coroutines/flow/FlowCollector;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; +Lorg/meshtastic/core/prefs/ui/UiPrefsImpl$special$$inlined$map$12$1; +HSPLorg/meshtastic/core/prefs/ui/UiPrefsImpl$special$$inlined$map$12$1;->(Lorg/meshtastic/core/prefs/ui/UiPrefsImpl$special$$inlined$map$12;Lkotlin/coroutines/Continuation;)V +Lorg/meshtastic/core/prefs/ui/UiPrefsImpl$special$$inlined$map$12$2; +HSPLorg/meshtastic/core/prefs/ui/UiPrefsImpl$special$$inlined$map$12$2;->(Lkotlinx/coroutines/flow/FlowCollector;)V +HSPLorg/meshtastic/core/prefs/ui/UiPrefsImpl$special$$inlined$map$12$2;->emit(Ljava/lang/Object;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; +Lorg/meshtastic/core/prefs/ui/UiPrefsImpl$special$$inlined$map$12$2$1; +HSPLorg/meshtastic/core/prefs/ui/UiPrefsImpl$special$$inlined$map$12$2$1;->(Lorg/meshtastic/core/prefs/ui/UiPrefsImpl$special$$inlined$map$12$2;Lkotlin/coroutines/Continuation;)V Lorg/meshtastic/core/prefs/ui/UiPrefsImpl$special$$inlined$map$13; HSPLorg/meshtastic/core/prefs/ui/UiPrefsImpl$special$$inlined$map$13;->(Lkotlinx/coroutines/flow/Flow;)V HSPLorg/meshtastic/core/prefs/ui/UiPrefsImpl$special$$inlined$map$13;->collect(Lkotlinx/coroutines/flow/FlowCollector;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; @@ -40794,56 +41142,6 @@ HSPLorg/meshtastic/core/prefs/ui/UiPrefsImpl$special$$inlined$map$24$2;->( HSPLorg/meshtastic/core/prefs/ui/UiPrefsImpl$special$$inlined$map$24$2;->emit(Ljava/lang/Object;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; Lorg/meshtastic/core/prefs/ui/UiPrefsImpl$special$$inlined$map$24$2$1; HSPLorg/meshtastic/core/prefs/ui/UiPrefsImpl$special$$inlined$map$24$2$1;->(Lorg/meshtastic/core/prefs/ui/UiPrefsImpl$special$$inlined$map$24$2;Lkotlin/coroutines/Continuation;)V -Lorg/meshtastic/core/prefs/ui/UiPrefsImpl$special$$inlined$map$25; -HSPLorg/meshtastic/core/prefs/ui/UiPrefsImpl$special$$inlined$map$25;->(Lkotlinx/coroutines/flow/Flow;)V -HSPLorg/meshtastic/core/prefs/ui/UiPrefsImpl$special$$inlined$map$25;->collect(Lkotlinx/coroutines/flow/FlowCollector;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; -Lorg/meshtastic/core/prefs/ui/UiPrefsImpl$special$$inlined$map$25$1; -HSPLorg/meshtastic/core/prefs/ui/UiPrefsImpl$special$$inlined$map$25$1;->(Lorg/meshtastic/core/prefs/ui/UiPrefsImpl$special$$inlined$map$25;Lkotlin/coroutines/Continuation;)V -Lorg/meshtastic/core/prefs/ui/UiPrefsImpl$special$$inlined$map$25$2; -HSPLorg/meshtastic/core/prefs/ui/UiPrefsImpl$special$$inlined$map$25$2;->(Lkotlinx/coroutines/flow/FlowCollector;)V -HSPLorg/meshtastic/core/prefs/ui/UiPrefsImpl$special$$inlined$map$25$2;->emit(Ljava/lang/Object;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; -Lorg/meshtastic/core/prefs/ui/UiPrefsImpl$special$$inlined$map$25$2$1; -HSPLorg/meshtastic/core/prefs/ui/UiPrefsImpl$special$$inlined$map$25$2$1;->(Lorg/meshtastic/core/prefs/ui/UiPrefsImpl$special$$inlined$map$25$2;Lkotlin/coroutines/Continuation;)V -Lorg/meshtastic/core/prefs/ui/UiPrefsImpl$special$$inlined$map$26; -HSPLorg/meshtastic/core/prefs/ui/UiPrefsImpl$special$$inlined$map$26;->(Lkotlinx/coroutines/flow/Flow;)V -HSPLorg/meshtastic/core/prefs/ui/UiPrefsImpl$special$$inlined$map$26;->collect(Lkotlinx/coroutines/flow/FlowCollector;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; -Lorg/meshtastic/core/prefs/ui/UiPrefsImpl$special$$inlined$map$26$1; -HSPLorg/meshtastic/core/prefs/ui/UiPrefsImpl$special$$inlined$map$26$1;->(Lorg/meshtastic/core/prefs/ui/UiPrefsImpl$special$$inlined$map$26;Lkotlin/coroutines/Continuation;)V -Lorg/meshtastic/core/prefs/ui/UiPrefsImpl$special$$inlined$map$26$2; -HSPLorg/meshtastic/core/prefs/ui/UiPrefsImpl$special$$inlined$map$26$2;->(Lkotlinx/coroutines/flow/FlowCollector;)V -HSPLorg/meshtastic/core/prefs/ui/UiPrefsImpl$special$$inlined$map$26$2;->emit(Ljava/lang/Object;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; -Lorg/meshtastic/core/prefs/ui/UiPrefsImpl$special$$inlined$map$26$2$1; -HSPLorg/meshtastic/core/prefs/ui/UiPrefsImpl$special$$inlined$map$26$2$1;->(Lorg/meshtastic/core/prefs/ui/UiPrefsImpl$special$$inlined$map$26$2;Lkotlin/coroutines/Continuation;)V -Lorg/meshtastic/core/prefs/ui/UiPrefsImpl$special$$inlined$map$27; -HSPLorg/meshtastic/core/prefs/ui/UiPrefsImpl$special$$inlined$map$27;->(Lkotlinx/coroutines/flow/Flow;)V -HSPLorg/meshtastic/core/prefs/ui/UiPrefsImpl$special$$inlined$map$27;->collect(Lkotlinx/coroutines/flow/FlowCollector;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; -Lorg/meshtastic/core/prefs/ui/UiPrefsImpl$special$$inlined$map$27$1; -HSPLorg/meshtastic/core/prefs/ui/UiPrefsImpl$special$$inlined$map$27$1;->(Lorg/meshtastic/core/prefs/ui/UiPrefsImpl$special$$inlined$map$27;Lkotlin/coroutines/Continuation;)V -Lorg/meshtastic/core/prefs/ui/UiPrefsImpl$special$$inlined$map$27$2; -HSPLorg/meshtastic/core/prefs/ui/UiPrefsImpl$special$$inlined$map$27$2;->(Lkotlinx/coroutines/flow/FlowCollector;)V -HSPLorg/meshtastic/core/prefs/ui/UiPrefsImpl$special$$inlined$map$27$2;->emit(Ljava/lang/Object;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; -Lorg/meshtastic/core/prefs/ui/UiPrefsImpl$special$$inlined$map$27$2$1; -HSPLorg/meshtastic/core/prefs/ui/UiPrefsImpl$special$$inlined$map$27$2$1;->(Lorg/meshtastic/core/prefs/ui/UiPrefsImpl$special$$inlined$map$27$2;Lkotlin/coroutines/Continuation;)V -Lorg/meshtastic/core/prefs/ui/UiPrefsImpl$special$$inlined$map$28; -HSPLorg/meshtastic/core/prefs/ui/UiPrefsImpl$special$$inlined$map$28;->(Lkotlinx/coroutines/flow/Flow;)V -HSPLorg/meshtastic/core/prefs/ui/UiPrefsImpl$special$$inlined$map$28;->collect(Lkotlinx/coroutines/flow/FlowCollector;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; -Lorg/meshtastic/core/prefs/ui/UiPrefsImpl$special$$inlined$map$28$1; -HSPLorg/meshtastic/core/prefs/ui/UiPrefsImpl$special$$inlined$map$28$1;->(Lorg/meshtastic/core/prefs/ui/UiPrefsImpl$special$$inlined$map$28;Lkotlin/coroutines/Continuation;)V -Lorg/meshtastic/core/prefs/ui/UiPrefsImpl$special$$inlined$map$28$2; -HSPLorg/meshtastic/core/prefs/ui/UiPrefsImpl$special$$inlined$map$28$2;->(Lkotlinx/coroutines/flow/FlowCollector;)V -HSPLorg/meshtastic/core/prefs/ui/UiPrefsImpl$special$$inlined$map$28$2;->emit(Ljava/lang/Object;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; -Lorg/meshtastic/core/prefs/ui/UiPrefsImpl$special$$inlined$map$28$2$1; -HSPLorg/meshtastic/core/prefs/ui/UiPrefsImpl$special$$inlined$map$28$2$1;->(Lorg/meshtastic/core/prefs/ui/UiPrefsImpl$special$$inlined$map$28$2;Lkotlin/coroutines/Continuation;)V -Lorg/meshtastic/core/prefs/ui/UiPrefsImpl$special$$inlined$map$29; -HSPLorg/meshtastic/core/prefs/ui/UiPrefsImpl$special$$inlined$map$29;->(Lkotlinx/coroutines/flow/Flow;)V -HSPLorg/meshtastic/core/prefs/ui/UiPrefsImpl$special$$inlined$map$29;->collect(Lkotlinx/coroutines/flow/FlowCollector;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; -Lorg/meshtastic/core/prefs/ui/UiPrefsImpl$special$$inlined$map$29$1; -HSPLorg/meshtastic/core/prefs/ui/UiPrefsImpl$special$$inlined$map$29$1;->(Lorg/meshtastic/core/prefs/ui/UiPrefsImpl$special$$inlined$map$29;Lkotlin/coroutines/Continuation;)V -Lorg/meshtastic/core/prefs/ui/UiPrefsImpl$special$$inlined$map$29$2; -HSPLorg/meshtastic/core/prefs/ui/UiPrefsImpl$special$$inlined$map$29$2;->(Lkotlinx/coroutines/flow/FlowCollector;)V -HSPLorg/meshtastic/core/prefs/ui/UiPrefsImpl$special$$inlined$map$29$2;->emit(Ljava/lang/Object;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; -Lorg/meshtastic/core/prefs/ui/UiPrefsImpl$special$$inlined$map$29$2$1; -HSPLorg/meshtastic/core/prefs/ui/UiPrefsImpl$special$$inlined$map$29$2$1;->(Lorg/meshtastic/core/prefs/ui/UiPrefsImpl$special$$inlined$map$29$2;Lkotlin/coroutines/Continuation;)V Lorg/meshtastic/core/prefs/ui/UiPrefsImpl$special$$inlined$map$3; HSPLorg/meshtastic/core/prefs/ui/UiPrefsImpl$special$$inlined$map$3;->(Lkotlinx/coroutines/flow/Flow;)V HSPLorg/meshtastic/core/prefs/ui/UiPrefsImpl$special$$inlined$map$3;->collect(Lkotlinx/coroutines/flow/FlowCollector;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; @@ -40854,16 +41152,6 @@ HSPLorg/meshtastic/core/prefs/ui/UiPrefsImpl$special$$inlined$map$3$2;->(L HSPLorg/meshtastic/core/prefs/ui/UiPrefsImpl$special$$inlined$map$3$2;->emit(Ljava/lang/Object;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; Lorg/meshtastic/core/prefs/ui/UiPrefsImpl$special$$inlined$map$3$2$1; HSPLorg/meshtastic/core/prefs/ui/UiPrefsImpl$special$$inlined$map$3$2$1;->(Lorg/meshtastic/core/prefs/ui/UiPrefsImpl$special$$inlined$map$3$2;Lkotlin/coroutines/Continuation;)V -Lorg/meshtastic/core/prefs/ui/UiPrefsImpl$special$$inlined$map$30; -HSPLorg/meshtastic/core/prefs/ui/UiPrefsImpl$special$$inlined$map$30;->(Lkotlinx/coroutines/flow/Flow;)V -HSPLorg/meshtastic/core/prefs/ui/UiPrefsImpl$special$$inlined$map$30;->collect(Lkotlinx/coroutines/flow/FlowCollector;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; -Lorg/meshtastic/core/prefs/ui/UiPrefsImpl$special$$inlined$map$30$1; -HSPLorg/meshtastic/core/prefs/ui/UiPrefsImpl$special$$inlined$map$30$1;->(Lorg/meshtastic/core/prefs/ui/UiPrefsImpl$special$$inlined$map$30;Lkotlin/coroutines/Continuation;)V -Lorg/meshtastic/core/prefs/ui/UiPrefsImpl$special$$inlined$map$30$2; -HSPLorg/meshtastic/core/prefs/ui/UiPrefsImpl$special$$inlined$map$30$2;->(Lkotlinx/coroutines/flow/FlowCollector;)V -HSPLorg/meshtastic/core/prefs/ui/UiPrefsImpl$special$$inlined$map$30$2;->emit(Ljava/lang/Object;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; -Lorg/meshtastic/core/prefs/ui/UiPrefsImpl$special$$inlined$map$30$2$1; -HSPLorg/meshtastic/core/prefs/ui/UiPrefsImpl$special$$inlined$map$30$2$1;->(Lorg/meshtastic/core/prefs/ui/UiPrefsImpl$special$$inlined$map$30$2;Lkotlin/coroutines/Continuation;)V Lorg/meshtastic/core/prefs/ui/UiPrefsImpl$special$$inlined$map$4; HSPLorg/meshtastic/core/prefs/ui/UiPrefsImpl$special$$inlined$map$4;->(Lkotlinx/coroutines/flow/Flow;)V HSPLorg/meshtastic/core/prefs/ui/UiPrefsImpl$special$$inlined$map$4;->collect(Lkotlinx/coroutines/flow/FlowCollector;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; @@ -40877,13 +41165,37 @@ HSPLorg/meshtastic/core/prefs/ui/UiPrefsImpl$special$$inlined$map$4$2$1;-> Lorg/meshtastic/core/prefs/ui/UiPrefsImpl$special$$inlined$map$5; HSPLorg/meshtastic/core/prefs/ui/UiPrefsImpl$special$$inlined$map$5;->(Lkotlinx/coroutines/flow/Flow;)V Lorg/meshtastic/core/prefs/ui/UiPrefsImpl$special$$inlined$map$6; -HSPLorg/meshtastic/core/prefs/ui/UiPrefsImpl$special$$inlined$map$6;->(Lkotlinx/coroutines/flow/Flow;)V +HSPLorg/meshtastic/core/prefs/ui/UiPrefsImpl$special$$inlined$map$6;->(Lkotlinx/coroutines/flow/Flow;Lorg/meshtastic/core/prefs/ui/UiPrefsImpl;)V Lorg/meshtastic/core/prefs/ui/UiPrefsImpl$special$$inlined$map$7; HSPLorg/meshtastic/core/prefs/ui/UiPrefsImpl$special$$inlined$map$7;->(Lkotlinx/coroutines/flow/Flow;)V +HSPLorg/meshtastic/core/prefs/ui/UiPrefsImpl$special$$inlined$map$7;->collect(Lkotlinx/coroutines/flow/FlowCollector;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; +Lorg/meshtastic/core/prefs/ui/UiPrefsImpl$special$$inlined$map$7$1; +HSPLorg/meshtastic/core/prefs/ui/UiPrefsImpl$special$$inlined$map$7$1;->(Lorg/meshtastic/core/prefs/ui/UiPrefsImpl$special$$inlined$map$7;Lkotlin/coroutines/Continuation;)V +Lorg/meshtastic/core/prefs/ui/UiPrefsImpl$special$$inlined$map$7$2; +HSPLorg/meshtastic/core/prefs/ui/UiPrefsImpl$special$$inlined$map$7$2;->(Lkotlinx/coroutines/flow/FlowCollector;)V +HSPLorg/meshtastic/core/prefs/ui/UiPrefsImpl$special$$inlined$map$7$2;->emit(Ljava/lang/Object;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; +Lorg/meshtastic/core/prefs/ui/UiPrefsImpl$special$$inlined$map$7$2$1; +HSPLorg/meshtastic/core/prefs/ui/UiPrefsImpl$special$$inlined$map$7$2$1;->(Lorg/meshtastic/core/prefs/ui/UiPrefsImpl$special$$inlined$map$7$2;Lkotlin/coroutines/Continuation;)V Lorg/meshtastic/core/prefs/ui/UiPrefsImpl$special$$inlined$map$8; HSPLorg/meshtastic/core/prefs/ui/UiPrefsImpl$special$$inlined$map$8;->(Lkotlinx/coroutines/flow/Flow;)V +HSPLorg/meshtastic/core/prefs/ui/UiPrefsImpl$special$$inlined$map$8;->collect(Lkotlinx/coroutines/flow/FlowCollector;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; +Lorg/meshtastic/core/prefs/ui/UiPrefsImpl$special$$inlined$map$8$1; +HSPLorg/meshtastic/core/prefs/ui/UiPrefsImpl$special$$inlined$map$8$1;->(Lorg/meshtastic/core/prefs/ui/UiPrefsImpl$special$$inlined$map$8;Lkotlin/coroutines/Continuation;)V +Lorg/meshtastic/core/prefs/ui/UiPrefsImpl$special$$inlined$map$8$2; +HSPLorg/meshtastic/core/prefs/ui/UiPrefsImpl$special$$inlined$map$8$2;->(Lkotlinx/coroutines/flow/FlowCollector;)V +HSPLorg/meshtastic/core/prefs/ui/UiPrefsImpl$special$$inlined$map$8$2;->emit(Ljava/lang/Object;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; +Lorg/meshtastic/core/prefs/ui/UiPrefsImpl$special$$inlined$map$8$2$1; +HSPLorg/meshtastic/core/prefs/ui/UiPrefsImpl$special$$inlined$map$8$2$1;->(Lorg/meshtastic/core/prefs/ui/UiPrefsImpl$special$$inlined$map$8$2;Lkotlin/coroutines/Continuation;)V Lorg/meshtastic/core/prefs/ui/UiPrefsImpl$special$$inlined$map$9; HSPLorg/meshtastic/core/prefs/ui/UiPrefsImpl$special$$inlined$map$9;->(Lkotlinx/coroutines/flow/Flow;)V +HSPLorg/meshtastic/core/prefs/ui/UiPrefsImpl$special$$inlined$map$9;->collect(Lkotlinx/coroutines/flow/FlowCollector;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; +Lorg/meshtastic/core/prefs/ui/UiPrefsImpl$special$$inlined$map$9$1; +HSPLorg/meshtastic/core/prefs/ui/UiPrefsImpl$special$$inlined$map$9$1;->(Lorg/meshtastic/core/prefs/ui/UiPrefsImpl$special$$inlined$map$9;Lkotlin/coroutines/Continuation;)V +Lorg/meshtastic/core/prefs/ui/UiPrefsImpl$special$$inlined$map$9$2; +HSPLorg/meshtastic/core/prefs/ui/UiPrefsImpl$special$$inlined$map$9$2;->(Lkotlinx/coroutines/flow/FlowCollector;)V +HSPLorg/meshtastic/core/prefs/ui/UiPrefsImpl$special$$inlined$map$9$2;->emit(Ljava/lang/Object;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; +Lorg/meshtastic/core/prefs/ui/UiPrefsImpl$special$$inlined$map$9$2$1; +HSPLorg/meshtastic/core/prefs/ui/UiPrefsImpl$special$$inlined$map$9$2$1;->(Lorg/meshtastic/core/prefs/ui/UiPrefsImpl$special$$inlined$map$9$2;Lkotlin/coroutines/Continuation;)V Lorg/meshtastic/core/prefs/ui/UiPrefsUnitsOverrideSource; Lorg/meshtastic/core/repository/ActiveConversationTracker; Lorg/meshtastic/core/repository/AdminController; @@ -40958,6 +41270,9 @@ Lorg/meshtastic/core/repository/MqttManager; Lorg/meshtastic/core/repository/NeighborInfoHandler; Lorg/meshtastic/core/repository/NeighborInfoResponseProvider; Lorg/meshtastic/core/repository/NodeController; +Lorg/meshtastic/core/repository/NodeFilterPrefs; +HSPLorg/meshtastic/core/repository/NodeFilterPrefs;->(ZZZZZZZZZ)V +HSPLorg/meshtastic/core/repository/NodeFilterPrefs;->(ZZZZZZZZZILkotlin/jvm/internal/DefaultConstructorMarker;)V Lorg/meshtastic/core/repository/NodeManager; Lorg/meshtastic/core/repository/NodeRepository; Lorg/meshtastic/core/repository/NodeRestartTracker; @@ -41807,33 +42122,26 @@ Lorg/meshtastic/core/resources/String10_commonMainKt$$ExternalSyntheticLambda66; HSPLorg/meshtastic/core/resources/String10_commonMainKt$$ExternalSyntheticLambda66;->()V Lorg/meshtastic/core/resources/String10_commonMainKt$$ExternalSyntheticLambda67; HSPLorg/meshtastic/core/resources/String10_commonMainKt$$ExternalSyntheticLambda67;->()V -HSPLorg/meshtastic/core/resources/String10_commonMainKt$$ExternalSyntheticLambda67;->invoke()Ljava/lang/Object; Lorg/meshtastic/core/resources/String10_commonMainKt$$ExternalSyntheticLambda68; HSPLorg/meshtastic/core/resources/String10_commonMainKt$$ExternalSyntheticLambda68;->()V -HSPLorg/meshtastic/core/resources/String10_commonMainKt$$ExternalSyntheticLambda68;->invoke()Ljava/lang/Object; Lorg/meshtastic/core/resources/String10_commonMainKt$$ExternalSyntheticLambda69; HSPLorg/meshtastic/core/resources/String10_commonMainKt$$ExternalSyntheticLambda69;->()V Lorg/meshtastic/core/resources/String10_commonMainKt$$ExternalSyntheticLambda7; HSPLorg/meshtastic/core/resources/String10_commonMainKt$$ExternalSyntheticLambda7;->()V Lorg/meshtastic/core/resources/String10_commonMainKt$$ExternalSyntheticLambda70; HSPLorg/meshtastic/core/resources/String10_commonMainKt$$ExternalSyntheticLambda70;->()V -HSPLorg/meshtastic/core/resources/String10_commonMainKt$$ExternalSyntheticLambda70;->invoke()Ljava/lang/Object; Lorg/meshtastic/core/resources/String10_commonMainKt$$ExternalSyntheticLambda71; HSPLorg/meshtastic/core/resources/String10_commonMainKt$$ExternalSyntheticLambda71;->()V -HSPLorg/meshtastic/core/resources/String10_commonMainKt$$ExternalSyntheticLambda71;->invoke()Ljava/lang/Object; Lorg/meshtastic/core/resources/String10_commonMainKt$$ExternalSyntheticLambda72; HSPLorg/meshtastic/core/resources/String10_commonMainKt$$ExternalSyntheticLambda72;->()V -HSPLorg/meshtastic/core/resources/String10_commonMainKt$$ExternalSyntheticLambda72;->invoke()Ljava/lang/Object; Lorg/meshtastic/core/resources/String10_commonMainKt$$ExternalSyntheticLambda73; HSPLorg/meshtastic/core/resources/String10_commonMainKt$$ExternalSyntheticLambda73;->()V Lorg/meshtastic/core/resources/String10_commonMainKt$$ExternalSyntheticLambda74; HSPLorg/meshtastic/core/resources/String10_commonMainKt$$ExternalSyntheticLambda74;->()V Lorg/meshtastic/core/resources/String10_commonMainKt$$ExternalSyntheticLambda75; HSPLorg/meshtastic/core/resources/String10_commonMainKt$$ExternalSyntheticLambda75;->()V -HSPLorg/meshtastic/core/resources/String10_commonMainKt$$ExternalSyntheticLambda75;->invoke()Ljava/lang/Object; Lorg/meshtastic/core/resources/String10_commonMainKt$$ExternalSyntheticLambda76; HSPLorg/meshtastic/core/resources/String10_commonMainKt$$ExternalSyntheticLambda76;->()V -HSPLorg/meshtastic/core/resources/String10_commonMainKt$$ExternalSyntheticLambda76;->invoke()Ljava/lang/Object; Lorg/meshtastic/core/resources/String10_commonMainKt$$ExternalSyntheticLambda77; HSPLorg/meshtastic/core/resources/String10_commonMainKt$$ExternalSyntheticLambda77;->()V Lorg/meshtastic/core/resources/String10_commonMainKt$$ExternalSyntheticLambda78; @@ -41848,26 +42156,33 @@ Lorg/meshtastic/core/resources/String10_commonMainKt$$ExternalSyntheticLambda80; HSPLorg/meshtastic/core/resources/String10_commonMainKt$$ExternalSyntheticLambda80;->()V Lorg/meshtastic/core/resources/String10_commonMainKt$$ExternalSyntheticLambda81; HSPLorg/meshtastic/core/resources/String10_commonMainKt$$ExternalSyntheticLambda81;->()V +HSPLorg/meshtastic/core/resources/String10_commonMainKt$$ExternalSyntheticLambda81;->invoke()Ljava/lang/Object; Lorg/meshtastic/core/resources/String10_commonMainKt$$ExternalSyntheticLambda82; HSPLorg/meshtastic/core/resources/String10_commonMainKt$$ExternalSyntheticLambda82;->()V +HSPLorg/meshtastic/core/resources/String10_commonMainKt$$ExternalSyntheticLambda82;->invoke()Ljava/lang/Object; Lorg/meshtastic/core/resources/String10_commonMainKt$$ExternalSyntheticLambda83; HSPLorg/meshtastic/core/resources/String10_commonMainKt$$ExternalSyntheticLambda83;->()V +HSPLorg/meshtastic/core/resources/String10_commonMainKt$$ExternalSyntheticLambda83;->invoke()Ljava/lang/Object; Lorg/meshtastic/core/resources/String10_commonMainKt$$ExternalSyntheticLambda84; HSPLorg/meshtastic/core/resources/String10_commonMainKt$$ExternalSyntheticLambda84;->()V Lorg/meshtastic/core/resources/String10_commonMainKt$$ExternalSyntheticLambda85; HSPLorg/meshtastic/core/resources/String10_commonMainKt$$ExternalSyntheticLambda85;->()V +HSPLorg/meshtastic/core/resources/String10_commonMainKt$$ExternalSyntheticLambda85;->invoke()Ljava/lang/Object; Lorg/meshtastic/core/resources/String10_commonMainKt$$ExternalSyntheticLambda86; HSPLorg/meshtastic/core/resources/String10_commonMainKt$$ExternalSyntheticLambda86;->()V Lorg/meshtastic/core/resources/String10_commonMainKt$$ExternalSyntheticLambda87; HSPLorg/meshtastic/core/resources/String10_commonMainKt$$ExternalSyntheticLambda87;->()V +HSPLorg/meshtastic/core/resources/String10_commonMainKt$$ExternalSyntheticLambda87;->invoke()Ljava/lang/Object; Lorg/meshtastic/core/resources/String10_commonMainKt$$ExternalSyntheticLambda88; HSPLorg/meshtastic/core/resources/String10_commonMainKt$$ExternalSyntheticLambda88;->()V Lorg/meshtastic/core/resources/String10_commonMainKt$$ExternalSyntheticLambda89; HSPLorg/meshtastic/core/resources/String10_commonMainKt$$ExternalSyntheticLambda89;->()V +HSPLorg/meshtastic/core/resources/String10_commonMainKt$$ExternalSyntheticLambda89;->invoke()Ljava/lang/Object; Lorg/meshtastic/core/resources/String10_commonMainKt$$ExternalSyntheticLambda9; HSPLorg/meshtastic/core/resources/String10_commonMainKt$$ExternalSyntheticLambda9;->()V Lorg/meshtastic/core/resources/String10_commonMainKt$$ExternalSyntheticLambda90; HSPLorg/meshtastic/core/resources/String10_commonMainKt$$ExternalSyntheticLambda90;->()V +HSPLorg/meshtastic/core/resources/String10_commonMainKt$$ExternalSyntheticLambda90;->invoke()Ljava/lang/Object; Lorg/meshtastic/core/resources/String10_commonMainKt$$ExternalSyntheticLambda91; HSPLorg/meshtastic/core/resources/String10_commonMainKt$$ExternalSyntheticLambda91;->()V Lorg/meshtastic/core/resources/String10_commonMainKt$$ExternalSyntheticLambda92; @@ -42010,7 +42325,6 @@ Lorg/meshtastic/core/resources/String12_commonMainKt$$ExternalSyntheticLambda61; HSPLorg/meshtastic/core/resources/String12_commonMainKt$$ExternalSyntheticLambda61;->()V Lorg/meshtastic/core/resources/String12_commonMainKt$$ExternalSyntheticLambda62; HSPLorg/meshtastic/core/resources/String12_commonMainKt$$ExternalSyntheticLambda62;->()V -HSPLorg/meshtastic/core/resources/String12_commonMainKt$$ExternalSyntheticLambda62;->invoke()Ljava/lang/Object; Lorg/meshtastic/core/resources/String12_commonMainKt$$ExternalSyntheticLambda63; HSPLorg/meshtastic/core/resources/String12_commonMainKt$$ExternalSyntheticLambda63;->()V Lorg/meshtastic/core/resources/String12_commonMainKt$$ExternalSyntheticLambda64; @@ -42019,6 +42333,7 @@ Lorg/meshtastic/core/resources/String12_commonMainKt$$ExternalSyntheticLambda65; HSPLorg/meshtastic/core/resources/String12_commonMainKt$$ExternalSyntheticLambda65;->()V Lorg/meshtastic/core/resources/String12_commonMainKt$$ExternalSyntheticLambda66; HSPLorg/meshtastic/core/resources/String12_commonMainKt$$ExternalSyntheticLambda66;->()V +HSPLorg/meshtastic/core/resources/String12_commonMainKt$$ExternalSyntheticLambda66;->invoke()Ljava/lang/Object; Lorg/meshtastic/core/resources/String12_commonMainKt$$ExternalSyntheticLambda67; HSPLorg/meshtastic/core/resources/String12_commonMainKt$$ExternalSyntheticLambda67;->()V Lorg/meshtastic/core/resources/String12_commonMainKt$$ExternalSyntheticLambda68; @@ -42125,6 +42440,7 @@ Lorg/meshtastic/core/resources/String14_commonMainKt$$ExternalSyntheticLambda20; HSPLorg/meshtastic/core/resources/String14_commonMainKt$$ExternalSyntheticLambda20;->()V Lorg/meshtastic/core/resources/String14_commonMainKt$$ExternalSyntheticLambda21; HSPLorg/meshtastic/core/resources/String14_commonMainKt$$ExternalSyntheticLambda21;->()V +HSPLorg/meshtastic/core/resources/String14_commonMainKt$$ExternalSyntheticLambda21;->invoke()Ljava/lang/Object; Lorg/meshtastic/core/resources/String14_commonMainKt$$ExternalSyntheticLambda22; HSPLorg/meshtastic/core/resources/String14_commonMainKt$$ExternalSyntheticLambda22;->()V Lorg/meshtastic/core/resources/String14_commonMainKt$$ExternalSyntheticLambda23; @@ -42253,7 +42569,6 @@ Lorg/meshtastic/core/resources/String14_commonMainKt$$ExternalSyntheticLambda79; HSPLorg/meshtastic/core/resources/String14_commonMainKt$$ExternalSyntheticLambda79;->()V Lorg/meshtastic/core/resources/String14_commonMainKt$$ExternalSyntheticLambda8; HSPLorg/meshtastic/core/resources/String14_commonMainKt$$ExternalSyntheticLambda8;->()V -HSPLorg/meshtastic/core/resources/String14_commonMainKt$$ExternalSyntheticLambda8;->invoke()Ljava/lang/Object; Lorg/meshtastic/core/resources/String14_commonMainKt$$ExternalSyntheticLambda80; HSPLorg/meshtastic/core/resources/String14_commonMainKt$$ExternalSyntheticLambda80;->()V Lorg/meshtastic/core/resources/String14_commonMainKt$$ExternalSyntheticLambda81; @@ -42299,9 +42614,7 @@ HSPLorg/meshtastic/core/resources/String14_commonMainKt$$ExternalSyntheticLambda Lorg/meshtastic/core/resources/String16_commonMainKt; HSPLorg/meshtastic/core/resources/String16_commonMainKt;->()V HSPLorg/meshtastic/core/resources/String16_commonMainKt;->getShare_your_location_in_real_time(Lorg/meshtastic/core/resources/Res$string;)Lorg/jetbrains/compose/resources/StringResource; -HSPLorg/meshtastic/core/resources/String16_commonMainKt;->getStay_connected_anywhere(Lorg/meshtastic/core/resources/Res$string;)Lorg/jetbrains/compose/resources/StringResource; HSPLorg/meshtastic/core/resources/String16_commonMainKt;->share_your_location_in_real_time_delegate$lambda$0()Lorg/jetbrains/compose/resources/StringResource; -HSPLorg/meshtastic/core/resources/String16_commonMainKt;->stay_connected_anywhere_delegate$lambda$0()Lorg/jetbrains/compose/resources/StringResource; Lorg/meshtastic/core/resources/String16_commonMainKt$$ExternalSyntheticLambda0; HSPLorg/meshtastic/core/resources/String16_commonMainKt$$ExternalSyntheticLambda0;->()V Lorg/meshtastic/core/resources/String16_commonMainKt$$ExternalSyntheticLambda1; @@ -42332,7 +42645,6 @@ Lorg/meshtastic/core/resources/String16_commonMainKt$$ExternalSyntheticLambda20; HSPLorg/meshtastic/core/resources/String16_commonMainKt$$ExternalSyntheticLambda20;->()V Lorg/meshtastic/core/resources/String16_commonMainKt$$ExternalSyntheticLambda21; HSPLorg/meshtastic/core/resources/String16_commonMainKt$$ExternalSyntheticLambda21;->()V -HSPLorg/meshtastic/core/resources/String16_commonMainKt$$ExternalSyntheticLambda21;->invoke()Ljava/lang/Object; Lorg/meshtastic/core/resources/String16_commonMainKt$$ExternalSyntheticLambda22; HSPLorg/meshtastic/core/resources/String16_commonMainKt$$ExternalSyntheticLambda22;->()V Lorg/meshtastic/core/resources/String16_commonMainKt$$ExternalSyntheticLambda23; @@ -42393,6 +42705,7 @@ Lorg/meshtastic/core/resources/String16_commonMainKt$$ExternalSyntheticLambda48; HSPLorg/meshtastic/core/resources/String16_commonMainKt$$ExternalSyntheticLambda48;->()V Lorg/meshtastic/core/resources/String16_commonMainKt$$ExternalSyntheticLambda49; HSPLorg/meshtastic/core/resources/String16_commonMainKt$$ExternalSyntheticLambda49;->()V +HSPLorg/meshtastic/core/resources/String16_commonMainKt$$ExternalSyntheticLambda49;->invoke()Ljava/lang/Object; Lorg/meshtastic/core/resources/String16_commonMainKt$$ExternalSyntheticLambda5; HSPLorg/meshtastic/core/resources/String16_commonMainKt$$ExternalSyntheticLambda5;->()V Lorg/meshtastic/core/resources/String16_commonMainKt$$ExternalSyntheticLambda50; @@ -42499,11 +42812,215 @@ Lorg/meshtastic/core/resources/String16_commonMainKt$$ExternalSyntheticLambda96; HSPLorg/meshtastic/core/resources/String16_commonMainKt$$ExternalSyntheticLambda96;->()V Lorg/meshtastic/core/resources/String16_commonMainKt$$ExternalSyntheticLambda97; HSPLorg/meshtastic/core/resources/String16_commonMainKt$$ExternalSyntheticLambda97;->()V -HSPLorg/meshtastic/core/resources/String16_commonMainKt$$ExternalSyntheticLambda97;->invoke()Ljava/lang/Object; Lorg/meshtastic/core/resources/String16_commonMainKt$$ExternalSyntheticLambda98; HSPLorg/meshtastic/core/resources/String16_commonMainKt$$ExternalSyntheticLambda98;->()V Lorg/meshtastic/core/resources/String16_commonMainKt$$ExternalSyntheticLambda99; HSPLorg/meshtastic/core/resources/String16_commonMainKt$$ExternalSyntheticLambda99;->()V +Lorg/meshtastic/core/resources/String17_commonMainKt; +HSPLorg/meshtastic/core/resources/String17_commonMainKt;->()V +HSPLorg/meshtastic/core/resources/String17_commonMainKt;->getStay_connected_anywhere(Lorg/meshtastic/core/resources/Res$string;)Lorg/jetbrains/compose/resources/StringResource; +HSPLorg/meshtastic/core/resources/String17_commonMainKt;->stay_connected_anywhere_delegate$lambda$0()Lorg/jetbrains/compose/resources/StringResource; +Lorg/meshtastic/core/resources/String17_commonMainKt$$ExternalSyntheticLambda0; +HSPLorg/meshtastic/core/resources/String17_commonMainKt$$ExternalSyntheticLambda0;->()V +Lorg/meshtastic/core/resources/String17_commonMainKt$$ExternalSyntheticLambda1; +HSPLorg/meshtastic/core/resources/String17_commonMainKt$$ExternalSyntheticLambda1;->()V +Lorg/meshtastic/core/resources/String17_commonMainKt$$ExternalSyntheticLambda10; +HSPLorg/meshtastic/core/resources/String17_commonMainKt$$ExternalSyntheticLambda10;->()V +Lorg/meshtastic/core/resources/String17_commonMainKt$$ExternalSyntheticLambda11; +HSPLorg/meshtastic/core/resources/String17_commonMainKt$$ExternalSyntheticLambda11;->()V +Lorg/meshtastic/core/resources/String17_commonMainKt$$ExternalSyntheticLambda12; +HSPLorg/meshtastic/core/resources/String17_commonMainKt$$ExternalSyntheticLambda12;->()V +Lorg/meshtastic/core/resources/String17_commonMainKt$$ExternalSyntheticLambda13; +HSPLorg/meshtastic/core/resources/String17_commonMainKt$$ExternalSyntheticLambda13;->()V +Lorg/meshtastic/core/resources/String17_commonMainKt$$ExternalSyntheticLambda14; +HSPLorg/meshtastic/core/resources/String17_commonMainKt$$ExternalSyntheticLambda14;->()V +Lorg/meshtastic/core/resources/String17_commonMainKt$$ExternalSyntheticLambda15; +HSPLorg/meshtastic/core/resources/String17_commonMainKt$$ExternalSyntheticLambda15;->()V +Lorg/meshtastic/core/resources/String17_commonMainKt$$ExternalSyntheticLambda16; +HSPLorg/meshtastic/core/resources/String17_commonMainKt$$ExternalSyntheticLambda16;->()V +Lorg/meshtastic/core/resources/String17_commonMainKt$$ExternalSyntheticLambda17; +HSPLorg/meshtastic/core/resources/String17_commonMainKt$$ExternalSyntheticLambda17;->()V +Lorg/meshtastic/core/resources/String17_commonMainKt$$ExternalSyntheticLambda18; +HSPLorg/meshtastic/core/resources/String17_commonMainKt$$ExternalSyntheticLambda18;->()V +Lorg/meshtastic/core/resources/String17_commonMainKt$$ExternalSyntheticLambda19; +HSPLorg/meshtastic/core/resources/String17_commonMainKt$$ExternalSyntheticLambda19;->()V +Lorg/meshtastic/core/resources/String17_commonMainKt$$ExternalSyntheticLambda2; +HSPLorg/meshtastic/core/resources/String17_commonMainKt$$ExternalSyntheticLambda2;->()V +Lorg/meshtastic/core/resources/String17_commonMainKt$$ExternalSyntheticLambda20; +HSPLorg/meshtastic/core/resources/String17_commonMainKt$$ExternalSyntheticLambda20;->()V +Lorg/meshtastic/core/resources/String17_commonMainKt$$ExternalSyntheticLambda21; +HSPLorg/meshtastic/core/resources/String17_commonMainKt$$ExternalSyntheticLambda21;->()V +Lorg/meshtastic/core/resources/String17_commonMainKt$$ExternalSyntheticLambda22; +HSPLorg/meshtastic/core/resources/String17_commonMainKt$$ExternalSyntheticLambda22;->()V +Lorg/meshtastic/core/resources/String17_commonMainKt$$ExternalSyntheticLambda23; +HSPLorg/meshtastic/core/resources/String17_commonMainKt$$ExternalSyntheticLambda23;->()V +Lorg/meshtastic/core/resources/String17_commonMainKt$$ExternalSyntheticLambda24; +HSPLorg/meshtastic/core/resources/String17_commonMainKt$$ExternalSyntheticLambda24;->()V +Lorg/meshtastic/core/resources/String17_commonMainKt$$ExternalSyntheticLambda25; +HSPLorg/meshtastic/core/resources/String17_commonMainKt$$ExternalSyntheticLambda25;->()V +Lorg/meshtastic/core/resources/String17_commonMainKt$$ExternalSyntheticLambda26; +HSPLorg/meshtastic/core/resources/String17_commonMainKt$$ExternalSyntheticLambda26;->()V +Lorg/meshtastic/core/resources/String17_commonMainKt$$ExternalSyntheticLambda27; +HSPLorg/meshtastic/core/resources/String17_commonMainKt$$ExternalSyntheticLambda27;->()V +Lorg/meshtastic/core/resources/String17_commonMainKt$$ExternalSyntheticLambda28; +HSPLorg/meshtastic/core/resources/String17_commonMainKt$$ExternalSyntheticLambda28;->()V +Lorg/meshtastic/core/resources/String17_commonMainKt$$ExternalSyntheticLambda29; +HSPLorg/meshtastic/core/resources/String17_commonMainKt$$ExternalSyntheticLambda29;->()V +Lorg/meshtastic/core/resources/String17_commonMainKt$$ExternalSyntheticLambda3; +HSPLorg/meshtastic/core/resources/String17_commonMainKt$$ExternalSyntheticLambda3;->()V +Lorg/meshtastic/core/resources/String17_commonMainKt$$ExternalSyntheticLambda30; +HSPLorg/meshtastic/core/resources/String17_commonMainKt$$ExternalSyntheticLambda30;->()V +Lorg/meshtastic/core/resources/String17_commonMainKt$$ExternalSyntheticLambda31; +HSPLorg/meshtastic/core/resources/String17_commonMainKt$$ExternalSyntheticLambda31;->()V +Lorg/meshtastic/core/resources/String17_commonMainKt$$ExternalSyntheticLambda32; +HSPLorg/meshtastic/core/resources/String17_commonMainKt$$ExternalSyntheticLambda32;->()V +Lorg/meshtastic/core/resources/String17_commonMainKt$$ExternalSyntheticLambda33; +HSPLorg/meshtastic/core/resources/String17_commonMainKt$$ExternalSyntheticLambda33;->()V +Lorg/meshtastic/core/resources/String17_commonMainKt$$ExternalSyntheticLambda34; +HSPLorg/meshtastic/core/resources/String17_commonMainKt$$ExternalSyntheticLambda34;->()V +Lorg/meshtastic/core/resources/String17_commonMainKt$$ExternalSyntheticLambda35; +HSPLorg/meshtastic/core/resources/String17_commonMainKt$$ExternalSyntheticLambda35;->()V +Lorg/meshtastic/core/resources/String17_commonMainKt$$ExternalSyntheticLambda36; +HSPLorg/meshtastic/core/resources/String17_commonMainKt$$ExternalSyntheticLambda36;->()V +Lorg/meshtastic/core/resources/String17_commonMainKt$$ExternalSyntheticLambda37; +HSPLorg/meshtastic/core/resources/String17_commonMainKt$$ExternalSyntheticLambda37;->()V +Lorg/meshtastic/core/resources/String17_commonMainKt$$ExternalSyntheticLambda38; +HSPLorg/meshtastic/core/resources/String17_commonMainKt$$ExternalSyntheticLambda38;->()V +Lorg/meshtastic/core/resources/String17_commonMainKt$$ExternalSyntheticLambda39; +HSPLorg/meshtastic/core/resources/String17_commonMainKt$$ExternalSyntheticLambda39;->()V +Lorg/meshtastic/core/resources/String17_commonMainKt$$ExternalSyntheticLambda4; +HSPLorg/meshtastic/core/resources/String17_commonMainKt$$ExternalSyntheticLambda4;->()V +Lorg/meshtastic/core/resources/String17_commonMainKt$$ExternalSyntheticLambda40; +HSPLorg/meshtastic/core/resources/String17_commonMainKt$$ExternalSyntheticLambda40;->()V +Lorg/meshtastic/core/resources/String17_commonMainKt$$ExternalSyntheticLambda41; +HSPLorg/meshtastic/core/resources/String17_commonMainKt$$ExternalSyntheticLambda41;->()V +Lorg/meshtastic/core/resources/String17_commonMainKt$$ExternalSyntheticLambda42; +HSPLorg/meshtastic/core/resources/String17_commonMainKt$$ExternalSyntheticLambda42;->()V +Lorg/meshtastic/core/resources/String17_commonMainKt$$ExternalSyntheticLambda43; +HSPLorg/meshtastic/core/resources/String17_commonMainKt$$ExternalSyntheticLambda43;->()V +Lorg/meshtastic/core/resources/String17_commonMainKt$$ExternalSyntheticLambda44; +HSPLorg/meshtastic/core/resources/String17_commonMainKt$$ExternalSyntheticLambda44;->()V +Lorg/meshtastic/core/resources/String17_commonMainKt$$ExternalSyntheticLambda45; +HSPLorg/meshtastic/core/resources/String17_commonMainKt$$ExternalSyntheticLambda45;->()V +Lorg/meshtastic/core/resources/String17_commonMainKt$$ExternalSyntheticLambda46; +HSPLorg/meshtastic/core/resources/String17_commonMainKt$$ExternalSyntheticLambda46;->()V +Lorg/meshtastic/core/resources/String17_commonMainKt$$ExternalSyntheticLambda47; +HSPLorg/meshtastic/core/resources/String17_commonMainKt$$ExternalSyntheticLambda47;->()V +Lorg/meshtastic/core/resources/String17_commonMainKt$$ExternalSyntheticLambda48; +HSPLorg/meshtastic/core/resources/String17_commonMainKt$$ExternalSyntheticLambda48;->()V +Lorg/meshtastic/core/resources/String17_commonMainKt$$ExternalSyntheticLambda49; +HSPLorg/meshtastic/core/resources/String17_commonMainKt$$ExternalSyntheticLambda49;->()V +Lorg/meshtastic/core/resources/String17_commonMainKt$$ExternalSyntheticLambda5; +HSPLorg/meshtastic/core/resources/String17_commonMainKt$$ExternalSyntheticLambda5;->()V +HSPLorg/meshtastic/core/resources/String17_commonMainKt$$ExternalSyntheticLambda5;->invoke()Ljava/lang/Object; +Lorg/meshtastic/core/resources/String17_commonMainKt$$ExternalSyntheticLambda50; +HSPLorg/meshtastic/core/resources/String17_commonMainKt$$ExternalSyntheticLambda50;->()V +Lorg/meshtastic/core/resources/String17_commonMainKt$$ExternalSyntheticLambda51; +HSPLorg/meshtastic/core/resources/String17_commonMainKt$$ExternalSyntheticLambda51;->()V +Lorg/meshtastic/core/resources/String17_commonMainKt$$ExternalSyntheticLambda52; +HSPLorg/meshtastic/core/resources/String17_commonMainKt$$ExternalSyntheticLambda52;->()V +Lorg/meshtastic/core/resources/String17_commonMainKt$$ExternalSyntheticLambda53; +HSPLorg/meshtastic/core/resources/String17_commonMainKt$$ExternalSyntheticLambda53;->()V +Lorg/meshtastic/core/resources/String17_commonMainKt$$ExternalSyntheticLambda54; +HSPLorg/meshtastic/core/resources/String17_commonMainKt$$ExternalSyntheticLambda54;->()V +Lorg/meshtastic/core/resources/String17_commonMainKt$$ExternalSyntheticLambda55; +HSPLorg/meshtastic/core/resources/String17_commonMainKt$$ExternalSyntheticLambda55;->()V +Lorg/meshtastic/core/resources/String17_commonMainKt$$ExternalSyntheticLambda56; +HSPLorg/meshtastic/core/resources/String17_commonMainKt$$ExternalSyntheticLambda56;->()V +Lorg/meshtastic/core/resources/String17_commonMainKt$$ExternalSyntheticLambda57; +HSPLorg/meshtastic/core/resources/String17_commonMainKt$$ExternalSyntheticLambda57;->()V +Lorg/meshtastic/core/resources/String17_commonMainKt$$ExternalSyntheticLambda58; +HSPLorg/meshtastic/core/resources/String17_commonMainKt$$ExternalSyntheticLambda58;->()V +Lorg/meshtastic/core/resources/String17_commonMainKt$$ExternalSyntheticLambda59; +HSPLorg/meshtastic/core/resources/String17_commonMainKt$$ExternalSyntheticLambda59;->()V +Lorg/meshtastic/core/resources/String17_commonMainKt$$ExternalSyntheticLambda6; +HSPLorg/meshtastic/core/resources/String17_commonMainKt$$ExternalSyntheticLambda6;->()V +Lorg/meshtastic/core/resources/String17_commonMainKt$$ExternalSyntheticLambda60; +HSPLorg/meshtastic/core/resources/String17_commonMainKt$$ExternalSyntheticLambda60;->()V +Lorg/meshtastic/core/resources/String17_commonMainKt$$ExternalSyntheticLambda61; +HSPLorg/meshtastic/core/resources/String17_commonMainKt$$ExternalSyntheticLambda61;->()V +Lorg/meshtastic/core/resources/String17_commonMainKt$$ExternalSyntheticLambda62; +HSPLorg/meshtastic/core/resources/String17_commonMainKt$$ExternalSyntheticLambda62;->()V +Lorg/meshtastic/core/resources/String17_commonMainKt$$ExternalSyntheticLambda63; +HSPLorg/meshtastic/core/resources/String17_commonMainKt$$ExternalSyntheticLambda63;->()V +Lorg/meshtastic/core/resources/String17_commonMainKt$$ExternalSyntheticLambda64; +HSPLorg/meshtastic/core/resources/String17_commonMainKt$$ExternalSyntheticLambda64;->()V +Lorg/meshtastic/core/resources/String17_commonMainKt$$ExternalSyntheticLambda65; +HSPLorg/meshtastic/core/resources/String17_commonMainKt$$ExternalSyntheticLambda65;->()V +Lorg/meshtastic/core/resources/String17_commonMainKt$$ExternalSyntheticLambda66; +HSPLorg/meshtastic/core/resources/String17_commonMainKt$$ExternalSyntheticLambda66;->()V +Lorg/meshtastic/core/resources/String17_commonMainKt$$ExternalSyntheticLambda67; +HSPLorg/meshtastic/core/resources/String17_commonMainKt$$ExternalSyntheticLambda67;->()V +Lorg/meshtastic/core/resources/String17_commonMainKt$$ExternalSyntheticLambda68; +HSPLorg/meshtastic/core/resources/String17_commonMainKt$$ExternalSyntheticLambda68;->()V +Lorg/meshtastic/core/resources/String17_commonMainKt$$ExternalSyntheticLambda69; +HSPLorg/meshtastic/core/resources/String17_commonMainKt$$ExternalSyntheticLambda69;->()V +Lorg/meshtastic/core/resources/String17_commonMainKt$$ExternalSyntheticLambda7; +HSPLorg/meshtastic/core/resources/String17_commonMainKt$$ExternalSyntheticLambda7;->()V +Lorg/meshtastic/core/resources/String17_commonMainKt$$ExternalSyntheticLambda70; +HSPLorg/meshtastic/core/resources/String17_commonMainKt$$ExternalSyntheticLambda70;->()V +Lorg/meshtastic/core/resources/String17_commonMainKt$$ExternalSyntheticLambda71; +HSPLorg/meshtastic/core/resources/String17_commonMainKt$$ExternalSyntheticLambda71;->()V +Lorg/meshtastic/core/resources/String17_commonMainKt$$ExternalSyntheticLambda72; +HSPLorg/meshtastic/core/resources/String17_commonMainKt$$ExternalSyntheticLambda72;->()V +Lorg/meshtastic/core/resources/String17_commonMainKt$$ExternalSyntheticLambda73; +HSPLorg/meshtastic/core/resources/String17_commonMainKt$$ExternalSyntheticLambda73;->()V +Lorg/meshtastic/core/resources/String17_commonMainKt$$ExternalSyntheticLambda74; +HSPLorg/meshtastic/core/resources/String17_commonMainKt$$ExternalSyntheticLambda74;->()V +Lorg/meshtastic/core/resources/String17_commonMainKt$$ExternalSyntheticLambda75; +HSPLorg/meshtastic/core/resources/String17_commonMainKt$$ExternalSyntheticLambda75;->()V +Lorg/meshtastic/core/resources/String17_commonMainKt$$ExternalSyntheticLambda76; +HSPLorg/meshtastic/core/resources/String17_commonMainKt$$ExternalSyntheticLambda76;->()V +Lorg/meshtastic/core/resources/String17_commonMainKt$$ExternalSyntheticLambda77; +HSPLorg/meshtastic/core/resources/String17_commonMainKt$$ExternalSyntheticLambda77;->()V +Lorg/meshtastic/core/resources/String17_commonMainKt$$ExternalSyntheticLambda78; +HSPLorg/meshtastic/core/resources/String17_commonMainKt$$ExternalSyntheticLambda78;->()V +Lorg/meshtastic/core/resources/String17_commonMainKt$$ExternalSyntheticLambda79; +HSPLorg/meshtastic/core/resources/String17_commonMainKt$$ExternalSyntheticLambda79;->()V +Lorg/meshtastic/core/resources/String17_commonMainKt$$ExternalSyntheticLambda8; +HSPLorg/meshtastic/core/resources/String17_commonMainKt$$ExternalSyntheticLambda8;->()V +Lorg/meshtastic/core/resources/String17_commonMainKt$$ExternalSyntheticLambda80; +HSPLorg/meshtastic/core/resources/String17_commonMainKt$$ExternalSyntheticLambda80;->()V +Lorg/meshtastic/core/resources/String17_commonMainKt$$ExternalSyntheticLambda81; +HSPLorg/meshtastic/core/resources/String17_commonMainKt$$ExternalSyntheticLambda81;->()V +Lorg/meshtastic/core/resources/String17_commonMainKt$$ExternalSyntheticLambda82; +HSPLorg/meshtastic/core/resources/String17_commonMainKt$$ExternalSyntheticLambda82;->()V +Lorg/meshtastic/core/resources/String17_commonMainKt$$ExternalSyntheticLambda83; +HSPLorg/meshtastic/core/resources/String17_commonMainKt$$ExternalSyntheticLambda83;->()V +Lorg/meshtastic/core/resources/String17_commonMainKt$$ExternalSyntheticLambda84; +HSPLorg/meshtastic/core/resources/String17_commonMainKt$$ExternalSyntheticLambda84;->()V +Lorg/meshtastic/core/resources/String17_commonMainKt$$ExternalSyntheticLambda85; +HSPLorg/meshtastic/core/resources/String17_commonMainKt$$ExternalSyntheticLambda85;->()V +Lorg/meshtastic/core/resources/String17_commonMainKt$$ExternalSyntheticLambda86; +HSPLorg/meshtastic/core/resources/String17_commonMainKt$$ExternalSyntheticLambda86;->()V +Lorg/meshtastic/core/resources/String17_commonMainKt$$ExternalSyntheticLambda87; +HSPLorg/meshtastic/core/resources/String17_commonMainKt$$ExternalSyntheticLambda87;->()V +Lorg/meshtastic/core/resources/String17_commonMainKt$$ExternalSyntheticLambda88; +HSPLorg/meshtastic/core/resources/String17_commonMainKt$$ExternalSyntheticLambda88;->()V +Lorg/meshtastic/core/resources/String17_commonMainKt$$ExternalSyntheticLambda89; +HSPLorg/meshtastic/core/resources/String17_commonMainKt$$ExternalSyntheticLambda89;->()V +Lorg/meshtastic/core/resources/String17_commonMainKt$$ExternalSyntheticLambda9; +HSPLorg/meshtastic/core/resources/String17_commonMainKt$$ExternalSyntheticLambda9;->()V +Lorg/meshtastic/core/resources/String17_commonMainKt$$ExternalSyntheticLambda90; +HSPLorg/meshtastic/core/resources/String17_commonMainKt$$ExternalSyntheticLambda90;->()V +Lorg/meshtastic/core/resources/String17_commonMainKt$$ExternalSyntheticLambda91; +HSPLorg/meshtastic/core/resources/String17_commonMainKt$$ExternalSyntheticLambda91;->()V +Lorg/meshtastic/core/resources/String17_commonMainKt$$ExternalSyntheticLambda92; +HSPLorg/meshtastic/core/resources/String17_commonMainKt$$ExternalSyntheticLambda92;->()V +Lorg/meshtastic/core/resources/String17_commonMainKt$$ExternalSyntheticLambda93; +HSPLorg/meshtastic/core/resources/String17_commonMainKt$$ExternalSyntheticLambda93;->()V +Lorg/meshtastic/core/resources/String17_commonMainKt$$ExternalSyntheticLambda94; +HSPLorg/meshtastic/core/resources/String17_commonMainKt$$ExternalSyntheticLambda94;->()V +Lorg/meshtastic/core/resources/String17_commonMainKt$$ExternalSyntheticLambda95; +HSPLorg/meshtastic/core/resources/String17_commonMainKt$$ExternalSyntheticLambda95;->()V +Lorg/meshtastic/core/resources/String17_commonMainKt$$ExternalSyntheticLambda96; +HSPLorg/meshtastic/core/resources/String17_commonMainKt$$ExternalSyntheticLambda96;->()V +Lorg/meshtastic/core/resources/String17_commonMainKt$$ExternalSyntheticLambda97; +HSPLorg/meshtastic/core/resources/String17_commonMainKt$$ExternalSyntheticLambda97;->()V +Lorg/meshtastic/core/resources/String17_commonMainKt$$ExternalSyntheticLambda98; +HSPLorg/meshtastic/core/resources/String17_commonMainKt$$ExternalSyntheticLambda98;->()V +Lorg/meshtastic/core/resources/String17_commonMainKt$$ExternalSyntheticLambda99; +HSPLorg/meshtastic/core/resources/String17_commonMainKt$$ExternalSyntheticLambda99;->()V Lorg/meshtastic/core/resources/String18_commonMainKt; HSPLorg/meshtastic/core/resources/String18_commonMainKt;->()V HSPLorg/meshtastic/core/resources/String18_commonMainKt;->getTrack_and_share_locations(Lorg/meshtastic/core/resources/Res$string;)Lorg/jetbrains/compose/resources/StringResource; @@ -42522,7 +43039,6 @@ Lorg/meshtastic/core/resources/String18_commonMainKt$$ExternalSyntheticLambda13; HSPLorg/meshtastic/core/resources/String18_commonMainKt$$ExternalSyntheticLambda13;->()V Lorg/meshtastic/core/resources/String18_commonMainKt$$ExternalSyntheticLambda14; HSPLorg/meshtastic/core/resources/String18_commonMainKt$$ExternalSyntheticLambda14;->()V -HSPLorg/meshtastic/core/resources/String18_commonMainKt$$ExternalSyntheticLambda14;->invoke()Ljava/lang/Object; Lorg/meshtastic/core/resources/String18_commonMainKt$$ExternalSyntheticLambda15; HSPLorg/meshtastic/core/resources/String18_commonMainKt$$ExternalSyntheticLambda15;->()V Lorg/meshtastic/core/resources/String18_commonMainKt$$ExternalSyntheticLambda16; @@ -42667,6 +43183,7 @@ Lorg/meshtastic/core/resources/String18_commonMainKt$$ExternalSyntheticLambda79; HSPLorg/meshtastic/core/resources/String18_commonMainKt$$ExternalSyntheticLambda79;->()V Lorg/meshtastic/core/resources/String18_commonMainKt$$ExternalSyntheticLambda8; HSPLorg/meshtastic/core/resources/String18_commonMainKt$$ExternalSyntheticLambda8;->()V +HSPLorg/meshtastic/core/resources/String18_commonMainKt$$ExternalSyntheticLambda8;->invoke()Ljava/lang/Object; Lorg/meshtastic/core/resources/String18_commonMainKt$$ExternalSyntheticLambda80; HSPLorg/meshtastic/core/resources/String18_commonMainKt$$ExternalSyntheticLambda80;->()V Lorg/meshtastic/core/resources/String18_commonMainKt$$ExternalSyntheticLambda81; @@ -42719,19 +43236,20 @@ Lorg/meshtastic/core/resources/String2_commonMainKt$$ExternalSyntheticLambda0; HSPLorg/meshtastic/core/resources/String2_commonMainKt$$ExternalSyntheticLambda0;->()V Lorg/meshtastic/core/resources/String2_commonMainKt$$ExternalSyntheticLambda1; HSPLorg/meshtastic/core/resources/String2_commonMainKt$$ExternalSyntheticLambda1;->()V +HSPLorg/meshtastic/core/resources/String2_commonMainKt$$ExternalSyntheticLambda1;->invoke()Ljava/lang/Object; Lorg/meshtastic/core/resources/String2_commonMainKt$$ExternalSyntheticLambda10; HSPLorg/meshtastic/core/resources/String2_commonMainKt$$ExternalSyntheticLambda10;->()V Lorg/meshtastic/core/resources/String2_commonMainKt$$ExternalSyntheticLambda11; HSPLorg/meshtastic/core/resources/String2_commonMainKt$$ExternalSyntheticLambda11;->()V Lorg/meshtastic/core/resources/String2_commonMainKt$$ExternalSyntheticLambda12; HSPLorg/meshtastic/core/resources/String2_commonMainKt$$ExternalSyntheticLambda12;->()V -HSPLorg/meshtastic/core/resources/String2_commonMainKt$$ExternalSyntheticLambda12;->invoke()Ljava/lang/Object; Lorg/meshtastic/core/resources/String2_commonMainKt$$ExternalSyntheticLambda13; HSPLorg/meshtastic/core/resources/String2_commonMainKt$$ExternalSyntheticLambda13;->()V Lorg/meshtastic/core/resources/String2_commonMainKt$$ExternalSyntheticLambda14; HSPLorg/meshtastic/core/resources/String2_commonMainKt$$ExternalSyntheticLambda14;->()V Lorg/meshtastic/core/resources/String2_commonMainKt$$ExternalSyntheticLambda15; HSPLorg/meshtastic/core/resources/String2_commonMainKt$$ExternalSyntheticLambda15;->()V +HSPLorg/meshtastic/core/resources/String2_commonMainKt$$ExternalSyntheticLambda15;->invoke()Ljava/lang/Object; Lorg/meshtastic/core/resources/String2_commonMainKt$$ExternalSyntheticLambda16; HSPLorg/meshtastic/core/resources/String2_commonMainKt$$ExternalSyntheticLambda16;->()V Lorg/meshtastic/core/resources/String2_commonMainKt$$ExternalSyntheticLambda17; @@ -42892,7 +43410,6 @@ Lorg/meshtastic/core/resources/String2_commonMainKt$$ExternalSyntheticLambda87; HSPLorg/meshtastic/core/resources/String2_commonMainKt$$ExternalSyntheticLambda87;->()V Lorg/meshtastic/core/resources/String2_commonMainKt$$ExternalSyntheticLambda88; HSPLorg/meshtastic/core/resources/String2_commonMainKt$$ExternalSyntheticLambda88;->()V -HSPLorg/meshtastic/core/resources/String2_commonMainKt$$ExternalSyntheticLambda88;->invoke()Ljava/lang/Object; Lorg/meshtastic/core/resources/String2_commonMainKt$$ExternalSyntheticLambda89; HSPLorg/meshtastic/core/resources/String2_commonMainKt$$ExternalSyntheticLambda89;->()V Lorg/meshtastic/core/resources/String2_commonMainKt$$ExternalSyntheticLambda9; @@ -42921,10 +43438,8 @@ Lorg/meshtastic/core/resources/String3_commonMainKt; HSPLorg/meshtastic/core/resources/String3_commonMainKt;->()V HSPLorg/meshtastic/core/resources/String3_commonMainKt;->create_your_own_networks_delegate$lambda$0()Lorg/jetbrains/compose/resources/StringResource; HSPLorg/meshtastic/core/resources/String3_commonMainKt;->datadog_link_delegate$lambda$0()Lorg/jetbrains/compose/resources/StringResource; -HSPLorg/meshtastic/core/resources/String3_commonMainKt;->disconnected_delegate$lambda$0()Lorg/jetbrains/compose/resources/StringResource; HSPLorg/meshtastic/core/resources/String3_commonMainKt;->getCreate_your_own_networks(Lorg/meshtastic/core/resources/Res$string;)Lorg/jetbrains/compose/resources/StringResource; HSPLorg/meshtastic/core/resources/String3_commonMainKt;->getDatadog_link(Lorg/meshtastic/core/resources/Res$string;)Lorg/jetbrains/compose/resources/StringResource; -HSPLorg/meshtastic/core/resources/String3_commonMainKt;->getDisconnected(Lorg/meshtastic/core/resources/Res$string;)Lorg/jetbrains/compose/resources/StringResource; Lorg/meshtastic/core/resources/String3_commonMainKt$$ExternalSyntheticLambda0; HSPLorg/meshtastic/core/resources/String3_commonMainKt$$ExternalSyntheticLambda0;->()V Lorg/meshtastic/core/resources/String3_commonMainKt$$ExternalSyntheticLambda1; @@ -42933,13 +43448,13 @@ Lorg/meshtastic/core/resources/String3_commonMainKt$$ExternalSyntheticLambda10; HSPLorg/meshtastic/core/resources/String3_commonMainKt$$ExternalSyntheticLambda10;->()V Lorg/meshtastic/core/resources/String3_commonMainKt$$ExternalSyntheticLambda11; HSPLorg/meshtastic/core/resources/String3_commonMainKt$$ExternalSyntheticLambda11;->()V +HSPLorg/meshtastic/core/resources/String3_commonMainKt$$ExternalSyntheticLambda11;->invoke()Ljava/lang/Object; Lorg/meshtastic/core/resources/String3_commonMainKt$$ExternalSyntheticLambda12; HSPLorg/meshtastic/core/resources/String3_commonMainKt$$ExternalSyntheticLambda12;->()V Lorg/meshtastic/core/resources/String3_commonMainKt$$ExternalSyntheticLambda13; HSPLorg/meshtastic/core/resources/String3_commonMainKt$$ExternalSyntheticLambda13;->()V Lorg/meshtastic/core/resources/String3_commonMainKt$$ExternalSyntheticLambda14; HSPLorg/meshtastic/core/resources/String3_commonMainKt$$ExternalSyntheticLambda14;->()V -HSPLorg/meshtastic/core/resources/String3_commonMainKt$$ExternalSyntheticLambda14;->invoke()Ljava/lang/Object; Lorg/meshtastic/core/resources/String3_commonMainKt$$ExternalSyntheticLambda15; HSPLorg/meshtastic/core/resources/String3_commonMainKt$$ExternalSyntheticLambda15;->()V Lorg/meshtastic/core/resources/String3_commonMainKt$$ExternalSyntheticLambda16; @@ -42992,6 +43507,7 @@ Lorg/meshtastic/core/resources/String3_commonMainKt$$ExternalSyntheticLambda37; HSPLorg/meshtastic/core/resources/String3_commonMainKt$$ExternalSyntheticLambda37;->()V Lorg/meshtastic/core/resources/String3_commonMainKt$$ExternalSyntheticLambda38; HSPLorg/meshtastic/core/resources/String3_commonMainKt$$ExternalSyntheticLambda38;->()V +HSPLorg/meshtastic/core/resources/String3_commonMainKt$$ExternalSyntheticLambda38;->invoke()Ljava/lang/Object; Lorg/meshtastic/core/resources/String3_commonMainKt$$ExternalSyntheticLambda39; HSPLorg/meshtastic/core/resources/String3_commonMainKt$$ExternalSyntheticLambda39;->()V Lorg/meshtastic/core/resources/String3_commonMainKt$$ExternalSyntheticLambda4; @@ -43098,7 +43614,6 @@ Lorg/meshtastic/core/resources/String3_commonMainKt$$ExternalSyntheticLambda85; HSPLorg/meshtastic/core/resources/String3_commonMainKt$$ExternalSyntheticLambda85;->()V Lorg/meshtastic/core/resources/String3_commonMainKt$$ExternalSyntheticLambda86; HSPLorg/meshtastic/core/resources/String3_commonMainKt$$ExternalSyntheticLambda86;->()V -HSPLorg/meshtastic/core/resources/String3_commonMainKt$$ExternalSyntheticLambda86;->invoke()Ljava/lang/Object; Lorg/meshtastic/core/resources/String3_commonMainKt$$ExternalSyntheticLambda87; HSPLorg/meshtastic/core/resources/String3_commonMainKt$$ExternalSyntheticLambda87;->()V Lorg/meshtastic/core/resources/String3_commonMainKt$$ExternalSyntheticLambda88; @@ -43123,11 +43638,215 @@ Lorg/meshtastic/core/resources/String3_commonMainKt$$ExternalSyntheticLambda96; HSPLorg/meshtastic/core/resources/String3_commonMainKt$$ExternalSyntheticLambda96;->()V Lorg/meshtastic/core/resources/String3_commonMainKt$$ExternalSyntheticLambda97; HSPLorg/meshtastic/core/resources/String3_commonMainKt$$ExternalSyntheticLambda97;->()V -HSPLorg/meshtastic/core/resources/String3_commonMainKt$$ExternalSyntheticLambda97;->invoke()Ljava/lang/Object; Lorg/meshtastic/core/resources/String3_commonMainKt$$ExternalSyntheticLambda98; HSPLorg/meshtastic/core/resources/String3_commonMainKt$$ExternalSyntheticLambda98;->()V Lorg/meshtastic/core/resources/String3_commonMainKt$$ExternalSyntheticLambda99; HSPLorg/meshtastic/core/resources/String3_commonMainKt$$ExternalSyntheticLambda99;->()V +Lorg/meshtastic/core/resources/String4_commonMainKt; +HSPLorg/meshtastic/core/resources/String4_commonMainKt;->()V +HSPLorg/meshtastic/core/resources/String4_commonMainKt;->disconnected_delegate$lambda$0()Lorg/jetbrains/compose/resources/StringResource; +HSPLorg/meshtastic/core/resources/String4_commonMainKt;->getDisconnected(Lorg/meshtastic/core/resources/Res$string;)Lorg/jetbrains/compose/resources/StringResource; +Lorg/meshtastic/core/resources/String4_commonMainKt$$ExternalSyntheticLambda0; +HSPLorg/meshtastic/core/resources/String4_commonMainKt$$ExternalSyntheticLambda0;->()V +Lorg/meshtastic/core/resources/String4_commonMainKt$$ExternalSyntheticLambda1; +HSPLorg/meshtastic/core/resources/String4_commonMainKt$$ExternalSyntheticLambda1;->()V +Lorg/meshtastic/core/resources/String4_commonMainKt$$ExternalSyntheticLambda10; +HSPLorg/meshtastic/core/resources/String4_commonMainKt$$ExternalSyntheticLambda10;->()V +Lorg/meshtastic/core/resources/String4_commonMainKt$$ExternalSyntheticLambda11; +HSPLorg/meshtastic/core/resources/String4_commonMainKt$$ExternalSyntheticLambda11;->()V +Lorg/meshtastic/core/resources/String4_commonMainKt$$ExternalSyntheticLambda12; +HSPLorg/meshtastic/core/resources/String4_commonMainKt$$ExternalSyntheticLambda12;->()V +Lorg/meshtastic/core/resources/String4_commonMainKt$$ExternalSyntheticLambda13; +HSPLorg/meshtastic/core/resources/String4_commonMainKt$$ExternalSyntheticLambda13;->()V +Lorg/meshtastic/core/resources/String4_commonMainKt$$ExternalSyntheticLambda14; +HSPLorg/meshtastic/core/resources/String4_commonMainKt$$ExternalSyntheticLambda14;->()V +Lorg/meshtastic/core/resources/String4_commonMainKt$$ExternalSyntheticLambda15; +HSPLorg/meshtastic/core/resources/String4_commonMainKt$$ExternalSyntheticLambda15;->()V +Lorg/meshtastic/core/resources/String4_commonMainKt$$ExternalSyntheticLambda16; +HSPLorg/meshtastic/core/resources/String4_commonMainKt$$ExternalSyntheticLambda16;->()V +Lorg/meshtastic/core/resources/String4_commonMainKt$$ExternalSyntheticLambda17; +HSPLorg/meshtastic/core/resources/String4_commonMainKt$$ExternalSyntheticLambda17;->()V +Lorg/meshtastic/core/resources/String4_commonMainKt$$ExternalSyntheticLambda18; +HSPLorg/meshtastic/core/resources/String4_commonMainKt$$ExternalSyntheticLambda18;->()V +Lorg/meshtastic/core/resources/String4_commonMainKt$$ExternalSyntheticLambda19; +HSPLorg/meshtastic/core/resources/String4_commonMainKt$$ExternalSyntheticLambda19;->()V +Lorg/meshtastic/core/resources/String4_commonMainKt$$ExternalSyntheticLambda2; +HSPLorg/meshtastic/core/resources/String4_commonMainKt$$ExternalSyntheticLambda2;->()V +HSPLorg/meshtastic/core/resources/String4_commonMainKt$$ExternalSyntheticLambda2;->invoke()Ljava/lang/Object; +Lorg/meshtastic/core/resources/String4_commonMainKt$$ExternalSyntheticLambda20; +HSPLorg/meshtastic/core/resources/String4_commonMainKt$$ExternalSyntheticLambda20;->()V +Lorg/meshtastic/core/resources/String4_commonMainKt$$ExternalSyntheticLambda21; +HSPLorg/meshtastic/core/resources/String4_commonMainKt$$ExternalSyntheticLambda21;->()V +Lorg/meshtastic/core/resources/String4_commonMainKt$$ExternalSyntheticLambda22; +HSPLorg/meshtastic/core/resources/String4_commonMainKt$$ExternalSyntheticLambda22;->()V +Lorg/meshtastic/core/resources/String4_commonMainKt$$ExternalSyntheticLambda23; +HSPLorg/meshtastic/core/resources/String4_commonMainKt$$ExternalSyntheticLambda23;->()V +Lorg/meshtastic/core/resources/String4_commonMainKt$$ExternalSyntheticLambda24; +HSPLorg/meshtastic/core/resources/String4_commonMainKt$$ExternalSyntheticLambda24;->()V +Lorg/meshtastic/core/resources/String4_commonMainKt$$ExternalSyntheticLambda25; +HSPLorg/meshtastic/core/resources/String4_commonMainKt$$ExternalSyntheticLambda25;->()V +Lorg/meshtastic/core/resources/String4_commonMainKt$$ExternalSyntheticLambda26; +HSPLorg/meshtastic/core/resources/String4_commonMainKt$$ExternalSyntheticLambda26;->()V +Lorg/meshtastic/core/resources/String4_commonMainKt$$ExternalSyntheticLambda27; +HSPLorg/meshtastic/core/resources/String4_commonMainKt$$ExternalSyntheticLambda27;->()V +Lorg/meshtastic/core/resources/String4_commonMainKt$$ExternalSyntheticLambda28; +HSPLorg/meshtastic/core/resources/String4_commonMainKt$$ExternalSyntheticLambda28;->()V +Lorg/meshtastic/core/resources/String4_commonMainKt$$ExternalSyntheticLambda29; +HSPLorg/meshtastic/core/resources/String4_commonMainKt$$ExternalSyntheticLambda29;->()V +Lorg/meshtastic/core/resources/String4_commonMainKt$$ExternalSyntheticLambda3; +HSPLorg/meshtastic/core/resources/String4_commonMainKt$$ExternalSyntheticLambda3;->()V +Lorg/meshtastic/core/resources/String4_commonMainKt$$ExternalSyntheticLambda30; +HSPLorg/meshtastic/core/resources/String4_commonMainKt$$ExternalSyntheticLambda30;->()V +Lorg/meshtastic/core/resources/String4_commonMainKt$$ExternalSyntheticLambda31; +HSPLorg/meshtastic/core/resources/String4_commonMainKt$$ExternalSyntheticLambda31;->()V +Lorg/meshtastic/core/resources/String4_commonMainKt$$ExternalSyntheticLambda32; +HSPLorg/meshtastic/core/resources/String4_commonMainKt$$ExternalSyntheticLambda32;->()V +Lorg/meshtastic/core/resources/String4_commonMainKt$$ExternalSyntheticLambda33; +HSPLorg/meshtastic/core/resources/String4_commonMainKt$$ExternalSyntheticLambda33;->()V +Lorg/meshtastic/core/resources/String4_commonMainKt$$ExternalSyntheticLambda34; +HSPLorg/meshtastic/core/resources/String4_commonMainKt$$ExternalSyntheticLambda34;->()V +Lorg/meshtastic/core/resources/String4_commonMainKt$$ExternalSyntheticLambda35; +HSPLorg/meshtastic/core/resources/String4_commonMainKt$$ExternalSyntheticLambda35;->()V +Lorg/meshtastic/core/resources/String4_commonMainKt$$ExternalSyntheticLambda36; +HSPLorg/meshtastic/core/resources/String4_commonMainKt$$ExternalSyntheticLambda36;->()V +Lorg/meshtastic/core/resources/String4_commonMainKt$$ExternalSyntheticLambda37; +HSPLorg/meshtastic/core/resources/String4_commonMainKt$$ExternalSyntheticLambda37;->()V +Lorg/meshtastic/core/resources/String4_commonMainKt$$ExternalSyntheticLambda38; +HSPLorg/meshtastic/core/resources/String4_commonMainKt$$ExternalSyntheticLambda38;->()V +Lorg/meshtastic/core/resources/String4_commonMainKt$$ExternalSyntheticLambda39; +HSPLorg/meshtastic/core/resources/String4_commonMainKt$$ExternalSyntheticLambda39;->()V +Lorg/meshtastic/core/resources/String4_commonMainKt$$ExternalSyntheticLambda4; +HSPLorg/meshtastic/core/resources/String4_commonMainKt$$ExternalSyntheticLambda4;->()V +Lorg/meshtastic/core/resources/String4_commonMainKt$$ExternalSyntheticLambda40; +HSPLorg/meshtastic/core/resources/String4_commonMainKt$$ExternalSyntheticLambda40;->()V +Lorg/meshtastic/core/resources/String4_commonMainKt$$ExternalSyntheticLambda41; +HSPLorg/meshtastic/core/resources/String4_commonMainKt$$ExternalSyntheticLambda41;->()V +Lorg/meshtastic/core/resources/String4_commonMainKt$$ExternalSyntheticLambda42; +HSPLorg/meshtastic/core/resources/String4_commonMainKt$$ExternalSyntheticLambda42;->()V +Lorg/meshtastic/core/resources/String4_commonMainKt$$ExternalSyntheticLambda43; +HSPLorg/meshtastic/core/resources/String4_commonMainKt$$ExternalSyntheticLambda43;->()V +Lorg/meshtastic/core/resources/String4_commonMainKt$$ExternalSyntheticLambda44; +HSPLorg/meshtastic/core/resources/String4_commonMainKt$$ExternalSyntheticLambda44;->()V +Lorg/meshtastic/core/resources/String4_commonMainKt$$ExternalSyntheticLambda45; +HSPLorg/meshtastic/core/resources/String4_commonMainKt$$ExternalSyntheticLambda45;->()V +Lorg/meshtastic/core/resources/String4_commonMainKt$$ExternalSyntheticLambda46; +HSPLorg/meshtastic/core/resources/String4_commonMainKt$$ExternalSyntheticLambda46;->()V +Lorg/meshtastic/core/resources/String4_commonMainKt$$ExternalSyntheticLambda47; +HSPLorg/meshtastic/core/resources/String4_commonMainKt$$ExternalSyntheticLambda47;->()V +Lorg/meshtastic/core/resources/String4_commonMainKt$$ExternalSyntheticLambda48; +HSPLorg/meshtastic/core/resources/String4_commonMainKt$$ExternalSyntheticLambda48;->()V +Lorg/meshtastic/core/resources/String4_commonMainKt$$ExternalSyntheticLambda49; +HSPLorg/meshtastic/core/resources/String4_commonMainKt$$ExternalSyntheticLambda49;->()V +Lorg/meshtastic/core/resources/String4_commonMainKt$$ExternalSyntheticLambda5; +HSPLorg/meshtastic/core/resources/String4_commonMainKt$$ExternalSyntheticLambda5;->()V +Lorg/meshtastic/core/resources/String4_commonMainKt$$ExternalSyntheticLambda50; +HSPLorg/meshtastic/core/resources/String4_commonMainKt$$ExternalSyntheticLambda50;->()V +Lorg/meshtastic/core/resources/String4_commonMainKt$$ExternalSyntheticLambda51; +HSPLorg/meshtastic/core/resources/String4_commonMainKt$$ExternalSyntheticLambda51;->()V +Lorg/meshtastic/core/resources/String4_commonMainKt$$ExternalSyntheticLambda52; +HSPLorg/meshtastic/core/resources/String4_commonMainKt$$ExternalSyntheticLambda52;->()V +Lorg/meshtastic/core/resources/String4_commonMainKt$$ExternalSyntheticLambda53; +HSPLorg/meshtastic/core/resources/String4_commonMainKt$$ExternalSyntheticLambda53;->()V +Lorg/meshtastic/core/resources/String4_commonMainKt$$ExternalSyntheticLambda54; +HSPLorg/meshtastic/core/resources/String4_commonMainKt$$ExternalSyntheticLambda54;->()V +Lorg/meshtastic/core/resources/String4_commonMainKt$$ExternalSyntheticLambda55; +HSPLorg/meshtastic/core/resources/String4_commonMainKt$$ExternalSyntheticLambda55;->()V +Lorg/meshtastic/core/resources/String4_commonMainKt$$ExternalSyntheticLambda56; +HSPLorg/meshtastic/core/resources/String4_commonMainKt$$ExternalSyntheticLambda56;->()V +Lorg/meshtastic/core/resources/String4_commonMainKt$$ExternalSyntheticLambda57; +HSPLorg/meshtastic/core/resources/String4_commonMainKt$$ExternalSyntheticLambda57;->()V +Lorg/meshtastic/core/resources/String4_commonMainKt$$ExternalSyntheticLambda58; +HSPLorg/meshtastic/core/resources/String4_commonMainKt$$ExternalSyntheticLambda58;->()V +Lorg/meshtastic/core/resources/String4_commonMainKt$$ExternalSyntheticLambda59; +HSPLorg/meshtastic/core/resources/String4_commonMainKt$$ExternalSyntheticLambda59;->()V +Lorg/meshtastic/core/resources/String4_commonMainKt$$ExternalSyntheticLambda6; +HSPLorg/meshtastic/core/resources/String4_commonMainKt$$ExternalSyntheticLambda6;->()V +Lorg/meshtastic/core/resources/String4_commonMainKt$$ExternalSyntheticLambda60; +HSPLorg/meshtastic/core/resources/String4_commonMainKt$$ExternalSyntheticLambda60;->()V +Lorg/meshtastic/core/resources/String4_commonMainKt$$ExternalSyntheticLambda61; +HSPLorg/meshtastic/core/resources/String4_commonMainKt$$ExternalSyntheticLambda61;->()V +Lorg/meshtastic/core/resources/String4_commonMainKt$$ExternalSyntheticLambda62; +HSPLorg/meshtastic/core/resources/String4_commonMainKt$$ExternalSyntheticLambda62;->()V +Lorg/meshtastic/core/resources/String4_commonMainKt$$ExternalSyntheticLambda63; +HSPLorg/meshtastic/core/resources/String4_commonMainKt$$ExternalSyntheticLambda63;->()V +Lorg/meshtastic/core/resources/String4_commonMainKt$$ExternalSyntheticLambda64; +HSPLorg/meshtastic/core/resources/String4_commonMainKt$$ExternalSyntheticLambda64;->()V +Lorg/meshtastic/core/resources/String4_commonMainKt$$ExternalSyntheticLambda65; +HSPLorg/meshtastic/core/resources/String4_commonMainKt$$ExternalSyntheticLambda65;->()V +Lorg/meshtastic/core/resources/String4_commonMainKt$$ExternalSyntheticLambda66; +HSPLorg/meshtastic/core/resources/String4_commonMainKt$$ExternalSyntheticLambda66;->()V +Lorg/meshtastic/core/resources/String4_commonMainKt$$ExternalSyntheticLambda67; +HSPLorg/meshtastic/core/resources/String4_commonMainKt$$ExternalSyntheticLambda67;->()V +Lorg/meshtastic/core/resources/String4_commonMainKt$$ExternalSyntheticLambda68; +HSPLorg/meshtastic/core/resources/String4_commonMainKt$$ExternalSyntheticLambda68;->()V +Lorg/meshtastic/core/resources/String4_commonMainKt$$ExternalSyntheticLambda69; +HSPLorg/meshtastic/core/resources/String4_commonMainKt$$ExternalSyntheticLambda69;->()V +Lorg/meshtastic/core/resources/String4_commonMainKt$$ExternalSyntheticLambda7; +HSPLorg/meshtastic/core/resources/String4_commonMainKt$$ExternalSyntheticLambda7;->()V +Lorg/meshtastic/core/resources/String4_commonMainKt$$ExternalSyntheticLambda70; +HSPLorg/meshtastic/core/resources/String4_commonMainKt$$ExternalSyntheticLambda70;->()V +Lorg/meshtastic/core/resources/String4_commonMainKt$$ExternalSyntheticLambda71; +HSPLorg/meshtastic/core/resources/String4_commonMainKt$$ExternalSyntheticLambda71;->()V +Lorg/meshtastic/core/resources/String4_commonMainKt$$ExternalSyntheticLambda72; +HSPLorg/meshtastic/core/resources/String4_commonMainKt$$ExternalSyntheticLambda72;->()V +Lorg/meshtastic/core/resources/String4_commonMainKt$$ExternalSyntheticLambda73; +HSPLorg/meshtastic/core/resources/String4_commonMainKt$$ExternalSyntheticLambda73;->()V +Lorg/meshtastic/core/resources/String4_commonMainKt$$ExternalSyntheticLambda74; +HSPLorg/meshtastic/core/resources/String4_commonMainKt$$ExternalSyntheticLambda74;->()V +Lorg/meshtastic/core/resources/String4_commonMainKt$$ExternalSyntheticLambda75; +HSPLorg/meshtastic/core/resources/String4_commonMainKt$$ExternalSyntheticLambda75;->()V +Lorg/meshtastic/core/resources/String4_commonMainKt$$ExternalSyntheticLambda76; +HSPLorg/meshtastic/core/resources/String4_commonMainKt$$ExternalSyntheticLambda76;->()V +Lorg/meshtastic/core/resources/String4_commonMainKt$$ExternalSyntheticLambda77; +HSPLorg/meshtastic/core/resources/String4_commonMainKt$$ExternalSyntheticLambda77;->()V +Lorg/meshtastic/core/resources/String4_commonMainKt$$ExternalSyntheticLambda78; +HSPLorg/meshtastic/core/resources/String4_commonMainKt$$ExternalSyntheticLambda78;->()V +Lorg/meshtastic/core/resources/String4_commonMainKt$$ExternalSyntheticLambda79; +HSPLorg/meshtastic/core/resources/String4_commonMainKt$$ExternalSyntheticLambda79;->()V +Lorg/meshtastic/core/resources/String4_commonMainKt$$ExternalSyntheticLambda8; +HSPLorg/meshtastic/core/resources/String4_commonMainKt$$ExternalSyntheticLambda8;->()V +Lorg/meshtastic/core/resources/String4_commonMainKt$$ExternalSyntheticLambda80; +HSPLorg/meshtastic/core/resources/String4_commonMainKt$$ExternalSyntheticLambda80;->()V +Lorg/meshtastic/core/resources/String4_commonMainKt$$ExternalSyntheticLambda81; +HSPLorg/meshtastic/core/resources/String4_commonMainKt$$ExternalSyntheticLambda81;->()V +Lorg/meshtastic/core/resources/String4_commonMainKt$$ExternalSyntheticLambda82; +HSPLorg/meshtastic/core/resources/String4_commonMainKt$$ExternalSyntheticLambda82;->()V +Lorg/meshtastic/core/resources/String4_commonMainKt$$ExternalSyntheticLambda83; +HSPLorg/meshtastic/core/resources/String4_commonMainKt$$ExternalSyntheticLambda83;->()V +Lorg/meshtastic/core/resources/String4_commonMainKt$$ExternalSyntheticLambda84; +HSPLorg/meshtastic/core/resources/String4_commonMainKt$$ExternalSyntheticLambda84;->()V +Lorg/meshtastic/core/resources/String4_commonMainKt$$ExternalSyntheticLambda85; +HSPLorg/meshtastic/core/resources/String4_commonMainKt$$ExternalSyntheticLambda85;->()V +Lorg/meshtastic/core/resources/String4_commonMainKt$$ExternalSyntheticLambda86; +HSPLorg/meshtastic/core/resources/String4_commonMainKt$$ExternalSyntheticLambda86;->()V +Lorg/meshtastic/core/resources/String4_commonMainKt$$ExternalSyntheticLambda87; +HSPLorg/meshtastic/core/resources/String4_commonMainKt$$ExternalSyntheticLambda87;->()V +Lorg/meshtastic/core/resources/String4_commonMainKt$$ExternalSyntheticLambda88; +HSPLorg/meshtastic/core/resources/String4_commonMainKt$$ExternalSyntheticLambda88;->()V +Lorg/meshtastic/core/resources/String4_commonMainKt$$ExternalSyntheticLambda89; +HSPLorg/meshtastic/core/resources/String4_commonMainKt$$ExternalSyntheticLambda89;->()V +Lorg/meshtastic/core/resources/String4_commonMainKt$$ExternalSyntheticLambda9; +HSPLorg/meshtastic/core/resources/String4_commonMainKt$$ExternalSyntheticLambda9;->()V +Lorg/meshtastic/core/resources/String4_commonMainKt$$ExternalSyntheticLambda90; +HSPLorg/meshtastic/core/resources/String4_commonMainKt$$ExternalSyntheticLambda90;->()V +Lorg/meshtastic/core/resources/String4_commonMainKt$$ExternalSyntheticLambda91; +HSPLorg/meshtastic/core/resources/String4_commonMainKt$$ExternalSyntheticLambda91;->()V +Lorg/meshtastic/core/resources/String4_commonMainKt$$ExternalSyntheticLambda92; +HSPLorg/meshtastic/core/resources/String4_commonMainKt$$ExternalSyntheticLambda92;->()V +Lorg/meshtastic/core/resources/String4_commonMainKt$$ExternalSyntheticLambda93; +HSPLorg/meshtastic/core/resources/String4_commonMainKt$$ExternalSyntheticLambda93;->()V +Lorg/meshtastic/core/resources/String4_commonMainKt$$ExternalSyntheticLambda94; +HSPLorg/meshtastic/core/resources/String4_commonMainKt$$ExternalSyntheticLambda94;->()V +Lorg/meshtastic/core/resources/String4_commonMainKt$$ExternalSyntheticLambda95; +HSPLorg/meshtastic/core/resources/String4_commonMainKt$$ExternalSyntheticLambda95;->()V +Lorg/meshtastic/core/resources/String4_commonMainKt$$ExternalSyntheticLambda96; +HSPLorg/meshtastic/core/resources/String4_commonMainKt$$ExternalSyntheticLambda96;->()V +Lorg/meshtastic/core/resources/String4_commonMainKt$$ExternalSyntheticLambda97; +HSPLorg/meshtastic/core/resources/String4_commonMainKt$$ExternalSyntheticLambda97;->()V +Lorg/meshtastic/core/resources/String4_commonMainKt$$ExternalSyntheticLambda98; +HSPLorg/meshtastic/core/resources/String4_commonMainKt$$ExternalSyntheticLambda98;->()V +Lorg/meshtastic/core/resources/String4_commonMainKt$$ExternalSyntheticLambda99; +HSPLorg/meshtastic/core/resources/String4_commonMainKt$$ExternalSyntheticLambda99;->()V Lorg/meshtastic/core/resources/String5_commonMainKt; HSPLorg/meshtastic/core/resources/String5_commonMainKt;->()V HSPLorg/meshtastic/core/resources/String5_commonMainKt;->easily_set_up_private_mesh_networks_delegate$lambda$0()Lorg/jetbrains/compose/resources/StringResource; @@ -43184,7 +43903,6 @@ Lorg/meshtastic/core/resources/String5_commonMainKt$$ExternalSyntheticLambda30; HSPLorg/meshtastic/core/resources/String5_commonMainKt$$ExternalSyntheticLambda30;->()V Lorg/meshtastic/core/resources/String5_commonMainKt$$ExternalSyntheticLambda31; HSPLorg/meshtastic/core/resources/String5_commonMainKt$$ExternalSyntheticLambda31;->()V -HSPLorg/meshtastic/core/resources/String5_commonMainKt$$ExternalSyntheticLambda31;->invoke()Ljava/lang/Object; Lorg/meshtastic/core/resources/String5_commonMainKt$$ExternalSyntheticLambda32; HSPLorg/meshtastic/core/resources/String5_commonMainKt$$ExternalSyntheticLambda32;->()V Lorg/meshtastic/core/resources/String5_commonMainKt$$ExternalSyntheticLambda33; @@ -43201,6 +43919,7 @@ Lorg/meshtastic/core/resources/String5_commonMainKt$$ExternalSyntheticLambda38; HSPLorg/meshtastic/core/resources/String5_commonMainKt$$ExternalSyntheticLambda38;->()V Lorg/meshtastic/core/resources/String5_commonMainKt$$ExternalSyntheticLambda39; HSPLorg/meshtastic/core/resources/String5_commonMainKt$$ExternalSyntheticLambda39;->()V +HSPLorg/meshtastic/core/resources/String5_commonMainKt$$ExternalSyntheticLambda39;->invoke()Ljava/lang/Object; Lorg/meshtastic/core/resources/String5_commonMainKt$$ExternalSyntheticLambda4; HSPLorg/meshtastic/core/resources/String5_commonMainKt$$ExternalSyntheticLambda4;->()V Lorg/meshtastic/core/resources/String5_commonMainKt$$ExternalSyntheticLambda40; @@ -43385,7 +44104,6 @@ Lorg/meshtastic/core/resources/String6_commonMainKt$$ExternalSyntheticLambda29; HSPLorg/meshtastic/core/resources/String6_commonMainKt$$ExternalSyntheticLambda29;->()V Lorg/meshtastic/core/resources/String6_commonMainKt$$ExternalSyntheticLambda3; HSPLorg/meshtastic/core/resources/String6_commonMainKt$$ExternalSyntheticLambda3;->()V -HSPLorg/meshtastic/core/resources/String6_commonMainKt$$ExternalSyntheticLambda3;->invoke()Ljava/lang/Object; Lorg/meshtastic/core/resources/String6_commonMainKt$$ExternalSyntheticLambda30; HSPLorg/meshtastic/core/resources/String6_commonMainKt$$ExternalSyntheticLambda30;->()V Lorg/meshtastic/core/resources/String6_commonMainKt$$ExternalSyntheticLambda31; @@ -43518,6 +44236,7 @@ Lorg/meshtastic/core/resources/String6_commonMainKt$$ExternalSyntheticLambda89; HSPLorg/meshtastic/core/resources/String6_commonMainKt$$ExternalSyntheticLambda89;->()V Lorg/meshtastic/core/resources/String6_commonMainKt$$ExternalSyntheticLambda9; HSPLorg/meshtastic/core/resources/String6_commonMainKt$$ExternalSyntheticLambda9;->()V +HSPLorg/meshtastic/core/resources/String6_commonMainKt$$ExternalSyntheticLambda9;->invoke()Ljava/lang/Object; Lorg/meshtastic/core/resources/String6_commonMainKt$$ExternalSyntheticLambda90; HSPLorg/meshtastic/core/resources/String6_commonMainKt$$ExternalSyntheticLambda90;->()V Lorg/meshtastic/core/resources/String6_commonMainKt$$ExternalSyntheticLambda91; @@ -43588,7 +44307,6 @@ Lorg/meshtastic/core/resources/String7_commonMainKt$$ExternalSyntheticLambda27; HSPLorg/meshtastic/core/resources/String7_commonMainKt$$ExternalSyntheticLambda27;->()V Lorg/meshtastic/core/resources/String7_commonMainKt$$ExternalSyntheticLambda28; HSPLorg/meshtastic/core/resources/String7_commonMainKt$$ExternalSyntheticLambda28;->()V -HSPLorg/meshtastic/core/resources/String7_commonMainKt$$ExternalSyntheticLambda28;->invoke()Ljava/lang/Object; Lorg/meshtastic/core/resources/String7_commonMainKt$$ExternalSyntheticLambda29; HSPLorg/meshtastic/core/resources/String7_commonMainKt$$ExternalSyntheticLambda29;->()V Lorg/meshtastic/core/resources/String7_commonMainKt$$ExternalSyntheticLambda3; @@ -43607,6 +44325,7 @@ Lorg/meshtastic/core/resources/String7_commonMainKt$$ExternalSyntheticLambda35; HSPLorg/meshtastic/core/resources/String7_commonMainKt$$ExternalSyntheticLambda35;->()V Lorg/meshtastic/core/resources/String7_commonMainKt$$ExternalSyntheticLambda36; HSPLorg/meshtastic/core/resources/String7_commonMainKt$$ExternalSyntheticLambda36;->()V +HSPLorg/meshtastic/core/resources/String7_commonMainKt$$ExternalSyntheticLambda36;->invoke()Ljava/lang/Object; Lorg/meshtastic/core/resources/String7_commonMainKt$$ExternalSyntheticLambda37; HSPLorg/meshtastic/core/resources/String7_commonMainKt$$ExternalSyntheticLambda37;->()V Lorg/meshtastic/core/resources/String7_commonMainKt$$ExternalSyntheticLambda38; @@ -43687,7 +44406,6 @@ Lorg/meshtastic/core/resources/String7_commonMainKt$$ExternalSyntheticLambda71; HSPLorg/meshtastic/core/resources/String7_commonMainKt$$ExternalSyntheticLambda71;->()V Lorg/meshtastic/core/resources/String7_commonMainKt$$ExternalSyntheticLambda72; HSPLorg/meshtastic/core/resources/String7_commonMainKt$$ExternalSyntheticLambda72;->()V -HSPLorg/meshtastic/core/resources/String7_commonMainKt$$ExternalSyntheticLambda72;->invoke()Ljava/lang/Object; Lorg/meshtastic/core/resources/String7_commonMainKt$$ExternalSyntheticLambda73; HSPLorg/meshtastic/core/resources/String7_commonMainKt$$ExternalSyntheticLambda73;->()V Lorg/meshtastic/core/resources/String7_commonMainKt$$ExternalSyntheticLambda74; @@ -43708,6 +44426,7 @@ Lorg/meshtastic/core/resources/String7_commonMainKt$$ExternalSyntheticLambda80; HSPLorg/meshtastic/core/resources/String7_commonMainKt$$ExternalSyntheticLambda80;->()V Lorg/meshtastic/core/resources/String7_commonMainKt$$ExternalSyntheticLambda81; HSPLorg/meshtastic/core/resources/String7_commonMainKt$$ExternalSyntheticLambda81;->()V +HSPLorg/meshtastic/core/resources/String7_commonMainKt$$ExternalSyntheticLambda81;->invoke()Ljava/lang/Object; Lorg/meshtastic/core/resources/String7_commonMainKt$$ExternalSyntheticLambda82; HSPLorg/meshtastic/core/resources/String7_commonMainKt$$ExternalSyntheticLambda82;->()V Lorg/meshtastic/core/resources/String7_commonMainKt$$ExternalSyntheticLambda83; @@ -43808,7 +44527,6 @@ Lorg/meshtastic/core/resources/String8_commonMainKt$$ExternalSyntheticLambda33; HSPLorg/meshtastic/core/resources/String8_commonMainKt$$ExternalSyntheticLambda33;->()V Lorg/meshtastic/core/resources/String8_commonMainKt$$ExternalSyntheticLambda34; HSPLorg/meshtastic/core/resources/String8_commonMainKt$$ExternalSyntheticLambda34;->()V -HSPLorg/meshtastic/core/resources/String8_commonMainKt$$ExternalSyntheticLambda34;->invoke()Ljava/lang/Object; Lorg/meshtastic/core/resources/String8_commonMainKt$$ExternalSyntheticLambda35; HSPLorg/meshtastic/core/resources/String8_commonMainKt$$ExternalSyntheticLambda35;->()V Lorg/meshtastic/core/resources/String8_commonMainKt$$ExternalSyntheticLambda36; @@ -43827,6 +44545,7 @@ Lorg/meshtastic/core/resources/String8_commonMainKt$$ExternalSyntheticLambda41; HSPLorg/meshtastic/core/resources/String8_commonMainKt$$ExternalSyntheticLambda41;->()V Lorg/meshtastic/core/resources/String8_commonMainKt$$ExternalSyntheticLambda42; HSPLorg/meshtastic/core/resources/String8_commonMainKt$$ExternalSyntheticLambda42;->()V +HSPLorg/meshtastic/core/resources/String8_commonMainKt$$ExternalSyntheticLambda42;->invoke()Ljava/lang/Object; Lorg/meshtastic/core/resources/String8_commonMainKt$$ExternalSyntheticLambda43; HSPLorg/meshtastic/core/resources/String8_commonMainKt$$ExternalSyntheticLambda43;->()V Lorg/meshtastic/core/resources/String8_commonMainKt$$ExternalSyntheticLambda44; @@ -44025,7 +44744,9 @@ HSPLorg/meshtastic/core/service/LockdownPassphraseStoreImpl$Companion;->() HSPLorg/meshtastic/core/service/LockdownPassphraseStoreImpl$Companion;->(Lkotlin/jvm/internal/DefaultConstructorMarker;)V Lorg/meshtastic/core/service/MeshNotificationManagerImpl; HSPLorg/meshtastic/core/service/MeshNotificationManagerImpl;->()V -HSPLorg/meshtastic/core/service/MeshNotificationManagerImpl;->(Landroid/content/Context;Lkotlin/Lazy;Lkotlin/Lazy;Lkotlin/Lazy;Lkotlin/Lazy;Lorg/meshtastic/core/common/di/ServiceScope;)V +HSPLorg/meshtastic/core/service/MeshNotificationManagerImpl;->(Landroid/content/Context;Lkotlin/Lazy;Lkotlin/Lazy;Lkotlin/Lazy;Lkotlin/Lazy;Lorg/meshtastic/core/common/state/RadioOperationLock;Lorg/meshtastic/core/common/di/ServiceScope;)V +HSPLorg/meshtastic/core/service/MeshNotificationManagerImpl;->access$getRadioOperationLock$p(Lorg/meshtastic/core/service/MeshNotificationManagerImpl;)Lorg/meshtastic/core/common/state/RadioOperationLock; +HSPLorg/meshtastic/core/service/MeshNotificationManagerImpl;->access$getServiceNotificationLock$p(Lorg/meshtastic/core/service/MeshNotificationManagerImpl;)Ljava/lang/Object; HSPLorg/meshtastic/core/service/MeshNotificationManagerImpl;->access$getServiceNotificationSnapshots$p(Lorg/meshtastic/core/service/MeshNotificationManagerImpl;)Lkotlinx/coroutines/flow/MutableSharedFlow; HSPLorg/meshtastic/core/service/MeshNotificationManagerImpl;->access$renderAndPostServiceNotification(Lorg/meshtastic/core/service/MeshNotificationManagerImpl;Lorg/meshtastic/core/service/MeshNotificationManagerImpl$ServiceNotificationSnapshot;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; HSPLorg/meshtastic/core/service/MeshNotificationManagerImpl;->access$renderServiceNotification(Lorg/meshtastic/core/service/MeshNotificationManagerImpl;Lorg/meshtastic/core/service/MeshNotificationManagerImpl$ServiceNotificationSnapshot;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; @@ -44049,6 +44770,8 @@ HSPLorg/meshtastic/core/service/MeshNotificationManagerImpl$$ExternalSyntheticLa Lorg/meshtastic/core/service/MeshNotificationManagerImpl$$ExternalSyntheticLambda1; HSPLorg/meshtastic/core/service/MeshNotificationManagerImpl$$ExternalSyntheticLambda1;->(Lorg/meshtastic/core/service/MeshNotificationManagerImpl;)V HSPLorg/meshtastic/core/service/MeshNotificationManagerImpl$$ExternalSyntheticLambda1;->invoke()Ljava/lang/Object; +Lorg/meshtastic/core/service/MeshNotificationManagerImpl$$ExternalSyntheticTypeSwitchClass2; +HSPLorg/meshtastic/core/service/MeshNotificationManagerImpl$$ExternalSyntheticTypeSwitchClass2;->switchDispatch$1(Ljava/lang/Object;I)I Lorg/meshtastic/core/service/MeshNotificationManagerImpl$1; HSPLorg/meshtastic/core/service/MeshNotificationManagerImpl$1;->(Lorg/meshtastic/core/service/MeshNotificationManagerImpl;Lkotlin/coroutines/Continuation;)V HSPLorg/meshtastic/core/service/MeshNotificationManagerImpl$1;->create(Ljava/lang/Object;Lkotlin/coroutines/Continuation;)Lkotlin/coroutines/Continuation; @@ -44057,6 +44780,14 @@ Lorg/meshtastic/core/service/MeshNotificationManagerImpl$1$1; HSPLorg/meshtastic/core/service/MeshNotificationManagerImpl$1$1;->(Ljava/lang/Object;)V HSPLorg/meshtastic/core/service/MeshNotificationManagerImpl$1$1;->invoke(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object; HSPLorg/meshtastic/core/service/MeshNotificationManagerImpl$1$1;->invoke(Lorg/meshtastic/core/service/MeshNotificationManagerImpl$ServiceNotificationSnapshot;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; +Lorg/meshtastic/core/service/MeshNotificationManagerImpl$2; +HSPLorg/meshtastic/core/service/MeshNotificationManagerImpl$2;->(Lorg/meshtastic/core/service/MeshNotificationManagerImpl;Lkotlin/coroutines/Continuation;)V +HSPLorg/meshtastic/core/service/MeshNotificationManagerImpl$2;->create(Ljava/lang/Object;Lkotlin/coroutines/Continuation;)Lkotlin/coroutines/Continuation; +HSPLorg/meshtastic/core/service/MeshNotificationManagerImpl$2;->invokeSuspend(Ljava/lang/Object;)Ljava/lang/Object; +Lorg/meshtastic/core/service/MeshNotificationManagerImpl$2$1; +HSPLorg/meshtastic/core/service/MeshNotificationManagerImpl$2$1;->(Lorg/meshtastic/core/service/MeshNotificationManagerImpl;)V +HSPLorg/meshtastic/core/service/MeshNotificationManagerImpl$2$1;->emit(Ljava/lang/Object;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; +HSPLorg/meshtastic/core/service/MeshNotificationManagerImpl$2$1;->emit(Ljava/util/Map;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; Lorg/meshtastic/core/service/MeshNotificationManagerImpl$Companion; HSPLorg/meshtastic/core/service/MeshNotificationManagerImpl$Companion;->()V HSPLorg/meshtastic/core/service/MeshNotificationManagerImpl$Companion;->(Lkotlin/jvm/internal/DefaultConstructorMarker;)V @@ -44103,7 +44834,8 @@ HSPLorg/meshtastic/core/service/MeshNotificationManagerImpl$RenderedServiceNotif HSPLorg/meshtastic/core/service/MeshNotificationManagerImpl$RenderedServiceNotification;->getMessage()Ljava/lang/String; HSPLorg/meshtastic/core/service/MeshNotificationManagerImpl$RenderedServiceNotification;->getNotification()Landroid/app/Notification; Lorg/meshtastic/core/service/MeshNotificationManagerImpl$ServiceNotificationSnapshot; -HSPLorg/meshtastic/core/service/MeshNotificationManagerImpl$ServiceNotificationSnapshot;->(Lorg/meshtastic/core/model/ConnectionState;Lorg/meshtastic/proto/LocalStats;Lorg/meshtastic/proto/DeviceMetrics;Ljava/lang/String;J)V +HSPLorg/meshtastic/core/service/MeshNotificationManagerImpl$ServiceNotificationSnapshot;->(Lorg/meshtastic/core/model/ConnectionState;Lorg/meshtastic/proto/LocalStats;Lorg/meshtastic/proto/DeviceMetrics;Ljava/lang/String;JLjava/util/Set;)V +HSPLorg/meshtastic/core/service/MeshNotificationManagerImpl$ServiceNotificationSnapshot;->getActiveOperations()Ljava/util/Set; HSPLorg/meshtastic/core/service/MeshNotificationManagerImpl$ServiceNotificationSnapshot;->getDeviceMetrics()Lorg/meshtastic/proto/DeviceMetrics; HSPLorg/meshtastic/core/service/MeshNotificationManagerImpl$ServiceNotificationSnapshot;->getLocalStats()Lorg/meshtastic/proto/LocalStats; HSPLorg/meshtastic/core/service/MeshNotificationManagerImpl$ServiceNotificationSnapshot;->getNextUpdateAt()J @@ -44115,31 +44847,50 @@ HSPLorg/meshtastic/core/service/MeshNotificationManagerImpl$renderAndPostService Lorg/meshtastic/core/service/MeshNotificationManagerImpl$renderServiceNotification$1; HSPLorg/meshtastic/core/service/MeshNotificationManagerImpl$renderServiceNotification$1;->(Lorg/meshtastic/core/service/MeshNotificationManagerImpl;Lkotlin/coroutines/Continuation;)V HSPLorg/meshtastic/core/service/MeshNotificationManagerImpl$renderServiceNotification$1;->invokeSuspend(Ljava/lang/Object;)Ljava/lang/Object; +Lorg/meshtastic/core/service/MeshNotificationManagerImplKt; +HSPLorg/meshtastic/core/service/MeshNotificationManagerImplKt;->notificationTitleResource(Ljava/util/Set;)Lorg/jetbrains/compose/resources/StringResource; Lorg/meshtastic/core/service/MeshService; HSPLorg/meshtastic/core/service/MeshService;->()V HSPLorg/meshtastic/core/service/MeshService;->()V +HSPLorg/meshtastic/core/service/MeshService;->access$getOperationWakeLock(Lorg/meshtastic/core/service/MeshService;)Lorg/meshtastic/core/service/OperationWakeLock; HSPLorg/meshtastic/core/service/MeshService;->access$getRadioInterfaceService(Lorg/meshtastic/core/service/MeshService;)Lorg/meshtastic/core/repository/RadioInterfaceService; +HSPLorg/meshtastic/core/service/MeshService;->access$getRadioOperationLock(Lorg/meshtastic/core/service/MeshService;)Lorg/meshtastic/core/common/state/RadioOperationLock; PLorg/meshtastic/core/service/MeshService;->access$stopServiceCleanly(Lorg/meshtastic/core/service/MeshService;)V HSPLorg/meshtastic/core/service/MeshService;->getAndroidNotifications()Lorg/meshtastic/core/service/MeshNotificationManagerImpl; HSPLorg/meshtastic/core/service/MeshService;->getConnectionManager()Lorg/meshtastic/core/repository/MeshConnectionManager; HSPLorg/meshtastic/core/service/MeshService;->getConversationShortcutPublisher()Lorg/meshtastic/core/service/ConversationShortcutPublisher; HSPLorg/meshtastic/core/service/MeshService;->getNotifications()Lorg/meshtastic/core/repository/MeshNotificationManager; +HSPLorg/meshtastic/core/service/MeshService;->getOperationWakeLock()Lorg/meshtastic/core/service/OperationWakeLock; HSPLorg/meshtastic/core/service/MeshService;->getOrchestrator()Lorg/meshtastic/core/service/MeshServiceOrchestrator; HSPLorg/meshtastic/core/service/MeshService;->getRadioInterfaceService()Lorg/meshtastic/core/repository/RadioInterfaceService; +HSPLorg/meshtastic/core/service/MeshService;->getRadioOperationLock()Lorg/meshtastic/core/common/state/RadioOperationLock; HSPLorg/meshtastic/core/service/MeshService;->onCreate()V PLorg/meshtastic/core/service/MeshService;->onDestroy()V HSPLorg/meshtastic/core/service/MeshService;->onStartCommand(Landroid/content/Intent;II)I +HSPLorg/meshtastic/core/service/MeshService;->operationWakeLock_delegate$lambda$0(Lorg/meshtastic/core/service/MeshService;)Lorg/meshtastic/core/service/OperationWakeLock; PLorg/meshtastic/core/service/MeshService;->releaseWakeLock()V HSPLorg/meshtastic/core/service/MeshService;->scheduleDeviceAddressResolution()V PLorg/meshtastic/core/service/MeshService;->stopServiceCleanly()V +Lorg/meshtastic/core/service/MeshService$$ExternalSyntheticLambda0; +HSPLorg/meshtastic/core/service/MeshService$$ExternalSyntheticLambda0;->(Lorg/meshtastic/core/service/MeshService;)V +HSPLorg/meshtastic/core/service/MeshService$$ExternalSyntheticLambda0;->invoke()Ljava/lang/Object; Lorg/meshtastic/core/service/MeshService$Companion; HSPLorg/meshtastic/core/service/MeshService$Companion;->()V HSPLorg/meshtastic/core/service/MeshService$Companion;->(Lkotlin/jvm/internal/DefaultConstructorMarker;)V HSPLorg/meshtastic/core/service/MeshService$Companion;->createIntent(Landroid/content/Context;)Landroid/content/Intent; +Lorg/meshtastic/core/service/MeshService$onCreate$3; +HSPLorg/meshtastic/core/service/MeshService$onCreate$3;->(Lorg/meshtastic/core/service/MeshService;Lkotlin/coroutines/Continuation;)V +HSPLorg/meshtastic/core/service/MeshService$onCreate$3;->create(Ljava/lang/Object;Lkotlin/coroutines/Continuation;)Lkotlin/coroutines/Continuation; +HSPLorg/meshtastic/core/service/MeshService$onCreate$3;->invokeSuspend(Ljava/lang/Object;)Ljava/lang/Object; +Lorg/meshtastic/core/service/MeshService$onCreate$3$1; +HSPLorg/meshtastic/core/service/MeshService$onCreate$3$1;->(Lorg/meshtastic/core/service/MeshService;)V +HSPLorg/meshtastic/core/service/MeshService$onCreate$3$1;->emit(Ljava/lang/Object;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; +HSPLorg/meshtastic/core/service/MeshService$onCreate$3$1;->emit(Ljava/util/Map;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; Lorg/meshtastic/core/service/MeshService$scheduleDeviceAddressResolution$1; HSPLorg/meshtastic/core/service/MeshService$scheduleDeviceAddressResolution$1;->(Lorg/meshtastic/core/service/MeshService;Lkotlin/coroutines/Continuation;)V HSPLorg/meshtastic/core/service/MeshService$scheduleDeviceAddressResolution$1;->create(Ljava/lang/Object;Lkotlin/coroutines/Continuation;)Lkotlin/coroutines/Continuation; HSPLorg/meshtastic/core/service/MeshService$scheduleDeviceAddressResolution$1;->invokeSuspend(Ljava/lang/Object;)Ljava/lang/Object; +PLorg/meshtastic/core/service/MeshService$scheduleDeviceAddressResolution$1$WhenMappings;->()V Lorg/meshtastic/core/service/MeshService$scheduleDeviceAddressResolution$1$resolved$1; HSPLorg/meshtastic/core/service/MeshService$scheduleDeviceAddressResolution$1$resolved$1;->(Lorg/meshtastic/core/service/MeshService;Lkotlin/coroutines/Continuation;)V HSPLorg/meshtastic/core/service/MeshService$scheduleDeviceAddressResolution$1$resolved$1;->create(Ljava/lang/Object;Lkotlin/coroutines/Continuation;)Lkotlin/coroutines/Continuation; @@ -44166,6 +44917,9 @@ HSPLorg/meshtastic/core/service/MeshService$special$$inlined$inject$default$4;-> Lorg/meshtastic/core/service/MeshService$special$$inlined$inject$default$5; HSPLorg/meshtastic/core/service/MeshService$special$$inlined$inject$default$5;->(Landroid/content/ComponentCallbacks;Lorg/koin/core/qualifier/Qualifier;Lkotlin/jvm/functions/Function0;)V HSPLorg/meshtastic/core/service/MeshService$special$$inlined$inject$default$5;->invoke()Ljava/lang/Object; +Lorg/meshtastic/core/service/MeshService$special$$inlined$inject$default$6; +HSPLorg/meshtastic/core/service/MeshService$special$$inlined$inject$default$6;->(Landroid/content/ComponentCallbacks;Lorg/koin/core/qualifier/Qualifier;Lkotlin/jvm/functions/Function0;)V +HSPLorg/meshtastic/core/service/MeshService$special$$inlined$inject$default$6;->invoke()Ljava/lang/Object; Lorg/meshtastic/core/service/MeshServiceOrchestrator; HSPLorg/meshtastic/core/service/MeshServiceOrchestrator;->(Lorg/meshtastic/core/repository/RadioInterfaceService;Lorg/meshtastic/core/repository/ServiceStateWriter;Lorg/meshtastic/core/repository/NodeManager;Lorg/meshtastic/core/repository/MeshMessageProcessor;Lorg/meshtastic/core/repository/MeshNotificationManager;Lorg/meshtastic/core/takserver/TAKServerManager;Lorg/meshtastic/core/takserver/TAKMeshIntegration;Lorg/meshtastic/core/repository/TakPrefs;Lorg/meshtastic/core/common/database/DatabaseManager;Lorg/meshtastic/core/repository/MeshConnectionManager;Lorg/meshtastic/core/di/CoroutineDispatchers;Lorg/meshtastic/core/service/LocalNetworkAccess;)V HSPLorg/meshtastic/core/service/MeshServiceOrchestrator;->access$coldStartConnect(Lorg/meshtastic/core/service/MeshServiceOrchestrator;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; @@ -44222,6 +44976,14 @@ Lorg/meshtastic/core/service/NotificationChannels; HSPLorg/meshtastic/core/service/NotificationChannels;->()V HSPLorg/meshtastic/core/service/NotificationChannels;->()V HSPLorg/meshtastic/core/service/NotificationChannels;->getLEGACY_CATEGORY_IDS()Ljava/util/List; +Lorg/meshtastic/core/service/OperationWakeLock; +HSPLorg/meshtastic/core/service/OperationWakeLock;->()V +HSPLorg/meshtastic/core/service/OperationWakeLock;->(Landroid/content/Context;Lkotlinx/coroutines/CoroutineScope;)V +HSPLorg/meshtastic/core/service/OperationWakeLock;->release()V +HSPLorg/meshtastic/core/service/OperationWakeLock;->stop()V +Lorg/meshtastic/core/service/OperationWakeLock$Companion; +HSPLorg/meshtastic/core/service/OperationWakeLock$Companion;->()V +HSPLorg/meshtastic/core/service/OperationWakeLock$Companion;->(Lkotlin/jvm/internal/DefaultConstructorMarker;)V Lorg/meshtastic/core/service/QueryControllerImpl; HSPLorg/meshtastic/core/service/QueryControllerImpl;->(Lorg/meshtastic/core/repository/CommandSender;Lorg/meshtastic/core/repository/NodeManager;Lorg/meshtastic/core/repository/UiPrefs;)V Lorg/meshtastic/core/service/RadioControllerImpl; @@ -44266,9 +45028,14 @@ Lorg/meshtastic/core/service/ServiceStartTrigger; HSPLorg/meshtastic/core/service/ServiceStartTrigger;->$values()[Lorg/meshtastic/core/service/ServiceStartTrigger; HSPLorg/meshtastic/core/service/ServiceStartTrigger;->()V HSPLorg/meshtastic/core/service/ServiceStartTrigger;->(Ljava/lang/String;I)V +PLorg/meshtastic/core/service/ServiceStayAliveDecision;->$values()[Lorg/meshtastic/core/service/ServiceStayAliveDecision; +PLorg/meshtastic/core/service/ServiceStayAliveDecision;->()V +PLorg/meshtastic/core/service/ServiceStayAliveDecision;->(Ljava/lang/String;I)V +PLorg/meshtastic/core/service/ServiceStayAliveDecision;->values()[Lorg/meshtastic/core/service/ServiceStayAliveDecision; +PLorg/meshtastic/core/service/ServiceStayAliveDecisionKt;->serviceStayAliveDecision(Ljava/lang/String;Z)Lorg/meshtastic/core/service/ServiceStayAliveDecision; Lorg/meshtastic/core/service/SharedRadioInterfaceService; HSPLorg/meshtastic/core/service/SharedRadioInterfaceService;->()V -HSPLorg/meshtastic/core/service/SharedRadioInterfaceService;->(Lorg/meshtastic/core/di/CoroutineDispatchers;Lorg/meshtastic/core/ble/BluetoothRepository;Lorg/meshtastic/core/network/repository/NetworkRepository;Lorg/meshtastic/core/network/repository/SerialDevicePresence;Landroidx/lifecycle/Lifecycle;Lorg/meshtastic/core/repository/RadioPrefs;Lorg/meshtastic/core/repository/RadioTransportFactory;Lorg/meshtastic/core/repository/PlatformAnalytics;Lorg/meshtastic/core/common/state/FirmwareMaintenanceLock;)V +HSPLorg/meshtastic/core/service/SharedRadioInterfaceService;->(Lorg/meshtastic/core/di/CoroutineDispatchers;Lorg/meshtastic/core/ble/BluetoothRepository;Lorg/meshtastic/core/network/repository/NetworkRepository;Lorg/meshtastic/core/network/repository/SerialDevicePresence;Landroidx/lifecycle/Lifecycle;Lorg/meshtastic/core/repository/RadioPrefs;Lorg/meshtastic/core/repository/RadioTransportFactory;Lorg/meshtastic/core/repository/PlatformAnalytics;Lorg/meshtastic/core/common/state/RadioOperationLock;)V PLorg/meshtastic/core/service/SharedRadioInterfaceService;->access$finishTransportTeardown(Lorg/meshtastic/core/service/SharedRadioInterfaceService;ZZLkotlin/coroutines/Continuation;)Ljava/lang/Object; HSPLorg/meshtastic/core/service/SharedRadioInterfaceService;->access$get_currentDeviceAddressFlow$p(Lorg/meshtastic/core/service/SharedRadioInterfaceService;)Lkotlinx/coroutines/flow/MutableStateFlow; PLorg/meshtastic/core/service/SharedRadioInterfaceService;->disconnect(Lkotlin/coroutines/Continuation;)Ljava/lang/Object; @@ -44452,7 +45219,7 @@ HSPLorg/meshtastic/core/service/di/OrgMeshtasticCoreServiceDiCoreServiceModuleMo Lorg/meshtastic/core/service/worker/MeshLogCleanupWorker; HSPLorg/meshtastic/core/service/worker/MeshLogCleanupWorker;->()V HSPLorg/meshtastic/core/service/worker/MeshLogCleanupWorker;->(Landroid/content/Context;Landroidx/work/WorkerParameters;Lorg/meshtastic/core/repository/MeshLogRepository;Lorg/meshtastic/core/repository/MeshLogPrefs;)V -HSPLorg/meshtastic/core/service/worker/MeshLogCleanupWorker;->doWork(Lkotlin/coroutines/Continuation;)Ljava/lang/Object; +PLorg/meshtastic/core/service/worker/MeshLogCleanupWorker;->doWork(Lkotlin/coroutines/Continuation;)Ljava/lang/Object; Lorg/meshtastic/core/service/worker/MeshLogCleanupWorker$Companion; HSPLorg/meshtastic/core/service/worker/MeshLogCleanupWorker$Companion;->()V HSPLorg/meshtastic/core/service/worker/MeshLogCleanupWorker$Companion;->(Lkotlin/jvm/internal/DefaultConstructorMarker;)V @@ -44576,23 +45343,23 @@ HSPLorg/meshtastic/core/ui/component/MentionSubstitution;->(Ljava/lang/Str HSPLorg/meshtastic/core/ui/component/MentionSubstitution;->getDisplay()Ljava/lang/String; HSPLorg/meshtastic/core/ui/component/MentionSubstitution;->getMentions()Ljava/util/List; Lorg/meshtastic/core/ui/component/MeshtasticNavDisplayKt; -HSPLorg/meshtastic/core/ui/component/MeshtasticNavDisplayKt;->MeshtasticNavDisplay$lambda$8(Lkotlin/jvm/functions/Function0;Landroidx/navigation3/runtime/NavBackStack;Landroidx/compose/material3/adaptive/navigation3/ListDetailSceneStrategy;Landroidx/compose/material3/adaptive/navigation3/SupportingPaneSceneStrategy;Landroidx/compose/ui/Modifier;Ljava/util/List;Lkotlin/jvm/functions/Function1;Landroidx/compose/animation/SharedTransitionScope;Landroidx/compose/runtime/Composer;I)Lkotlin/Unit; +HSPLorg/meshtastic/core/ui/component/MeshtasticNavDisplayKt;->MeshtasticNavDisplay$lambda$8(Lkotlin/jvm/functions/Function0;Landroidx/navigation3/runtime/NavBackStack;Landroidx/compose/material3/adaptive/navigation3/ListDetailSceneStrategy;Landroidx/compose/material3/adaptive/navigation3/SupportingPaneSceneStrategy;Landroidx/compose/animation/core/FiniteAnimationSpec;Landroidx/compose/ui/Modifier;Ljava/util/List;Lkotlin/jvm/functions/Function1;Landroidx/compose/animation/SharedTransitionScope;Landroidx/compose/runtime/Composer;I)Lkotlin/Unit; HSPLorg/meshtastic/core/ui/component/MeshtasticNavDisplayKt;->MeshtasticNavDisplay(Landroidx/navigation3/runtime/NavBackStack;Lkotlin/jvm/functions/Function1;Landroidx/compose/ui/Modifier;Lkotlin/jvm/functions/Function0;Lorg/meshtastic/core/repository/PlatformAnalytics;Landroidx/compose/runtime/Composer;II)V -HSPLorg/meshtastic/core/ui/component/MeshtasticNavDisplayKt;->meshtasticPredictivePopTransitionSpec()Lkotlin/jvm/functions/Function2; -HSPLorg/meshtastic/core/ui/component/MeshtasticNavDisplayKt;->meshtasticTransitionSpec$lambda$0(Landroidx/compose/animation/AnimatedContentTransitionScope;)Landroidx/compose/animation/ContentTransform; -HSPLorg/meshtastic/core/ui/component/MeshtasticNavDisplayKt;->meshtasticTransitionSpec()Lkotlin/jvm/functions/Function1; -Lorg/meshtastic/core/ui/component/MeshtasticNavDisplayKt$$ExternalSyntheticLambda0; -HSPLorg/meshtastic/core/ui/component/MeshtasticNavDisplayKt$$ExternalSyntheticLambda0;->()V +HSPLorg/meshtastic/core/ui/component/MeshtasticNavDisplayKt;->meshtasticPredictivePopTransitionSpec(Landroidx/compose/animation/core/FiniteAnimationSpec;)Lkotlin/jvm/functions/Function2; +HSPLorg/meshtastic/core/ui/component/MeshtasticNavDisplayKt;->meshtasticTransitionSpec$lambda$0(Landroidx/compose/animation/core/FiniteAnimationSpec;Landroidx/compose/animation/AnimatedContentTransitionScope;)Landroidx/compose/animation/ContentTransform; +HSPLorg/meshtastic/core/ui/component/MeshtasticNavDisplayKt;->meshtasticTransitionSpec(Landroidx/compose/animation/core/FiniteAnimationSpec;)Lkotlin/jvm/functions/Function1; +Lorg/meshtastic/core/ui/component/MeshtasticNavDisplayKt$$ExternalSyntheticLambda1; +HSPLorg/meshtastic/core/ui/component/MeshtasticNavDisplayKt$$ExternalSyntheticLambda1;->(Lkotlin/jvm/functions/Function0;Landroidx/navigation3/runtime/NavBackStack;Landroidx/compose/material3/adaptive/navigation3/ListDetailSceneStrategy;Landroidx/compose/material3/adaptive/navigation3/SupportingPaneSceneStrategy;Landroidx/compose/animation/core/FiniteAnimationSpec;Landroidx/compose/ui/Modifier;Ljava/util/List;Lkotlin/jvm/functions/Function1;)V +HSPLorg/meshtastic/core/ui/component/MeshtasticNavDisplayKt$$ExternalSyntheticLambda1;->invoke(Ljava/lang/Object;Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object; Lorg/meshtastic/core/ui/component/MeshtasticNavDisplayKt$$ExternalSyntheticLambda2; -HSPLorg/meshtastic/core/ui/component/MeshtasticNavDisplayKt$$ExternalSyntheticLambda2;->(Lkotlin/jvm/functions/Function0;Landroidx/navigation3/runtime/NavBackStack;Landroidx/compose/material3/adaptive/navigation3/ListDetailSceneStrategy;Landroidx/compose/material3/adaptive/navigation3/SupportingPaneSceneStrategy;Landroidx/compose/ui/Modifier;Ljava/util/List;Lkotlin/jvm/functions/Function1;)V -HSPLorg/meshtastic/core/ui/component/MeshtasticNavDisplayKt$$ExternalSyntheticLambda2;->invoke(Ljava/lang/Object;Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object; -Lorg/meshtastic/core/ui/component/MeshtasticNavDisplayKt$$ExternalSyntheticLambda3; -HSPLorg/meshtastic/core/ui/component/MeshtasticNavDisplayKt$$ExternalSyntheticLambda3;->(Landroidx/navigation3/runtime/NavBackStack;Lkotlin/jvm/functions/Function1;Landroidx/compose/ui/Modifier;Lkotlin/jvm/functions/Function0;Lorg/meshtastic/core/repository/PlatformAnalytics;II)V +HSPLorg/meshtastic/core/ui/component/MeshtasticNavDisplayKt$$ExternalSyntheticLambda2;->(Landroidx/navigation3/runtime/NavBackStack;Lkotlin/jvm/functions/Function1;Landroidx/compose/ui/Modifier;Lkotlin/jvm/functions/Function0;Lorg/meshtastic/core/repository/PlatformAnalytics;II)V Lorg/meshtastic/core/ui/component/MeshtasticNavDisplayKt$$ExternalSyntheticLambda4; -HSPLorg/meshtastic/core/ui/component/MeshtasticNavDisplayKt$$ExternalSyntheticLambda4;->()V +HSPLorg/meshtastic/core/ui/component/MeshtasticNavDisplayKt$$ExternalSyntheticLambda4;->(Landroidx/compose/animation/core/FiniteAnimationSpec;)V HSPLorg/meshtastic/core/ui/component/MeshtasticNavDisplayKt$$ExternalSyntheticLambda4;->invoke(Ljava/lang/Object;)Ljava/lang/Object; -Lorg/meshtastic/core/ui/component/MeshtasticNavDisplayKt$$ExternalSyntheticLambda7; -HSPLorg/meshtastic/core/ui/component/MeshtasticNavDisplayKt$$ExternalSyntheticLambda7;->(Landroidx/navigation3/runtime/NavBackStack;)V +Lorg/meshtastic/core/ui/component/MeshtasticNavDisplayKt$$ExternalSyntheticLambda5; +HSPLorg/meshtastic/core/ui/component/MeshtasticNavDisplayKt$$ExternalSyntheticLambda5;->(Landroidx/compose/animation/core/FiniteAnimationSpec;)V +Lorg/meshtastic/core/ui/component/MeshtasticNavDisplayKt$$ExternalSyntheticLambda8; +HSPLorg/meshtastic/core/ui/component/MeshtasticNavDisplayKt$$ExternalSyntheticLambda8;->(Landroidx/navigation3/runtime/NavBackStack;)V Lorg/meshtastic/core/ui/di/CoreUiModule; HSPLorg/meshtastic/core/ui/di/CoreUiModule;->()V HSPLorg/meshtastic/core/ui/di/CoreUiModule;->()V @@ -44787,6 +45554,8 @@ Lorg/meshtastic/core/ui/util/LocalMapMainScreenProviderKt$$ExternalSyntheticLamb HSPLorg/meshtastic/core/ui/util/LocalMapMainScreenProviderKt$$ExternalSyntheticLambda0;->()V Lorg/meshtastic/core/ui/util/LocalNfcScannerProviderKt; HSPLorg/meshtastic/core/ui/util/LocalNfcScannerProviderKt;->()V +HSPLorg/meshtastic/core/ui/util/LocalNfcScannerProviderKt;->getLocalNfcEmulationSupported()Landroidx/compose/runtime/ProvidableCompositionLocal; +HSPLorg/meshtastic/core/ui/util/LocalNfcScannerProviderKt;->getLocalNfcEmulatorProvider()Landroidx/compose/runtime/ProvidableCompositionLocal; HSPLorg/meshtastic/core/ui/util/LocalNfcScannerProviderKt;->getLocalNfcScannerProvider()Landroidx/compose/runtime/ProvidableCompositionLocal; HSPLorg/meshtastic/core/ui/util/LocalNfcScannerProviderKt;->getLocalNfcScannerSupported()Landroidx/compose/runtime/ProvidableCompositionLocal; HSPLorg/meshtastic/core/ui/util/LocalNfcScannerProviderKt;->getLocalNfcWriterProvider()Landroidx/compose/runtime/ProvidableCompositionLocal; @@ -44796,6 +45565,10 @@ Lorg/meshtastic/core/ui/util/LocalNfcScannerProviderKt$$ExternalSyntheticLambda1 HSPLorg/meshtastic/core/ui/util/LocalNfcScannerProviderKt$$ExternalSyntheticLambda1;->()V Lorg/meshtastic/core/ui/util/LocalNfcScannerProviderKt$$ExternalSyntheticLambda2; HSPLorg/meshtastic/core/ui/util/LocalNfcScannerProviderKt$$ExternalSyntheticLambda2;->()V +Lorg/meshtastic/core/ui/util/LocalNfcScannerProviderKt$$ExternalSyntheticLambda3; +HSPLorg/meshtastic/core/ui/util/LocalNfcScannerProviderKt$$ExternalSyntheticLambda3;->()V +Lorg/meshtastic/core/ui/util/LocalNfcScannerProviderKt$$ExternalSyntheticLambda4; +HSPLorg/meshtastic/core/ui/util/LocalNfcScannerProviderKt$$ExternalSyntheticLambda4;->()V Lorg/meshtastic/core/ui/util/LocalNodeMapScreenProviderKt; HSPLorg/meshtastic/core/ui/util/LocalNodeMapScreenProviderKt;->()V HSPLorg/meshtastic/core/ui/util/LocalNodeMapScreenProviderKt;->getLocalNodeMapScreenProvider()Landroidx/compose/runtime/ProvidableCompositionLocal; @@ -44865,10 +45638,10 @@ HSPLorg/meshtastic/core/ui/util/PlatformUtilsKt$$ExternalSyntheticLambda0;->(Landroidx/compose/runtime/MutableState;[Ljava/lang/String;ZLandroid/app/Activity;Ljava/lang/String;Lorg/meshtastic/core/ui/util/PermissionRequestTracker;Landroid/content/Context;)V HSPLorg/meshtastic/core/ui/util/PlatformUtilsKt$$ExternalSyntheticLambda11;->invoke()Ljava/lang/Object; -Lorg/meshtastic/core/ui/util/PlatformUtilsKt$$ExternalSyntheticLambda21; -HSPLorg/meshtastic/core/ui/util/PlatformUtilsKt$$ExternalSyntheticLambda21;->(Landroidx/activity/compose/ManagedActivityResultLauncher;[Ljava/lang/String;)V -Lorg/meshtastic/core/ui/util/PlatformUtilsKt$$ExternalSyntheticLambda23; -HSPLorg/meshtastic/core/ui/util/PlatformUtilsKt$$ExternalSyntheticLambda23;->(Landroid/content/Context;)V +Lorg/meshtastic/core/ui/util/PlatformUtilsKt$$ExternalSyntheticLambda22; +HSPLorg/meshtastic/core/ui/util/PlatformUtilsKt$$ExternalSyntheticLambda22;->(Landroidx/activity/compose/ManagedActivityResultLauncher;[Ljava/lang/String;)V +Lorg/meshtastic/core/ui/util/PlatformUtilsKt$$ExternalSyntheticLambda24; +HSPLorg/meshtastic/core/ui/util/PlatformUtilsKt$$ExternalSyntheticLambda24;->(Landroid/content/Context;)V Lorg/meshtastic/core/ui/util/SnackbarManager; HSPLorg/meshtastic/core/ui/util/SnackbarManager;->()V HSPLorg/meshtastic/core/ui/util/SnackbarManager;->()V @@ -44893,14 +45666,14 @@ HSPLorg/meshtastic/core/ui/viewmodel/UIViewModel$Companion;->()V HSPLorg/meshtastic/core/ui/viewmodel/UIViewModel$Companion;->(Lkotlin/jvm/internal/DefaultConstructorMarker;)V Lorg/meshtastic/core/ui/viewmodel/UIViewModel$eventEdition$1; HSPLorg/meshtastic/core/ui/viewmodel/UIViewModel$eventEdition$1;->(Lkotlin/coroutines/Continuation;)V -PLorg/meshtastic/core/ui/viewmodel/UIViewModel$eventEdition$1;->invoke(Ljava/lang/Object;Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object; -PLorg/meshtastic/core/ui/viewmodel/UIViewModel$eventEdition$1;->invoke(Lorg/meshtastic/proto/FirmwareEdition;Lorg/meshtastic/core/model/ConnectionState;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; -PLorg/meshtastic/core/ui/viewmodel/UIViewModel$eventEdition$1;->invokeSuspend(Ljava/lang/Object;)Ljava/lang/Object; +HSPLorg/meshtastic/core/ui/viewmodel/UIViewModel$eventEdition$1;->invoke(Ljava/lang/Object;Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object; +HSPLorg/meshtastic/core/ui/viewmodel/UIViewModel$eventEdition$1;->invoke(Lorg/meshtastic/proto/FirmwareEdition;Lorg/meshtastic/core/model/ConnectionState;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; +HSPLorg/meshtastic/core/ui/viewmodel/UIViewModel$eventEdition$1;->invokeSuspend(Ljava/lang/Object;)Ljava/lang/Object; Lorg/meshtastic/core/ui/viewmodel/UIViewModel$special$$inlined$flatMapLatest$1; HSPLorg/meshtastic/core/ui/viewmodel/UIViewModel$special$$inlined$flatMapLatest$1;->(Lkotlin/coroutines/Continuation;Lorg/meshtastic/core/ui/viewmodel/UIViewModel;)V -PLorg/meshtastic/core/ui/viewmodel/UIViewModel$special$$inlined$flatMapLatest$1;->invoke(Ljava/lang/Object;Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object; -PLorg/meshtastic/core/ui/viewmodel/UIViewModel$special$$inlined$flatMapLatest$1;->invoke(Lkotlinx/coroutines/flow/FlowCollector;Ljava/lang/Object;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; -PLorg/meshtastic/core/ui/viewmodel/UIViewModel$special$$inlined$flatMapLatest$1;->invokeSuspend(Ljava/lang/Object;)Ljava/lang/Object; +HSPLorg/meshtastic/core/ui/viewmodel/UIViewModel$special$$inlined$flatMapLatest$1;->invoke(Ljava/lang/Object;Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object; +HSPLorg/meshtastic/core/ui/viewmodel/UIViewModel$special$$inlined$flatMapLatest$1;->invoke(Lkotlinx/coroutines/flow/FlowCollector;Ljava/lang/Object;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; +HSPLorg/meshtastic/core/ui/viewmodel/UIViewModel$special$$inlined$flatMapLatest$1;->invokeSuspend(Ljava/lang/Object;)Ljava/lang/Object; Lorg/meshtastic/core/ui/viewmodel/UIViewModel$special$$inlined$map$1; HSPLorg/meshtastic/core/ui/viewmodel/UIViewModel$special$$inlined$map$1;->(Lkotlinx/coroutines/flow/Flow;)V HSPLorg/meshtastic/core/ui/viewmodel/UIViewModel$special$$inlined$map$1;->collect(Lkotlinx/coroutines/flow/FlowCollector;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; @@ -44908,7 +45681,7 @@ Lorg/meshtastic/core/ui/viewmodel/UIViewModel$special$$inlined$map$1$1; HSPLorg/meshtastic/core/ui/viewmodel/UIViewModel$special$$inlined$map$1$1;->(Lorg/meshtastic/core/ui/viewmodel/UIViewModel$special$$inlined$map$1;Lkotlin/coroutines/Continuation;)V Lorg/meshtastic/core/ui/viewmodel/UIViewModel$special$$inlined$map$1$2; HSPLorg/meshtastic/core/ui/viewmodel/UIViewModel$special$$inlined$map$1$2;->(Lkotlinx/coroutines/flow/FlowCollector;)V -PLorg/meshtastic/core/ui/viewmodel/UIViewModel$special$$inlined$map$1$2;->emit(Ljava/lang/Object;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; +HSPLorg/meshtastic/core/ui/viewmodel/UIViewModel$special$$inlined$map$1$2;->emit(Ljava/lang/Object;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; PLorg/meshtastic/core/ui/viewmodel/UIViewModel$special$$inlined$map$1$2$1;->(Lorg/meshtastic/core/ui/viewmodel/UIViewModel$special$$inlined$map$1$2;Lkotlin/coroutines/Continuation;)V PLorg/meshtastic/core/ui/viewmodel/UIViewModel$special$$inlined$map$1$2$1;->invokeSuspend(Ljava/lang/Object;)Ljava/lang/Object; Lorg/meshtastic/core/ui/viewmodel/UIViewModel$special$$inlined$map$2; @@ -44969,7 +45742,7 @@ HSPLorg/meshtastic/feature/discovery/DiscoveryInterruptedSessionRecovery$watch$3 Lorg/meshtastic/feature/discovery/DiscoveryMapViewModel; Lorg/meshtastic/feature/discovery/DiscoveryScanEngine; HSPLorg/meshtastic/feature/discovery/DiscoveryScanEngine;->()V -HSPLorg/meshtastic/feature/discovery/DiscoveryScanEngine;->(Lorg/meshtastic/core/repository/RadioController;Lorg/meshtastic/core/repository/ServiceRepository;Lorg/meshtastic/core/repository/NodeRepository;Lorg/meshtastic/core/repository/RadioConfigRepository;Lorg/meshtastic/core/repository/DiscoveryPacketCollectorRegistry;Lorg/meshtastic/core/database/dao/DiscoveryDao;Lorg/meshtastic/feature/discovery/ai/DiscoverySummaryAiProvider;Lorg/meshtastic/core/common/di/ApplicationCoroutineScope;Lorg/meshtastic/core/di/CoroutineDispatchers;Lorg/meshtastic/core/repository/MeshPrefs;)V +HSPLorg/meshtastic/feature/discovery/DiscoveryScanEngine;->(Lorg/meshtastic/core/repository/RadioController;Lorg/meshtastic/core/repository/ServiceRepository;Lorg/meshtastic/core/repository/NodeRepository;Lorg/meshtastic/core/repository/RadioConfigRepository;Lorg/meshtastic/core/repository/DiscoveryPacketCollectorRegistry;Lorg/meshtastic/core/database/dao/DiscoveryDao;Lorg/meshtastic/feature/discovery/ai/DiscoverySummaryAiProvider;Lorg/meshtastic/core/common/di/ApplicationCoroutineScope;Lorg/meshtastic/core/di/CoroutineDispatchers;Lorg/meshtastic/core/repository/MeshPrefs;Lorg/meshtastic/core/common/state/RadioOperationLock;)V HSPLorg/meshtastic/feature/discovery/DiscoveryScanEngine;->restoreInterruptedSessionsOnReconnect(Lkotlin/jvm/functions/Function2;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; Lorg/meshtastic/feature/discovery/DiscoveryScanEngine$$ExternalSyntheticLambda1; HSPLorg/meshtastic/feature/discovery/DiscoveryScanEngine$$ExternalSyntheticLambda1;->(Lorg/meshtastic/feature/discovery/DiscoveryScanEngine;)V @@ -45516,11 +46289,12 @@ Lorg/meshtastic/mqtt/ConnectionState$Reconnecting; Lorg/meshtastic/mqtt/MqttException; Lorg/meshtastic/proto/ChannelSet; HSPLorg/meshtastic/proto/ChannelSet;->()V -HSPLorg/meshtastic/proto/ChannelSet;->(Ljava/util/List;Lorg/meshtastic/proto/Config$LoRaConfig;Lokio/ByteString;)V -HSPLorg/meshtastic/proto/ChannelSet;->(Ljava/util/List;Lorg/meshtastic/proto/Config$LoRaConfig;Lokio/ByteString;ILkotlin/jvm/internal/DefaultConstructorMarker;)V +HSPLorg/meshtastic/proto/ChannelSet;->(Lorg/meshtastic/proto/ChannelSet$Builder;Lokio/ByteString;)V +HSPLorg/meshtastic/proto/ChannelSet;->(Lorg/meshtastic/proto/ChannelSet$Builder;Lokio/ByteString;Lkotlin/jvm/internal/DefaultConstructorMarker;)V HSPLorg/meshtastic/proto/ChannelSet;->equals(Ljava/lang/Object;)Z -HSPLorg/meshtastic/proto/ChannelSet;->getLora_config()Lorg/meshtastic/proto/Config$LoRaConfig; -HSPLorg/meshtastic/proto/ChannelSet;->getSettings()Ljava/util/List; +Lorg/meshtastic/proto/ChannelSet$Builder; +HSPLorg/meshtastic/proto/ChannelSet$Builder;->()V +HSPLorg/meshtastic/proto/ChannelSet$Builder;->build()Lorg/meshtastic/proto/ChannelSet; Lorg/meshtastic/proto/ChannelSet$Companion; HSPLorg/meshtastic/proto/ChannelSet$Companion;->()V HSPLorg/meshtastic/proto/ChannelSet$Companion;->(Lkotlin/jvm/internal/DefaultConstructorMarker;)V @@ -45528,8 +46302,11 @@ Lorg/meshtastic/proto/ChannelSet$Companion$ADAPTER$1; HSPLorg/meshtastic/proto/ChannelSet$Companion$ADAPTER$1;->(Lcom/squareup/wire/FieldEncoding;Lkotlin/reflect/KClass;Lcom/squareup/wire/Syntax;)V Lorg/meshtastic/proto/ChannelSettings; HSPLorg/meshtastic/proto/ChannelSettings;->()V -HSPLorg/meshtastic/proto/ChannelSettings;->(ILokio/ByteString;Ljava/lang/String;IZZLorg/meshtastic/proto/ModuleSettings;Lokio/ByteString;)V -HSPLorg/meshtastic/proto/ChannelSettings;->(ILokio/ByteString;Ljava/lang/String;IZZLorg/meshtastic/proto/ModuleSettings;Lokio/ByteString;ILkotlin/jvm/internal/DefaultConstructorMarker;)V +HSPLorg/meshtastic/proto/ChannelSettings;->(Lorg/meshtastic/proto/ChannelSettings$Builder;Lokio/ByteString;)V +HSPLorg/meshtastic/proto/ChannelSettings;->(Lorg/meshtastic/proto/ChannelSettings$Builder;Lokio/ByteString;Lkotlin/jvm/internal/DefaultConstructorMarker;)V +Lorg/meshtastic/proto/ChannelSettings$Builder; +HSPLorg/meshtastic/proto/ChannelSettings$Builder;->()V +HSPLorg/meshtastic/proto/ChannelSettings$Builder;->build()Lorg/meshtastic/proto/ChannelSettings; Lorg/meshtastic/proto/ChannelSettings$Companion; HSPLorg/meshtastic/proto/ChannelSettings$Companion;->()V HSPLorg/meshtastic/proto/ChannelSettings$Companion;->(Lkotlin/jvm/internal/DefaultConstructorMarker;)V @@ -45549,8 +46326,11 @@ HSPLorg/meshtastic/proto/Config$DeviceConfig$Role$Companion$ADAPTER$1;->(L Lorg/meshtastic/proto/Config$DisplayConfig; Lorg/meshtastic/proto/Config$LoRaConfig; HSPLorg/meshtastic/proto/Config$LoRaConfig;->()V -HSPLorg/meshtastic/proto/Config$LoRaConfig;->(ZLorg/meshtastic/proto/Config$LoRaConfig$ModemPreset;IIIFLorg/meshtastic/proto/Config$LoRaConfig$RegionCode;IZIIZZFZLjava/util/List;ZZLorg/meshtastic/proto/Config$LoRaConfig$FEM_LNA_Mode;ZLokio/ByteString;)V -HSPLorg/meshtastic/proto/Config$LoRaConfig;->(ZLorg/meshtastic/proto/Config$LoRaConfig$ModemPreset;IIIFLorg/meshtastic/proto/Config$LoRaConfig$RegionCode;IZIIZZFZLjava/util/List;ZZLorg/meshtastic/proto/Config$LoRaConfig$FEM_LNA_Mode;ZLokio/ByteString;ILkotlin/jvm/internal/DefaultConstructorMarker;)V +HSPLorg/meshtastic/proto/Config$LoRaConfig;->(Lorg/meshtastic/proto/Config$LoRaConfig$Builder;Lokio/ByteString;)V +HSPLorg/meshtastic/proto/Config$LoRaConfig;->(Lorg/meshtastic/proto/Config$LoRaConfig$Builder;Lokio/ByteString;Lkotlin/jvm/internal/DefaultConstructorMarker;)V +Lorg/meshtastic/proto/Config$LoRaConfig$Builder; +HSPLorg/meshtastic/proto/Config$LoRaConfig$Builder;->()V +HSPLorg/meshtastic/proto/Config$LoRaConfig$Builder;->build()Lorg/meshtastic/proto/Config$LoRaConfig; Lorg/meshtastic/proto/Config$LoRaConfig$Companion; HSPLorg/meshtastic/proto/Config$LoRaConfig$Companion;->()V HSPLorg/meshtastic/proto/Config$LoRaConfig$Companion;->(Lkotlin/jvm/internal/DefaultConstructorMarker;)V @@ -45590,12 +46370,13 @@ Lorg/meshtastic/proto/Config$SecurityConfig; Lorg/meshtastic/proto/DeviceMetrics; Lorg/meshtastic/proto/LocalConfig; HSPLorg/meshtastic/proto/LocalConfig;->()V -HSPLorg/meshtastic/proto/LocalConfig;->(Lorg/meshtastic/proto/Config$DeviceConfig;Lorg/meshtastic/proto/Config$PositionConfig;Lorg/meshtastic/proto/Config$PowerConfig;Lorg/meshtastic/proto/Config$NetworkConfig;Lorg/meshtastic/proto/Config$DisplayConfig;Lorg/meshtastic/proto/Config$LoRaConfig;Lorg/meshtastic/proto/Config$BluetoothConfig;ILorg/meshtastic/proto/Config$SecurityConfig;Lokio/ByteString;)V -HSPLorg/meshtastic/proto/LocalConfig;->(Lorg/meshtastic/proto/Config$DeviceConfig;Lorg/meshtastic/proto/Config$PositionConfig;Lorg/meshtastic/proto/Config$PowerConfig;Lorg/meshtastic/proto/Config$NetworkConfig;Lorg/meshtastic/proto/Config$DisplayConfig;Lorg/meshtastic/proto/Config$LoRaConfig;Lorg/meshtastic/proto/Config$BluetoothConfig;ILorg/meshtastic/proto/Config$SecurityConfig;Lokio/ByteString;ILkotlin/jvm/internal/DefaultConstructorMarker;)V +HSPLorg/meshtastic/proto/LocalConfig;->(Lorg/meshtastic/proto/LocalConfig$Builder;Lokio/ByteString;)V +HSPLorg/meshtastic/proto/LocalConfig;->(Lorg/meshtastic/proto/LocalConfig$Builder;Lokio/ByteString;Lkotlin/jvm/internal/DefaultConstructorMarker;)V HSPLorg/meshtastic/proto/LocalConfig;->equals(Ljava/lang/Object;)Z -HSPLorg/meshtastic/proto/LocalConfig;->getDevice()Lorg/meshtastic/proto/Config$DeviceConfig; -HSPLorg/meshtastic/proto/LocalConfig;->getPower()Lorg/meshtastic/proto/Config$PowerConfig; HSPLorg/meshtastic/proto/LocalConfig;->hashCode()I +Lorg/meshtastic/proto/LocalConfig$Builder; +HSPLorg/meshtastic/proto/LocalConfig$Builder;->()V +HSPLorg/meshtastic/proto/LocalConfig$Builder;->build()Lorg/meshtastic/proto/LocalConfig; Lorg/meshtastic/proto/LocalConfig$Companion; HSPLorg/meshtastic/proto/LocalConfig$Companion;->()V HSPLorg/meshtastic/proto/LocalConfig$Companion;->(Lkotlin/jvm/internal/DefaultConstructorMarker;)V @@ -45603,10 +46384,13 @@ Lorg/meshtastic/proto/LocalConfig$Companion$ADAPTER$1; HSPLorg/meshtastic/proto/LocalConfig$Companion$ADAPTER$1;->(Lcom/squareup/wire/FieldEncoding;Lkotlin/reflect/KClass;Lcom/squareup/wire/Syntax;)V Lorg/meshtastic/proto/LocalModuleConfig; HSPLorg/meshtastic/proto/LocalModuleConfig;->()V -HSPLorg/meshtastic/proto/LocalModuleConfig;->(Lorg/meshtastic/proto/ModuleConfig$MQTTConfig;Lorg/meshtastic/proto/ModuleConfig$SerialConfig;Lorg/meshtastic/proto/ModuleConfig$ExternalNotificationConfig;Lorg/meshtastic/proto/ModuleConfig$StoreForwardConfig;Lorg/meshtastic/proto/ModuleConfig$RangeTestConfig;Lorg/meshtastic/proto/ModuleConfig$TelemetryConfig;Lorg/meshtastic/proto/ModuleConfig$CannedMessageConfig;Lorg/meshtastic/proto/ModuleConfig$AudioConfig;Lorg/meshtastic/proto/ModuleConfig$RemoteHardwareConfig;Lorg/meshtastic/proto/ModuleConfig$NeighborInfoConfig;Lorg/meshtastic/proto/ModuleConfig$AmbientLightingConfig;Lorg/meshtastic/proto/ModuleConfig$DetectionSensorConfig;Lorg/meshtastic/proto/ModuleConfig$PaxcounterConfig;Lorg/meshtastic/proto/ModuleConfig$StatusMessageConfig;Lorg/meshtastic/proto/ModuleConfig$TrafficManagementConfig;Lorg/meshtastic/proto/ModuleConfig$TAKConfig;Lorg/meshtastic/proto/ModuleConfig$MeshBeaconConfig;ILokio/ByteString;)V -HSPLorg/meshtastic/proto/LocalModuleConfig;->(Lorg/meshtastic/proto/ModuleConfig$MQTTConfig;Lorg/meshtastic/proto/ModuleConfig$SerialConfig;Lorg/meshtastic/proto/ModuleConfig$ExternalNotificationConfig;Lorg/meshtastic/proto/ModuleConfig$StoreForwardConfig;Lorg/meshtastic/proto/ModuleConfig$RangeTestConfig;Lorg/meshtastic/proto/ModuleConfig$TelemetryConfig;Lorg/meshtastic/proto/ModuleConfig$CannedMessageConfig;Lorg/meshtastic/proto/ModuleConfig$AudioConfig;Lorg/meshtastic/proto/ModuleConfig$RemoteHardwareConfig;Lorg/meshtastic/proto/ModuleConfig$NeighborInfoConfig;Lorg/meshtastic/proto/ModuleConfig$AmbientLightingConfig;Lorg/meshtastic/proto/ModuleConfig$DetectionSensorConfig;Lorg/meshtastic/proto/ModuleConfig$PaxcounterConfig;Lorg/meshtastic/proto/ModuleConfig$StatusMessageConfig;Lorg/meshtastic/proto/ModuleConfig$TrafficManagementConfig;Lorg/meshtastic/proto/ModuleConfig$TAKConfig;Lorg/meshtastic/proto/ModuleConfig$MeshBeaconConfig;ILokio/ByteString;ILkotlin/jvm/internal/DefaultConstructorMarker;)V +HSPLorg/meshtastic/proto/LocalModuleConfig;->(Lorg/meshtastic/proto/LocalModuleConfig$Builder;Lokio/ByteString;)V +HSPLorg/meshtastic/proto/LocalModuleConfig;->(Lorg/meshtastic/proto/LocalModuleConfig$Builder;Lokio/ByteString;Lkotlin/jvm/internal/DefaultConstructorMarker;)V HSPLorg/meshtastic/proto/LocalModuleConfig;->equals(Ljava/lang/Object;)Z HSPLorg/meshtastic/proto/LocalModuleConfig;->hashCode()I +Lorg/meshtastic/proto/LocalModuleConfig$Builder; +HSPLorg/meshtastic/proto/LocalModuleConfig$Builder;->()V +HSPLorg/meshtastic/proto/LocalModuleConfig$Builder;->build()Lorg/meshtastic/proto/LocalModuleConfig; Lorg/meshtastic/proto/LocalModuleConfig$Companion; HSPLorg/meshtastic/proto/LocalModuleConfig$Companion;->()V HSPLorg/meshtastic/proto/LocalModuleConfig$Companion;->(Lkotlin/jvm/internal/DefaultConstructorMarker;)V @@ -45614,21 +46398,13 @@ Lorg/meshtastic/proto/LocalModuleConfig$Companion$ADAPTER$1; HSPLorg/meshtastic/proto/LocalModuleConfig$Companion$ADAPTER$1;->(Lcom/squareup/wire/FieldEncoding;Lkotlin/reflect/KClass;Lcom/squareup/wire/Syntax;)V Lorg/meshtastic/proto/LocalStats; HSPLorg/meshtastic/proto/LocalStats;->()V -HSPLorg/meshtastic/proto/LocalStats;->(IFFIIIIIIIIIIIILokio/ByteString;)V -HSPLorg/meshtastic/proto/LocalStats;->(IFFIIIIIIIIIIIILokio/ByteString;ILkotlin/jvm/internal/DefaultConstructorMarker;)V +HSPLorg/meshtastic/proto/LocalStats;->(Lorg/meshtastic/proto/LocalStats$Builder;Lokio/ByteString;)V +HSPLorg/meshtastic/proto/LocalStats;->(Lorg/meshtastic/proto/LocalStats$Builder;Lokio/ByteString;Lkotlin/jvm/internal/DefaultConstructorMarker;)V HSPLorg/meshtastic/proto/LocalStats;->equals(Ljava/lang/Object;)Z -HSPLorg/meshtastic/proto/LocalStats;->getHeap_free_bytes()I -HSPLorg/meshtastic/proto/LocalStats;->getHeap_total_bytes()I -HSPLorg/meshtastic/proto/LocalStats;->getNoise_floor()I -HSPLorg/meshtastic/proto/LocalStats;->getNum_packets_rx()I -HSPLorg/meshtastic/proto/LocalStats;->getNum_packets_rx_bad()I -HSPLorg/meshtastic/proto/LocalStats;->getNum_packets_tx()I -HSPLorg/meshtastic/proto/LocalStats;->getNum_rx_dupe()I -HSPLorg/meshtastic/proto/LocalStats;->getNum_tx_dropped()I -HSPLorg/meshtastic/proto/LocalStats;->getNum_tx_relay()I -HSPLorg/meshtastic/proto/LocalStats;->getNum_tx_relay_canceled()I -HSPLorg/meshtastic/proto/LocalStats;->getUptime_seconds()I HSPLorg/meshtastic/proto/LocalStats;->hashCode()I +Lorg/meshtastic/proto/LocalStats$Builder; +HSPLorg/meshtastic/proto/LocalStats$Builder;->()V +HSPLorg/meshtastic/proto/LocalStats$Builder;->build()Lorg/meshtastic/proto/LocalStats; Lorg/meshtastic/proto/LocalStats$Companion; HSPLorg/meshtastic/proto/LocalStats$Companion;->()V HSPLorg/meshtastic/proto/LocalStats$Companion;->(Lkotlin/jvm/internal/DefaultConstructorMarker;)V @@ -45670,84 +46446,4 @@ HSPLorg/meshtastic/proto/Team$Companion;->()V HSPLorg/meshtastic/proto/Team$Companion;->(Lkotlin/jvm/internal/DefaultConstructorMarker;)V Lorg/meshtastic/proto/Team$Companion$ADAPTER$1; HSPLorg/meshtastic/proto/Team$Companion$ADAPTER$1;->(Lkotlin/reflect/KClass;Lcom/squareup/wire/Syntax;Lorg/meshtastic/proto/Team;)V -Lorg/slf4j/ILoggerFactory; -Lorg/slf4j/IMarkerFactory; -Lorg/slf4j/Logger; -Lorg/slf4j/LoggerFactory; -HSPLorg/slf4j/LoggerFactory;->()V -HSPLorg/slf4j/LoggerFactory;->bind()V -HSPLorg/slf4j/LoggerFactory;->earlyBindMDCAdapter()V -HSPLorg/slf4j/LoggerFactory;->findServiceProviders()Ljava/util/List; -HSPLorg/slf4j/LoggerFactory;->fixSubstituteLoggers()V -HSPLorg/slf4j/LoggerFactory;->getILoggerFactory()Lorg/slf4j/ILoggerFactory; -HSPLorg/slf4j/LoggerFactory;->getLogger(Ljava/lang/String;)Lorg/slf4j/Logger; -HSPLorg/slf4j/LoggerFactory;->getProvider()Lorg/slf4j/spi/SLF4JServiceProvider; -HSPLorg/slf4j/LoggerFactory;->getServiceLoader(Ljava/lang/ClassLoader;)Ljava/util/ServiceLoader; -HSPLorg/slf4j/LoggerFactory;->isAmbiguousProviderList(Ljava/util/List;)Z -HSPLorg/slf4j/LoggerFactory;->loadExplicitlySpecified(Ljava/lang/ClassLoader;)Lorg/slf4j/spi/SLF4JServiceProvider; -HSPLorg/slf4j/LoggerFactory;->performInitialization()V -HSPLorg/slf4j/LoggerFactory;->postBindCleanUp()V -HSPLorg/slf4j/LoggerFactory;->replayEvents()V -HSPLorg/slf4j/LoggerFactory;->reportActualBinding(Ljava/util/List;)V -HSPLorg/slf4j/LoggerFactory;->reportMultipleBindingAmbiguity(Ljava/util/List;)V -HSPLorg/slf4j/LoggerFactory;->safelyInstantiate(Ljava/util/List;Ljava/util/Iterator;)V -HSPLorg/slf4j/LoggerFactory;->versionSanityCheck()V -Lorg/slf4j/MDC; -HSPLorg/slf4j/MDC;->setMDCAdapter(Lorg/slf4j/spi/MDCAdapter;)V -Lorg/slf4j/helpers/BasicMDCAdapter; -HSPLorg/slf4j/helpers/BasicMDCAdapter;->()V -Lorg/slf4j/helpers/BasicMDCAdapter$1; -HSPLorg/slf4j/helpers/BasicMDCAdapter$1;->(Lorg/slf4j/helpers/BasicMDCAdapter;)V -Lorg/slf4j/helpers/BasicMarkerFactory; -HSPLorg/slf4j/helpers/BasicMarkerFactory;->()V -Lorg/slf4j/helpers/NOPLogger; -HSPLorg/slf4j/helpers/NOPLogger;->()V -HSPLorg/slf4j/helpers/NOPLogger;->()V -Lorg/slf4j/helpers/NOPLoggerFactory; -HSPLorg/slf4j/helpers/NOPLoggerFactory;->()V -HSPLorg/slf4j/helpers/NOPLoggerFactory;->getLogger(Ljava/lang/String;)Lorg/slf4j/Logger; -Lorg/slf4j/helpers/NOPMDCAdapter; -HSPLorg/slf4j/helpers/NOPMDCAdapter;->()V -Lorg/slf4j/helpers/NOP_FallbackServiceProvider; -HSPLorg/slf4j/helpers/NOP_FallbackServiceProvider;->()V -HSPLorg/slf4j/helpers/NOP_FallbackServiceProvider;->()V -Lorg/slf4j/helpers/NamedLoggerBase; -HSPLorg/slf4j/helpers/NamedLoggerBase;->()V -Lorg/slf4j/helpers/Reporter; -HSPLorg/slf4j/helpers/Reporter;->()V -HSPLorg/slf4j/helpers/Reporter;->debug(Ljava/lang/String;)V -HSPLorg/slf4j/helpers/Reporter;->getTargetChoice()Lorg/slf4j/helpers/Reporter$TargetChoice; -HSPLorg/slf4j/helpers/Reporter;->initVerbosity()Lorg/slf4j/helpers/Reporter$Level; -HSPLorg/slf4j/helpers/Reporter;->isEnabledFor(Lorg/slf4j/helpers/Reporter$Level;)Z -Lorg/slf4j/helpers/Reporter$Level; -HSPLorg/slf4j/helpers/Reporter$Level;->$values()[Lorg/slf4j/helpers/Reporter$Level; -HSPLorg/slf4j/helpers/Reporter$Level;->()V -HSPLorg/slf4j/helpers/Reporter$Level;->(Ljava/lang/String;II)V -Lorg/slf4j/helpers/Reporter$TargetChoice; -HSPLorg/slf4j/helpers/Reporter$TargetChoice;->$values()[Lorg/slf4j/helpers/Reporter$TargetChoice; -HSPLorg/slf4j/helpers/Reporter$TargetChoice;->()V -HSPLorg/slf4j/helpers/Reporter$TargetChoice;->(Ljava/lang/String;I)V -Lorg/slf4j/helpers/SubstituteLoggerFactory; -HSPLorg/slf4j/helpers/SubstituteLoggerFactory;->()V -HSPLorg/slf4j/helpers/SubstituteLoggerFactory;->clear()V -HSPLorg/slf4j/helpers/SubstituteLoggerFactory;->getEventQueue()Ljava/util/concurrent/LinkedBlockingQueue; -HSPLorg/slf4j/helpers/SubstituteLoggerFactory;->getLoggers()Ljava/util/List; -HSPLorg/slf4j/helpers/SubstituteLoggerFactory;->postInitialization()V -Lorg/slf4j/helpers/SubstituteServiceProvider; -HSPLorg/slf4j/helpers/SubstituteServiceProvider;->()V -HSPLorg/slf4j/helpers/SubstituteServiceProvider;->getSubstituteLoggerFactory()Lorg/slf4j/helpers/SubstituteLoggerFactory; -Lorg/slf4j/helpers/ThreadLocalMapOfStacks; -HSPLorg/slf4j/helpers/ThreadLocalMapOfStacks;->()V -Lorg/slf4j/helpers/Util; -HSPLorg/slf4j/helpers/Util;->()V -HSPLorg/slf4j/helpers/Util;->safeGetBooleanSystemProperty(Ljava/lang/String;)Z -HSPLorg/slf4j/helpers/Util;->safeGetSystemProperty(Ljava/lang/String;)Ljava/lang/String; -Lorg/slf4j/nop/NOPServiceProvider; -HSPLorg/slf4j/nop/NOPServiceProvider;->()V -HSPLorg/slf4j/nop/NOPServiceProvider;->()V -HSPLorg/slf4j/nop/NOPServiceProvider;->getLoggerFactory()Lorg/slf4j/ILoggerFactory; -HSPLorg/slf4j/nop/NOPServiceProvider;->getMDCAdapter()Lorg/slf4j/spi/MDCAdapter; -HSPLorg/slf4j/nop/NOPServiceProvider;->getRequestedApiVersion()Ljava/lang/String; -HSPLorg/slf4j/nop/NOPServiceProvider;->initialize()V -Lorg/slf4j/spi/MDCAdapter; -Lorg/slf4j/spi/SLF4JServiceProvider; \ No newline at end of file +Lorg/slf4j/Logger; \ No newline at end of file diff --git a/androidApp/src/main/AndroidManifest.xml b/androidApp/src/main/AndroidManifest.xml index 00554ff0ad..58c60f1c41 100644 --- a/androidApp/src/main/AndroidManifest.xml +++ b/androidApp/src/main/AndroidManifest.xml @@ -93,6 +93,10 @@ android:name="android.hardware.nfc" android:required="false" /> + + + + + + + + + + + [!WARNING]\r\n> **New US nodes now default to LongTurbo, not LongFast.** Only applies to first-time region selection on the device screen. LongTurbo and LongFast can't hear each other. You will need to switch the preset back to LongFast to join an existing mesh on the prior default. (#11637)\r\n\r\n> [!IMPORTANT]\r\n> If you hit a bootloop or repeated crashes after upgrading, we recommend a full erase and flash. Export your config first to backup first, if you have any important settings you want to save.\r\n\r\nThere are a number of fundamental changes to 2.8 that require a heads-up. Please review some of these higher profile changes so that you are aware of them before installing:\r\n\r\n- Node numbers (ID) are now derived from the public-key identity of the node instead of hardware mac address\r\n- XEdDSA based packet signing\r\n- Reduction of long-name to 25 bytes\r\n- Precise position no longer allowed on known-keys (public mesh. Please use private channels for this)\r\n- Telemetry and position are off-by-default and must be opted into\r\n- Ground-up redesigned NodeDB storage and traffic management\r\n- Completely new allocation of LoRa Regions and presets, including ham specific carve-outs\r\n\r\n## Other high-impact changes\r\n\r\n### Radio & mesh protocol\r\n\r\n- **Packet Signing via XEdDSA** (#10478) plus a **BaseUI signing status UI** (#10841), unsigned-packet policy hardening with test coverage (#10858), and runtime-toggleable `MESHTASTIC_LOCKDOWN` hardening for nRF52 (#10349, opt-in via #10712).\r\n- **Traffic Management Module** for packet forwarding — dedup, rate limiting, role-aware policing (#9358 base, #10706 dedup/rate-limit expansion, #10745 next-hop cache overflow store, #9921 congestion-aware position interval/hop-exhaustion tuning).\r\n- **Automatic variable hop limits** based on live mesh activity and message-size estimation (#10176).\r\n- **Mesh beacon** feature and admin controls (#10618 base implementation, #10839 second pass adding beacon admin/config).\r\n- **Noise floor** tracking with a sliding-window estimate (#9347).\r\n- **Hash table index for O(1) packet history lookups** — improves routing/dedup performance on busy meshes (#9499).\r\n- New amateur-radio regions: 70cm (#10627), 1.25m/125cm (#10638), 2m/~144MHz (#10623); EU regions merge plus Narrow/Lite region enablement for EU (#10675, #10120); LoRa region preset map for cleaner per-region defaults (#10736).\r\n- **TinyFast and TinySlow modem presets** added to config and menu (#10597).\r\n- LoRa config changes now apply live without a reboot (#9962); LoRa settings expansion and validation improvements (#9878).\r\n- Position privacy: direct-send position packets are clamped to channel precision (#10383), and public/known-key position precision is clamped similarly (#10665) and honors explicit channel settings to prevent location leaks (#10513).\r\n- Licensed operators are now prevented from rebroadcasting packets to/from unlicensed users, for regulatory compliance (#9958).\r\n- Packets with missing/invalid `hop_start` (pre-hop firmware) are now deprecated/blocked (#9476).\r\n- Spoof detection added for UDP multicast packets (#9905).\r\n- Low-bandwidth conversion support added to MeshRadio (#10595).\r\n- ATAK Plugin V2 implemented (drops legacy unishox2 compression) (#10105), including TAKTALK voice/text chat message and room data structures.\r\n- Ethernet HTTP/HTTPS API server ported to RP2350 + W5500 boards (#10573), plus Ethernet OTA support for RP2350/W5500 (#10136).\r\n- Optional `LED_LORA` indicator to show LoRa TX activity (#10465), and an LED indicator for LoRa RX (#10674).\r\n- GPS time sync: device now sets its clock from GPS every 30 minutes (#10737); GPS is skipped at startup if the LoRa region is unset, to save battery (#10386); GPS model/baudrate now cached across reboots to skip a full sweep (#10544).\r\n- Config: **position & telemetry broadcast are now opt-in** rather than always-on (#10929).\r\n\r\n### UI / display — BaseUI\r\n\r\n*BaseUI is the current default graphical UI (`src/graphics/Screen.cpp`, `draw/UIRenderer`, `draw/MenuHandler`, `draw/CompassRenderer`, `draw/NotificationRenderer`, `draw/NodeListRenderer`).*\r\n\r\n- **Color support for TFT-equipped nodes** (#10233).\r\n- **\"Ham Mode\"** — prompts for new Amateur Radio regions (#10663).\r\n- Status-message display added to Favorite/NodeList screens (#9504, #10197).\r\n- **Hex picker** UI added for entering hex values (#10650).\r\n- Save/restore of frame visibility state across sessions (#10576).\r\n- BLE pairing PIN now shown via a proper on-screen banner (#8902).\r\n- Compass rendering/behavior improvements (#10166).\r\n- Emote handling refactor (#9896).\r\n\r\n### UI / display — InkHUD\r\n\r\n*InkHUD is the e-ink-optimized UI (`src/graphics/niche/InkHUD`).*\r\n\r\n- **Offline map tiles with zoom controls** (#10785) and general GPS UX improvements (#10846).\r\n- **Full touch support** for the T5 E-Paper S3 (#10286) and a full InkHUD port for the LilyGo T5 E-Paper S3 Pro (#10211).\r\n- **\"Wipe all messages\"** option (#10721).\r\n- T-mini E-Ink S3 support added (#9856).\r\n\r\n### UI / display — MUI\r\n\r\n- Send message on main Return key in MUI composer (X11/native-tft) by @p0ns (meshtastic/device-ui#345)\r\n- CURLService (portduino map HTTP downloads) by @mverch67 (meshtastic/device-ui#353)\r\n- Enable LODEPNG decoder in lv_conf.h by @mverch67 (meshtastic/device-ui#358)\r\n- Support SD cards behind a co-processor (SenseCAP Indicator) by @caveman99 (meshtastic/device-ui#342)\r\n- Refactor URLService::load() to improve HTTP handling by @mverch67 (meshtastic/device-ui#362)\r\n- SPI lock; revert http flags by @mverch67 (meshtastic/device-ui#366)\r\n- Improve acttime sync by @mverch67 (meshtastic/device-ui#368)\r\n- Async loading of remote map tiles in background task by @mverch67 (meshtastic/device-ui#367)\r\n- Compilation PR10908 (some targets fail) by @mverch67 (meshtastic/device-ui#370)\r\n- Modify lock method in ISpiLock to return false by @mverch67 (meshtastic/device-ui#372)\r\n- Transport driver orientation from config.yaml - default 0 rotation. by @caveman99 (meshtastic/device-ui#355)\r\n- Use external reference instead of duplicate SPI1 by @mverch67 (meshtastic/device-ui#373)\r\n- Update LoRa frequency in JP region by @t-miura (meshtastic/device-ui#376)\r\n- Seeed Wio Tracker L2 by @mverch67 (meshtastic/device-ui#335)\r\n- ToggleDisplay() by @mverch67 (meshtastic/device-ui#378)\r\n- WiFi map-tile download adapted for Heltec V4 (#10011).\r\n\r\n### UI / display — shared / cross-cutting\r\n\r\n*Touches the display driver layer or more than one UI system at once.*\r\n\r\n- InkHUD and BaseUI **message store unified** into a shared implementation (#10596).\r\n- T-mini E-Ink S3 support landed for both InkHUD and BaseUI (#9856).\r\n- Board-specific TFT driver support and simplified TFT ifdef chains for easier porting (#10827, #10803); faster TFT color conversion (#10814); touchscreen variant flags (`VARIANT_TOUCHSCREEN`/`ENABLE_TOUCH_INT`) added for new touch boards (#10815).\r\n\r\n## Backend / developer-facing features\r\n\r\n### Memory & stability infrastructure\r\n\r\n- **MemClass.h** — a central memory-class ladder providing fail-safe-small defaults across constrained platforms (#10901).\r\n- **MemAudit** — per-subsystem heap accounting reported in the boot log (#10900).\r\n- nRF52 heap tiers and SoftDevice RAM reservation right-sized, freeing an extra ~8KB heap arena (#10898, #10903).\r\n- Native Portduino malloc shim added for more realistic memory behavior in simulation (#10677).\r\n- Bluetooth memory freed automatically when Wi-Fi is enabled or Bluetooth is disabled (#10398); Bluetooth wait is skipped entirely when disabled (#10571).\r\n- NodeDB \"warm store\" — new persistent warm-tier node storage layer (#10705 base, #10746/#10759 right-sizing for constrained platforms, #10809 fixing associated spiLock deadlocks).\r\n- 2.8 NodeDB shrink/decoupling/restructuring to reduce per-node memory footprint (#10413).\r\n- Flash hardening, filesystem platform unification, and a write-behind LFS cache for STM32WL/nRF52 — a storage-format break (#10171).\r\n\r\n### New APIs / module-author infrastructure\r\n\r\n- **MCP server** added for interacting with Meshtastic devices and driving a testing framework / TUI, later extracted to its own repo (#10194, #10861).\r\n- Native \"sensors\" simulation support for Portduino (#10748); `PortduinoSetOptions` to override the `realhardware` flag for tests (#10157).\r\n- Hardfault handler added for STM32, making crashes visibly obvious in logs (#10071).\r\n- `BinarySemaphorePosix` implemented with proper pthread synchronization for native builds (#9895).\r\n- NimBLE parameter overhaul, including a fix attempt for incompatible BLE bond cleanup (#10741).\r\n- STM32 ADC support added to `AnalogBatteryLevel` (#9369).\r\n- JSON library dependency removed from firmware builds while retaining full JSON support in meshtasticd (#10152) — reduces firmware footprint for module authors relying on the JSON path.\r\n- Improved manual build flow / developer build ergonomics (#8839).- External Notifications module logic fully reworked for more flexible notification rules (#10006).\r\n\r\n**Full Changelog**: https://github.com/meshtastic/firmware/compare/v2.7.23.b246bcd...v2.8.0.47db0e3\r\n" }, { diff --git a/androidApp/src/main/kotlin/org/meshtastic/app/MainActivity.kt b/androidApp/src/main/kotlin/org/meshtastic/app/MainActivity.kt index 42d263773e..d9b393676d 100644 --- a/androidApp/src/main/kotlin/org/meshtastic/app/MainActivity.kt +++ b/androidApp/src/main/kotlin/org/meshtastic/app/MainActivity.kt @@ -19,6 +19,7 @@ package org.meshtastic.app import android.app.PendingIntent import android.app.TaskStackBuilder import android.content.Intent +import android.content.pm.PackageManager import android.graphics.Color import android.hardware.usb.UsbManager import android.net.Uri @@ -41,7 +42,9 @@ import androidx.compose.runtime.remember import androidx.core.content.IntentCompat import androidx.core.net.toUri import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen +import androidx.lifecycle.Lifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle +import androidx.lifecycle.compose.currentStateAsState import androidx.lifecycle.lifecycleScope import co.touchlab.kermit.Logger import com.eygraber.uri.toKmpUri @@ -59,6 +62,7 @@ import org.meshtastic.app.ui.MainScreen import org.meshtastic.core.barcode.rememberBarcodeScanner import org.meshtastic.core.navigation.DEEP_LINK_BASE_URI import org.meshtastic.core.network.repository.UsbRepository +import org.meshtastic.core.nfc.NfcEmulatorEffect import org.meshtastic.core.nfc.NfcScannerEffect import org.meshtastic.core.nfc.NfcWriterEffect import org.meshtastic.core.resources.Res @@ -81,6 +85,8 @@ import org.meshtastic.core.ui.util.LocalEventBranding import org.meshtastic.core.ui.util.LocalInlineMapProvider import org.meshtastic.core.ui.util.LocalMapMainScreenProvider import org.meshtastic.core.ui.util.LocalMapViewProvider +import org.meshtastic.core.ui.util.LocalNfcEmulationSupported +import org.meshtastic.core.ui.util.LocalNfcEmulatorProvider import org.meshtastic.core.ui.util.LocalNfcScannerProvider import org.meshtastic.core.ui.util.LocalNfcScannerSupported import org.meshtastic.core.ui.util.LocalNfcWriterProvider @@ -223,7 +229,20 @@ class MainActivity : AppCompatActivity() { LocalNfcScannerProvider provides { onResult, onDisabled -> NfcScannerEffect(onResult, onDisabled) }, LocalNfcWriterProvider provides { url, onResult, onDisabled -> NfcWriterEffect(url, onResult, onDisabled) }, LocalBarcodeScannerSupported provides true, - LocalNfcScannerSupported provides true, + // Was a constant true, so the NFC affordance appeared on phones with no NFC radio and + // failed when used. uses-feature declares nfc as not required, so such devices do install. + LocalNfcScannerSupported provides packageManager.hasSystemFeature(PackageManager.FEATURE_NFC), + // Arm card emulation only while the app is actually in the foreground. A share dialog left open behind a + // home-press stays composed, and a channel URL carries the channel PSK. This reads the activity's own + // lifecycle, not LocalLifecycleOwner: the effect is composed inside a Dialog, whose own lifecycle stays + // RESUMED when the activity is backgrounded. + LocalNfcEmulatorProvider provides + { url -> + val lifecycleState by lifecycle.currentStateAsState() + if (lifecycleState.isAtLeast(Lifecycle.State.RESUMED)) NfcEmulatorEffect(url) + }, + LocalNfcEmulationSupported provides + packageManager.hasSystemFeature(PackageManager.FEATURE_NFC_HOST_CARD_EMULATION), LocalAnalyticsIntroProvider provides { AnalyticsIntro() }, LocalMapViewProvider provides getMapViewProvider(), LocalSitePlannerAvailable provides true, diff --git a/androidApp/src/main/kotlin/org/meshtastic/app/MeshUtilApplication.kt b/androidApp/src/main/kotlin/org/meshtastic/app/MeshUtilApplication.kt index 9831bf4348..0bcf3f71c6 100644 --- a/androidApp/src/main/kotlin/org/meshtastic/app/MeshUtilApplication.kt +++ b/androidApp/src/main/kotlin/org/meshtastic/app/MeshUtilApplication.kt @@ -44,6 +44,7 @@ import kotlinx.coroutines.runBlocking import kotlinx.coroutines.withTimeout import org.koin.android.ext.android.get import org.koin.android.ext.koin.androidContext +import org.koin.androidx.workmanager.factory.KoinWorkerFactory import org.koin.androidx.workmanager.koin.workManagerFactory import org.koin.plugin.module.dsl.startKoin import org.meshtastic.app.di.AndroidKoinApp @@ -247,6 +248,10 @@ open class MeshUtilApplication : ) } + /** + * Dead unless WorkManager falls back to on-demand init: [workManagerFactory] initializes it eagerly during + * [startKoin]. Constructed rather than resolved because nothing declares a [WorkerFactory] in the graph. + */ override val workManagerConfiguration: Configuration - get() = Configuration.Builder().setWorkerFactory(get()).build() + get() = Configuration.Builder().setWorkerFactory(KoinWorkerFactory()).build() } diff --git a/androidApp/src/main/kotlin/org/meshtastic/app/map/SitePlannerRunner.kt b/androidApp/src/main/kotlin/org/meshtastic/app/map/SitePlannerRunner.kt index 53224cff3b..8b95b95ff2 100644 --- a/androidApp/src/main/kotlin/org/meshtastic/app/map/SitePlannerRunner.kt +++ b/androidApp/src/main/kotlin/org/meshtastic/app/map/SitePlannerRunner.kt @@ -35,7 +35,6 @@ import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.material3.CircularWavyProgressIndicator -import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Surface import androidx.compose.material3.Text @@ -80,7 +79,6 @@ private const val SITE_PLANNER_TIMEOUT_MS = 45_000L * ([onRequestCurrentLocation], when permission is granted), this node ([onUseNodeLocation]), or the map center * ([onUseMapCenter]). Google-flavor affordance; targets the official hosted planner. */ -@OptIn(ExperimentalMaterial3ExpressiveApi::class) @Suppress("LongMethod", "LambdaParameterInRestartableEffect") @Composable fun SitePlannerHost( diff --git a/androidApp/src/main/res/values/strings.xml b/androidApp/src/main/res/values/strings.xml index ef6e3c9f77..910579fce4 100644 --- a/androidApp/src/main/res/values/strings.xml +++ b/androidApp/src/main/res/values/strings.xml @@ -18,4 +18,5 @@ Meshtastic Off-grid mesh networking for secure, long-range communications via LoRa radio. + Meshtastic contact and channel sharing diff --git a/androidApp/src/main/res/xml/nfc_apduservice.xml b/androidApp/src/main/res/xml/nfc_apduservice.xml new file mode 100644 index 0000000000..64617f90ca --- /dev/null +++ b/androidApp/src/main/res/xml/nfc_apduservice.xml @@ -0,0 +1,28 @@ + + + + + + + + + diff --git a/androidApp/src/test/kotlin/org/meshtastic/app/CoilImageLoaderLifecycleTest.kt b/androidApp/src/test/kotlin/org/meshtastic/app/CoilImageLoaderLifecycleTest.kt index 6b48c18da4..8572158583 100644 --- a/androidApp/src/test/kotlin/org/meshtastic/app/CoilImageLoaderLifecycleTest.kt +++ b/androidApp/src/test/kotlin/org/meshtastic/app/CoilImageLoaderLifecycleTest.kt @@ -17,6 +17,9 @@ package org.meshtastic.app import androidx.test.core.app.ApplicationProvider +import androidx.work.Configuration +import androidx.work.testing.SynchronousExecutor +import androidx.work.testing.WorkManagerTestInitHelper import coil3.ImageLoader import coil3.SingletonImageLoader import coil3.annotation.DelicateCoilApi @@ -35,6 +38,7 @@ class CoilImageLoaderLifecycleTest { @OptIn(DelicateCoilApi::class) fun tearDown() { SingletonImageLoader.reset() + WorkManagerTestInitHelper.closeWorkDatabase() } @Test @@ -47,9 +51,21 @@ class CoilImageLoaderLifecycleTest { } /** - * Boots the production Application with its background init suppressed: what this asserts is the Koin/Coil wiring, and - * the real [MeshUtilApplication.startBackgroundInit] opens a database whose connection can outlive the test. + * Boots the production Application with no database open left in flight at teardown: [startBackgroundInit] is + * suppressed, and WorkManager is initialized synchronously first so Koin's eager `workManagerFactory()` skips its own + * `initialize`, whose `ForceStopRunnable` would otherwise open WorkDatabase on framework SQLite from a background + * thread. + * + * Must not be private: Robolectric instantiates the `@Config` application through `AppComponentFactory`, whose + * `Class.newInstance()` call cannot reach a package-private class. */ -private class ImageLoaderOnlyApplication : MeshUtilApplication() { +internal class ImageLoaderOnlyApplication : MeshUtilApplication() { + override fun onCreate() { + val configuration = + Configuration.Builder().setExecutor(SynchronousExecutor()).setTaskExecutor(SynchronousExecutor()).build() + WorkManagerTestInitHelper.initializeTestWorkManager(this, configuration) + super.onCreate() + } + override fun startBackgroundInit() = Unit } diff --git a/androidApp/src/test/kotlin/org/meshtastic/app/ui/DeepLinkManifestConsistencyTest.kt b/androidApp/src/test/kotlin/org/meshtastic/app/ui/DeepLinkManifestConsistencyTest.kt index a841d3da3a..4a789953b0 100644 --- a/androidApp/src/test/kotlin/org/meshtastic/app/ui/DeepLinkManifestConsistencyTest.kt +++ b/androidApp/src/test/kotlin/org/meshtastic/app/ui/DeepLinkManifestConsistencyTest.kt @@ -16,8 +16,11 @@ */ package org.meshtastic.app.ui +import org.junit.runner.RunWith import org.meshtastic.core.common.util.CommonUri import org.meshtastic.core.navigation.DeepLinkRouter +import org.robolectric.RobolectricTestRunner +import org.robolectric.annotation.Config import org.w3c.dom.Element import java.io.File import javax.xml.parsers.DocumentBuilderFactory @@ -34,7 +37,12 @@ import kotlin.test.fail * filter — otherwise `https://meshtastic.org/{path}` links open in the browser instead of the app, even though the * `meshtastic://` scheme works. The segments come straight from [DeepLinkRouter.topLevelPathSegments], the set * [DeepLinkRouter.route] gates its dispatch on, so a new router segment fails here until the manifest declares it. + * + * Robolectric because on Android the router's `DeepLinkUri` is `android.net.Uri`, a stub on the plain unit-test + * classpath. */ +@RunWith(RobolectricTestRunner::class) +@Config(sdk = [34]) class DeepLinkManifestConsistencyTest { @Test diff --git a/androidApp/src/test/kotlin/org/meshtastic/app/ui/metrics/EnvironmentMetricsTest.kt b/androidApp/src/test/kotlin/org/meshtastic/app/ui/metrics/EnvironmentMetricsTest.kt index a5cf90dc32..4ea59e5efa 100644 --- a/androidApp/src/test/kotlin/org/meshtastic/app/ui/metrics/EnvironmentMetricsTest.kt +++ b/androidApp/src/test/kotlin/org/meshtastic/app/ui/metrics/EnvironmentMetricsTest.kt @@ -33,14 +33,18 @@ class EnvironmentMetricsTest { val expectedSoilTemperatureFahrenheit = celsiusToFahrenheit(initialSoilTemperatureCelsius) val telemetry = - Telemetry( - environment_metrics = - EnvironmentMetrics( - temperature = initialTemperatureCelsius, - soil_temperature = initialSoilTemperatureCelsius, - ), - time = 1000, - ) + Telemetry.Builder() + .also { wb -> + wb.environment_metrics = + EnvironmentMetrics.Builder() + .also { wb -> + wb.temperature = initialTemperatureCelsius + wb.soil_temperature = initialSoilTemperatureCelsius + } + .build() + wb.time = 1000 + } + .build() val data = listOf(telemetry) @@ -52,13 +56,18 @@ class EnvironmentMetricsTest { val metrics = tel.environment_metrics!! val temperatureFahrenheit = celsiusToFahrenheit(metrics.temperature ?: 0f) val soilTemperatureFahrenheit = celsiusToFahrenheit(metrics.soil_temperature ?: 0f) - tel.copy( - environment_metrics = - metrics.copy( - temperature = temperatureFahrenheit, - soil_temperature = soilTemperatureFahrenheit, - ), - ) + tel.newBuilder() + .also { wb -> + wb.environment_metrics = + metrics + .newBuilder() + .also { wb -> + wb.temperature = temperatureFahrenheit + wb.soil_temperature = soilTemperatureFahrenheit + } + .build() + } + .build() } } else { data diff --git a/androidApp/src/test/resources/robolectric.properties b/androidApp/src/test/resources/robolectric.properties index aca49b6fa3..2ffa457492 100644 --- a/androidApp/src/test/resources/robolectric.properties +++ b/androidApp/src/test/resources/robolectric.properties @@ -3,9 +3,4 @@ # in flight, leaving the connection live while Robolectric deletes the temp data dir under it -- a SIGSEGV in # the sqlite JNI that kills the whole test fork. Booting the real Application is therefore opt-in per test. application=android.app.Application -# Legacy SQLite keeps Robolectric's own native SQLite off androidApp's unit-test path. The native -# implementation segfaults the whole test fork if its nativeruntime temp dir is torn down while a -# background database open is still in flight, which fails the task outright (retry cannot help). -# It says nothing about the bundled driver above -- that never goes through android.database.sqlite. sdk=34 -sqliteMode=LEGACY diff --git a/androidApp/src/testFdroid/kotlin/org/meshtastic/app/di/FdroidBindingWinnerTest.kt b/androidApp/src/testFdroid/kotlin/org/meshtastic/app/di/FdroidBindingWinnerTest.kt new file mode 100644 index 0000000000..1808d775a1 --- /dev/null +++ b/androidApp/src/testFdroid/kotlin/org/meshtastic/app/di/FdroidBindingWinnerTest.kt @@ -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 . + */ +package org.meshtastic.app.di + +import org.koin.plugin.module.dsl.koinApplication +import org.meshtastic.feature.docs.translation.DocTranslationService +import org.meshtastic.feature.docs.translation.NoOpDocTranslator +import org.meshtastic.feature.messaging.translation.MessageTranslationService +import org.meshtastic.feature.messaging.translation.NoOpMessageTranslator +import kotlin.test.Test +import kotlin.test.assertIs + +class FdroidBindingWinnerTest { + + @Test + fun `flavor bindings win over the shared graph`() { + // The flavor modules are @Configuration, which loads them before the ones listed in @KoinApplication, and + // Koin is last-wins. KoinVerificationTest only checks definitions exist, never which one survives, so a + // core-level default added later would silently take these over. Only the Fdroid no-ops are asserted: + // the Google flavor's MlKitMessageTranslator builds a RemoteModelManager in a field initializer. + val app = koinApplication() + try { + val koin = app.koin + assertIs(koin.get()) + assertIs(koin.get()) + } finally { + app.close() + } + } +} diff --git a/androidApp/src/testGoogle/kotlin/org/meshtastic/app/map/MapNodeClusterItemsTest.kt b/androidApp/src/testGoogle/kotlin/org/meshtastic/app/map/MapNodeClusterItemsTest.kt index ed82534217..f567a21a28 100644 --- a/androidApp/src/testGoogle/kotlin/org/meshtastic/app/map/MapNodeClusterItemsTest.kt +++ b/androidApp/src/testGoogle/kotlin/org/meshtastic/app/map/MapNodeClusterItemsTest.kt @@ -98,7 +98,7 @@ class MapNodeClusterItemsTest { runOnIdle { val node = nodes.single() - nodes = listOf(node.copy(user = node.user.copy(short_name = "NEW"))) + nodes = listOf(node.copy(user = node.user.newBuilder().also { wb -> wb.short_name = "NEW" }.build())) } waitForIdle() val changedNodeItems = latestItems @@ -133,13 +133,22 @@ class MapNodeClusterItemsTest { private fun testNodes(count: Int): List = List(count) { index -> Node( num = index + 1, - user = User(id = "!${index + 1}", long_name = "Node ${index + 1}", short_name = "N$index"), + user = + User.Builder() + .also { wb -> + wb.id = "!${index + 1}" + wb.long_name = "Node ${index + 1}" + wb.short_name = "N$index" + } + .build(), position = - Position( - latitude_i = 210_000_000 + index, - longitude_i = -1_570_000_000 + index, - time = 1_700_000_000, - ), + Position.Builder() + .also { wb -> + wb.latitude_i = 210_000_000 + index + wb.longitude_i = -1_570_000_000 + index + wb.time = 1_700_000_000 + } + .build(), ) } diff --git a/build-logic/convention/src/main/kotlin/KoinConventionPlugin.kt b/build-logic/convention/src/main/kotlin/KoinConventionPlugin.kt index 214bbed8d1..6e79955aac 100644 --- a/build-logic/convention/src/main/kotlin/KoinConventionPlugin.kt +++ b/build-logic/convention/src/main/kotlin/KoinConventionPlugin.kt @@ -29,13 +29,10 @@ class KoinConventionPlugin : Plugin { // Configure Koin K2 Compiler Plugin (1.1.0+) extensions.configure(KoinGradleExtension::class.java) { - // 1.1.0 moved validation to the entry points, which suits this graph's shape, but - // its definition index still can't resolve two structural patterns here: modules - // reached through FlavorModule's nested `includes` are invisible to it, and DSL - // declarations (desktopApp's whole root, workManagerFactory()) are never indexed at - // all. Every entry point therefore fails on definitions that exist. Runtime graph - // verification is handled by KoinVerificationTest instead. - compileSafety.set(false) + // Validation is whole-graph and happens at the @KoinApplication entry points, so + // it is only enabled there. A library module validates locally, cannot see the + // assembled graph, and reports KOIN-D003 on definitions its consumers supply. + compileSafety.set(path in KOIN_ENTRY_POINTS) } val koinAnnotations = libs.findLibrary("koin-annotations").get() @@ -80,3 +77,6 @@ class KoinConventionPlugin : Plugin { } } } + +/** Modules declaring a `@KoinApplication`. A new app target must be added here or it is never validated. */ +private val KOIN_ENTRY_POINTS = setOf(":androidApp", ":desktopApp") diff --git a/build-logic/convention/src/main/kotlin/org/meshtastic/buildlogic/MaplibreDesktopRuntime.kt b/build-logic/convention/src/main/kotlin/org/meshtastic/buildlogic/MaplibreDesktopRuntime.kt new file mode 100644 index 0000000000..e31b9a270b --- /dev/null +++ b/build-logic/convention/src/main/kotlin/org/meshtastic/buildlogic/MaplibreDesktopRuntime.kt @@ -0,0 +1,47 @@ +/* + * 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 . + */ +package org.meshtastic.buildlogic + +import org.gradle.api.Project + +/** + * Adds exactly one maplibre-compose native runtime — the one matching this build host — as `runtimeOnly`. + * + * Each runtime artifact carries that platform's maplibre-native blob, so adding them all would bloat every + * distribution with four unusable copies. Upstream publishes no macos-x64 artifact, which matches our release matrix; + * on an Intel Mac no runtime is added and the map degrades to its engine-unavailable state at runtime, rather than + * failing every Gradle task at configuration time for a host that only builds the Android app. + * Shared by `:desktopApp` and `:marketing-screenshots`, the two JVM modules that draw a MapLibre map. + */ +fun Project.maplibreDesktopRuntime() { + val osName = providers.systemProperty("os.name").get().lowercase() + val osArch = providers.systemProperty("os.arch").get().lowercase() + val isArm = osArch.contains("aarch64") || osArch.contains("arm64") + if (osName.contains("mac") && !isArm) { + logger.warn("maplibre-compose publishes no macOS x64 runtime; the desktop map is unavailable on this host.") + return + } + val alias = + when { + osName.contains("mac") -> "maplibre-compose-runtime-metal-macos-arm64" + osName.contains("win") && isArm -> "maplibre-compose-runtime-vulkan-windows-arm64" + osName.contains("win") -> "maplibre-compose-runtime-vulkan-windows-x64" + isArm -> "maplibre-compose-runtime-vulkan-linux-arm64" + else -> "maplibre-compose-runtime-vulkan-linux-x64" + } + dependencies.add("runtimeOnly", libs.library(alias)) +} diff --git a/config/detekt/detekt.yml b/config/detekt/detekt.yml index bd3f186f80..f518ac5b9d 100644 --- a/config/detekt/detekt.yml +++ b/config/detekt/detekt.yml @@ -86,7 +86,10 @@ complexity: ignoreOverloaded: false CyclomaticComplexMethod: active: true - allowedComplexity: 15 + # 35, not 15: Wire's buildersOnly protos are constructed through + # `Builder().also { }`, so a settings screen scores one point per row it + # writes without gaining a branch. MQTTConfigScreen reads 33 that way. + allowedComplexity: 35 ignoreSingleWhenExpression: false ignoreSimpleWhenEntries: false ignoreNestingFunctions: false @@ -106,10 +109,13 @@ complexity: ignoredLabels: [] LargeClass: active: true - allowedLines: 600 + # Same cause as CyclomaticComplexMethod: builder blocks are several lines + # where a named-argument constructor call was one. + allowedLines: 1100 LongMethod: active: true - allowedLines: 60 + # Same cause as CyclomaticComplexMethod. + allowedLines: 70 ignoreAnnotated: [ 'Preview', 'PreviewLightDark', 'PreviewScreenSizes' ] LongParameterList: active: true @@ -691,6 +697,7 @@ style: - '0' - '1' - '2' + ignoreAnnotated: [ 'Preview', 'PreviewLightDark', 'PreviewScreenSizes' ] ignoreHashCodeFunction: true ignorePropertyDeclaration: true ignoreLocalVariableDeclaration: false diff --git a/core/ble/src/androidMain/kotlin/org/meshtastic/core/ble/KableGattCacheRefresh.kt b/core/ble/src/androidMain/kotlin/org/meshtastic/core/ble/KableGattCacheRefresh.kt index 818d377c41..046762b32c 100644 --- a/core/ble/src/androidMain/kotlin/org/meshtastic/core/ble/KableGattCacheRefresh.kt +++ b/core/ble/src/androidMain/kotlin/org/meshtastic/core/ble/KableGattCacheRefresh.kt @@ -23,7 +23,7 @@ import kotlinx.coroutines.flow.StateFlow @Suppress("ReturnCount") internal actual fun Peripheral.refreshGattCache(): Boolean { - // Direct 2-hop reflection on Kable 0.43.1 internals. + // Direct 2-hop reflection on Kable 0.45.0 internals. // Path: BluetoothDeviceAndroidPeripheral.connection (MutableStateFlow) → .value → Connection.gatt // Re-verify field names on Kable version bumps. val gatt = diff --git a/core/ble/src/androidMain/kotlin/org/meshtastic/core/ble/KablePlatformSetup.kt b/core/ble/src/androidMain/kotlin/org/meshtastic/core/ble/KablePlatformSetup.kt index 5deccbe785..332b4ec48b 100644 --- a/core/ble/src/androidMain/kotlin/org/meshtastic/core/ble/KablePlatformSetup.kt +++ b/core/ble/src/androidMain/kotlin/org/meshtastic/core/ble/KablePlatformSetup.kt @@ -25,10 +25,10 @@ import com.juul.kable.ScannerBuilder import com.juul.kable.toIdentifier import org.meshtastic.core.model.util.anonymize -// Kable's default trySendBlocking can park the scan-callback (sometimes main) thread in dense BLE -// environments, causing ANRs; preConflate drops excess advertisements instead (kable#654). +// The scan callback never blocks; a capacity of 1 conflates, so a dense BLE environment cannot back +// advertisements up behind a slow collector. internal actual fun ScannerBuilder.platformScanConfig() { - preConflate = true + bufferCapacity = 1 } /** Android's scanner filters on address in hardware, so Kable's `Filter.Address` works natively here. */ diff --git a/core/common/src/commonMain/kotlin/org/meshtastic/core/common/state/FirmwareMaintenanceLock.kt b/core/common/src/commonMain/kotlin/org/meshtastic/core/common/state/FirmwareMaintenanceLock.kt deleted file mode 100644 index 0b273d2335..0000000000 --- a/core/common/src/commonMain/kotlin/org/meshtastic/core/common/state/FirmwareMaintenanceLock.kt +++ /dev/null @@ -1,54 +0,0 @@ -/* - * Copyright (c) 2026 Meshtastic LLC - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ -package org.meshtastic.core.common.state - -import kotlinx.atomicfu.atomic -import org.koin.core.annotation.Single - -/** - * Marks a USB firmware-maintenance sequence as in flight, so the radio transport does not fight it for the device. - * - * A factory erase leaves the device enumerating as a bare CDC port with no Meshtastic protocol on it. The serial - * transport will happily bind to that — its device lookup falls back to whatever probed device it can find, not just - * the saved address — and the environmental-recovery listeners re-enter transport startup whenever Bluetooth or the - * network flips. Both would claim the port the maintenance flow needs. - * - * Lives in `:core:common` because the two parties sit in modules that cannot see each other: the flow that takes the - * lock is in `:feature:firmware`, and the code that must respect it is in `:core:service`. - * - * The consequence of ignoring the lock is bounded rather than destructive — a transport that claims the port asserts - * DTR itself, which happens to unblock the erase — but it makes the flow's own signal unreliable, and a mesh handshake - * against erase firmware is noise nobody needs to debug. - */ -@Single -class FirmwareMaintenanceLock { - private val active = atomic(false) - - /** True while a maintenance sequence holds the lock. */ - val isActive: Boolean - get() = active.value - - /** Takes the lock. Idempotent — re-taking an already-held lock is a no-op. */ - fun acquire() { - active.value = true - } - - /** Releases the lock. Must run even on failure, so callers use `try`/`finally`. */ - fun release() { - active.value = false - } -} diff --git a/core/common/src/commonMain/kotlin/org/meshtastic/core/common/state/RadioOperationLock.kt b/core/common/src/commonMain/kotlin/org/meshtastic/core/common/state/RadioOperationLock.kt new file mode 100644 index 0000000000..91340b558c --- /dev/null +++ b/core/common/src/commonMain/kotlin/org/meshtastic/core/common/state/RadioOperationLock.kt @@ -0,0 +1,129 @@ +/* + * 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 . + */ +package org.meshtastic.core.common.state + +import kotlinx.atomicfu.atomic +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.update +import org.koin.core.annotation.Single + +/** + * A long-running operation that owns the radio and must not be interrupted halfway. + * + * [suppressesTransport] is per-operation rather than blanket because the operations genuinely differ: a USB factory + * erase needs the transport to stay off the port, while a discovery scan needs the transport to keep running. + */ +enum class RadioOperation(val suppressesTransport: Boolean) { + /** + * A USB firmware-maintenance sequence (factory erase). + * + * A factory erase leaves the device enumerating as a bare CDC port with no Meshtastic protocol on it. The serial + * transport will happily bind to that — its device lookup falls back to whatever probed device it can find, not + * just the saved address — and the environmental-recovery listeners re-enter transport startup whenever Bluetooth + * or the network flips. Both would claim the port the maintenance flow needs. + */ + FirmwareMaintenance(suppressesTransport = true), + + /** + * An OTA/DFU flash. + * + * Does not suppress the transport: the flow already frees it by deselecting the device + * (`radioController.setDeviceAddress("n")`), and the post-flash verification deliberately reconnects the radio, so + * suppression would block the very step it is meant to protect. + */ + FirmwareUpdate(suppressesTransport = false), + + /** A Local Mesh Discovery preset sweep. Needs the transport, so it must never suppress it. */ + DiscoveryScan(suppressesTransport = false), +} + +/** + * One holder's claim on a [RadioOperation]. Releasing a lease affects only that holder. + * + * Identity matters: two concurrent claims on the same operation produce two distinct leases, so a late release from an + * abandoned holder cannot retire a live one. + */ +class OperationLease internal constructor(internal val id: Long, val operation: RadioOperation) + +/** + * Tracks which [RadioOperation]s are in flight, so the rest of the app can keep out of their way. + * + * Three consumers, which is why one lock replaced the single-purpose `FirmwareMaintenanceLock` it grew out of: + * `SharedRadioInterfaceService` reads [suppressesTransport] to stay off the device, `MeshService` reads [holders] to + * refuse to stop itself and to hold a wake lock, and the service notification reads it to say what is running. + * + * Holders are tracked as individual leases rather than a set of operation types. The set was wrong: a caller that + * cancels a job and immediately starts a replacement — which `FirmwareUpdateViewModel` does, without joining — lets the + * cancelled job's `finally` run *after* the replacement has acquired, and a shared entry would be retired out from + * under the live holder. Two holders of the same operation are now independent. + * + * Lives in `:core:common` because the parties sit in modules that cannot see each other: the flows that take an + * operation are in `:feature:firmware` and `:feature:discovery`, and the code that must respect it is in + * `:core:service`. + * + * Process-local by design. A process death drops every lease, which is correct — nothing is in flight any more — but it + * means this narrows the window in which an interrupted flash can strand a device rather than closing it. + */ +@Single +class RadioOperationLock { + private val nextLeaseId = atomic(0L) + private val _holders = MutableStateFlow>(emptyMap()) + + /** Every live lease, keyed by lease id. Emits on every acquire and release. */ + val holders: StateFlow> = _holders.asStateFlow() + + /** The distinct operations currently in flight. */ + val activeOperations: Set + get() = _holders.value.values.toSet() + + /** True while any operation is in flight. */ + val isActive: Boolean + get() = _holders.value.isNotEmpty() + + /** True while an operation that needs the transport off the device is in flight. */ + val suppressesTransport: Boolean + get() = _holders.value.values.any(RadioOperation::suppressesTransport) + + /** Takes [operation] and returns the lease that holds it. Each call is a distinct holder. */ + fun acquire(operation: RadioOperation): OperationLease { + val lease = OperationLease(nextLeaseId.incrementAndGet(), operation) + _holders.update { it + (lease.id to operation) } + return lease + } + + /** + * Releases [lease]. Idempotent, and null-safe so a caller can release a hold it may never have taken. + * + * Releasing a stale lease is harmless: its id is already gone, and other holders of the same operation keep theirs. + */ + fun release(lease: OperationLease?) { + val id = lease?.id ?: return + _holders.update { it - id } + } + + /** Runs [block] with [operation] held, releasing that exact lease even on failure or cancellation. */ + suspend fun withOperation(operation: RadioOperation, block: suspend () -> T): T { + val lease = acquire(operation) + return try { + block() + } finally { + release(lease) + } + } +} diff --git a/core/common/src/commonTest/kotlin/org/meshtastic/core/common/state/RadioOperationLockTest.kt b/core/common/src/commonTest/kotlin/org/meshtastic/core/common/state/RadioOperationLockTest.kt new file mode 100644 index 0000000000..b87aa07fe0 --- /dev/null +++ b/core/common/src/commonTest/kotlin/org/meshtastic/core/common/state/RadioOperationLockTest.kt @@ -0,0 +1,153 @@ +/* + * 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 . + */ +package org.meshtastic.core.common.state + +import kotlinx.coroutines.CancellationException +import kotlinx.coroutines.test.runTest +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertFailsWith +import kotlin.test.assertFalse +import kotlin.test.assertTrue + +class RadioOperationLockTest { + + @Test + fun startsIdle() { + val lock = RadioOperationLock() + assertFalse(lock.isActive) + assertFalse(lock.suppressesTransport) + assertEquals(emptySet(), lock.activeOperations) + } + + @Test + fun releasingAnUnheldLeaseIsSafe() { + val lock = RadioOperationLock() + lock.release(null) + val lease = lock.acquire(RadioOperation.FirmwareUpdate) + lock.release(lease) + lock.release(lease) + assertFalse(lock.isActive) + } + + @Test + fun twoHoldersOfTheSameOperationAreIndependent() { + // The firmware view model cancels an update job and starts a replacement without joining, so the cancelled + // job's release can land after the replacement has acquired. The late release must not retire the live holder. + val lock = RadioOperationLock() + val abandoned = lock.acquire(RadioOperation.FirmwareUpdate) + val replacement = lock.acquire(RadioOperation.FirmwareUpdate) + + lock.release(abandoned) + + assertTrue(lock.isActive, "the replacement holder must still hold the operation") + assertEquals(setOf(RadioOperation.FirmwareUpdate), lock.activeOperations) + + lock.release(replacement) + assertFalse(lock.isActive) + } + + @Test + fun differentOperationsDoNotClobberEachOther() { + val lock = RadioOperationLock() + val update = lock.acquire(RadioOperation.FirmwareUpdate) + lock.acquire(RadioOperation.DiscoveryScan) + + lock.release(update) + + assertEquals(setOf(RadioOperation.DiscoveryScan), lock.activeOperations) + } + + @Test + fun onlyMaintenanceSuppressesTheTransport() { + val lock = RadioOperationLock() + + lock.acquire(RadioOperation.DiscoveryScan) + assertFalse(lock.suppressesTransport, "a scan needs the transport running") + + lock.acquire(RadioOperation.FirmwareUpdate) + assertFalse(lock.suppressesTransport, "an OTA frees the transport by deselecting, not by suppression") + + val maintenance = lock.acquire(RadioOperation.FirmwareMaintenance) + assertTrue(lock.suppressesTransport) + + lock.release(maintenance) + assertFalse(lock.suppressesTransport, "suppression must end with the maintenance pass") + } + + @Test + fun suppressionSurvivesAnotherMaintenanceHolder() { + val lock = RadioOperationLock() + val first = lock.acquire(RadioOperation.FirmwareMaintenance) + lock.acquire(RadioOperation.FirmwareMaintenance) + + lock.release(first) + + assertTrue(lock.suppressesTransport, "the second maintenance holder still needs the transport off the port") + } + + @Test + fun withOperationReleasesOnSuccess() = runTest { + val lock = RadioOperationLock() + assertEquals("done", lock.withOperation(RadioOperation.FirmwareUpdate) { "done" }) + assertFalse(lock.isActive) + } + + @Test + fun withOperationReleasesOnFailure() = runTest { + val lock = RadioOperationLock() + assertFailsWith { + lock.withOperation(RadioOperation.FirmwareUpdate) { error("flash failed") } + } + assertFalse(lock.isActive, "a failed operation must not leak the lock") + } + + @Test + fun withOperationReleasesOnCancellation() = runTest { + val lock = RadioOperationLock() + assertFailsWith { + lock.withOperation(RadioOperation.DiscoveryScan) { throw CancellationException("stopped") } + } + assertFalse(lock.isActive, "a cancelled operation must not leak the lock") + } + + @Test + fun withOperationCancellationLeavesAConcurrentHolderIntact() = runTest { + val lock = RadioOperationLock() + val other = lock.acquire(RadioOperation.FirmwareUpdate) + + assertFailsWith { + lock.withOperation(RadioOperation.FirmwareUpdate) { throw CancellationException("abandoned") } + } + + assertTrue(lock.isActive, "the surviving holder keeps the operation") + lock.release(other) + assertFalse(lock.isActive) + } + + @Test + fun holdersFlowTracksAcquireAndRelease() = runTest { + val lock = RadioOperationLock() + assertEquals(emptyMap(), lock.holders.value) + + lock.withOperation(RadioOperation.DiscoveryScan) { + assertEquals(listOf(RadioOperation.DiscoveryScan), lock.holders.value.values.toList()) + } + + assertEquals(emptyMap(), lock.holders.value) + } +} diff --git a/core/common/src/jvmMain/kotlin/org/meshtastic/core/common/util/JvmPlatformUtils.kt b/core/common/src/jvmMain/kotlin/org/meshtastic/core/common/util/JvmPlatformUtils.kt index 39501cc1a5..16c03d4f97 100644 --- a/core/common/src/jvmMain/kotlin/org/meshtastic/core/common/util/JvmPlatformUtils.kt +++ b/core/common/src/jvmMain/kotlin/org/meshtastic/core/common/util/JvmPlatformUtils.kt @@ -37,6 +37,11 @@ actual object DateFormatter { private val shortDateTimeFormatter: DateTimeFormatter = DateTimeFormatter.ofLocalizedDateTime(FormatStyle.SHORT, FormatStyle.MEDIUM) + // A localized formatter binds the default locale of the moment it was built, and the desktop app changes the + // default locale after this object exists (the language preference resolves in composition), so each call + // rebinds to the current one; withLocale is a cheap copy. + private fun DateTimeFormatter.localized(): DateTimeFormatter = withLocale(Locale.getDefault()) + actual fun formatRelativeTime(timestampMillis: Long): String { val deltaMillis = nowMillis - timestampMillis val absDeltaMillis = abs(deltaMillis) @@ -51,29 +56,29 @@ actual object DateFormatter { } actual fun formatDateTime(timestampMillis: Long): String = - shortDateTimeFormatter.format(java.time.Instant.ofEpochMilli(timestampMillis).atZone(zoneId)) + shortDateTimeFormatter.localized().format(java.time.Instant.ofEpochMilli(timestampMillis).atZone(zoneId)) actual fun formatShortDate(timestampMillis: Long): String { val isWithin24Hours = (nowMillis - timestampMillis) <= DAY_MILLIS val zonedDateTime = java.time.Instant.ofEpochMilli(timestampMillis).atZone(zoneId) return if (isWithin24Hours) { - shortTimeFormatter.format(zonedDateTime) + shortTimeFormatter.localized().format(zonedDateTime) } else { - shortDateFormatter.format(zonedDateTime) + shortDateFormatter.localized().format(zonedDateTime) } } actual fun formatTime(timestampMillis: Long): String = - shortTimeFormatter.format(java.time.Instant.ofEpochMilli(timestampMillis).atZone(zoneId)) + shortTimeFormatter.localized().format(java.time.Instant.ofEpochMilli(timestampMillis).atZone(zoneId)) actual fun formatTimeWithSeconds(timestampMillis: Long): String = - mediumTimeFormatter.format(java.time.Instant.ofEpochMilli(timestampMillis).atZone(zoneId)) + mediumTimeFormatter.localized().format(java.time.Instant.ofEpochMilli(timestampMillis).atZone(zoneId)) actual fun formatDate(timestampMillis: Long): String = - shortDateFormatter.format(java.time.Instant.ofEpochMilli(timestampMillis).atZone(zoneId)) + shortDateFormatter.localized().format(java.time.Instant.ofEpochMilli(timestampMillis).atZone(zoneId)) actual fun formatDateTimeShort(timestampMillis: Long): String = - shortDateTimeFormatter.format(java.time.Instant.ofEpochMilli(timestampMillis).atZone(zoneId)) + shortDateTimeFormatter.localized().format(java.time.Instant.ofEpochMilli(timestampMillis).atZone(zoneId)) } // No ICU measurement data on the JVM, so the region table in core:common is the source. Desktop diff --git a/core/data/src/commonMain/kotlin/org/meshtastic/core/data/datasource/NodeInfoReadDataSource.kt b/core/data/src/commonMain/kotlin/org/meshtastic/core/data/datasource/NodeInfoReadDataSource.kt index be2d600592..568a3b0736 100644 --- a/core/data/src/commonMain/kotlin/org/meshtastic/core/data/datasource/NodeInfoReadDataSource.kt +++ b/core/data/src/commonMain/kotlin/org/meshtastic/core/data/datasource/NodeInfoReadDataSource.kt @@ -29,7 +29,7 @@ interface NodeInfoReadDataSource { fun getNodesFlow( sort: String, includeUnknown: Boolean, - hopsAwayMax: Int, + onlyDirect: Boolean, lastHeardMin: Int, ): Flow> diff --git a/core/data/src/commonMain/kotlin/org/meshtastic/core/data/datasource/SwitchingChannelSetDataSource.kt b/core/data/src/commonMain/kotlin/org/meshtastic/core/data/datasource/SwitchingChannelSetDataSource.kt index c927871ccd..cdb62db803 100644 --- a/core/data/src/commonMain/kotlin/org/meshtastic/core/data/datasource/SwitchingChannelSetDataSource.kt +++ b/core/data/src/commonMain/kotlin/org/meshtastic/core/data/datasource/SwitchingChannelSetDataSource.kt @@ -16,6 +16,9 @@ */ package org.meshtastic.core.data.datasource +import androidx.room3.immediateTransaction +import androidx.room3.useWriterConnection +import co.touchlab.kermit.Logger import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.map @@ -24,9 +27,11 @@ import kotlinx.coroutines.sync.withLock import kotlinx.coroutines.withContext import org.koin.core.annotation.Single import org.meshtastic.core.database.DatabaseProvider +import org.meshtastic.core.database.MeshtasticDatabase import org.meshtastic.core.database.entity.ChannelSetEntity import org.meshtastic.core.database.retryOnDbPoolFailure import org.meshtastic.core.di.CoroutineDispatchers +import org.meshtastic.core.model.util.planChannelReconciliation import org.meshtastic.proto.Channel import org.meshtastic.proto.ChannelSet import org.meshtastic.proto.ChannelSettings @@ -54,13 +59,37 @@ class SwitchingChannelSetDataSource( dbManager .observeCurrentDb { db -> db.channelSetDao().observe() } .retryOnDbPoolFailure("channelSet") - .map { entity -> entity?.channelSet ?: ChannelSet() } + .map { entity -> entity?.channelSet ?: ChannelSet.Builder().build() } .distinctUntilChanged() suspend fun clearChannelSet() { // Must take writeMutex like mutate() -- otherwise a concurrent mutate() can read the row before this delete, // then upsert it back afterwards, silently undoing the clear. - withContext(dispatchers.io) { writeMutex.withLock { dbManager.withDb { it.channelSetDao().clear() } } } + withContext(dispatchers.io) { + writeMutex.withLock { + dbManager.withDb { db -> + val dao = db.channelSetDao() + val existing = dao.get() + val empty = ChannelSet.ADAPTER.encode(ChannelSet.Builder().build()) + when { + existing == null -> Unit + + // Keep the baseline: the messages did not move because the cache was dropped. + existing.lastReconciled != null -> dao.clearRetainingBaseline(empty) + + // No baseline yet, but a cached set from before this column existed. Adopt it on the way out, + // so the first handshake after upgrading can still tell whether the channels changed while + // the app was away instead of silently accepting whatever the radio now reports. + existing.channelSet.settings.isNotEmpty() -> { + dao.setLastReconciled(ChannelSetEntity.encodeBaseline(existing.channelSet)) + dao.clearRetainingBaseline(empty) + } + + else -> dao.clear() + } + } + } + } } /** Replaces all [ChannelSettings] in a single atomic operation. */ @@ -68,23 +97,53 @@ class SwitchingChannelSetDataSource( updateChannelSet(settingsList = settingsList, loraConfig = null) } - /** Atomically updates supplied [ChannelSet] fields while preserving fields omitted by the caller. */ + /** + * Atomically updates supplied [ChannelSet] fields while preserving fields omitted by the caller. + * + * Conversations are reconciled in the same lock, but **only when [settingsList] is supplied**. A caller that passes + * the whole channel list is stating a complete set — a manual channel edit, a scanned channel URL, a profile + * install — which is the only safe moment to decide a channel has gone. A LoRa-only write is not: the radio sends + * its LoRa config during the handshake, before any channel has arrived, so reconciling there would see an empty + * channel list and archive every conversation on the device on every reconnect. Those changes are picked up by + * [reconcileConversations] once the handshake completes, which a preset change always triggers because the radio + * reboots. + */ suspend fun updateChannelSet(settingsList: List?, loraConfig: Config.LoRaConfig?) { - mutate { current -> - current.copy(settings = settingsList ?: current.settings, lora_config = loraConfig ?: current.lora_config) + mutate(reconcile = settingsList != null) { current -> + current + .newBuilder() + .also { wb -> + wb.settings = settingsList ?: current.settings + wb.lora_config = loraConfig ?: current.lora_config + } + .build() } } + /** + * Re-keys stored conversations onto the current channel set, and records it as the new baseline. + * + * Conversations are keyed by channel index, so a slot that changes occupant shows the previous channel's history + * under the new channel until this runs. Idempotent: a second call with nothing changed does nothing. + * + * Called explicitly after the handshake because the handshake streams channels in one slot at a time (and its LoRa + * config arrives as its own write). Reconciling on each of those would see a channel momentarily missing, retire + * it, and then see it come back. + */ + suspend fun reconcileConversations() { + withContext(dispatchers.io) { writeMutex.withLock { dbManager.withDb { db -> db.reconcile() } } } + } + /** Places [channel]'s settings at its index, resizing with blank channels to fill any gap (parity with legacy). */ suspend fun updateChannelSettings(channel: Channel) { if (channel.role == Channel.Role.DISABLED) return mutate { current -> val settings = current.settings.toMutableList() while (settings.size <= channel.index) { - settings.add(ChannelSettings()) + settings.add(ChannelSettings.Builder().build()) } - settings[channel.index] = channel.settings ?: ChannelSettings() - current.copy(settings = settings) + settings[channel.index] = channel.settings ?: ChannelSettings.Builder().build() + current.newBuilder().also { wb -> wb.settings = settings }.build() } } @@ -92,15 +151,53 @@ class SwitchingChannelSetDataSource( updateChannelSet(settingsList = null, loraConfig = config) } - private suspend fun mutate(transform: (ChannelSet) -> ChannelSet) { + private suspend fun mutate(reconcile: Boolean = false, transform: (ChannelSet) -> ChannelSet) { withContext(dispatchers.io) { writeMutex.withLock { dbManager.withDb { db -> val dao = db.channelSetDao() - val current = dao.get()?.channelSet ?: ChannelSet() - dao.upsert(ChannelSetEntity(channelSet = transform(current))) + val existing = dao.get() + val current = existing?.channelSet ?: ChannelSet.Builder().build() + dao.upsert( + ChannelSetEntity(channelSet = transform(current), lastReconciled = existing?.lastReconciled), + ) + if (reconcile) db.reconcile() } } } } + + /** + * Runs one reconciliation pass against the stored channel set. Caller holds [writeMutex]. + * + * The re-key and the new baseline commit together. Apply-then-record as two transactions would, on a crash between + * them, recompute the identical plan next time and apply it to rows that had already moved — swapping a pair of + * channels back, or sweeping the *new* occupant's messages into the previous one's archive. + */ + private suspend fun MeshtasticDatabase.reconcile() { + useWriterConnection { transactor -> + transactor.immediateTransaction { + val dao = channelSetDao() + val entity = dao.get() ?: return@immediateTransaction + val current = entity.channelSet + val baseline = entity.lastReconciledChannelSet + // No baseline yet (fresh install, or upgrading from before this existed): adopt the current set as + // the starting point. Messages already misfiled by an earlier switch stay where they are -- there is + // nothing recorded to tell us where they belonged. + if (baseline != null) { + val changes = + planChannelReconciliation( + old = baseline, + new = current, + retiredTokens = packetDao().getRetiredContactTokens(), + ) + if (changes.isNotEmpty()) { + Logger.i { "Re-keying ${changes.size} conversation(s) after a channel-set change" } + packetDao().applyChannelReconciliation(changes) + } + } + dao.setLastReconciled(ChannelSetEntity.encodeBaseline(current)) + } + } + } } diff --git a/core/data/src/commonMain/kotlin/org/meshtastic/core/data/datasource/SwitchingNodeInfoReadDataSource.kt b/core/data/src/commonMain/kotlin/org/meshtastic/core/data/datasource/SwitchingNodeInfoReadDataSource.kt index c2378c030b..d7314bb7aa 100644 --- a/core/data/src/commonMain/kotlin/org/meshtastic/core/data/datasource/SwitchingNodeInfoReadDataSource.kt +++ b/core/data/src/commonMain/kotlin/org/meshtastic/core/data/datasource/SwitchingNodeInfoReadDataSource.kt @@ -38,7 +38,7 @@ class SwitchingNodeInfoReadDataSource(private val dbManager: DatabaseProvider) : override fun getNodesFlow( sort: String, includeUnknown: Boolean, - hopsAwayMax: Int, + onlyDirect: Boolean, lastHeardMin: Int, ): Flow> = dbManager .observeCurrentDb { db -> @@ -46,7 +46,7 @@ class SwitchingNodeInfoReadDataSource(private val dbManager: DatabaseProvider) : .getNodes( sort = sort, includeUnknown = includeUnknown, - hopsAwayMax = hopsAwayMax, + onlyDirect = onlyDirect, lastHeardMin = lastHeardMin, ) } diff --git a/core/data/src/commonMain/kotlin/org/meshtastic/core/data/manager/CommandSenderImpl.kt b/core/data/src/commonMain/kotlin/org/meshtastic/core/data/manager/CommandSenderImpl.kt index 38b1a8d5bd..923c0ddddb 100644 --- a/core/data/src/commonMain/kotlin/org/meshtastic/core/data/manager/CommandSenderImpl.kt +++ b/core/data/src/commonMain/kotlin/org/meshtastic/core/data/manager/CommandSenderImpl.kt @@ -81,8 +81,8 @@ class CommandSenderImpl( ) : CommandSender { private val currentPacketId = atomic(Random(nowMillis).nextLong().absoluteValue) - private val localConfig = MutableStateFlow(LocalConfig()) - private val channelSet = MutableStateFlow(ChannelSet()) + private val localConfig = MutableStateFlow(LocalConfig.Builder().build()) + private val channelSet = MutableStateFlow(ChannelSet.Builder().build()) init { radioConfigRepository.localConfigFlow.onEach { localConfig.value = it }.launchIn(scope) @@ -143,12 +143,14 @@ class CommandSenderImpl( // Use Wire extension for accurate size validation val data = - Data( - portnum = PortNum.fromValue(p.dataType) ?: PortNum.UNKNOWN_APP, - payload = bytes, - reply_id = p.replyId ?: 0, - emoji = p.emoji, - ) + Data.Builder() + .also { wb -> + wb.portnum = PortNum.fromValue(p.dataType) ?: PortNum.UNKNOWN_APP + wb.payload = bytes + wb.reply_id = p.replyId ?: 0 + wb.emoji = p.emoji + } + .build() if (!Data.ADAPTER.isWithinSizeLimit(data, Constants.DATA_PAYLOAD_LEN.value)) { val actualSize = Data.ADAPTER.encodedSize(data) @@ -175,12 +177,14 @@ class CommandSenderImpl( hopLimit = if (p.hopLimit > 0) p.hopLimit else computeHopLimit(), channel = p.channel, decoded = - Data( - portnum = PortNum.fromValue(p.dataType) ?: PortNum.UNKNOWN_APP, - payload = p.bytes ?: ByteString.EMPTY, - reply_id = p.replyId ?: 0, - emoji = p.emoji, - ), + Data.Builder() + .also { wb -> + wb.portnum = PortNum.fromValue(p.dataType) ?: PortNum.UNKNOWN_APP + wb.payload = p.bytes ?: ByteString.EMPTY + wb.reply_id = p.replyId ?: 0 + wb.emoji = p.emoji + } + .build(), ) return packetHandler.sendToRadio(meshPacket) } @@ -204,7 +208,8 @@ class CommandSenderImpl( to = destNum, id = requestId.nonZeroRequestId(), wantResponse = wantResponse, - adminMessage = initFn().copy(session_passkey = sessionManager.getPasskey(destNum)), + adminMessage = + initFn().newBuilder().also { wb -> wb.session_passkey = sessionManager.getPasskey(destNum) }.build(), ) override suspend fun sendAdmin(destNum: Int, requestId: Int, wantResponse: Boolean, initFn: () -> AdminMessage) { @@ -226,9 +231,10 @@ class CommandSenderImpl( } override fun sendAdminImmediate(destNum: Int, initFn: () -> AdminMessage) { - val adminMsg = initFn().copy(session_passkey = sessionManager.getPasskey(destNum)) + val adminMsg = + initFn().newBuilder().also { wb -> wb.session_passkey = sessionManager.getPasskey(destNum) }.build() val packet = buildAdminPacket(to = destNum, adminMessage = adminMsg) - packetHandler.sendToRadio(ToRadio(packet = packet)) + packetHandler.sendToRadio(ToRadio.Builder().also { wb -> wb.packet = packet }.build()) } override suspend fun sendAdminAwaitResult( @@ -250,11 +256,13 @@ class CommandSenderImpl( channel = if (destNum == null) 0 else getChannelIndex(destNum), priority = MeshPacket.Priority.BACKGROUND, decoded = - Data( - portnum = PortNum.POSITION_APP, - payload = pos.encode().toByteString(), - want_response = wantResponse, - ), + Data.Builder() + .also { wb -> + wb.portnum = PortNum.POSITION_APP + wb.payload = pos.encode().toByteString() + wb.want_response = wantResponse + } + .build(), ), "Position update", ) @@ -265,23 +273,27 @@ class CommandSenderImpl( override suspend fun requestPosition(destNum: Int, currentPosition: Position) { val meshPosition = - ProtoPosition( - latitude_i = Position.degI(currentPosition.latitude), - longitude_i = Position.degI(currentPosition.longitude), - altitude = currentPosition.altitude, - time = (nowMillis / 1000L).toInt(), - ) + ProtoPosition.Builder() + .also { wb -> + wb.latitude_i = Position.degI(currentPosition.latitude) + wb.longitude_i = Position.degI(currentPosition.longitude) + wb.altitude = currentPosition.altitude + wb.time = (nowMillis / MILLIS_PER_SECOND).toInt() + } + .build() enqueueOrThrow( buildMeshPacket( to = destNum, channel = getChannelIndex(destNum), priority = MeshPacket.Priority.BACKGROUND, decoded = - Data( - portnum = PortNum.POSITION_APP, - payload = meshPosition.encode().toByteString(), - want_response = true, - ), + Data.Builder() + .also { wb -> + wb.portnum = PortNum.POSITION_APP + wb.payload = meshPosition.encode().toByteString() + wb.want_response = true + } + .build(), ), "Position request", ) @@ -309,11 +321,13 @@ class CommandSenderImpl( to = destNum, channel = getChannelIndex(destNum), decoded = - Data( - portnum = PortNum.NODEINFO_APP, - want_response = true, - payload = myNode.user.encode().toByteString(), - ), + Data.Builder() + .also { wb -> + wb.portnum = PortNum.NODEINFO_APP + wb.want_response = true + wb.payload = myNode.user.encode().toByteString() + } + .build(), ), "User-info request", ) @@ -327,7 +341,14 @@ class CommandSenderImpl( wantAck = true, id = effectiveRequestId, channel = getChannelIndex(destNum), - decoded = Data(portnum = PortNum.TRACEROUTE_APP, want_response = true, dest = destNum), + decoded = + Data.Builder() + .also { wb -> + wb.portnum = PortNum.TRACEROUTE_APP + wb.want_response = true + wb.dest = destNum + } + .build(), ), "Traceroute request", ) @@ -361,18 +382,22 @@ class CommandSenderImpl( if (type == TelemetryType.PAX) { portNum = PortNum.PAXCOUNTER_APP - payloadBytes = Paxcount().encode().toByteString() + payloadBytes = Paxcount.Builder().build().encode().toByteString() } else { portNum = PortNum.TELEMETRY_APP payloadBytes = - Telemetry( - device_metrics = if (type == TelemetryType.DEVICE) DeviceMetrics() else null, - environment_metrics = if (type == TelemetryType.ENVIRONMENT) EnvironmentMetrics() else null, - air_quality_metrics = if (type == TelemetryType.AIR_QUALITY) AirQualityMetrics() else null, - power_metrics = if (type == TelemetryType.POWER) PowerMetrics() else null, - local_stats = if (type == TelemetryType.LOCAL_STATS) LocalStats() else null, - host_metrics = if (type == TelemetryType.HOST) HostMetrics() else null, - ) + Telemetry.Builder() + .also { wb -> + wb.device_metrics = if (type == TelemetryType.DEVICE) DeviceMetrics.Builder().build() else null + wb.environment_metrics = + if (type == TelemetryType.ENVIRONMENT) EnvironmentMetrics.Builder().build() else null + wb.air_quality_metrics = + if (type == TelemetryType.AIR_QUALITY) AirQualityMetrics.Builder().build() else null + wb.power_metrics = if (type == TelemetryType.POWER) PowerMetrics.Builder().build() else null + wb.local_stats = if (type == TelemetryType.LOCAL_STATS) LocalStats.Builder().build() else null + wb.host_metrics = if (type == TelemetryType.HOST) HostMetrics.Builder().build() else null + } + .build() .encode() .toByteString() } @@ -382,7 +407,15 @@ class CommandSenderImpl( to = destNum, id = effectiveRequestId, channel = getChannelIndex(destNum), - decoded = Data(portnum = portNum, payload = payloadBytes, want_response = true, dest = destNum), + decoded = + Data.Builder() + .also { wb -> + wb.portnum = portNum + wb.payload = payloadBytes + wb.want_response = true + wb.dest = destNum + } + .build(), ), "Telemetry request", expectedConnectionVersion, @@ -399,20 +432,25 @@ class CommandSenderImpl( ?: run { val oneHour = 1.hours.inWholeMinutes.toInt() Logger.d { "No stored neighbor info from connected radio, sending dummy data" } - NeighborInfo( - node_id = myNum, - last_sent_by_id = myNum, - node_broadcast_interval_secs = oneHour, - neighbors = - listOf( - Neighbor( - node_id = 0, // Dummy node ID that can be intercepted - snr = 0f, - last_rx_time = (nowMillis / 1000L).toInt(), - node_broadcast_interval_secs = oneHour, - ), - ), - ) + NeighborInfo.Builder() + .also { wb -> + wb.node_id = myNum + wb.last_sent_by_id = myNum + wb.node_broadcast_interval_secs = oneHour + wb.neighbors = + listOf( + Neighbor.Builder() + .also { wb -> + wb.node_id = 0 + // Dummy node ID that can be intercepted + wb.snr = 0f + wb.last_rx_time = (nowMillis / MILLIS_PER_SECOND).toInt() + wb.node_broadcast_interval_secs = oneHour + } + .build(), + ) + } + .build() } // Send the neighbor info from our connected radio to ourselves (simulated) @@ -422,11 +460,13 @@ class CommandSenderImpl( id = effectiveRequestId, channel = getChannelIndex(destNum), decoded = - Data( - portnum = PortNum.NEIGHBORINFO_APP, - payload = neighborInfoToSend.encode().toByteString(), - want_response = true, - ), + Data.Builder() + .also { wb -> + wb.portnum = PortNum.NEIGHBORINFO_APP + wb.payload = neighborInfoToSend.encode().toByteString() + wb.want_response = true + } + .build(), ) } else { // Send request to remote @@ -435,7 +475,14 @@ class CommandSenderImpl( wantAck = true, id = effectiveRequestId, channel = getChannelIndex(destNum), - decoded = Data(portnum = PortNum.NEIGHBORINFO_APP, want_response = true, dest = destNum), + decoded = + Data.Builder() + .also { wb -> + wb.portnum = PortNum.NEIGHBORINFO_APP + wb.want_response = true + wb.dest = destNum + } + .build(), ) } enqueueOrThrow(packet, "Neighbor-info request") @@ -456,32 +503,46 @@ class CommandSenderImpl( 0 } val lockdownAuth = - LockdownAuth( - passphrase = passphrase.encodeToByteArray().toByteString(), - boots_remaining = boots.coerceAtLeast(0), - valid_until_epoch = validUntilEpoch, - max_session_seconds = maxSessionSeconds.coerceAtLeast(0), - disable = disable, - ) - return sendLockdownAdmin(AdminMessage(lockdown_auth = lockdownAuth)) + LockdownAuth.Builder() + .also { wb -> + wb.passphrase = passphrase.encodeToByteArray().toByteString() + wb.boots_remaining = boots.coerceAtLeast(0) + wb.valid_until_epoch = validUntilEpoch + wb.max_session_seconds = maxSessionSeconds.coerceAtLeast(0) + wb.disable = disable + } + .build() + return sendLockdownAdmin(AdminMessage.Builder().also { wb -> wb.lockdown_auth = lockdownAuth }.build()) } - override fun sendLockNow(): Boolean = sendLockdownAdmin(AdminMessage(lockdown_auth = LockdownAuth(lock_now = true))) + override fun sendLockNow(): Boolean = sendLockdownAdmin( + AdminMessage.Builder() + .also { wb -> wb.lockdown_auth = LockdownAuth.Builder().also { wb -> wb.lock_now = true }.build() } + .build(), + ) private fun sendLockdownAdmin(adminMessage: AdminMessage): Boolean { val myNum = nodeManager.myNodeNum.value ?: return false val packet = - MeshPacket( - to = myNum, - id = generatePacketId(), - channel = 0, - want_ack = true, - hop_limit = DEFAULT_HOP_LIMIT, - hop_start = DEFAULT_HOP_LIMIT, - priority = MeshPacket.Priority.RELIABLE, - decoded = Data(portnum = PortNum.ADMIN_APP, payload = adminMessage.encode().toByteString()), - ) - return packetHandler.trySendToRadio(ToRadio(packet = packet)) + MeshPacket.Builder() + .also { wb -> + wb.to = myNum + wb.id = generatePacketId() + wb.channel = 0 + wb.want_ack = true + wb.hop_limit = DEFAULT_HOP_LIMIT + wb.hop_start = DEFAULT_HOP_LIMIT + wb.priority = MeshPacket.Priority.RELIABLE + wb.decoded = + Data.Builder() + .also { wb -> + wb.portnum = PortNum.ADMIN_APP + wb.payload = adminMessage.encode().toByteString() + } + .build() + } + .build() + return packetHandler.trySendToRadio(ToRadio.Builder().also { wb -> wb.packet = packet }.build()) } fun resolveNodeNum(address: NodeAddress): Int = when (address) { @@ -524,19 +585,21 @@ class CommandSenderImpl( actualChannel = 0 } - return MeshPacket( - from = nodeManager.myNodeNum.value ?: 0, - to = to, - id = id, - want_ack = wantAck, - hop_limit = actualHopLimit, - hop_start = actualHopLimit, - priority = priority, - pki_encrypted = pkiEncrypted, - public_key = publicKey, - channel = actualChannel, - decoded = decoded, - ) + return MeshPacket.Builder() + .also { wb -> + wb.from = nodeManager.myNodeNum.value ?: 0 + wb.to = to + wb.id = id + wb.want_ack = wantAck + wb.hop_limit = actualHopLimit + wb.hop_start = actualHopLimit + wb.priority = priority + wb.pki_encrypted = pkiEncrypted + wb.public_key = publicKey + wb.channel = actualChannel + wb.decoded = decoded + } + .build() } private fun buildAdminPacket( @@ -552,11 +615,13 @@ class CommandSenderImpl( channel = getAdminChannelIndex(to), priority = MeshPacket.Priority.RELIABLE, decoded = - Data( - want_response = wantResponse, - portnum = PortNum.ADMIN_APP, - payload = adminMessage.encode().toByteString(), - ), + Data.Builder() + .also { wb -> + wb.want_response = wantResponse + wb.portnum = PortNum.ADMIN_APP + wb.payload = adminMessage.encode().toByteString() + } + .build(), ) companion object { diff --git a/core/data/src/commonMain/kotlin/org/meshtastic/core/data/manager/DataLayerHeartbeatSender.kt b/core/data/src/commonMain/kotlin/org/meshtastic/core/data/manager/DataLayerHeartbeatSender.kt index b55ed9e47b..2273e0767c 100644 --- a/core/data/src/commonMain/kotlin/org/meshtastic/core/data/manager/DataLayerHeartbeatSender.kt +++ b/core/data/src/commonMain/kotlin/org/meshtastic/core/data/manager/DataLayerHeartbeatSender.kt @@ -47,7 +47,11 @@ class DataLayerHeartbeatSender(private val packetHandler: PacketHandler) { fun sendHeartbeat(tag: String = "handshake") { try { val n = nonce.getAndIncrement() - packetHandler.sendToRadio(ToRadio(heartbeat = Heartbeat(nonce = n))) + packetHandler.sendToRadio( + ToRadio.Builder() + .also { wb -> wb.heartbeat = Heartbeat.Builder().also { wb -> wb.nonce = n }.build() } + .build(), + ) Logger.d { "[$tag] Heartbeat enqueued (nonce=$n)" } } catch (e: Exception) { Logger.w(e) { "[$tag] Failed to enqueue heartbeat; proceeding" } diff --git a/core/data/src/commonMain/kotlin/org/meshtastic/core/data/manager/HistoryManagerImpl.kt b/core/data/src/commonMain/kotlin/org/meshtastic/core/data/manager/HistoryManagerImpl.kt index 9a5629f73f..16dd537d0f 100644 --- a/core/data/src/commonMain/kotlin/org/meshtastic/core/data/manager/HistoryManagerImpl.kt +++ b/core/data/src/commonMain/kotlin/org/meshtastic/core/data/manager/HistoryManagerImpl.kt @@ -51,12 +51,19 @@ class HistoryManagerImpl( historyReturnMax: Int, ): StoreAndForward { val history = - StoreAndForward.History( - last_request = lastRequest.coerceAtLeast(0), - window = historyReturnWindow.coerceAtLeast(0), - history_messages = historyReturnMax.coerceAtLeast(0), - ) - return StoreAndForward(rr = StoreAndForward.RequestResponse.CLIENT_HISTORY, history = history) + StoreAndForward.History.Builder() + .also { wb -> + wb.last_request = lastRequest.coerceAtLeast(0) + wb.window = historyReturnWindow.coerceAtLeast(0) + wb.history_messages = historyReturnMax.coerceAtLeast(0) + } + .build() + return StoreAndForward.Builder() + .also { wb -> + wb.rr = StoreAndForward.RequestResponse.CLIENT_HISTORY + wb.history = history + } + .build() } fun resolveHistoryRequestParameters(window: Int, max: Int): Pair { @@ -100,13 +107,21 @@ class HistoryManagerImpl( val accepted = packetHandler.sendToRadioForConnection( - MeshPacket( - from = nodeNum, - to = nodeNum, - id = commandSender.generatePacketId(), - decoded = Data(portnum = PortNum.STORE_FORWARD_APP, payload = request.encode().toByteString()), - priority = MeshPacket.Priority.BACKGROUND, - ), + MeshPacket.Builder() + .also { wb -> + wb.from = nodeNum + wb.to = nodeNum + wb.id = commandSender.generatePacketId() + wb.decoded = + Data.Builder() + .also { wb -> + wb.portnum = PortNum.STORE_FORWARD_APP + wb.payload = request.encode().toByteString() + } + .build() + wb.priority = MeshPacket.Priority.BACKGROUND + } + .build(), expectedConnectionVersion, ) if (!accepted) throw PacketQueueRejectedException("History replay") diff --git a/core/data/src/commonMain/kotlin/org/meshtastic/core/data/manager/MeshConfigFlowManagerImpl.kt b/core/data/src/commonMain/kotlin/org/meshtastic/core/data/manager/MeshConfigFlowManagerImpl.kt index 8a6386fe1c..2c325a675d 100644 --- a/core/data/src/commonMain/kotlin/org/meshtastic/core/data/manager/MeshConfigFlowManagerImpl.kt +++ b/core/data/src/commonMain/kotlin/org/meshtastic/core/data/manager/MeshConfigFlowManagerImpl.kt @@ -282,6 +282,15 @@ class MeshConfigFlowManagerImpl( private suspend fun installAndPublishNodeDatabase(state: HandshakeState.ReceivingNodeInfo) { val session = state.session val info = state.myNodeInfo + + // Stage 1 streamed the radio's channels in one slot at a time, and its LoRa config arrived as a separate + // write, so no single one of those could tell a replaced channel from a half-downloaded set. Now that the + // whole set has landed, re-key the conversations once -- before Connected, so the list is never rendered + // with one channel's history under another's name. + safeCatching { radioConfigRepository.reconcileConversations() } + .onFailure { e -> Logger.e(e) { "Conversation reconciliation failed after handshake" } } + if (!isActiveSession(session)) return + val entities = mutableListOf() state.nodes.forEach { nodeInfo -> nodeManager.installNodeInfo(nodeInfo) @@ -385,7 +394,7 @@ class MeshConfigFlowManagerImpl( handshakeState.value = state.copy(metadata = metadata) // Persist the metadata immediately, but never let a queued old-session write target the next // session's selected database. - if (metadata != DeviceMetadata()) { + if (metadata != DeviceMetadata.Builder().build()) { metadataNodeNum = state.rawMyNodeInfo.my_node_num } connectionManager.value.onHandshakeProgress() diff --git a/core/data/src/commonMain/kotlin/org/meshtastic/core/data/manager/MeshConfigHandlerImpl.kt b/core/data/src/commonMain/kotlin/org/meshtastic/core/data/manager/MeshConfigHandlerImpl.kt index 1353e85302..4acd3294ef 100644 --- a/core/data/src/commonMain/kotlin/org/meshtastic/core/data/manager/MeshConfigHandlerImpl.kt +++ b/core/data/src/commonMain/kotlin/org/meshtastic/core/data/manager/MeshConfigHandlerImpl.kt @@ -51,10 +51,10 @@ class MeshConfigHandlerImpl( private val scope: ServiceScope, ) : MeshConfigHandler { - private val _localConfig = MutableStateFlow(LocalConfig()) + private val _localConfig = MutableStateFlow(LocalConfig.Builder().build()) override val localConfig = _localConfig.asStateFlow() - private val _moduleConfig = MutableStateFlow(LocalModuleConfig()) + private val _moduleConfig = MutableStateFlow(LocalModuleConfig.Builder().build()) override val moduleConfig = _moduleConfig.asStateFlow() init { diff --git a/core/data/src/commonMain/kotlin/org/meshtastic/core/data/manager/MeshConnectionManagerImpl.kt b/core/data/src/commonMain/kotlin/org/meshtastic/core/data/manager/MeshConnectionManagerImpl.kt index d7d970f3cf..3bc89908c9 100644 --- a/core/data/src/commonMain/kotlin/org/meshtastic/core/data/manager/MeshConnectionManagerImpl.kt +++ b/core/data/src/commonMain/kotlin/org/meshtastic/core/data/manager/MeshConnectionManagerImpl.kt @@ -550,7 +550,7 @@ class MeshConnectionManagerImpl( // packet - without this, signal-quality rating would silently blend the *previous* radio's noise floor into // the new one's readings until it gets around to reporting its own. Clearing back to "no reading yet" makes // that gap an explicit SNR-only fallback (design#15) instead of a stale, wrong number. - nodeRepository.updateLocalStats(LocalStats()) + nodeRepository.updateLocalStats(LocalStats.Builder().build()) analytics.track( EVENT_MESH_DISCONNECT, @@ -563,7 +563,9 @@ class MeshConnectionManagerImpl( override fun startConfigOnly() { stageProgressSignals.value = 0 armStageGuard(1, HANDSHAKE_TIMEOUT_STAGE1) - packetHandler.sendToRadio(ToRadio(want_config_id = HandshakeConstants.CONFIG_NONCE)) + packetHandler.sendToRadio( + ToRadio.Builder().also { wb -> wb.want_config_id = HandshakeConstants.CONFIG_NONCE }.build(), + ) } override fun clearRadioConfig() { @@ -577,7 +579,9 @@ class MeshConnectionManagerImpl( override fun startNodeInfoOnly() { stageProgressSignals.value = 0 armStageGuard(2, HANDSHAKE_TIMEOUT_STAGE2) - packetHandler.sendToRadio(ToRadio(want_config_id = HandshakeConstants.NODE_INFO_NONCE)) + packetHandler.sendToRadio( + ToRadio.Builder().also { wb -> wb.want_config_id = HandshakeConstants.NODE_INFO_NONCE }.build(), + ) } override fun onRadioConfigLoaded() { @@ -601,7 +605,9 @@ class MeshConnectionManagerImpl( // config/node-info bursts avoids the GATT contention that pushed the old // onRadioConfigLoaded-time send out of Stage 1. Must bypass the outbound packet queue: // it only drains once Connected, which would hold this until after the backlog flush. - commandSender.sendAdminImmediate(myNodeNum) { AdminMessage(set_time_only = nowSeconds.toInt()) } + commandSender.sendAdminImmediate(myNodeNum) { + AdminMessage.Builder().also { wb -> wb.set_time_only = nowSeconds.toInt() }.build() + } } override suspend fun onNodeDbReady() { @@ -645,7 +651,7 @@ class MeshConnectionManagerImpl( expectedConnectionVersion = connectedLifecycle.version, wantResponse = true, ) { - AdminMessage(get_owner_request = true) + AdminMessage.Builder().also { wb -> wb.get_owner_request = true }.build() } } } diff --git a/core/data/src/commonMain/kotlin/org/meshtastic/core/data/manager/MeshDataHandlerImpl.kt b/core/data/src/commonMain/kotlin/org/meshtastic/core/data/manager/MeshDataHandlerImpl.kt index 7a8a5ec3c0..b42462d3b7 100644 --- a/core/data/src/commonMain/kotlin/org/meshtastic/core/data/manager/MeshDataHandlerImpl.kt +++ b/core/data/src/commonMain/kotlin/org/meshtastic/core/data/manager/MeshDataHandlerImpl.kt @@ -307,7 +307,7 @@ class MeshDataHandlerImpl( // guard (0 means "use the default pushpin"). if (!u.icon.isValidCodePoint()) { Logger.w { "Clearing an out-of-range waypoint icon code point (${u.icon})" } - dataPacket.bytes = Waypoint.ADAPTER.encode(u.copy(icon = 0)).toByteString() + dataPacket.bytes = Waypoint.ADAPTER.encode(u.newBuilder().also { wb -> wb.icon = 0 }.build()).toByteString() } val updateNotification = u.expire > nowSeconds.toInt() radioInterfaceService.launchSessionWork(scope, session) { @@ -356,10 +356,16 @@ class MeshDataHandlerImpl( val payload = packet.decoded?.payload ?: return val u = User.ADAPTER.decode(payload) - .let { if (it.is_licensed == true) it.copy(public_key = ByteString.EMPTY) else it } + .let { + if (it.is_licensed == true) { + it.newBuilder().also { wb -> wb.public_key = ByteString.EMPTY }.build() + } else { + it + } + } .let { if (packet.via_mqtt == true && !it.long_name.endsWith(" (MQTT)")) { - it.copy(long_name = "${it.long_name} (MQTT)") + it.newBuilder().also { wb -> wb.long_name = "${it.long_name} (MQTT)" }.build() } else { it } @@ -600,7 +606,7 @@ class MeshDataHandlerImpl( val contactKey = dataPacket.contactKey(myNodeNum) val fromNode = nodeManager.nodeDBbyNodeNum[packet.from] ?: Node(num = packet.from) - val fromUser = fromNode.user.copy(id = fromNode.user.id.ifEmpty { fromId }) + val fromUser = fromNode.user.newBuilder().also { wb -> wb.id = fromNode.user.id.ifEmpty { fromId } }.build() val reaction = Reaction( diff --git a/core/data/src/commonMain/kotlin/org/meshtastic/core/data/manager/MeshMessageProcessorImpl.kt b/core/data/src/commonMain/kotlin/org/meshtastic/core/data/manager/MeshMessageProcessorImpl.kt index 7571018f06..92250c1e41 100644 --- a/core/data/src/commonMain/kotlin/org/meshtastic/core/data/manager/MeshMessageProcessorImpl.kt +++ b/core/data/src/commonMain/kotlin/org/meshtastic/core/data/manager/MeshMessageProcessorImpl.kt @@ -115,7 +115,9 @@ class MeshMessageProcessorImpl( val proto = safeCatching { FromRadio.ADAPTER.decode(bytes) } .getOrElse { primaryException -> - safeCatching { FromRadio(log_record = LogRecord.ADAPTER.decode(bytes)) } + safeCatching { + FromRadio.Builder().also { wb -> wb.log_record = LogRecord.ADAPTER.decode(bytes) }.build() + } .getOrElse { Logger.e(primaryException) { "Failed to parse radio packet (len=${bytes.size}). Not a valid FromRadio or LogRecord." @@ -184,7 +186,8 @@ class MeshMessageProcessorImpl( /** Test seam for packet-only fixtures with explicit transport authority. */ internal suspend fun handleReceivedMeshPacket(packet: MeshPacket, myNodeNum: Int?, session: RadioSessionContext) { // Single normalization point: every consumer downstream of this copy sees a stamped packet. - val preparedPacket = packet.copy(rx_time = packet.rxTimeOrNull() ?: nowSeconds.toInt()) + val preparedPacket = + packet.newBuilder().also { wb -> wb.rx_time = packet.rxTimeOrNull() ?: nowSeconds.toInt() }.build() // Require myNodeNum to be known before storing: processReceivedMeshPacket only keys a local packet under // NODE_NUM_LOCAL when packet.from == myNodeNum. If myNodeNum is still null (early in a (re)connect, before @@ -273,7 +276,7 @@ class MeshMessageProcessorImpl( raw_message = packet.toString(), fromNum = if (packet.from == myNodeNum) MeshLog.NODE_NUM_LOCAL else packet.from, portNum = decoded.portnum.value, - fromRadio = FromRadio(packet = packet), + fromRadio = FromRadio.Builder().also { wb -> wb.packet = packet }.build(), ) val logJob = insertMeshLog(log, session) diff --git a/core/data/src/commonMain/kotlin/org/meshtastic/core/data/manager/MqttManagerImpl.kt b/core/data/src/commonMain/kotlin/org/meshtastic/core/data/manager/MqttManagerImpl.kt index f3779b7802..78a722dff6 100644 --- a/core/data/src/commonMain/kotlin/org/meshtastic/core/data/manager/MqttManagerImpl.kt +++ b/core/data/src/commonMain/kotlin/org/meshtastic/core/data/manager/MqttManagerImpl.kt @@ -86,7 +86,11 @@ class MqttManagerImpl( _proxyActive.value = true mqttMessageFlow = mqttRepository.proxyMessageFlow - .onEach { message -> packetHandler.sendToRadio(ToRadio(mqttClientProxyMessage = message)) } + .onEach { message -> + packetHandler.sendToRadio( + ToRadio.Builder().also { wb -> wb.mqttClientProxyMessage = message }.build(), + ) + } .catch { throwable -> _proxyActive.value = false // safeCatchingAll swallows the Skiko ExceptionInInitializerError that diff --git a/core/data/src/commonMain/kotlin/org/meshtastic/core/data/manager/NodeManagerImpl.kt b/core/data/src/commonMain/kotlin/org/meshtastic/core/data/manager/NodeManagerImpl.kt index d2e1f48fa9..06f89908f2 100644 --- a/core/data/src/commonMain/kotlin/org/meshtastic/core/data/manager/NodeManagerImpl.kt +++ b/core/data/src/commonMain/kotlin/org/meshtastic/core/data/manager/NodeManagerImpl.kt @@ -378,11 +378,14 @@ class NodeManagerImpl( */ internal fun preservingKnownPrecision(incoming: ProtoPosition, stored: ProtoPosition): ProtoPosition = if (isRedundantCoarsePosition(incoming, stored)) { - incoming.copy( - latitude_i = stored.latitude_i, - longitude_i = stored.longitude_i, - precision_bits = stored.precision_bits, - ) + incoming + .newBuilder() + .also { wb -> + wb.latitude_i = stored.latitude_i + wb.longitude_i = stored.longitude_i + wb.precision_bits = stored.precision_bits + } + .build() } else { incoming } @@ -721,15 +724,18 @@ class NodeManagerImpl( val newPos = when { isZeroPos -> - p.copy( - time = posTime, - latitude_i = node.position.latitude_i, - longitude_i = node.position.longitude_i, - altitude = p.altitude ?: node.position.altitude, - sats_in_view = p.sats_in_view, - ) + p.newBuilder() + .also { wb -> + wb.time = posTime + wb.latitude_i = node.position.latitude_i + wb.longitude_i = node.position.longitude_i + wb.altitude = p.altitude ?: node.position.altitude + wb.sats_in_view = p.sats_in_view + } + .build() - else -> preservingKnownPrecision(p.copy(time = posTime), node.position) + else -> + preservingKnownPrecision(p.newBuilder().also { wb -> wb.time = posTime }.build(), node.position) } node.copy(position = newPos, lastHeard = newLastHeard) @@ -743,6 +749,7 @@ class NodeManagerImpl( telemetry.environment_metrics?.let { nextNode = nextNode.copy(environmentMetrics = it) } telemetry.power_metrics?.let { nextNode = nextNode.copy(powerMetrics = it) } telemetry.air_quality_metrics?.let { nextNode = nextNode.copy(airQualityMetrics = it) } + telemetry.soil_water_metrics?.let { nextNode = nextNode.copy(soilWaterMetrics = it) } val telemetryTime = if (telemetry.time != 0) telemetry.time else node.lastHeard val newLastHeard = clampTimestampToNow(maxOf(node.lastHeard, telemetryTime)) nextNode.copy(lastHeard = newLastHeard) @@ -785,16 +792,23 @@ class NodeManagerImpl( var next = node val user = info.user if (user != null && !shouldPreserveExistingUser(node.user, user)) { - var newUser = user.let { if (it.is_licensed == true) it.copy(public_key = ByteString.EMPTY) else it } + var newUser = + user.let { + if (it.is_licensed == true) { + it.newBuilder().also { wb -> wb.public_key = ByteString.EMPTY }.build() + } else { + it + } + } if (info.via_mqtt && !newUser.long_name.endsWith(" (MQTT)")) { - newUser = newUser.copy(long_name = "${newUser.long_name} (MQTT)") + newUser = newUser.newBuilder().also { wb -> wb.long_name = "${newUser.long_name} (MQTT)" }.build() } next = next.copy(user = newUser, publicKey = newUser.public_key) } info.position?.let { position -> // The NodeDB snapshot from the connected device carries the coarsened position for nodes on a // precision-limited channel; do not let it clobber a more precise coordinate we already hold (#6360). - val timed = position.copy(time = clampTimestampToNow(position.time)) + val timed = position.newBuilder().also { wb -> wb.time = clampTimestampToNow(position.time) }.build() next = next.copy(position = preservingKnownPrecision(timed, next.position)) } // Firmware that predates the field never sends it, and a proto3 bool decodes as false - which would mark @@ -1108,12 +1122,14 @@ class NodeManagerImpl( return Node( num = num, user = - User( - id = userId, - long_name = "Meshtastic ${userId.takeLast(GENERATED_NODE_NAME_SUFFIX_LENGTH)}", - short_name = userId.takeLast(GENERATED_NODE_NAME_SUFFIX_LENGTH), - hw_model = HardwareModel.UNSET, - ), + User.Builder() + .also { wb -> + wb.id = userId + wb.long_name = "Meshtastic ${userId.takeLast(GENERATED_NODE_NAME_SUFFIX_LENGTH)}" + wb.short_name = userId.takeLast(GENERATED_NODE_NAME_SUFFIX_LENGTH) + wb.hw_model = HardwareModel.UNSET + } + .build(), channel = channel, ) } @@ -1127,14 +1143,21 @@ class NodeManagerImpl( node.copy(channel = channel, manuallyVerified = manuallyVerified) } else { val incomingKey = resolveValidatedPublicKeyHint(p.public_key) - val sanitizedUser = if (incomingKey == null) p.copy(public_key = ByteString.EMPTY) else p // Prefer node.publicKey when valid (the authoritative stored key); fall back to node.user.public_key. val existingKey = resolveNodePublicKeyHint(node) - val keyMatch = existingKey == null || existingKey == incomingKey - val newUser = if (keyMatch) sanitizedUser else sanitizedUser.copy(public_key = ByteString.EMPTY) + // Only two valid, different keys are a mismatch. A packet with no usable key says nothing about the one + // on file, so it neither flags nor clears anything, and the stored key stays. + val keyMismatch = existingKey != null && incomingKey != null && existingKey != incomingKey + // First-wins, matching the DAO and the firmware: a different key for a node we already hold one for is + // refused and recorded, never applied. Clearing the stored key here would break PKC direct messages to + // that contact on the word of whoever sent the substitute. + val keptKey = if (incomingKey == null || keyMismatch) existingKey else incomingKey + val newUser = p.newBuilder().also { wb -> wb.public_key = keptKey ?: ByteString.EMPTY }.build() node.copy( user = newUser, publicKey = newUser.public_key, + keyMatch = node.keyMatch && !keyMismatch, + newPublicKey = if (keyMismatch) incomingKey else node.newPublicKey, channel = channel, manuallyVerified = manuallyVerified, ) diff --git a/core/data/src/commonMain/kotlin/org/meshtastic/core/data/manager/PacketHandlerImpl.kt b/core/data/src/commonMain/kotlin/org/meshtastic/core/data/manager/PacketHandlerImpl.kt index 2ea640a00f..4c137d44f4 100644 --- a/core/data/src/commonMain/kotlin/org/meshtastic/core/data/manager/PacketHandlerImpl.kt +++ b/core/data/src/commonMain/kotlin/org/meshtastic/core/data/manager/PacketHandlerImpl.kt @@ -224,7 +224,7 @@ class PacketHandlerImpl( raw_message = packet.toString(), fromNum = MeshLog.NODE_NUM_LOCAL, portNum = packet.decoded?.portnum?.value ?: 0, - fromRadio = FromRadio(packet = packet), + fromRadio = FromRadio.Builder().also { wb -> wb.packet = packet }.build(), ) insertMeshLog(packetToSave) } @@ -680,7 +680,7 @@ class PacketHandlerImpl( throw RadioNotConnectedException() } val departureEpoch = lifecycle.epochs.departures - val accepted = dispatchToRadio(ToRadio(packet = packet)) + val accepted = dispatchToRadio(ToRadio.Builder().also { wb -> wb.packet = packet }.build()) pending.recordDispatch(accepted = accepted, departureEpoch = departureEpoch) if (!accepted) pending.completeAll(AwaitedSendStatus.SEND_FAILED) } diff --git a/core/data/src/commonMain/kotlin/org/meshtastic/core/data/manager/StoreForwardPacketHandlerImpl.kt b/core/data/src/commonMain/kotlin/org/meshtastic/core/data/manager/StoreForwardPacketHandlerImpl.kt index 11542122fe..432aab0686 100644 --- a/core/data/src/commonMain/kotlin/org/meshtastic/core/data/manager/StoreForwardPacketHandlerImpl.kt +++ b/core/data/src/commonMain/kotlin/org/meshtastic/core/data/manager/StoreForwardPacketHandlerImpl.kt @@ -202,7 +202,17 @@ class StoreForwardPacketHandlerImpl( if (s.rr == StoreAndForward.RequestResponse.ROUTER_TEXT_BROADCAST) { dataPacket.to = NodeAddress.ID_BROADCAST } - val u = dataPacket.copy(bytes = s.text, dataType = PortNum.TEXT_MESSAGE_APP.value) + // Only a router-text replay travels under a fresh header id with the stored + // message's id in original_id; dedupe against that, and fall back when it is absent. + val isRouterText = + s.rr == StoreAndForward.RequestResponse.ROUTER_TEXT_DIRECT || + s.rr == StoreAndForward.RequestResponse.ROUTER_TEXT_BROADCAST + val u = + dataPacket.copy( + bytes = s.text, + dataType = PortNum.TEXT_MESSAGE_APP.value, + id = s.original_id.takeIf { isRouterText && it != 0 } ?: dataPacket.id, + ) dataHandler.value.rememberDataPacket(u, myNodeNum, session = session) } diff --git a/core/data/src/commonMain/kotlin/org/meshtastic/core/data/manager/TelemetryPacketHandlerImpl.kt b/core/data/src/commonMain/kotlin/org/meshtastic/core/data/manager/TelemetryPacketHandlerImpl.kt index 8b9254c2f7..0f35f6aef0 100644 --- a/core/data/src/commonMain/kotlin/org/meshtastic/core/data/manager/TelemetryPacketHandlerImpl.kt +++ b/core/data/src/commonMain/kotlin/org/meshtastic/core/data/manager/TelemetryPacketHandlerImpl.kt @@ -67,7 +67,13 @@ class TelemetryPacketHandlerImpl( val payload = packet.decoded?.payload ?: return val telemetry = (Telemetry.ADAPTER.decodeOrNull(payload, Logger) ?: return).let { - if (it.time == 0) it.copy(time = (dataPacket.time.milliseconds.inWholeSeconds).toInt()) else it + if (it.time == 0) { + it.newBuilder() + .also { wb -> wb.time = (dataPacket.time.milliseconds.inWholeSeconds).toInt() } + .build() + } else { + it + } } Logger.d { "Telemetry from ${packet.from}: ${Telemetry.ADAPTER.toOneLiner(telemetry)}" } val fromNum = packet.from @@ -80,6 +86,7 @@ class TelemetryPacketHandlerImpl( telemetry.environment_metrics?.let { nextNode = nextNode.copy(environmentMetrics = it) } telemetry.power_metrics?.let { nextNode = nextNode.copy(powerMetrics = it) } telemetry.air_quality_metrics?.let { nextNode = nextNode.copy(airQualityMetrics = it) } + telemetry.soil_water_metrics?.let { nextNode = nextNode.copy(soilWaterMetrics = it) } val telemetryTime = if (telemetry.time != 0) telemetry.time else nextNode.lastHeard val newLastHeard = clampTimestampToNow(maxOf(nextNode.lastHeard, telemetryTime)) nextNode.copy(lastHeard = newLastHeard) diff --git a/core/data/src/commonMain/kotlin/org/meshtastic/core/data/manager/XModemManagerImpl.kt b/core/data/src/commonMain/kotlin/org/meshtastic/core/data/manager/XModemManagerImpl.kt index 5858d19ba1..6eed5d6e0d 100644 --- a/core/data/src/commonMain/kotlin/org/meshtastic/core/data/manager/XModemManagerImpl.kt +++ b/core/data/src/commonMain/kotlin/org/meshtastic/core/data/manager/XModemManagerImpl.kt @@ -147,7 +147,11 @@ class XModemManagerImpl(private val packetHandler: PacketHandler) : XModemManage } private fun sendControl(control: XModem.Control) { - packetHandler.sendToRadio(ToRadio(xmodemPacket = XModem(control = control))) + packetHandler.sendToRadio( + ToRadio.Builder() + .also { wb -> wb.xmodemPacket = XModem.Builder().also { wb -> wb.control = control }.build() } + .build(), + ) } private fun reset() { diff --git a/core/data/src/commonMain/kotlin/org/meshtastic/core/data/repository/MeshLogRepositoryImpl.kt b/core/data/src/commonMain/kotlin/org/meshtastic/core/data/repository/MeshLogRepositoryImpl.kt index 16350420da..c081e994d1 100644 --- a/core/data/src/commonMain/kotlin/org/meshtastic/core/data/repository/MeshLogRepositoryImpl.kt +++ b/core/data/src/commonMain/kotlin/org/meshtastic/core/data/repository/MeshLogRepositoryImpl.kt @@ -128,10 +128,13 @@ open class MeshLogRepositoryImpl( if (decoded.want_response == true) return@runCatching null val telemetry = Telemetry.ADAPTER.decode(decoded.payload) - telemetry.copy( - time = (log.received_date / MILLIS_PER_SEC).toInt(), - environment_metrics = telemetry.environment_metrics?.withSentinelsForAbsentReadings(), - ) + telemetry + .newBuilder() + .also { wb -> + wb.time = (log.received_date / MILLIS_PER_SEC).toInt() + wb.environment_metrics = telemetry.environment_metrics?.withSentinelsForAbsentReadings() + } + .build() } .getOrNull() @@ -247,19 +250,21 @@ open class MeshLogRepositoryImpl( private fun EnvironmentMetrics.withSentinelsForAbsentReadings(): EnvironmentMetrics = withLegacyOneWireTemperatures().withScalarSentinels().withChannelSentinels() -private fun EnvironmentMetrics.withScalarSentinels(): EnvironmentMetrics = copy( - temperature = temperature ?: Float.NaN, - relative_humidity = relative_humidity ?: Float.NaN, - soil_temperature = soil_temperature ?: Float.NaN, - barometric_pressure = barometric_pressure ?: Float.NaN, - gas_resistance = gas_resistance ?: Float.NaN, - voltage = voltage ?: Float.NaN, - current = current ?: Float.NaN, - lux = lux ?: Float.NaN, - uv_lux = uv_lux ?: Float.NaN, - iaq = iaq ?: Int.MIN_VALUE, - soil_moisture = soil_moisture ?: Int.MIN_VALUE, -) +private fun EnvironmentMetrics.withScalarSentinels(): EnvironmentMetrics = this.newBuilder() + .also { wb -> + wb.temperature = temperature ?: Float.NaN + wb.relative_humidity = relative_humidity ?: Float.NaN + wb.soil_temperature = soil_temperature ?: Float.NaN + wb.barometric_pressure = barometric_pressure ?: Float.NaN + wb.gas_resistance = gas_resistance ?: Float.NaN + wb.voltage = voltage ?: Float.NaN + wb.current = current ?: Float.NaN + wb.lux = lux ?: Float.NaN + wb.uv_lux = uv_lux ?: Float.NaN + wb.iaq = iaq ?: Int.MIN_VALUE + wb.soil_moisture = soil_moisture ?: Int.MIN_VALUE + } + .build() private fun EnvironmentMetrics.withChannelSentinels(): EnvironmentMetrics = (0 until TELEMETRY_CHANNEL_COUNT).fold(this) { metrics, channel -> diff --git a/core/data/src/commonMain/kotlin/org/meshtastic/core/data/repository/NodeRepositoryImpl.kt b/core/data/src/commonMain/kotlin/org/meshtastic/core/data/repository/NodeRepositoryImpl.kt index a660407dad..431fb58936 100644 --- a/core/data/src/commonMain/kotlin/org/meshtastic/core/data/repository/NodeRepositoryImpl.kt +++ b/core/data/src/commonMain/kotlin/org/meshtastic/core/data/repository/NodeRepositoryImpl.kt @@ -91,7 +91,7 @@ class NodeRepositoryImpl( localStatsDataSource.localStatsFlow.stateIn( processLifecycle.coroutineScope, SharingStarted.Eagerly, - LocalStats(), + LocalStats.Builder().build(), ) /** Update the cached local stats telemetry. */ @@ -176,10 +176,20 @@ class NodeRepositoryImpl( fallbackId } - return found?.copy( - long_name = found.long_name.takeIf { it.isNotBlank() } ?: defaultLong, - short_name = found.short_name.takeIf { it.isNotBlank() } ?: defaultShort, - ) ?: User(id = userId, long_name = defaultLong, short_name = defaultShort) + return found + ?.newBuilder() + ?.also { wb -> + wb.long_name = found.long_name.takeIf { it.isNotBlank() } ?: defaultLong + wb.short_name = found.short_name.takeIf { it.isNotBlank() } ?: defaultShort + } + ?.build() + ?: User.Builder() + .also { wb -> + wb.id = userId + wb.long_name = defaultLong + wb.short_name = defaultShort + } + .build() } /** Returns a flow of nodes filtered and sorted according to the parameters. */ @@ -193,7 +203,7 @@ class NodeRepositoryImpl( .getNodesFlow( sort = sort.sqlValue, includeUnknown = includeUnknown, - hopsAwayMax = if (onlyDirect) 0 else -1, + onlyDirect = onlyDirect, lastHeardMin = if (onlyOnline) onlineTimeThreshold() else -1, ) .mapLatest { list -> list.map { it.toModel() }.filter { node -> node.matchesSearch(filter) } } @@ -286,16 +296,28 @@ class NodeRepositoryImpl( snr = snr, rssi = rssi, lastHeard = lastHeard, - deviceTelemetry = org.meshtastic.proto.Telemetry(device_metrics = deviceMetrics), + deviceTelemetry = + org.meshtastic.proto.Telemetry.Builder().also { wb -> wb.device_metrics = deviceMetrics }.build(), channel = channel, viaMqtt = viaMqtt, hopsAway = hopsAway, isFavorite = isFavorite, isIgnored = isIgnored, isMuted = isMuted, - environmentTelemetry = org.meshtastic.proto.Telemetry(environment_metrics = environmentMetrics), - powerTelemetry = org.meshtastic.proto.Telemetry(power_metrics = powerMetrics), - airQualityTelemetry = org.meshtastic.proto.Telemetry(air_quality_metrics = airQualityMetrics), + environmentTelemetry = + org.meshtastic.proto.Telemetry.Builder() + .also { wb -> wb.environment_metrics = environmentMetrics } + .build(), + powerTelemetry = + org.meshtastic.proto.Telemetry.Builder().also { wb -> wb.power_metrics = powerMetrics }.build(), + airQualityTelemetry = + org.meshtastic.proto.Telemetry.Builder() + .also { wb -> wb.air_quality_metrics = airQualityMetrics } + .build(), + soilWaterTelemetry = + org.meshtastic.proto.Telemetry.Builder() + .also { wb -> wb.soil_water_metrics = soilWaterMetrics } + .build(), paxcounter = paxcounter, publicKey = publicKey, notes = notes, @@ -304,5 +326,7 @@ class NodeRepositoryImpl( lastTransport = lastTransport, signsPackets = signsPackets, heardOnCurrentLora = heardOnCurrentLora, + keyMatch = keyMatch, + newPublicKey = newPublicKey, ) } diff --git a/core/data/src/commonMain/kotlin/org/meshtastic/core/data/repository/PacketRepositoryImpl.kt b/core/data/src/commonMain/kotlin/org/meshtastic/core/data/repository/PacketRepositoryImpl.kt index 4d86f5eb32..93268874e0 100644 --- a/core/data/src/commonMain/kotlin/org/meshtastic/core/data/repository/PacketRepositoryImpl.kt +++ b/core/data/src/commonMain/kotlin/org/meshtastic/core/data/repository/PacketRepositoryImpl.kt @@ -44,7 +44,6 @@ import org.meshtastic.core.repository.PersistedPacket import org.meshtastic.core.repository.PersistedPacketId import org.meshtastic.core.repository.PersistedReaction import org.meshtastic.core.repository.PersistedReactionId -import org.meshtastic.proto.ChannelSettings import org.meshtastic.proto.MeshPacket import org.meshtastic.proto.PortNum import org.meshtastic.core.database.entity.ContactSettings as ContactSettingsEntity @@ -527,12 +526,6 @@ class PacketRepositoryImpl(private val dbManager: DatabaseProvider, private val withContext(dispatchers.io) { dbManager.withDb { it.packetDao().deleteAll() } } } - override suspend fun migrateChannelsByPSK(oldSettings: List, newSettings: List) { - withContext(dispatchers.io) { - dbManager.withDb { it.packetDao().migrateChannelsByPSK(oldSettings, newSettings) } - } - } - override suspend fun updateFilteredBySender(senderId: String, filtered: Boolean) { val pattern = "%\"from\":\"${senderId}\"%" withContext(dispatchers.io) { dbManager.withDb { it.packetDao().updateFilteredBySender(pattern, filtered) } } @@ -550,6 +543,7 @@ class PacketRepositoryImpl(private val dbManager: DatabaseProvider, private val isMuted = isMuted, draft = draft, pinned = pinned, + displayName = displayName, ) private fun Reaction.toEntity(myNodeNum: Int) = RoomReaction( diff --git a/core/data/src/commonMain/kotlin/org/meshtastic/core/data/repository/RadioConfigRepositoryImpl.kt b/core/data/src/commonMain/kotlin/org/meshtastic/core/data/repository/RadioConfigRepositoryImpl.kt index a32d21b6b7..2d5a9b4f09 100644 --- a/core/data/src/commonMain/kotlin/org/meshtastic/core/data/repository/RadioConfigRepositoryImpl.kt +++ b/core/data/src/commonMain/kotlin/org/meshtastic/core/data/repository/RadioConfigRepositoryImpl.kt @@ -73,6 +73,10 @@ open class RadioConfigRepositoryImpl( channelSetDataSource.updateChannelSet(settingsList, loraConfig) } + override suspend fun reconcileConversations() { + channelSetDataSource.reconcileConversations() + } + /** * Updates the [ChannelSettings] list with the provided channel and returns the index of the admin channel after the * update (if not found, returns 0). @@ -182,20 +186,22 @@ open class RadioConfigRepositoryImpl( localConfig, localModuleConfig, -> - DeviceProfile( - long_name = node?.user?.long_name, - short_name = node?.user?.short_name, - channel_url = channels.getChannelUrl().toString(), - config = localConfig, - module_config = localModuleConfig, - is_unmessagable = node?.user?.is_unmessagable, - is_licensed = node?.user?.is_licensed, - fixed_position = - if (node != null && localConfig.position?.fixed_position == true) { - node.position - } else { - null - }, - ) + DeviceProfile.Builder() + .also { wb -> + wb.long_name = node?.user?.long_name + wb.short_name = node?.user?.short_name + wb.channel_url = channels.getChannelUrl().toString() + wb.config = localConfig + wb.module_config = localModuleConfig + wb.is_unmessagable = node?.user?.is_unmessagable + wb.is_licensed = node?.user?.is_licensed + wb.fixed_position = + if (node != null && localConfig.position?.fixed_position == true) { + node.position + } else { + null + } + } + .build() } } diff --git a/core/data/src/commonTest/kotlin/org/meshtastic/core/data/ai/AiFunctionProviderImplTest.kt b/core/data/src/commonTest/kotlin/org/meshtastic/core/data/ai/AiFunctionProviderImplTest.kt index bd8a86688c..7222b8af3a 100644 --- a/core/data/src/commonTest/kotlin/org/meshtastic/core/data/ai/AiFunctionProviderImplTest.kt +++ b/core/data/src/commonTest/kotlin/org/meshtastic/core/data/ai/AiFunctionProviderImplTest.kt @@ -99,7 +99,14 @@ class AiFunctionProviderImplTest { val testNode = Node( num = 0xabc, - user = User(id = "!00000abc", long_name = "Alice", short_name = "AL"), + user = + User.Builder() + .also { wb -> + wb.id = "!00000abc" + wb.long_name = "Alice" + wb.short_name = "AL" + } + .build(), lastHeard = 1_700_000_000, snr = 5.5f, rssi = -70, @@ -122,7 +129,18 @@ class AiFunctionProviderImplTest { @Test fun getNodeDetails_returns_null_position_when_no_fix() = runTest { // Node with (0.0, 0.0) position and time=0 → no valid position - val testNode = Node(num = 1, user = User(id = "!00000001", long_name = "NoGPS", short_name = "NG")) + val testNode = + Node( + num = 1, + user = + User.Builder() + .also { wb -> + wb.id = "!00000001" + wb.long_name = "NoGPS" + wb.short_name = "NG" + } + .build(), + ) val nodeMap = MutableStateFlow(mapOf(1 to testNode)) every { nodeRepository.nodeDBbyNum } returns nodeMap @@ -151,7 +169,18 @@ class AiFunctionProviderImplTest { fun getNodeDetails_round_trips_high_bit_node_num() = runTest { // A node num with the high bit set (-1 == 0xFFFFFFFF) must format and parse as the canonical // "!ffffffff", not the signed "!-1" — regression guard for the node-ID hex fix. - val testNode = Node(num = -1, user = User(id = "!ffffffff", long_name = "HighBit", short_name = "HB")) + val testNode = + Node( + num = -1, + user = + User.Builder() + .also { wb -> + wb.id = "!ffffffff" + wb.long_name = "HighBit" + wb.short_name = "HB" + } + .build(), + ) val nodeMap = MutableStateFlow(mapOf(-1 to testNode)) every { nodeRepository.nodeDBbyNum } returns nodeMap @@ -271,7 +300,7 @@ class AiFunctionProviderImplTest { @Test fun sendMessage_accepts_text_exactly_at_the_byte_limit() = runTest { - every { radioConfigRepository.channelSetFlow } returns flowOf(ChannelSet()) + every { radioConfigRepository.channelSetFlow } returns flowOf(ChannelSet.Builder().build()) everySuspend { sendMessageUseCase.invoke(any(), any(), any()) } returns 42 val text = "a".repeat(AiFunctionProviderImpl.MAX_MESSAGE_LENGTH) @@ -300,7 +329,7 @@ class AiFunctionProviderImplTest { @Test fun sendMessage_counts_multi_byte_text_in_bytes_not_characters() = runTest { - every { radioConfigRepository.channelSetFlow } returns flowOf(ChannelSet()) + every { radioConfigRepository.channelSetFlow } returns flowOf(ChannelSet.Builder().build()) everySuspend { sendMessageUseCase.invoke(any(), any(), any()) } returns 7 // "\u00fc" is two UTF-8 bytes, so half as many characters fit. @@ -320,12 +349,14 @@ class AiFunctionProviderImplTest { // Data proto's framing fails here instead of in the send queue. fun encodesWithinLimit(byteCount: Int): Boolean { val data = - Data( - portnum = PortNum.TEXT_MESSAGE_APP, - payload = ByteArray(byteCount) { 'a'.code.toByte() }.toByteString(), - reply_id = 0, - emoji = 0, - ) + Data.Builder() + .also { wb -> + wb.portnum = PortNum.TEXT_MESSAGE_APP + wb.payload = ByteArray(byteCount) { 'a'.code.toByte() }.toByteString() + wb.reply_id = 0 + wb.emoji = 0 + } + .build() return Data.ADAPTER.isWithinSizeLimit(data, Constants.DATA_PAYLOAD_LEN.value) } @@ -339,7 +370,7 @@ class AiFunctionProviderImplTest { fun getRecentMessages_contact_not_found() = runTest { val nodeMap = MutableStateFlow(emptyMap()) every { nodeRepository.nodeDBbyNum } returns nodeMap - every { radioConfigRepository.channelSetFlow } returns flowOf(org.meshtastic.proto.ChannelSet()) + every { radioConfigRepository.channelSetFlow } returns flowOf(org.meshtastic.proto.ChannelSet.Builder().build()) val provider = createProvider() val result = provider.getRecentMessages("NonExistent", 10) @@ -352,7 +383,7 @@ class AiFunctionProviderImplTest { fun getUnreadSummary_returns_empty_when_no_unread() = runTest { every { packetRepository.getContacts() } returns flowOf(emptyMap()) every { packetRepository.getContactSettings() } returns flowOf(emptyMap()) - every { radioConfigRepository.channelSetFlow } returns flowOf(org.meshtastic.proto.ChannelSet()) + every { radioConfigRepository.channelSetFlow } returns flowOf(org.meshtastic.proto.ChannelSet.Builder().build()) every { nodeRepository.nodeDBbyNum } returns MutableStateFlow(emptyMap()) val provider = createProvider() diff --git a/core/data/src/commonTest/kotlin/org/meshtastic/core/data/ai/FuzzyNameResolverTest.kt b/core/data/src/commonTest/kotlin/org/meshtastic/core/data/ai/FuzzyNameResolverTest.kt index 3e75bfe749..6cf47a17e7 100644 --- a/core/data/src/commonTest/kotlin/org/meshtastic/core/data/ai/FuzzyNameResolverTest.kt +++ b/core/data/src/commonTest/kotlin/org/meshtastic/core/data/ai/FuzzyNameResolverTest.kt @@ -101,8 +101,30 @@ class FuzzyNameResolverTest { val radioConfigRepository: RadioConfigRepository = mock(MockMode.autofill) val nodes = mapOf( - 1 to Node(num = 1, user = User(id = "!00000001", long_name = "Alice", short_name = "AL")), - 2 to Node(num = 2, user = User(id = "!00000002", long_name = "Bob", short_name = "BO")), + 1 to + Node( + num = 1, + user = + User.Builder() + .also { wb -> + wb.id = "!00000001" + wb.long_name = "Alice" + wb.short_name = "AL" + } + .build(), + ), + 2 to + Node( + num = 2, + user = + User.Builder() + .also { wb -> + wb.id = "!00000002" + wb.long_name = "Bob" + wb.short_name = "BO" + } + .build(), + ), ) every { nodeRepository.nodeDBbyNum } returns MutableStateFlow(nodes) @@ -120,8 +142,30 @@ class FuzzyNameResolverTest { val radioConfigRepository: RadioConfigRepository = mock(MockMode.autofill) val nodes = mapOf( - 1 to Node(num = 1, user = User(id = "!00000001", long_name = "Alexander", short_name = "AX")), - 2 to Node(num = 2, user = User(id = "!00000002", long_name = "Bob", short_name = "BO")), + 1 to + Node( + num = 1, + user = + User.Builder() + .also { wb -> + wb.id = "!00000001" + wb.long_name = "Alexander" + wb.short_name = "AX" + } + .build(), + ), + 2 to + Node( + num = 2, + user = + User.Builder() + .also { wb -> + wb.id = "!00000002" + wb.long_name = "Bob" + wb.short_name = "BO" + } + .build(), + ), ) every { nodeRepository.nodeDBbyNum } returns MutableStateFlow(nodes) @@ -138,8 +182,30 @@ class FuzzyNameResolverTest { val radioConfigRepository: RadioConfigRepository = mock(MockMode.autofill) val nodes = mapOf( - 1 to Node(num = 1, user = User(id = "!00000001", long_name = "Alice Smith", short_name = "AS")), - 2 to Node(num = 2, user = User(id = "!00000002", long_name = "Alice Jones", short_name = "AJ")), + 1 to + Node( + num = 1, + user = + User.Builder() + .also { wb -> + wb.id = "!00000001" + wb.long_name = "Alice Smith" + wb.short_name = "AS" + } + .build(), + ), + 2 to + Node( + num = 2, + user = + User.Builder() + .also { wb -> + wb.id = "!00000002" + wb.long_name = "Alice Jones" + wb.short_name = "AJ" + } + .build(), + ), ) every { nodeRepository.nodeDBbyNum } returns MutableStateFlow(nodes) @@ -167,7 +233,21 @@ class FuzzyNameResolverTest { fun resolveNodeName_not_found_when_no_match() { val nodeRepository: NodeRepository = mock(MockMode.autofill) val radioConfigRepository: RadioConfigRepository = mock(MockMode.autofill) - val nodes = mapOf(1 to Node(num = 1, user = User(id = "!00000001", long_name = "Alice", short_name = "AL"))) + val nodes = + mapOf( + 1 to + Node( + num = 1, + user = + User.Builder() + .also { wb -> + wb.id = "!00000001" + wb.long_name = "Alice" + wb.short_name = "AL" + } + .build(), + ), + ) every { nodeRepository.nodeDBbyNum } returns MutableStateFlow(nodes) val resolver = FuzzyNameResolver(nodeRepository, radioConfigRepository) @@ -183,7 +263,15 @@ class FuzzyNameResolverTest { val nodeRepository: NodeRepository = mock(MockMode.autofill) val radioConfigRepository: RadioConfigRepository = mock(MockMode.autofill) val channelSet = - ChannelSet(settings = listOf(ChannelSettings(name = "General"), ChannelSettings(name = "Emergency"))) + ChannelSet.Builder() + .also { wb -> + wb.settings = + listOf( + ChannelSettings.Builder().also { wb -> wb.name = "General" }.build(), + ChannelSettings.Builder().also { wb -> wb.name = "Emergency" }.build(), + ) + } + .build() every { radioConfigRepository.channelSetFlow } returns flowOf(channelSet) val resolver = FuzzyNameResolver(nodeRepository, radioConfigRepository) @@ -199,7 +287,15 @@ class FuzzyNameResolverTest { val nodeRepository: NodeRepository = mock(MockMode.autofill) val radioConfigRepository: RadioConfigRepository = mock(MockMode.autofill) val channelSet = - ChannelSet(settings = listOf(ChannelSettings(name = "admin"), ChannelSettings(name = "General"))) + ChannelSet.Builder() + .also { wb -> + wb.settings = + listOf( + ChannelSettings.Builder().also { wb -> wb.name = "admin" }.build(), + ChannelSettings.Builder().also { wb -> wb.name = "General" }.build(), + ) + } + .build() every { radioConfigRepository.channelSetFlow } returns flowOf(channelSet) val resolver = FuzzyNameResolver(nodeRepository, radioConfigRepository) @@ -213,7 +309,7 @@ class FuzzyNameResolverTest { fun resolveChannelName_not_found_when_empty() = runTest { val nodeRepository: NodeRepository = mock(MockMode.autofill) val radioConfigRepository: RadioConfigRepository = mock(MockMode.autofill) - val channelSet = ChannelSet(settings = emptyList()) + val channelSet = ChannelSet.Builder().also { wb -> wb.settings = emptyList() }.build() every { radioConfigRepository.channelSetFlow } returns flowOf(channelSet) val resolver = FuzzyNameResolver(nodeRepository, radioConfigRepository) diff --git a/core/data/src/commonTest/kotlin/org/meshtastic/core/data/datasource/NodeDeletionDatabaseSwitchTest.kt b/core/data/src/commonTest/kotlin/org/meshtastic/core/data/datasource/NodeDeletionDatabaseSwitchTest.kt index 92bcf3bed4..daa0eb094e 100644 --- a/core/data/src/commonTest/kotlin/org/meshtastic/core/data/datasource/NodeDeletionDatabaseSwitchTest.kt +++ b/core/data/src/commonTest/kotlin/org/meshtastic/core/data/datasource/NodeDeletionDatabaseSwitchTest.kt @@ -105,8 +105,17 @@ class NodeDeletionDatabaseSwitchTest { } private suspend fun seedNodeAndMetadata(database: MeshtasticDatabase, label: String) { - database.nodeInfoDao().upsert(NodeEntity(num = NODE_NUM, user = User(id = "!node-$label"))) - database.nodeInfoDao().upsert(MetadataEntity(num = NODE_NUM, proto = DeviceMetadata(firmware_version = label))) + database + .nodeInfoDao() + .upsert(NodeEntity(num = NODE_NUM, user = User.Builder().also { wb -> wb.id = "!node-$label" }.build())) + database + .nodeInfoDao() + .upsert( + MetadataEntity( + num = NODE_NUM, + proto = DeviceMetadata.Builder().also { wb -> wb.firmware_version = label }.build(), + ), + ) } private suspend fun MeshtasticDatabase.hasMetadata(num: Int): Boolean = diff --git a/core/data/src/commonTest/kotlin/org/meshtastic/core/data/datasource/SwitchingChannelSetDataSourceTest.kt b/core/data/src/commonTest/kotlin/org/meshtastic/core/data/datasource/SwitchingChannelSetDataSourceTest.kt index 88eb0a8d2d..3905c34a66 100644 --- a/core/data/src/commonTest/kotlin/org/meshtastic/core/data/datasource/SwitchingChannelSetDataSourceTest.kt +++ b/core/data/src/commonTest/kotlin/org/meshtastic/core/data/datasource/SwitchingChannelSetDataSourceTest.kt @@ -23,12 +23,17 @@ import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.flow.first import kotlinx.coroutines.test.UnconfinedTestDispatcher import kotlinx.coroutines.test.runTest +import org.meshtastic.core.database.entity.Packet import org.meshtastic.core.di.CoroutineDispatchers +import org.meshtastic.core.model.ContactKey +import org.meshtastic.core.model.DataPacket +import org.meshtastic.core.model.NodeAddress import org.meshtastic.core.testing.FakeDatabaseProvider import org.meshtastic.proto.Channel import org.meshtastic.proto.ChannelSet import org.meshtastic.proto.ChannelSettings import org.meshtastic.proto.Config +import org.meshtastic.proto.PortNum import kotlin.test.AfterTest import kotlin.test.Test import kotlin.test.assertEquals @@ -43,8 +48,13 @@ class SwitchingChannelSetDataSourceTest { @AfterTest fun tearDown() = dbProvider.close() - private fun secondary(index: Int, name: String) = - Channel(index = index, settings = ChannelSettings(name = name), role = Channel.Role.SECONDARY) + private fun secondary(index: Int, name: String) = Channel.Builder() + .also { wb -> + wb.index = index + wb.settings = ChannelSettings.Builder().also { wb -> wb.name = name }.build() + wb.role = Channel.Role.SECONDARY + } + .build() @Test fun `updateChannelSettings places channel at its index`() = runTest(testDispatcher) { @@ -69,16 +79,28 @@ class SwitchingChannelSetDataSourceTest { @Test fun `disabled channels are ignored`() = runTest(testDispatcher) { - dataSource.updateChannelSettings(Channel(index = 0, role = Channel.Role.DISABLED)) + dataSource.updateChannelSettings( + Channel.Builder() + .also { wb -> + wb.index = 0 + wb.role = Channel.Role.DISABLED + } + .build(), + ) assertTrue(dataSource.channelSetFlow.first().settings.isEmpty()) } @Test fun `replaceAllSettings replaces the whole list`() = runTest(testDispatcher) { dataSource.updateChannelSettings(secondary(0, "old")) - val originalLora = Config.LoRaConfig(channel_num = 4) + val originalLora = Config.LoRaConfig.Builder().also { wb -> wb.channel_num = 4 }.build() dataSource.setLoraConfig(originalLora) - dataSource.replaceAllSettings(listOf(ChannelSettings(name = "a"), ChannelSettings(name = "b"))) + dataSource.replaceAllSettings( + listOf( + ChannelSettings.Builder().also { wb -> wb.name = "a" }.build(), + ChannelSettings.Builder().also { wb -> wb.name = "b" }.build(), + ), + ) val set = dataSource.channelSetFlow.first() assertEquals(listOf("a", "b"), set.settings.map { it.name }) @@ -87,18 +109,26 @@ class SwitchingChannelSetDataSourceTest { @Test fun `updateChannelSet atomically replaces settings and lora`() = runTest(testDispatcher) { - val settings = listOf(ChannelSettings(name = "new")) - val lora = Config.LoRaConfig(channel_num = 7) + val settings = listOf(ChannelSettings.Builder().also { wb -> wb.name = "new" }.build()) + val lora = Config.LoRaConfig.Builder().also { wb -> wb.channel_num = 7 }.build() dataSource.updateChannelSet(settingsList = settings, loraConfig = lora) - assertEquals(ChannelSet(settings = settings, lora_config = lora), dataSource.channelSetFlow.first()) + assertEquals( + ChannelSet.Builder() + .also { wb -> + wb.settings = settings + wb.lora_config = lora + } + .build(), + dataSource.channelSetFlow.first(), + ) } @Test fun `setLoraConfig preserves existing channel settings`() = runTest(testDispatcher) { dataSource.updateChannelSettings(secondary(0, "keep")) - dataSource.setLoraConfig(Config.LoRaConfig(channel_num = 7)) + dataSource.setLoraConfig(Config.LoRaConfig.Builder().also { wb -> wb.channel_num = 7 }.build()) val set = dataSource.channelSetFlow.first() assertEquals("keep", set.settings.single().name) @@ -130,6 +160,115 @@ class SwitchingChannelSetDataSourceTest { ) } + /** + * The first reconcile has no baseline to diff against, so it captures one and moves nothing. Without this an + * upgrade would try to reconcile every stored conversation against a set it had never seen. + */ + @Test + fun `the first reconcile only captures a baseline`() = runTest(testDispatcher) { + dataSource.updateChannelSettings(secondary(0, "primary")) + insertBroadcastPacket(channel = 0) + + dataSource.reconcileConversations() + + assertEquals(1, packetsOn(ContactKey.broadcast(0).value), "nothing to compare against, so nothing moves") + assertEquals( + "primary", + currentBaseline()?.settings?.single()?.name, + "the current set becomes the baseline for the next change", + ) + } + + /** + * The handshake streams channels one slot at a time, so reconciling on each of those writes would see a channel + * momentarily missing and retire a conversation that never went anywhere. + */ + @Test + fun `per-slot writes do not reconcile`() = runTest(testDispatcher) { + dataSource.updateChannelSet( + listOf(ChannelSettings.Builder().also { wb -> wb.name = "A" }.build()), + Config.LoRaConfig.Builder().also { wb -> wb.use_preset = true }.build(), + ) + insertBroadcastPacket(channel = 0) + + dataSource.updateChannelSettings(secondary(0, "B")) + + assertEquals(1, packetsOn(ContactKey.broadcast(0).value), "a mid-handshake slot write must not re-key") + } + + /** A whole-set write is the app's own change, so it is safe — and necessary — to re-key immediately. */ + @Test + fun `a whole-set write reconciles immediately`() = runTest(testDispatcher) { + val lora = Config.LoRaConfig.Builder().also { wb -> wb.use_preset = true }.build() + dataSource.updateChannelSet(listOf(ChannelSettings.Builder().also { wb -> wb.name = "A" }.build()), lora) + insertBroadcastPacket(channel = 0) + + dataSource.updateChannelSet(listOf(ChannelSettings.Builder().also { wb -> wb.name = "B" }.build()), lora) + + assertEquals(0, packetsOn(ContactKey.broadcast(0).value), "the slot has a new occupant") + assertEquals(1, dbProvider.currentDb.value.packetDao().getRetiredPacketContactKeys().size) + } + + /** + * Replays the handshake's real order: the cached channel set is dropped, then the radio's LoRa config arrives as + * its own write, and only after that do the channels stream in slot by slot. + * + * The LoRa write must not reconcile. It lands while the settings list is still empty, so reconciling there would + * see every channel as deleted and archive every conversation on the device — on every single reconnect. + */ + @Test + fun `the handshake lora write does not archive conversations`() = runTest(testDispatcher) { + val lora = Config.LoRaConfig.Builder().also { wb -> wb.use_preset = true }.build() + dataSource.updateChannelSet(listOf(ChannelSettings.Builder().also { wb -> wb.name = "A" }.build()), lora) + insertBroadcastPacket(channel = 0) + + dataSource.clearChannelSet() + dataSource.setLoraConfig(lora) + + assertEquals(1, packetsOn(ContactKey.broadcast(0).value), "a mid-handshake lora write must not re-key") + assertEquals( + 0, + dbProvider.currentDb.value.packetDao().getRetiredPacketContactKeys().size, + "nothing may be archived while the channel list is still empty", + ) + } + + /** The messages did not move because the handshake dropped the cached channel set, so the baseline must survive. */ + @Test + fun `clearing the channel set keeps the reconciliation baseline`() = runTest(testDispatcher) { + dataSource.updateChannelSet( + listOf(ChannelSettings.Builder().also { wb -> wb.name = "A" }.build()), + Config.LoRaConfig.Builder().also { wb -> wb.use_preset = true }.build(), + ) + + dataSource.clearChannelSet() + + assertTrue(dataSource.channelSetFlow.first().settings.isEmpty()) + assertEquals("A", currentBaseline()?.settings?.single()?.name) + } + + private suspend fun currentBaseline(): ChannelSet? = + dbProvider.currentDb.value.channelSetDao().get()?.lastReconciledChannelSet + + private suspend fun packetsOn(contactKey: String): Int = + dbProvider.currentDb.value.packetDao().getPacketsForContact(contactKey).size + + private suspend fun insertBroadcastPacket(channel: Int) { + dbProvider.currentDb.value + .packetDao() + .insert( + Packet( + uuid = 0L, + myNodeNum = 1, + port_num = PortNum.TEXT_MESSAGE_APP.value, + contact_key = ContactKey.broadcast(channel).value, + received_time = 1L, + read = false, + data = DataPacket(to = NodeAddress.ID_BROADCAST, channel = channel, text = "hi"), + ), + ) + } + /** Handshake fires overlapping updateChannelSettings as it streams channels; the mutex must not drop any. */ @Test fun `concurrent updateChannelSettings does not lose updates`() = runTest(testDispatcher) { diff --git a/core/data/src/commonTest/kotlin/org/meshtastic/core/data/manager/AdminPacketHandlerImplTest.kt b/core/data/src/commonTest/kotlin/org/meshtastic/core/data/manager/AdminPacketHandlerImplTest.kt index bfa8ed42e6..26915839ed 100644 --- a/core/data/src/commonTest/kotlin/org/meshtastic/core/data/manager/AdminPacketHandlerImplTest.kt +++ b/core/data/src/commonTest/kotlin/org/meshtastic/core/data/manager/AdminPacketHandlerImplTest.kt @@ -64,7 +64,18 @@ class AdminPacketHandlerImplTest { private fun makePacket(from: Int, adminMessage: AdminMessage): MeshPacket { val payload = adminMessage.encode().toByteString() - return MeshPacket(from = from, decoded = Data(portnum = PortNum.ADMIN_APP, payload = payload)) + return MeshPacket.Builder() + .also { wb -> + wb.from = from + wb.decoded = + Data.Builder() + .also { wb -> + wb.portnum = PortNum.ADMIN_APP + wb.payload = payload + } + .build() + } + .build() } private fun handle(packet: MeshPacket) = handler.handleAdminMessage(packet, myNodeNum, session) @@ -74,7 +85,7 @@ class AdminPacketHandlerImplTest { @Test fun `session passkey is updated when present`() { val passkey = ByteString.of(1, 2, 3, 4) - val adminMsg = AdminMessage(session_passkey = passkey) + val adminMsg = AdminMessage.Builder().also { wb -> wb.session_passkey = passkey }.build() val packet = makePacket(myNodeNum, adminMsg) handle(packet) @@ -84,7 +95,7 @@ class AdminPacketHandlerImplTest { @Test fun `empty session passkey does not record refresh`() { - val adminMsg = AdminMessage(session_passkey = ByteString.EMPTY) + val adminMsg = AdminMessage.Builder().also { wb -> wb.session_passkey = ByteString.EMPTY }.build() val packet = makePacket(myNodeNum, adminMsg) handle(packet) @@ -95,8 +106,14 @@ class AdminPacketHandlerImplTest { @Test fun `get_config_response from own node delegates to configHandler`() { - val config = Config(device = Config.DeviceConfig(role = Config.DeviceConfig.Role.CLIENT)) - val adminMsg = AdminMessage(get_config_response = config) + val config = + Config.Builder() + .also { wb -> + wb.device = + Config.DeviceConfig.Builder().also { wb -> wb.role = Config.DeviceConfig.Role.CLIENT }.build() + } + .build() + val adminMsg = AdminMessage.Builder().also { wb -> wb.get_config_response = config }.build() val packet = makePacket(myNodeNum, adminMsg) handle(packet) @@ -106,8 +123,8 @@ class AdminPacketHandlerImplTest { @Test fun `get_config_response from remote node is ignored`() { - val config = Config(device = Config.DeviceConfig()) - val adminMsg = AdminMessage(get_config_response = config) + val config = Config.Builder().also { wb -> wb.device = Config.DeviceConfig.Builder().build() }.build() + val adminMsg = AdminMessage.Builder().also { wb -> wb.get_config_response = config }.build() val packet = makePacket(99999, adminMsg) handle(packet) @@ -118,8 +135,11 @@ class AdminPacketHandlerImplTest { @Test fun `get_module_config_response from own node delegates to configHandler`() { - val moduleConfig = ModuleConfig(mqtt = ModuleConfig.MQTTConfig(enabled = true)) - val adminMsg = AdminMessage(get_module_config_response = moduleConfig) + val moduleConfig = + ModuleConfig.Builder() + .also { wb -> wb.mqtt = ModuleConfig.MQTTConfig.Builder().also { wb -> wb.enabled = true }.build() } + .build() + val adminMsg = AdminMessage.Builder().also { wb -> wb.get_module_config_response = moduleConfig }.build() val packet = makePacket(myNodeNum, adminMsg) handle(packet) @@ -129,8 +149,14 @@ class AdminPacketHandlerImplTest { @Test fun `get_module_config_response from remote node updates node status`() { - val moduleConfig = ModuleConfig(statusmessage = ModuleConfig.StatusMessageConfig(node_status = "Battery Low")) - val adminMsg = AdminMessage(get_module_config_response = moduleConfig) + val moduleConfig = + ModuleConfig.Builder() + .also { wb -> + wb.statusmessage = + ModuleConfig.StatusMessageConfig.Builder().also { wb -> wb.node_status = "Battery Low" }.build() + } + .build() + val adminMsg = AdminMessage.Builder().also { wb -> wb.get_module_config_response = moduleConfig }.build() val remoteNode = 99999 val packet = makePacket(remoteNode, adminMsg) @@ -141,8 +167,11 @@ class AdminPacketHandlerImplTest { @Test fun `get_module_config_response from remote without status message does not crash`() { - val moduleConfig = ModuleConfig(mqtt = ModuleConfig.MQTTConfig(enabled = true)) - val adminMsg = AdminMessage(get_module_config_response = moduleConfig) + val moduleConfig = + ModuleConfig.Builder() + .also { wb -> wb.mqtt = ModuleConfig.MQTTConfig.Builder().also { wb -> wb.enabled = true }.build() } + .build() + val adminMsg = AdminMessage.Builder().also { wb -> wb.get_module_config_response = moduleConfig }.build() val packet = makePacket(99999, adminMsg) handle(packet) @@ -153,8 +182,8 @@ class AdminPacketHandlerImplTest { @Test fun `get_channel_response from own node delegates to configHandler`() { - val channel = Channel(index = 0) - val adminMsg = AdminMessage(get_channel_response = channel) + val channel = Channel.Builder().also { wb -> wb.index = 0 }.build() + val adminMsg = AdminMessage.Builder().also { wb -> wb.get_channel_response = channel }.build() val packet = makePacket(myNodeNum, adminMsg) handle(packet) @@ -164,8 +193,8 @@ class AdminPacketHandlerImplTest { @Test fun `get_channel_response from remote node is ignored`() { - val channel = Channel(index = 0) - val adminMsg = AdminMessage(get_channel_response = channel) + val channel = Channel.Builder().also { wb -> wb.index = 0 }.build() + val adminMsg = AdminMessage.Builder().also { wb -> wb.get_channel_response = channel }.build() val packet = makePacket(99999, adminMsg) handle(packet) @@ -176,8 +205,14 @@ class AdminPacketHandlerImplTest { @Test fun `device metadata from own node delegates to configFlowManager`() { - val metadata = DeviceMetadata(firmware_version = "2.6.0", hw_model = HardwareModel.HELTEC_V3) - val adminMsg = AdminMessage(get_device_metadata_response = metadata) + val metadata = + DeviceMetadata.Builder() + .also { wb -> + wb.firmware_version = "2.6.0" + wb.hw_model = HardwareModel.HELTEC_V3 + } + .build() + val adminMsg = AdminMessage.Builder().also { wb -> wb.get_device_metadata_response = metadata }.build() val packet = makePacket(myNodeNum, adminMsg) handle(packet) @@ -187,8 +222,14 @@ class AdminPacketHandlerImplTest { @Test fun `device metadata from remote node delegates to nodeManager`() { - val metadata = DeviceMetadata(firmware_version = "2.5.0", hw_model = HardwareModel.TBEAM) - val adminMsg = AdminMessage(get_device_metadata_response = metadata) + val metadata = + DeviceMetadata.Builder() + .also { wb -> + wb.firmware_version = "2.5.0" + wb.hw_model = HardwareModel.TBEAM + } + .build() + val adminMsg = AdminMessage.Builder().also { wb -> wb.get_device_metadata_response = metadata }.build() val remoteNode = 99999 val packet = makePacket(remoteNode, adminMsg) @@ -201,7 +242,13 @@ class AdminPacketHandlerImplTest { @Test fun `packet with null decoded payload is ignored`() { - val packet = MeshPacket(from = myNodeNum, decoded = null) + val packet = + MeshPacket.Builder() + .also { wb -> + wb.from = myNodeNum + wb.decoded = null + } + .build() handle(packet) // No crash } @@ -209,7 +256,18 @@ class AdminPacketHandlerImplTest { @Test fun `packet with empty payload bytes is ignored`() { val packet = - MeshPacket(from = myNodeNum, decoded = Data(portnum = PortNum.ADMIN_APP, payload = ByteString.EMPTY)) + MeshPacket.Builder() + .also { wb -> + wb.from = myNodeNum + wb.decoded = + Data.Builder() + .also { wb -> + wb.portnum = PortNum.ADMIN_APP + wb.payload = ByteString.EMPTY + } + .build() + } + .build() handle(packet) // No crash — decodes as default AdminMessage with no fields set } @@ -217,8 +275,14 @@ class AdminPacketHandlerImplTest { @Test fun `combined admin message with passkey and config response`() { val passkey = ByteString.of(5, 6, 7, 8) - val config = Config(lora = Config.LoRaConfig()) - val adminMsg = AdminMessage(session_passkey = passkey, get_config_response = config) + val config = Config.Builder().also { wb -> wb.lora = Config.LoRaConfig.Builder().build() }.build() + val adminMsg = + AdminMessage.Builder() + .also { wb -> + wb.session_passkey = passkey + wb.get_config_response = config + } + .build() val packet = makePacket(myNodeNum, adminMsg) handle(packet) diff --git a/core/data/src/commonTest/kotlin/org/meshtastic/core/data/manager/CommandSenderImplTest.kt b/core/data/src/commonTest/kotlin/org/meshtastic/core/data/manager/CommandSenderImplTest.kt index e0644d620d..7250c28285 100644 --- a/core/data/src/commonTest/kotlin/org/meshtastic/core/data/manager/CommandSenderImplTest.kt +++ b/core/data/src/commonTest/kotlin/org/meshtastic/core/data/manager/CommandSenderImplTest.kt @@ -78,8 +78,8 @@ class CommandSenderImplTest { @BeforeTest fun setup() { - every { radioConfigRepository.localConfigFlow } returns flowOf(LocalConfig()) - every { radioConfigRepository.channelSetFlow } returns flowOf(ChannelSet()) + every { radioConfigRepository.localConfigFlow } returns flowOf(LocalConfig.Builder().build()) + every { radioConfigRepository.channelSetFlow } returns flowOf(ChannelSet.Builder().build()) every { nodeManager.myNodeNum } returns MutableStateFlow(MY_NODE_NUM) every { nodeManager.nodeDBbyNodeNum } returns emptyMap() every { sessionManager.getPasskey(any()) } returns ByteString.EMPTY @@ -153,7 +153,7 @@ class CommandSenderImplTest { @Test fun resolveNodeNum_byId_looksUpAndReturns() { - val node = Node(num = 99, user = User(id = "!deadbeef")) + val node = Node(num = 99, user = User.Builder().also { wb -> wb.id = "!deadbeef" }.build()) every { nodeManager.getNodeById("!deadbeef") } returns node assertEquals(99, commandSender.resolveNodeNum(NodeAddress.ById("!deadbeef"))) } @@ -224,7 +224,7 @@ class CommandSenderImplTest { val packets = mutableListOf() everySuspend { packetHandler.sendToRadio(capture(packets)) } returns true - commandSender.sendAdmin(DEST_NODE) { AdminMessage(get_owner_request = true) } + commandSender.sendAdmin(DEST_NODE) { AdminMessage.Builder().also { wb -> wb.get_owner_request = true }.build() } val adminMessage = AdminMessage.ADAPTER.decode(requireNotNull(packets.single().decoded).payload) assertEquals(passkey, adminMessage.session_passkey) @@ -235,7 +235,9 @@ class CommandSenderImplTest { val packets = mutableListOf() everySuspend { packetHandler.sendToRadio(capture(packets)) } returns true - commandSender.sendAdmin(DEST_NODE, requestId = 0) { AdminMessage(get_owner_request = true) } + commandSender.sendAdmin(DEST_NODE, requestId = 0) { + AdminMessage.Builder().also { wb -> wb.get_owner_request = true }.build() + } assertNotEquals(0, packets.single().id) } @@ -244,7 +246,9 @@ class CommandSenderImplTest { fun sendAdminSurfacesQueueRejection() = runTest { everySuspend { packetHandler.sendToRadio(any()) } returns false - assertFailsWith { commandSender.sendAdmin(DEST_NODE) { AdminMessage() } } + assertFailsWith { + commandSender.sendAdmin(DEST_NODE) { AdminMessage.Builder().build() } + } } @Test @@ -252,7 +256,9 @@ class CommandSenderImplTest { val packets = mutableListOf() everySuspend { packetHandler.sendToRadioForConnection(capture(packets), 17L) } returns true - commandSender.sendAdminForConnection(DEST_NODE, expectedConnectionVersion = 17L) { AdminMessage() } + commandSender.sendAdminForConnection(DEST_NODE, expectedConnectionVersion = 17L) { + AdminMessage.Builder().build() + } assertEquals(1, packets.size) verifySuspend { packetHandler.sendToRadioForConnection(any(), 17L) } @@ -264,7 +270,9 @@ class CommandSenderImplTest { everySuspend { packetHandler.sendToRadioAndAwaitResult(capture(packets)) } returns AwaitedSendResult(AwaitedSendStatus.ACCEPTED, departureEpochAtDispatch = 0) - commandSender.sendAdminAwaitResult(DEST_NODE, requestId = 0) { AdminMessage(get_owner_request = true) } + commandSender.sendAdminAwaitResult(DEST_NODE, requestId = 0) { + AdminMessage.Builder().also { wb -> wb.get_owner_request = true }.build() + } assertNotEquals(0, packets.single().id) } @@ -274,7 +282,10 @@ class CommandSenderImplTest { val expected = AwaitedSendResult(AwaitedSendStatus.REJECTED) everySuspend { packetHandler.sendToRadioAndAwaitResult(any()) } returns expected - val result = commandSender.sendAdminAwaitResult(DEST_NODE) { AdminMessage(get_owner_request = true) } + val result = + commandSender.sendAdminAwaitResult(DEST_NODE) { + AdminMessage.Builder().also { wb -> wb.get_owner_request = true }.build() + } assertEquals(expected, result) assertFalse(result.accepted) @@ -317,7 +328,9 @@ class CommandSenderImplTest { every { sessionManager.getPasskey(DEST_NODE) } returns passkey every { packetHandler.sendToRadio(any()) } returns Unit - commandSender.sendAdminImmediate(DEST_NODE) { AdminMessage(set_time_only = 12345) } + commandSender.sendAdminImmediate(DEST_NODE) { + AdminMessage.Builder().also { wb -> wb.set_time_only = 12345 }.build() + } // Direct ToRadio dispatch (not the Connected-gated MeshPacket queue), correct destination, // no want_response, and the session passkey injected into the admin payload. @@ -410,7 +423,13 @@ class CommandSenderImplTest { @Test fun requestNeighborInfo_localNode_usesCachedNeighborInfo() = runTest { - val cached = NeighborInfo(node_id = MY_NODE_NUM, last_sent_by_id = MY_NODE_NUM) + val cached = + NeighborInfo.Builder() + .also { wb -> + wb.node_id = MY_NODE_NUM + wb.last_sent_by_id = MY_NODE_NUM + } + .build() every { neighborInfoHandler.lastNeighborInfo } returns cached everySuspend { packetHandler.sendToRadio(any()) } returns true @@ -470,7 +489,13 @@ class CommandSenderImplTest { fun sendPosition_updatesLocalPositionWhenNotFixed() = runTest { everySuspend { packetHandler.sendToRadio(any()) } returns true - val pos = org.meshtastic.proto.Position(latitude_i = 10000000, longitude_i = 20000000) + val pos = + org.meshtastic.proto.Position.Builder() + .also { wb -> + wb.latitude_i = 10000000 + wb.longitude_i = 20000000 + } + .build() commandSender.sendPosition(pos) verify { nodeManager.handleReceivedPosition(MY_NODE_NUM, MY_NODE_NUM, any(), any()) } @@ -479,7 +504,13 @@ class CommandSenderImplTest { @Test fun sendPosition_doesNotUpdateLocalPositionWhenQueueRejectsPacket() = runTest { everySuspend { packetHandler.sendToRadio(any()) } returns false - val pos = org.meshtastic.proto.Position(latitude_i = 10000000, longitude_i = 20000000) + val pos = + org.meshtastic.proto.Position.Builder() + .also { wb -> + wb.latitude_i = 10000000 + wb.longitude_i = 20000000 + } + .build() assertFailsWith { commandSender.sendPosition(pos) } @@ -490,7 +521,9 @@ class CommandSenderImplTest { fun sendPosition_rejectsWhenLocalNodeIdentityIsUnavailable() = runTest { every { nodeManager.myNodeNum } returns MutableStateFlow(null) - assertFailsWith { commandSender.sendPosition(org.meshtastic.proto.Position()) } + assertFailsWith { + commandSender.sendPosition(org.meshtastic.proto.Position.Builder().build()) + } verifySuspend(exactly(0)) { packetHandler.sendToRadio(any()) } } @@ -544,9 +577,18 @@ class CommandSenderImplTest { fun sendPosition_skipsLocalUpdateWhenFixedPosition() = runTest { // Use MutableStateFlow so the init launchIn picks it up immediately in TestScope val configFlow = - MutableStateFlow(LocalConfig(position = org.meshtastic.proto.Config.PositionConfig(fixed_position = true))) + MutableStateFlow( + LocalConfig.Builder() + .also { wb -> + wb.position = + org.meshtastic.proto.Config.PositionConfig.Builder() + .also { wb -> wb.fixed_position = true } + .build() + } + .build(), + ) every { radioConfigRepository.localConfigFlow } returns configFlow - every { radioConfigRepository.channelSetFlow } returns MutableStateFlow(ChannelSet()) + every { radioConfigRepository.channelSetFlow } returns MutableStateFlow(ChannelSet.Builder().build()) val testScope = TestScope() val fixedSender = CommandSenderImpl( @@ -561,7 +603,13 @@ class CommandSenderImplTest { testScope.testScheduler.advanceUntilIdle() everySuspend { packetHandler.sendToRadio(any()) } returns true - val pos = org.meshtastic.proto.Position(latitude_i = 10000000, longitude_i = 20000000) + val pos = + org.meshtastic.proto.Position.Builder() + .also { wb -> + wb.latitude_i = 10000000 + wb.longitude_i = 20000000 + } + .build() fixedSender.sendPosition(pos) verify(mode = dev.mokkery.verify.VerifyMode.not) { diff --git a/core/data/src/commonTest/kotlin/org/meshtastic/core/data/manager/FromRadioPacketHandlerImplTest.kt b/core/data/src/commonTest/kotlin/org/meshtastic/core/data/manager/FromRadioPacketHandlerImplTest.kt index 8153c0240e..6965b70f73 100644 --- a/core/data/src/commonTest/kotlin/org/meshtastic/core/data/manager/FromRadioPacketHandlerImplTest.kt +++ b/core/data/src/commonTest/kotlin/org/meshtastic/core/data/manager/FromRadioPacketHandlerImplTest.kt @@ -123,8 +123,8 @@ class FromRadioPacketHandlerImplTest { @Test fun `handleFromRadio routes MY_INFO to configFlowManager`() { - val myInfo = MyNodeInfo(my_node_num = 1234) - val proto = FromRadio(my_info = myInfo) + val myInfo = MyNodeInfo.Builder().also { wb -> wb.my_node_num = 1234 }.build() + val proto = FromRadio.Builder().also { wb -> wb.my_info = myInfo }.build() handle(proto) @@ -133,8 +133,8 @@ class FromRadioPacketHandlerImplTest { @Test fun `handleFromRadio routes METADATA to configFlowManager`() { - val metadata = DeviceMetadata(firmware_version = "v1.0") - val proto = FromRadio(metadata = metadata) + val metadata = DeviceMetadata.Builder().also { wb -> wb.firmware_version = "v1.0" }.build() + val proto = FromRadio.Builder().also { wb -> wb.metadata = metadata }.build() handle(proto) @@ -143,8 +143,8 @@ class FromRadioPacketHandlerImplTest { @Test fun `handleFromRadio routes NODE_INFO to configFlowManager and updates status`() { - val nodeInfo = ProtoNodeInfo(num = 1234) - val proto = FromRadio(node_info = nodeInfo) + val nodeInfo = ProtoNodeInfo.Builder().also { wb -> wb.num = 1234 }.build() + val proto = FromRadio.Builder().also { wb -> wb.node_info = nodeInfo }.build() every { configFlowManager.newNodeCount } returns 1 @@ -157,7 +157,7 @@ class FromRadioPacketHandlerImplTest { @Test fun `handleFromRadio routes CONFIG_COMPLETE_ID to configFlowManager`() { val nonce = 69420 - val proto = FromRadio(config_complete_id = nonce) + val proto = FromRadio.Builder().also { wb -> wb.config_complete_id = nonce }.build() handle(proto) @@ -167,10 +167,10 @@ class FromRadioPacketHandlerImplTest { @Test fun `stale NODE_INFO does not update connection progress`() { - val nodeInfo = ProtoNodeInfo(num = 1234) + val nodeInfo = ProtoNodeInfo.Builder().also { wb -> wb.num = 1234 }.build() every { configFlowManager.handleNodeInfo(nodeInfo, session) } returns false - handle(FromRadio(node_info = nodeInfo)) + handle(FromRadio.Builder().also { wb -> wb.node_info = nodeInfo }.build()) verify { configFlowManager.handleNodeInfo(nodeInfo, session) } verify(mode = VerifyMode.exactly(0)) { serviceRepository.setConnectionProgress(any()) } @@ -181,7 +181,7 @@ class FromRadioPacketHandlerImplTest { val nonce = 69420 every { configFlowManager.handleConfigComplete(nonce, session) } returns false - handle(FromRadio(config_complete_id = nonce)) + handle(FromRadio.Builder().also { wb -> wb.config_complete_id = nonce }.build()) verify { configFlowManager.handleConfigComplete(nonce, session) } assertFalse(lockdownCoordinator.configCompleteCalled) @@ -189,7 +189,7 @@ class FromRadioPacketHandlerImplTest { @Test fun `revoked session skips node progress after handler returns`() { - val nodeInfo = ProtoNodeInfo(num = 1234) + val nodeInfo = ProtoNodeInfo.Builder().also { wb -> wb.num = 1234 }.build() var active = true every { configFlowManager.handleNodeInfo(nodeInfo, session) } calls { @@ -208,7 +208,7 @@ class FromRadioPacketHandlerImplTest { } } - handle(FromRadio(node_info = nodeInfo)) + handle(FromRadio.Builder().also { wb -> wb.node_info = nodeInfo }.build()) verify(mode = VerifyMode.exactly(0)) { serviceRepository.setConnectionProgress(any()) } } @@ -216,17 +216,26 @@ class FromRadioPacketHandlerImplTest { @Test fun `revoked session skips direct packet-dispatch branches`() { every { radioInterfaceService.runIfSessionActive(session, any()) } returns false - val proxyMessage = MqttClientProxyMessage(topic = "test/topic") - val queueStatus = QueueStatus(free = 10) - val xmodemPacket = XModem() - val lockdownStatus = LockdownStatus(state = LockdownStatus.State.LOCKED) + val proxyMessage = MqttClientProxyMessage.Builder().also { wb -> wb.topic = "test/topic" }.build() + val queueStatus = QueueStatus.Builder().also { wb -> wb.free = 10 }.build() + val xmodemPacket = XModem.Builder().build() + val lockdownStatus = LockdownStatus.Builder().also { wb -> wb.state = LockdownStatus.State.LOCKED }.build() - handle(FromRadio(mqttClientProxyMessage = proxyMessage)) - handle(FromRadio(queueStatus = queueStatus)) - handle(FromRadio(xmodemPacket = xmodemPacket)) - handle(FromRadio(lockdown_status = lockdownStatus)) - handle(FromRadio(display_frame = DisplayFrame(width = 128, height = 64))) - handle(FromRadio(display_palette = DisplayPalette(signature = 1))) + val displayFrame = + DisplayFrame.Builder() + .also { wb -> + wb.width = 128 + wb.height = 64 + } + .build() + val displayPalette = DisplayPalette.Builder().also { wb -> wb.signature = 1 }.build() + + handle(FromRadio.Builder().also { wb -> wb.mqttClientProxyMessage = proxyMessage }.build()) + handle(FromRadio.Builder().also { wb -> wb.queueStatus = queueStatus }.build()) + handle(FromRadio.Builder().also { wb -> wb.xmodemPacket = xmodemPacket }.build()) + handle(FromRadio.Builder().also { wb -> wb.lockdown_status = lockdownStatus }.build()) + handle(FromRadio.Builder().also { wb -> wb.display_frame = displayFrame }.build()) + handle(FromRadio.Builder().also { wb -> wb.display_palette = displayPalette }.build()) verify(mode = VerifyMode.exactly(0)) { mqttManager.handleMqttProxyMessage(any()) } verify(mode = VerifyMode.exactly(0)) { packetHandler.handleQueueStatus(any()) } @@ -238,26 +247,46 @@ class FromRadioPacketHandlerImplTest { @Test fun `handleFromRadio routes DISPLAY_FRAME to displayMirrorManager`() { - val frame = DisplayFrame(width = 128, height = 64, frame_id = 1, total_size = 1024) + val frame = + DisplayFrame.Builder() + .also { wb -> + wb.width = 128 + wb.height = 64 + wb.frame_id = 1 + wb.total_size = 1024 + } + .build() - handle(FromRadio(display_frame = frame)) + handle(FromRadio.Builder().also { wb -> wb.display_frame = frame }.build()) verify { displayMirrorManager.handleIncomingFrame(frame) } } @Test fun `handleFromRadio routes DISPLAY_PALETTE to displayMirrorManager`() { - val palette = DisplayPalette(signature = 7, region_total = 0) + val palette = + DisplayPalette.Builder() + .also { wb -> + wb.signature = 7 + wb.region_total = 0 + } + .build() - handle(FromRadio(display_palette = palette)) + handle(FromRadio.Builder().also { wb -> wb.display_palette = palette }.build()) verify { displayMirrorManager.handleIncomingPalette(palette) } } @Test fun `handleFromRadio routes LOCKDOWN_STATUS to lockdownCoordinator`() { - val lockdownStatus = LockdownStatus(state = LockdownStatus.State.LOCKED, lock_reason = "token_missing") - val proto = FromRadio(lockdown_status = lockdownStatus) + val lockdownStatus = + LockdownStatus.Builder() + .also { wb -> + wb.state = LockdownStatus.State.LOCKED + wb.lock_reason = "token_missing" + } + .build() + val proto = FromRadio.Builder().also { wb -> wb.lockdown_status = lockdownStatus }.build() handle(proto) @@ -266,8 +295,8 @@ class FromRadioPacketHandlerImplTest { @Test fun `handleFromRadio routes QUEUESTATUS to packetHandler`() { - val queueStatus = QueueStatus(free = 10) - val proto = FromRadio(queueStatus = queueStatus) + val queueStatus = QueueStatus.Builder().also { wb -> wb.free = 10 }.build() + val proto = FromRadio.Builder().also { wb -> wb.queueStatus = queueStatus }.build() handle(proto) @@ -276,8 +305,11 @@ class FromRadioPacketHandlerImplTest { @Test fun `handleFromRadio routes CONFIG to configHandler`() { - val config = Config(lora = Config.LoRaConfig(use_preset = true)) - val proto = FromRadio(config = config) + val config = + Config.Builder() + .also { wb -> wb.lora = Config.LoRaConfig.Builder().also { wb -> wb.use_preset = true }.build() } + .build() + val proto = FromRadio.Builder().also { wb -> wb.config = config }.build() handle(proto) @@ -286,8 +318,11 @@ class FromRadioPacketHandlerImplTest { @Test fun `handleFromRadio routes MODULE_CONFIG to configHandler`() { - val moduleConfig = ModuleConfig(mqtt = ModuleConfig.MQTTConfig(enabled = true)) - val proto = FromRadio(moduleConfig = moduleConfig) + val moduleConfig = + ModuleConfig.Builder() + .also { wb -> wb.mqtt = ModuleConfig.MQTTConfig.Builder().also { wb -> wb.enabled = true }.build() } + .build() + val proto = FromRadio.Builder().also { wb -> wb.moduleConfig = moduleConfig }.build() handle(proto) @@ -296,8 +331,8 @@ class FromRadioPacketHandlerImplTest { @Test fun `handleFromRadio routes CHANNEL to configHandler`() { - val channel = Channel(index = 0) - val proto = FromRadio(channel = channel) + val channel = Channel.Builder().also { wb -> wb.index = 0 }.build() + val proto = FromRadio.Builder().also { wb -> wb.channel = channel }.build() handle(proto) @@ -306,8 +341,8 @@ class FromRadioPacketHandlerImplTest { @Test fun `handleFromRadio routes REGION_PRESETS to configHandler`() { - val map = LoRaRegionPresetMap() - val proto = FromRadio(region_presets = map) + val map = LoRaRegionPresetMap.Builder().build() + val proto = FromRadio.Builder().also { wb -> wb.region_presets = map }.build() handle(proto) @@ -316,8 +351,8 @@ class FromRadioPacketHandlerImplTest { @Test fun `handleFromRadio routes MQTT_CLIENT_PROXY_MESSAGE to mqttManager`() { - val proxyMsg = MqttClientProxyMessage(topic = "test/topic") - val proto = FromRadio(mqttClientProxyMessage = proxyMsg) + val proxyMsg = MqttClientProxyMessage.Builder().also { wb -> wb.topic = "test/topic" }.build() + val proto = FromRadio.Builder().also { wb -> wb.mqttClientProxyMessage = proxyMsg }.build() handle(proto) @@ -326,8 +361,8 @@ class FromRadioPacketHandlerImplTest { @Test fun `handleFromRadio routes CLIENTNOTIFICATION to serviceRepository`() { - val notification = ClientNotification(message = "test") - val proto = FromRadio(clientNotification = notification) + val notification = ClientNotification.Builder().also { wb -> wb.message = "test" }.build() + val proto = FromRadio.Builder().also { wb -> wb.clientNotification = notification }.build() // Note: getString() from Compose Resources requires Skiko native lib which // is not available in headless JVM tests. We test the parts that don't trigger it. @@ -345,7 +380,7 @@ class FromRadioPacketHandlerImplTest { val notification = protectedPositionAdvisory(replyId = 100, time = 1_000) every { notificationManager.suppressClientNotificationModal(notification) } returns true - handle(FromRadio(clientNotification = notification)) + handle(FromRadio.Builder().also { wb -> wb.clientNotification = notification }.build()) verify(mode = VerifyMode.exactly(0)) { serviceRepository.setClientNotification(any()) } verifySuspend(mode = VerifyMode.exactly(1)) { radioInterfaceService.runWithSessionLease(session, any()) } @@ -356,7 +391,7 @@ class FromRadioPacketHandlerImplTest { val notification = protectedPositionAdvisory(replyId = 200, time = 2_000) every { notificationManager.suppressClientNotificationModal(notification) } returns false - handle(FromRadio(clientNotification = notification)) + handle(FromRadio.Builder().also { wb -> wb.clientNotification = notification }.build()) verify { serviceRepository.setClientNotification(notification) } verifySuspend { radioInterfaceService.runWithSessionLease(session, any()) } @@ -364,10 +399,10 @@ class FromRadioPacketHandlerImplTest { @Test fun `stale client notification is discarded before publication`() { - val notification = ClientNotification(message = "stale") + val notification = ClientNotification.Builder().also { wb -> wb.message = "stale" }.build() every { radioInterfaceService.runIfSessionActive(session, any()) } returns false - handle(FromRadio(clientNotification = notification)) + handle(FromRadio.Builder().also { wb -> wb.clientNotification = notification }.build()) verify(mode = VerifyMode.exactly(0)) { serviceRepository.setClientNotification(any()) } verifySuspend(mode = VerifyMode.exactly(0)) { notificationManager.dispatchClientNotification(any(), any()) } @@ -375,10 +410,10 @@ class FromRadioPacketHandlerImplTest { @Test fun `client alert is skipped when the session retires after publication`() { - val notification = ClientNotification(message = "retired") + val notification = ClientNotification.Builder().also { wb -> wb.message = "retired" }.build() everySuspend { radioInterfaceService.runWithSessionLease(session, any()) } returns false - handle(FromRadio(clientNotification = notification)) + handle(FromRadio.Builder().also { wb -> wb.clientNotification = notification }.build()) verify { serviceRepository.setClientNotification(notification) } verifySuspend(mode = VerifyMode.exactly(0)) { notificationManager.dispatchClientNotification(any(), any()) } @@ -386,26 +421,55 @@ class FromRadioPacketHandlerImplTest { @Test fun `OTA status client notifications are identified`() { - assertTrue(ClientNotification(message = "Rebooting to WiFi OTA").isOtaStatusNotification()) - assertTrue(ClientNotification(message = "OTA Loader does not support WiFi").isOtaStatusNotification()) assertTrue( - ClientNotification(message = "Cannot start OTA: OTA Loader partition not found.").isOtaStatusNotification(), + ClientNotification.Builder() + .also { wb -> wb.message = "Rebooting to WiFi OTA" } + .build() + .isOtaStatusNotification(), + ) + assertTrue( + ClientNotification.Builder() + .also { wb -> wb.message = "OTA Loader does not support WiFi" } + .build() + .isOtaStatusNotification(), + ) + assertTrue( + ClientNotification.Builder() + .also { wb -> wb.message = "Cannot start OTA: OTA Loader partition not found." } + .build() + .isOtaStatusNotification(), + ) + assertTrue( + ClientNotification.Builder() + .also { wb -> wb.message = "Unable to switch to the OTA partition." } + .build() + .isOtaStatusNotification(), ) - assertTrue(ClientNotification(message = "Unable to switch to the OTA partition.").isOtaStatusNotification()) } @Test fun `non OTA client notifications are not identified as OTA status`() { - assertFalse(ClientNotification(message = "test").isOtaStatusNotification()) - assertFalse(ClientNotification(message = "Low battery").isOtaStatusNotification()) - assertFalse(ClientNotification(message = "ROTATE credentials").isOtaStatusNotification()) - assertFalse(ClientNotification(message = "Quota exceeded").isOtaStatusNotification()) + assertFalse(ClientNotification.Builder().also { wb -> wb.message = "test" }.build().isOtaStatusNotification()) + assertFalse( + ClientNotification.Builder().also { wb -> wb.message = "Low battery" }.build().isOtaStatusNotification(), + ) + assertFalse( + ClientNotification.Builder() + .also { wb -> wb.message = "ROTATE credentials" } + .build() + .isOtaStatusNotification(), + ) + assertFalse( + ClientNotification.Builder().also { wb -> wb.message = "Quota exceeded" }.build().isOtaStatusNotification(), + ) } - private fun protectedPositionAdvisory(replyId: Int, time: Int) = ClientNotification( - message = "Location sharing is disabled on this channel", - reply_id = replyId, - time = time, - level = LogRecord.Level.WARNING, - ) + private fun protectedPositionAdvisory(replyId: Int, time: Int) = ClientNotification.Builder() + .also { wb -> + wb.message = "Location sharing is disabled on this channel" + wb.reply_id = replyId + wb.time = time + wb.level = LogRecord.Level.WARNING + } + .build() } diff --git a/core/data/src/commonTest/kotlin/org/meshtastic/core/data/manager/GeofenceMonitorTest.kt b/core/data/src/commonTest/kotlin/org/meshtastic/core/data/manager/GeofenceMonitorTest.kt index d31327379f..4de2d9486a 100644 --- a/core/data/src/commonTest/kotlin/org/meshtastic/core/data/manager/GeofenceMonitorTest.kt +++ b/core/data/src/commonTest/kotlin/org/meshtastic/core/data/manager/GeofenceMonitorTest.kt @@ -62,19 +62,34 @@ class GeofenceMonitorTest { // Geofence centred at (10, 20), 1 km radius. private val centerLatI = 100_000_000 private val centerLonI = 200_000_000 - private val inside = Position(latitude_i = centerLatI, longitude_i = centerLonI) // distance 0 - private val outside = Position(latitude_i = 110_000_000, longitude_i = centerLonI) // ~111 km north + private val inside = + Position.Builder() + .also { wb -> + wb.latitude_i = centerLatI + wb.longitude_i = centerLonI + } + .build() // distance 0 + private val outside = + Position.Builder() + .also { wb -> + wb.latitude_i = 110_000_000 + wb.longitude_i = centerLonI + } + .build() // ~111 km north - private fun waypoint(enter: Boolean = true, exit: Boolean = false, favoritesOnly: Boolean = false) = Waypoint( - id = 42, - latitude_i = centerLatI, - longitude_i = centerLonI, - name = "Base", - geofence_radius = 1000, - notify_on_enter = enter, - notify_on_exit = exit, - notify_favorites_only = favoritesOnly, - ) + private fun waypoint(enter: Boolean = true, exit: Boolean = false, favoritesOnly: Boolean = false) = + Waypoint.Builder() + .also { wb -> + wb.id = 42 + wb.latitude_i = centerLatI + wb.longitude_i = centerLonI + wb.name = "Base" + wb.geofence_radius = 1000 + wb.notify_on_enter = enter + wb.notify_on_exit = exit + wb.notify_favorites_only = favoritesOnly + } + .build() private data class Mocks( val packetRepository: PacketRepository, diff --git a/core/data/src/commonTest/kotlin/org/meshtastic/core/data/manager/HistoryManagerImplTest.kt b/core/data/src/commonTest/kotlin/org/meshtastic/core/data/manager/HistoryManagerImplTest.kt index a31bc0681e..16e23df3f9 100644 --- a/core/data/src/commonTest/kotlin/org/meshtastic/core/data/manager/HistoryManagerImplTest.kt +++ b/core/data/src/commonTest/kotlin/org/meshtastic/core/data/manager/HistoryManagerImplTest.kt @@ -102,7 +102,8 @@ class HistoryManagerImplTest { .requestHistoryReplay( trigger = "test", myNodeNum = null, - storeForwardConfig = ModuleConfig.StoreForwardConfig(enabled = true), + storeForwardConfig = + ModuleConfig.StoreForwardConfig.Builder().also { wb -> wb.enabled = true }.build(), transport = "BLE", expectedConnectionVersion = 17L, ) @@ -121,7 +122,8 @@ class HistoryManagerImplTest { .requestHistoryReplay( trigger = "test", myNodeNum = 123, - storeForwardConfig = ModuleConfig.StoreForwardConfig(enabled = true), + storeForwardConfig = + ModuleConfig.StoreForwardConfig.Builder().also { wb -> wb.enabled = true }.build(), transport = "BLE", expectedConnectionVersion = 17L, ) @@ -142,7 +144,7 @@ class HistoryManagerImplTest { .requestHistoryReplay( trigger = "test", myNodeNum = 123, - storeForwardConfig = ModuleConfig.StoreForwardConfig(enabled = true), + storeForwardConfig = ModuleConfig.StoreForwardConfig.Builder().also { wb -> wb.enabled = true }.build(), transport = "BLE", expectedConnectionVersion = 17L, ) @@ -171,7 +173,8 @@ class HistoryManagerImplTest { .requestHistoryReplay( trigger = "test", myNodeNum = 123, - storeForwardConfig = ModuleConfig.StoreForwardConfig(enabled = true), + storeForwardConfig = + ModuleConfig.StoreForwardConfig.Builder().also { wb -> wb.enabled = true }.build(), transport = "BLE", expectedConnectionVersion = 23L, ) diff --git a/core/data/src/commonTest/kotlin/org/meshtastic/core/data/manager/LockdownCoordinatorImplTest.kt b/core/data/src/commonTest/kotlin/org/meshtastic/core/data/manager/LockdownCoordinatorImplTest.kt index 2edddf8aad..2db9d147f4 100644 --- a/core/data/src/commonTest/kotlin/org/meshtastic/core/data/manager/LockdownCoordinatorImplTest.kt +++ b/core/data/src/commonTest/kotlin/org/meshtastic/core/data/manager/LockdownCoordinatorImplTest.kt @@ -140,14 +140,18 @@ class LockdownCoordinatorImplTest { @Test fun `NEEDS_PROVISION sets NeedsProvision state`() { - coordinator.handleLockdownStatus(LockdownStatus(state = LockdownStatus.State.NEEDS_PROVISION)) + coordinator.handleLockdownStatus( + LockdownStatus.Builder().also { wb -> wb.state = LockdownStatus.State.NEEDS_PROVISION }.build(), + ) assertIs(serviceRepo.lockdownState.value) } @Test fun `NEEDS_PROVISION after lockNow does not trigger LockNowAcknowledged`() { coordinator.lockNow() - coordinator.handleLockdownStatus(LockdownStatus(state = LockdownStatus.State.NEEDS_PROVISION)) + coordinator.handleLockdownStatus( + LockdownStatus.Builder().also { wb -> wb.state = LockdownStatus.State.NEEDS_PROVISION }.build(), + ) // wasLockNow is only checked in handleLocked, not handleNeedsProvision assertIs(serviceRepo.lockdownState.value) @@ -157,7 +161,9 @@ class LockdownCoordinatorImplTest { fun `STATE_UNSPECIFIED leaves current state unchanged`() { serviceRepo.setLockdownState(LockdownState.Locked("needs_auth")) - coordinator.handleLockdownStatus(LockdownStatus(state = LockdownStatus.State.STATE_UNSPECIFIED)) + coordinator.handleLockdownStatus( + LockdownStatus.Builder().also { wb -> wb.state = LockdownStatus.State.STATE_UNSPECIFIED }.build(), + ) val state = serviceRepo.lockdownState.value assertIs(state) @@ -172,7 +178,12 @@ class LockdownCoordinatorImplTest { fun `LOCKED with no stored passphrase sets Locked state`() { radioService.setDeviceAddress(testDeviceAddress) coordinator.handleLockdownStatus( - LockdownStatus(state = LockdownStatus.State.LOCKED, lock_reason = "needs_auth"), + LockdownStatus.Builder() + .also { wb -> + wb.state = LockdownStatus.State.LOCKED + wb.lock_reason = "needs_auth" + } + .build(), ) val state = serviceRepo.lockdownState.value assertIs(state) @@ -182,7 +193,9 @@ class LockdownCoordinatorImplTest { @Test fun `LOCKED with no device address sets Locked state`() { radioService.setDeviceAddress(null) - coordinator.handleLockdownStatus(LockdownStatus(state = LockdownStatus.State.LOCKED)) + coordinator.handleLockdownStatus( + LockdownStatus.Builder().also { wb -> wb.state = LockdownStatus.State.LOCKED }.build(), + ) assertIs(serviceRepo.lockdownState.value) } @@ -198,7 +211,12 @@ class LockdownCoordinatorImplTest { commandSender.onLockdownPassphraseDispatchAttempt = { stateDuringDispatch = serviceRepo.lockdownState.value } coordinator.handleLockdownStatus( - LockdownStatus(state = LockdownStatus.State.LOCKED, lock_reason = "needs_auth"), + LockdownStatus.Builder() + .also { wb -> + wb.state = LockdownStatus.State.LOCKED + wb.lock_reason = "needs_auth" + } + .build(), ) assertEquals("secret", commandSender.lastPassphrase) @@ -214,12 +232,21 @@ class LockdownCoordinatorImplTest { passphraseStore.saved[testDeviceAddress] = StoredPassphrase("secret", 10, 24) // Establish a previous successful auto-unlock so a stale auto-attempt marker would be observable below. - coordinator.handleLockdownStatus(LockdownStatus(state = LockdownStatus.State.LOCKED)) - coordinator.handleLockdownStatus(LockdownStatus(state = LockdownStatus.State.UNLOCKED)) + coordinator.handleLockdownStatus( + LockdownStatus.Builder().also { wb -> wb.state = LockdownStatus.State.LOCKED }.build(), + ) + coordinator.handleLockdownStatus( + LockdownStatus.Builder().also { wb -> wb.state = LockdownStatus.State.UNLOCKED }.build(), + ) commandSender.lockdownPassphraseDispatchAccepted = false coordinator.handleLockdownStatus( - LockdownStatus(state = LockdownStatus.State.LOCKED, lock_reason = "needs_auth"), + LockdownStatus.Builder() + .also { wb -> + wb.state = LockdownStatus.State.LOCKED + wb.lock_reason = "needs_auth" + } + .build(), ) val state = serviceRepo.lockdownState.value @@ -228,7 +255,9 @@ class LockdownCoordinatorImplTest { assertEquals("secret", passphraseStore.saved[testDeviceAddress]?.passphrase) // A later failure must not be treated as an auto-attempt that never left the app. - coordinator.handleLockdownStatus(LockdownStatus(state = LockdownStatus.State.UNLOCK_FAILED)) + coordinator.handleLockdownStatus( + LockdownStatus.Builder().also { wb -> wb.state = LockdownStatus.State.UNLOCK_FAILED }.build(), + ) assertEquals("secret", passphraseStore.saved[testDeviceAddress]?.passphrase) } @@ -238,7 +267,12 @@ class LockdownCoordinatorImplTest { passphraseStore.getThrows = RuntimeException("crypto failure") coordinator.handleLockdownStatus( - LockdownStatus(state = LockdownStatus.State.LOCKED, lock_reason = "needs_auth"), + LockdownStatus.Builder() + .also { wb -> + wb.state = LockdownStatus.State.LOCKED + wb.lock_reason = "needs_auth" + } + .build(), ) assertIs(serviceRepo.lockdownState.value) @@ -255,11 +289,13 @@ class LockdownCoordinatorImplTest { coordinator.submitPassphrase("mypass", boots = 20, hours = 48) coordinator.handleLockdownStatus( - LockdownStatus( - state = LockdownStatus.State.UNLOCKED, - boots_remaining = 19, - valid_until_epoch = 1_700_000_000, - ), + LockdownStatus.Builder() + .also { wb -> + wb.state = LockdownStatus.State.UNLOCKED + wb.boots_remaining = 19 + wb.valid_until_epoch = 1_700_000_000 + } + .build(), ) assertTrue(serviceRepo.sessionAuthorized.value) @@ -283,10 +319,22 @@ class LockdownCoordinatorImplTest { // Trigger auto-replay coordinator.handleLockdownStatus( - LockdownStatus(state = LockdownStatus.State.LOCKED, lock_reason = "needs_auth"), + LockdownStatus.Builder() + .also { wb -> + wb.state = LockdownStatus.State.LOCKED + wb.lock_reason = "needs_auth" + } + .build(), ) // Then unlock succeeds - coordinator.handleLockdownStatus(LockdownStatus(state = LockdownStatus.State.UNLOCKED, boots_remaining = 49)) + coordinator.handleLockdownStatus( + LockdownStatus.Builder() + .also { wb -> + wb.state = LockdownStatus.State.UNLOCKED + wb.boots_remaining = 49 + } + .build(), + ) // Store should still have original values (pendingPassphrase was null during auto-replay) assertEquals("original", passphraseStore.saved[testDeviceAddress]?.passphrase) @@ -299,7 +347,9 @@ class LockdownCoordinatorImplTest { passphraseStore.saveThrows = RuntimeException("disk full") coordinator.submitPassphrase("mypass", boots = 10, hours = 0) - coordinator.handleLockdownStatus(LockdownStatus(state = LockdownStatus.State.UNLOCKED)) + coordinator.handleLockdownStatus( + LockdownStatus.Builder().also { wb -> wb.state = LockdownStatus.State.UNLOCKED }.build(), + ) // Session should still be authorized even if save fails assertTrue(serviceRepo.sessionAuthorized.value) @@ -311,7 +361,14 @@ class LockdownCoordinatorImplTest { radioService.setDeviceAddress(null) coordinator.submitPassphrase("mypass", boots = 10, hours = 0) - coordinator.handleLockdownStatus(LockdownStatus(state = LockdownStatus.State.UNLOCKED, boots_remaining = 10)) + coordinator.handleLockdownStatus( + LockdownStatus.Builder() + .also { wb -> + wb.state = LockdownStatus.State.UNLOCKED + wb.boots_remaining = 10 + } + .build(), + ) assertTrue(serviceRepo.sessionAuthorized.value) assertIs(serviceRepo.lockdownState.value) @@ -322,7 +379,14 @@ class LockdownCoordinatorImplTest { fun `UNLOCKED converts uint32 epoch correctly`() { coordinator.submitPassphrase("p", boots = 1, hours = 1) // Use a large unsigned value that would be negative as Int: 0xFFFF_FFFF = -1 as Int - coordinator.handleLockdownStatus(LockdownStatus(state = LockdownStatus.State.UNLOCKED, valid_until_epoch = -1)) + coordinator.handleLockdownStatus( + LockdownStatus.Builder() + .also { wb -> + wb.state = LockdownStatus.State.UNLOCKED + wb.valid_until_epoch = -1 + } + .build(), + ) // -1 as Int -> toUInt().toLong() = 4_294_967_295L val tokenInfo = serviceRepo.lockdownTokenInfo.value @@ -337,7 +401,12 @@ class LockdownCoordinatorImplTest { fun `UNLOCK_FAILED with no backoff sets UnlockFailed state`() { coordinator.submitPassphrase("wrong", boots = 10, hours = 0) coordinator.handleLockdownStatus( - LockdownStatus(state = LockdownStatus.State.UNLOCK_FAILED, backoff_seconds = 0), + LockdownStatus.Builder() + .also { wb -> + wb.state = LockdownStatus.State.UNLOCK_FAILED + wb.backoff_seconds = 0 + } + .build(), ) assertIs(serviceRepo.lockdownState.value) } @@ -346,7 +415,12 @@ class LockdownCoordinatorImplTest { fun `UNLOCK_FAILED with backoff sets UnlockBackoff state`() { coordinator.submitPassphrase("wrong", boots = 10, hours = 0) coordinator.handleLockdownStatus( - LockdownStatus(state = LockdownStatus.State.UNLOCK_FAILED, backoff_seconds = 30), + LockdownStatus.Builder() + .also { wb -> + wb.state = LockdownStatus.State.UNLOCK_FAILED + wb.backoff_seconds = 30 + } + .build(), ) val state = serviceRepo.lockdownState.value assertIs(state) @@ -359,12 +433,23 @@ class LockdownCoordinatorImplTest { coordinator.submitPassphrase("wrong", boots = 10, hours = 0) coordinator.handleLockdownStatus( - LockdownStatus(state = LockdownStatus.State.UNLOCK_FAILED, backoff_seconds = 0), + LockdownStatus.Builder() + .also { wb -> + wb.state = LockdownStatus.State.UNLOCK_FAILED + wb.backoff_seconds = 0 + } + .build(), ) coordinator.submitPassphrase("correct", boots = 25, hours = 12) coordinator.handleLockdownStatus( - LockdownStatus(state = LockdownStatus.State.UNLOCKED, boots_remaining = 24, valid_until_epoch = 1234), + LockdownStatus.Builder() + .also { wb -> + wb.state = LockdownStatus.State.UNLOCKED + wb.boots_remaining = 24 + wb.valid_until_epoch = 1234 + } + .build(), ) val stored = passphraseStore.saved[testDeviceAddress] @@ -384,11 +469,21 @@ class LockdownCoordinatorImplTest { // Trigger auto-replay coordinator.handleLockdownStatus( - LockdownStatus(state = LockdownStatus.State.LOCKED, lock_reason = "needs_auth"), + LockdownStatus.Builder() + .also { wb -> + wb.state = LockdownStatus.State.LOCKED + wb.lock_reason = "needs_auth" + } + .build(), ) // Then failure coordinator.handleLockdownStatus( - LockdownStatus(state = LockdownStatus.State.UNLOCK_FAILED, backoff_seconds = 0), + LockdownStatus.Builder() + .also { wb -> + wb.state = LockdownStatus.State.UNLOCK_FAILED + wb.backoff_seconds = 0 + } + .build(), ) assertNull(passphraseStore.saved[testDeviceAddress]) @@ -400,9 +495,16 @@ class LockdownCoordinatorImplTest { radioService.setDeviceAddress(testDeviceAddress) passphraseStore.saved[testDeviceAddress] = StoredPassphrase("stale", 5, 0) - coordinator.handleLockdownStatus(LockdownStatus(state = LockdownStatus.State.LOCKED)) coordinator.handleLockdownStatus( - LockdownStatus(state = LockdownStatus.State.UNLOCK_FAILED, backoff_seconds = 60), + LockdownStatus.Builder().also { wb -> wb.state = LockdownStatus.State.LOCKED }.build(), + ) + coordinator.handleLockdownStatus( + LockdownStatus.Builder() + .also { wb -> + wb.state = LockdownStatus.State.UNLOCK_FAILED + wb.backoff_seconds = 60 + } + .build(), ) val state = serviceRepo.lockdownState.value @@ -416,9 +518,16 @@ class LockdownCoordinatorImplTest { passphraseStore.saved[testDeviceAddress] = StoredPassphrase("stale", 5, 0) passphraseStore.clearThrows = RuntimeException("crypto failure") - coordinator.handleLockdownStatus(LockdownStatus(state = LockdownStatus.State.LOCKED)) coordinator.handleLockdownStatus( - LockdownStatus(state = LockdownStatus.State.UNLOCK_FAILED, backoff_seconds = 0), + LockdownStatus.Builder().also { wb -> wb.state = LockdownStatus.State.LOCKED }.build(), + ) + coordinator.handleLockdownStatus( + LockdownStatus.Builder() + .also { wb -> + wb.state = LockdownStatus.State.UNLOCK_FAILED + wb.backoff_seconds = 0 + } + .build(), ) assertIs(serviceRepo.lockdownState.value) @@ -434,7 +543,12 @@ class LockdownCoordinatorImplTest { assertTrue(commandSender.lockNowCalled) coordinator.handleLockdownStatus( - LockdownStatus(state = LockdownStatus.State.LOCKED, lock_reason = "needs_auth"), + LockdownStatus.Builder() + .also { wb -> + wb.state = LockdownStatus.State.LOCKED + wb.lock_reason = "needs_auth" + } + .build(), ) assertIs(serviceRepo.lockdownState.value) @@ -448,7 +562,12 @@ class LockdownCoordinatorImplTest { assertFalse(coordinator.lockNow()) coordinator.handleLockdownStatus( - LockdownStatus(state = LockdownStatus.State.LOCKED, lock_reason = "needs_auth"), + LockdownStatus.Builder() + .also { wb -> + wb.state = LockdownStatus.State.LOCKED + wb.lock_reason = "needs_auth" + } + .build(), ) assertIs(serviceRepo.lockdownState.value) @@ -463,7 +582,12 @@ class LockdownCoordinatorImplTest { // After reconnect, LOCKED should not trigger LockNowAcknowledged radioService.setDeviceAddress(testDeviceAddress) coordinator.handleLockdownStatus( - LockdownStatus(state = LockdownStatus.State.LOCKED, lock_reason = "needs_auth"), + LockdownStatus.Builder() + .also { wb -> + wb.state = LockdownStatus.State.LOCKED + wb.lock_reason = "needs_auth" + } + .build(), ) assertIs(serviceRepo.lockdownState.value) @@ -490,7 +614,9 @@ class LockdownCoordinatorImplTest { // Subsequent LOCKED should not trigger LockNowAcknowledged. radioService.setDeviceAddress(testDeviceAddress) - coordinator.handleLockdownStatus(LockdownStatus(state = LockdownStatus.State.LOCKED)) + coordinator.handleLockdownStatus( + LockdownStatus.Builder().also { wb -> wb.state = LockdownStatus.State.LOCKED }.build(), + ) assertIs(serviceRepo.lockdownState.value) } @@ -507,7 +633,9 @@ class LockdownCoordinatorImplTest { assertEquals("needs_auth", state.lockReason) // A later status must not persist credentials from a command that never left the app. - coordinator.handleLockdownStatus(LockdownStatus(state = LockdownStatus.State.UNLOCKED)) + coordinator.handleLockdownStatus( + LockdownStatus.Builder().also { wb -> wb.state = LockdownStatus.State.UNLOCKED }.build(), + ) assertNull(passphraseStore.saved[testDeviceAddress]) } @@ -551,7 +679,9 @@ class LockdownCoordinatorImplTest { radioService.setDeviceAddress(testDeviceAddress) passphraseStore.saved[testDeviceAddress] = StoredPassphrase("stale", 50, 0) - coordinator.handleLockdownStatus(LockdownStatus(state = LockdownStatus.State.DISABLED)) + coordinator.handleLockdownStatus( + LockdownStatus.Builder().also { wb -> wb.state = LockdownStatus.State.DISABLED }.build(), + ) assertIs(serviceRepo.lockdownState.value) assertFalse(serviceRepo.sessionAuthorized.value) diff --git a/core/data/src/commonTest/kotlin/org/meshtastic/core/data/manager/MeshConfigFlowManagerImplTest.kt b/core/data/src/commonTest/kotlin/org/meshtastic/core/data/manager/MeshConfigFlowManagerImplTest.kt index 8bfc06a9e8..6f6d4b1636 100644 --- a/core/data/src/commonTest/kotlin/org/meshtastic/core/data/manager/MeshConfigFlowManagerImplTest.kt +++ b/core/data/src/commonTest/kotlin/org/meshtastic/core/data/manager/MeshConfigFlowManagerImplTest.kt @@ -97,15 +97,23 @@ class MeshConfigFlowManagerImplTest { private val activeSessionFlow = MutableStateFlow(activeSession) private val protoMyNodeInfo = - ProtoMyNodeInfo( - my_node_num = myNodeNum, - min_app_version = 30000, - device_id = "test-device".encodeUtf8(), - pio_env = "", - ) + ProtoMyNodeInfo.Builder() + .also { wb -> + wb.my_node_num = myNodeNum + wb.min_app_version = 30000 + wb.device_id = "test-device".encodeUtf8() + wb.pio_env = "" + } + .build() private val metadata = - DeviceMetadata(firmware_version = "2.6.0", hw_model = HardwareModel.HELTEC_V3, hasWifi = false) + DeviceMetadata.Builder() + .also { wb -> + wb.firmware_version = "2.6.0" + wb.hw_model = HardwareModel.HELTEC_V3 + wb.hasWifi = false + } + .build() @BeforeTest fun setUp() { @@ -204,7 +212,7 @@ class MeshConfigFlowManagerImplTest { // device_id is raw hardware bytes, not text: a lossy utf8 decode would collapse distinct // ids into the same replacement-character string. 0xFF bytes are invalid UTF-8 on purpose. val rawId = byteArrayOf(0xFF.toByte(), 0x00, 0xA1.toByte(), 0xB2.toByte()).toByteString() - handleMyInfo(protoMyNodeInfo.copy(device_id = rawId)) + handleMyInfo(protoMyNodeInfo.newBuilder().also { wb -> wb.device_id = rawId }.build()) advanceUntilIdle() verify { nodeManager.setMyDeviceId("ff00a1b2") } @@ -212,7 +220,7 @@ class MeshConfigFlowManagerImplTest { @Test fun `handleMyInfo reports an absent device_id as null`() = testScope.runTest { - handleMyInfo(protoMyNodeInfo.copy(device_id = okio.ByteString.EMPTY)) + handleMyInfo(protoMyNodeInfo.newBuilder().also { wb -> wb.device_id = okio.ByteString.EMPTY }.build()) advanceUntilIdle() verify { nodeManager.setMyDeviceId(null) } @@ -263,7 +271,7 @@ class MeshConfigFlowManagerImplTest { activeSessionFlow.value = nextSession assertFalse(manager.handleLocalMetadata(metadata, nextSession)) - assertFalse(manager.handleNodeInfo(NodeInfo(num = 100), nextSession)) + assertFalse(manager.handleNodeInfo(NodeInfo.Builder().also { wb -> wb.num = 100 }.build(), nextSession)) assertFalse(manager.handleConfigComplete(HandshakeConstants.CONFIG_NONCE, nextSession)) advanceUntilIdle() @@ -368,7 +376,7 @@ class MeshConfigFlowManagerImplTest { advanceUntilIdle() // Default/empty DeviceMetadata should not trigger insertMetadata - manager.handleLocalMetadata(DeviceMetadata()) + manager.handleLocalMetadata(DeviceMetadata.Builder().build()) advanceUntilIdle() // insertMetadata should only have been called zero times for default metadata @@ -430,7 +438,13 @@ class MeshConfigFlowManagerImplTest { @Test fun `Stage 1 complete with old firmware logs warning but continues handshake`() = testScope.runTest { val oldMetadata = - DeviceMetadata(firmware_version = "2.3.0", hw_model = HardwareModel.HELTEC_V3, hasWifi = false) + DeviceMetadata.Builder() + .also { wb -> + wb.firmware_version = "2.3.0" + wb.hw_model = HardwareModel.HELTEC_V3 + wb.hasWifi = false + } + .build() handleMyInfo(protoMyNodeInfo) advanceUntilIdle() manager.handleLocalMetadata(oldMetadata) @@ -510,8 +524,8 @@ class MeshConfigFlowManagerImplTest { runCurrent() // Now in ReceivingNodeInfo - manager.handleNodeInfo(NodeInfo(num = 100)) - manager.handleNodeInfo(NodeInfo(num = 200)) + manager.handleNodeInfo(NodeInfo.Builder().also { wb -> wb.num = 100 }.build()) + manager.handleNodeInfo(NodeInfo.Builder().also { wb -> wb.num = 200 }.build()) assertEquals(2, manager.newNodeCount) } @@ -524,7 +538,7 @@ class MeshConfigFlowManagerImplTest { handleMyInfo(protoMyNodeInfo) advanceUntilIdle() - manager.handleNodeInfo(NodeInfo(num = 100)) + manager.handleNodeInfo(NodeInfo.Builder().also { wb -> wb.num = 100 }.build()) assertEquals(1, manager.newNodeCount) manager.handleLocalMetadata(metadata) @@ -535,14 +549,14 @@ class MeshConfigFlowManagerImplTest { manager.handleConfigComplete(HandshakeConstants.NODE_INFO_NONCE) advanceUntilIdle() - verify { nodeManager.installNodeInfo(NodeInfo(num = 100)) } + verify { nodeManager.installNodeInfo(NodeInfo.Builder().also { wb -> wb.num = 100 }.build()) } verifySuspend { connectionManager.onNodeDbReady() } } @Test fun `handleNodeInfo ignored outside Stage 2`() = testScope.runTest { // State is Idle - manager.handleNodeInfo(NodeInfo(num = 999)) + manager.handleNodeInfo(NodeInfo.Builder().also { wb -> wb.num = 999 }.build()) assertEquals(0, manager.newNodeCount) } @@ -551,7 +565,7 @@ class MeshConfigFlowManagerImplTest { @Test fun `active session completes metadata node info and both handshake stages`() = testScope.runTest { - val nodeInfo = NodeInfo(num = 100) + val nodeInfo = NodeInfo.Builder().also { wb -> wb.num = 100 }.build() val testNode = org.meshtastic.core.testing.TestDataFactory.createTestNode(num = nodeInfo.num) every { nodeManager.nodeDBbyNodeNum } returns mapOf(nodeInfo.num to testNode) @@ -577,8 +591,8 @@ class MeshConfigFlowManagerImplTest { @Test fun `session revocation during Stage 2 stops remaining persistence and publication`() = testScope.runTest { - val firstNode = NodeInfo(num = 100) - val secondNode = NodeInfo(num = 200) + val firstNode = NodeInfo.Builder().also { wb -> wb.num = 100 }.build() + val secondNode = NodeInfo.Builder().also { wb -> wb.num = 200 }.build() val testNode = org.meshtastic.core.testing.TestDataFactory.createTestNode(num = firstNode.num) every { nodeManager.nodeDBbyNodeNum } returns mapOf(firstNode.num to testNode) every { nodeManager.installNodeInfo(any()) } calls { activeSessionFlow.value = null } @@ -617,7 +631,7 @@ class MeshConfigFlowManagerImplTest { advanceTimeBy(STAGE_TRANSITION_ADVANCE_MS) runCurrent() - manager.handleNodeInfo(NodeInfo(num = 100)) + manager.handleNodeInfo(NodeInfo.Builder().also { wb -> wb.num = 100 }.build()) manager.handleConfigComplete(HandshakeConstants.NODE_INFO_NONCE) advanceUntilIdle() @@ -751,7 +765,13 @@ class MeshConfigFlowManagerImplTest { @Test fun `handleFileInfo delegates to radioConfigRepository`() = testScope.runTest { - val fileInfo = FileInfo(file_name = "firmware.bin", size_bytes = 1024) + val fileInfo = + FileInfo.Builder() + .also { wb -> + wb.file_name = "firmware.bin" + wb.size_bytes = 1024 + } + .build() manager.handleFileInfo(fileInfo) advanceUntilIdle() @@ -789,7 +809,7 @@ class MeshConfigFlowManagerImplTest { verify { connectionManager.onRadioConfigLoaded() } // Receive NodeInfo during Stage 2 - manager.handleNodeInfo(NodeInfo(num = 100)) + manager.handleNodeInfo(NodeInfo.Builder().also { wb -> wb.num = 100 }.build()) assertEquals(1, manager.newNodeCount) // Stage 2 complete @@ -814,7 +834,7 @@ class MeshConfigFlowManagerImplTest { advanceUntilIdle() // Before Stage 1 completes, a new handleMyInfo arrives (device rebooted) - val newMyInfo = protoMyNodeInfo.copy(my_node_num = 99999) + val newMyInfo = protoMyNodeInfo.newBuilder().also { wb -> wb.my_node_num = 99999 }.build() handleMyInfo(newMyInfo) advanceUntilIdle() @@ -828,7 +848,9 @@ class MeshConfigFlowManagerImplTest { @Test fun `handleMyInfo applies the event node-event default for event firmware`() = testScope.runTest { - handleMyInfo(protoMyNodeInfo.copy(firmware_edition = FirmwareEdition.DEFCON)) + handleMyInfo( + protoMyNodeInfo.newBuilder().also { wb -> wb.firmware_edition = FirmwareEdition.DEFCON }.build(), + ) advanceUntilIdle() verify { notificationPrefs.applyEventFirmwareNodeEventDefault(isEventFirmware = true) } @@ -846,7 +868,9 @@ class MeshConfigFlowManagerImplTest { fun `handleMyInfo treats DIY_EDITION as event firmware`() = testScope.runTest { // Any non-vanilla edition counts, including the DIY catch-all — it has no branding metadata but the same // notification noise problem. - handleMyInfo(protoMyNodeInfo.copy(firmware_edition = FirmwareEdition.DIY_EDITION)) + handleMyInfo( + protoMyNodeInfo.newBuilder().also { wb -> wb.firmware_edition = FirmwareEdition.DIY_EDITION }.build(), + ) advanceUntilIdle() verify { notificationPrefs.applyEventFirmwareNodeEventDefault(isEventFirmware = true) } @@ -884,7 +908,13 @@ class MeshConfigFlowManagerImplTest { @Test fun `handleFileInfo calls onHandshakeProgress`() = testScope.runTest { - val fileInfo = FileInfo(file_name = "firmware.bin", size_bytes = 1024) + val fileInfo = + FileInfo.Builder() + .also { wb -> + wb.file_name = "firmware.bin" + wb.size_bytes = 1024 + } + .build() manager.handleFileInfo(fileInfo) advanceUntilIdle() @@ -895,7 +925,7 @@ class MeshConfigFlowManagerImplTest { fun `handleNodeInfo during Stage 1 calls onHandshakeProgress`() = testScope.runTest { handleMyInfo(protoMyNodeInfo) advanceUntilIdle() - manager.handleNodeInfo(NodeInfo(num = 100)) + manager.handleNodeInfo(NodeInfo.Builder().also { wb -> wb.num = 100 }.build()) // handleMyInfo fires one call; handleNodeInfo in ReceivingConfig adds exactly one more. verify(mode = VerifyMode.exactly(2)) { connectionManager.onHandshakeProgress() } @@ -912,7 +942,7 @@ class MeshConfigFlowManagerImplTest { runCurrent() // Now in ReceivingNodeInfo — a NodeInfo packet must reset the watchdog. - manager.handleNodeInfo(NodeInfo(num = 100)) + manager.handleNodeInfo(NodeInfo.Builder().also { wb -> wb.num = 100 }.build()) // Prior calls: handleMyInfo(1) + handleLocalMetadata(1) + handleConfigOnlyComplete(1) = 3. // handleNodeInfo adds exactly one more. @@ -936,7 +966,7 @@ class MeshConfigFlowManagerImplTest { @Test fun `handleNodeInfo outside active handshake does not call onHandshakeProgress`() = testScope.runTest { // State is Idle — NodeInfo is ignored and must not reset the watchdog. - manager.handleNodeInfo(NodeInfo(num = 999)) + manager.handleNodeInfo(NodeInfo.Builder().also { wb -> wb.num = 999 }.build()) verify(mode = VerifyMode.not) { connectionManager.onHandshakeProgress() } } @@ -960,7 +990,7 @@ class MeshConfigFlowManagerImplTest { manager.handleConfigComplete(HandshakeConstants.CONFIG_NONCE) advanceTimeBy(STAGE_TRANSITION_ADVANCE_MS) runCurrent() - manager.handleNodeInfo(NodeInfo(num = 100)) + manager.handleNodeInfo(NodeInfo.Builder().also { wb -> wb.num = 100 }.build()) manager.handleConfigComplete(HandshakeConstants.NODE_INFO_NONCE) advanceUntilIdle() @@ -988,7 +1018,7 @@ class MeshConfigFlowManagerImplTest { manager.handleConfigComplete(HandshakeConstants.CONFIG_NONCE) advanceTimeBy(STAGE_TRANSITION_ADVANCE_MS) runCurrent() - manager.handleNodeInfo(NodeInfo(num = 100)) + manager.handleNodeInfo(NodeInfo.Builder().also { wb -> wb.num = 100 }.build()) manager.handleConfigComplete(HandshakeConstants.NODE_INFO_NONCE) advanceUntilIdle() @@ -1027,7 +1057,7 @@ class MeshConfigFlowManagerImplTest { manager.handleConfigComplete(HandshakeConstants.CONFIG_NONCE) advanceTimeBy(STAGE_TRANSITION_ADVANCE_MS) runCurrent() - manager.handleNodeInfo(NodeInfo(num = 100)) + manager.handleNodeInfo(NodeInfo.Builder().also { wb -> wb.num = 100 }.build()) // Drive Stage 2 complete. handleNodeInfoComplete runs synchronously: state becomes // Complete, onHandshakeComplete() fires (cancelling the watchdog), then the async DB diff --git a/core/data/src/commonTest/kotlin/org/meshtastic/core/data/manager/MeshConfigHandlerImplTest.kt b/core/data/src/commonTest/kotlin/org/meshtastic/core/data/manager/MeshConfigHandlerImplTest.kt index 72b174cad5..7830c74b23 100644 --- a/core/data/src/commonTest/kotlin/org/meshtastic/core/data/manager/MeshConfigHandlerImplTest.kt +++ b/core/data/src/commonTest/kotlin/org/meshtastic/core/data/manager/MeshConfigHandlerImplTest.kt @@ -65,8 +65,8 @@ class MeshConfigHandlerImplTest { private val connectionManager = mock(MockMode.autofill) private val radioInterfaceService = mock(MockMode.autofill) - private val localConfigFlow = MutableStateFlow(LocalConfig()) - private val moduleConfigFlow = MutableStateFlow(LocalModuleConfig()) + private val localConfigFlow = MutableStateFlow(LocalConfig.Builder().build()) + private val moduleConfigFlow = MutableStateFlow(LocalModuleConfig.Builder().build()) private val testDispatcher = UnconfinedTestDispatcher() private val session = RadioSessionContext(generation = 1L, address = "tcp:test") @@ -107,7 +107,15 @@ class MeshConfigHandlerImplTest { @Test fun `start wires localConfig flow from repository`() = runTest(testDispatcher) { handler = createHandler(backgroundScope) - val config = LocalConfig(device = Config.DeviceConfig(role = Config.DeviceConfig.Role.ROUTER)) + val config = + LocalConfig.Builder() + .also { wb -> + wb.device = + Config.DeviceConfig.Builder() + .also { wb -> wb.role = Config.DeviceConfig.Role.ROUTER } + .build() + } + .build() localConfigFlow.value = config advanceUntilIdle() @@ -117,7 +125,10 @@ class MeshConfigHandlerImplTest { @Test fun `start wires moduleConfig flow from repository`() = runTest(testDispatcher) { handler = createHandler(backgroundScope) - val config = LocalModuleConfig(mqtt = ModuleConfig.MQTTConfig(enabled = true)) + val config = + LocalModuleConfig.Builder() + .also { wb -> wb.mqtt = ModuleConfig.MQTTConfig.Builder().also { wb -> wb.enabled = true }.build() } + .build() moduleConfigFlow.value = config advanceUntilIdle() @@ -129,7 +140,15 @@ class MeshConfigHandlerImplTest { @Test fun `handleDeviceConfig persists config and updates progress`() = runTest(testDispatcher) { handler = createHandler(backgroundScope) - val config = Config(device = Config.DeviceConfig(role = Config.DeviceConfig.Role.CLIENT)) + val config = + Config.Builder() + .also { wb -> + wb.device = + Config.DeviceConfig.Builder() + .also { wb -> wb.role = Config.DeviceConfig.Role.CLIENT } + .build() + } + .build() handler.handleDeviceConfig(config, session) advanceUntilIdle() @@ -140,7 +159,15 @@ class MeshConfigHandlerImplTest { @Test fun `stale session cannot persist config or advance handshake`() = runTest(testDispatcher) { handler = createHandler(backgroundScope) - val config = Config(device = Config.DeviceConfig(role = Config.DeviceConfig.Role.CLIENT)) + val config = + Config.Builder() + .also { wb -> + wb.device = + Config.DeviceConfig.Builder() + .also { wb -> wb.role = Config.DeviceConfig.Role.CLIENT } + .build() + } + .build() every { radioInterfaceService.runIfSessionActive(session, any()) } returns false assertFalse(handler.handleDeviceConfig(config, session)) @@ -154,7 +181,15 @@ class MeshConfigHandlerImplTest { @Test fun `active session persists device config and advances handshake`() = runTest(testDispatcher) { handler = createHandler(backgroundScope) - val config = Config(device = Config.DeviceConfig(role = Config.DeviceConfig.Role.CLIENT)) + val config = + Config.Builder() + .also { wb -> + wb.device = + Config.DeviceConfig.Builder() + .also { wb -> wb.role = Config.DeviceConfig.Role.CLIENT } + .build() + } + .build() every { radioInterfaceService.runIfSessionActive(session, any()) } calls { @Suppress("UNCHECKED_CAST") @@ -181,7 +216,15 @@ class MeshConfigHandlerImplTest { @Test fun `revoked session cannot complete queued config persistence`() = runTest(testDispatcher) { handler = createHandler(backgroundScope) - val config = Config(device = Config.DeviceConfig(role = Config.DeviceConfig.Role.CLIENT)) + val config = + Config.Builder() + .also { wb -> + wb.device = + Config.DeviceConfig.Builder() + .also { wb -> wb.role = Config.DeviceConfig.Role.CLIENT } + .build() + } + .build() every { radioInterfaceService.runIfSessionActive(session, any()) } calls { @Suppress("UNCHECKED_CAST") @@ -204,13 +247,13 @@ class MeshConfigHandlerImplTest { handler = createHandler(backgroundScope) val configs = listOf( - Config(position = Config.PositionConfig()), - Config(power = Config.PowerConfig()), - Config(network = Config.NetworkConfig()), - Config(display = Config.DisplayConfig()), - Config(lora = Config.LoRaConfig()), - Config(bluetooth = Config.BluetoothConfig()), - Config(security = Config.SecurityConfig()), + Config.Builder().also { wb -> wb.position = Config.PositionConfig.Builder().build() }.build(), + Config.Builder().also { wb -> wb.power = Config.PowerConfig.Builder().build() }.build(), + Config.Builder().also { wb -> wb.network = Config.NetworkConfig.Builder().build() }.build(), + Config.Builder().also { wb -> wb.display = Config.DisplayConfig.Builder().build() }.build(), + Config.Builder().also { wb -> wb.lora = Config.LoRaConfig.Builder().build() }.build(), + Config.Builder().also { wb -> wb.bluetooth = Config.BluetoothConfig.Builder().build() }.build(), + Config.Builder().also { wb -> wb.security = Config.SecurityConfig.Builder().build() }.build(), ) for (config in configs) { @@ -227,7 +270,10 @@ class MeshConfigHandlerImplTest { @Test fun `handleModuleConfig persists config and updates progress`() = runTest(testDispatcher) { handler = createHandler(backgroundScope) - val config = ModuleConfig(mqtt = ModuleConfig.MQTTConfig(enabled = true)) + val config = + ModuleConfig.Builder() + .also { wb -> wb.mqtt = ModuleConfig.MQTTConfig.Builder().also { wb -> wb.enabled = true }.build() } + .build() handler.handleModuleConfig(config, session) advanceUntilIdle() @@ -241,7 +287,13 @@ class MeshConfigHandlerImplTest { val myNum = 123 every { nodeManager.myNodeNum } returns MutableStateFlow(myNum) - val config = ModuleConfig(statusmessage = ModuleConfig.StatusMessageConfig(node_status = "Active")) + val config = + ModuleConfig.Builder() + .also { wb -> + wb.statusmessage = + ModuleConfig.StatusMessageConfig.Builder().also { wb -> wb.node_status = "Active" }.build() + } + .build() handler.handleModuleConfig(config, session) advanceUntilIdle() @@ -253,7 +305,13 @@ class MeshConfigHandlerImplTest { handler = createHandler(backgroundScope) every { nodeManager.myNodeNum } returns MutableStateFlow(null) - val config = ModuleConfig(statusmessage = ModuleConfig.StatusMessageConfig(node_status = "Active")) + val config = + ModuleConfig.Builder() + .also { wb -> + wb.statusmessage = + ModuleConfig.StatusMessageConfig.Builder().also { wb -> wb.node_status = "Active" }.build() + } + .build() handler.handleModuleConfig(config, session) advanceUntilIdle() // No crash — updateNodeStatus should not be called @@ -263,7 +321,13 @@ class MeshConfigHandlerImplTest { fun `module config remains persisted when node status persistence fails`() = runTest(testDispatcher) { handler = createHandler(backgroundScope) val myNum = 123 - val config = ModuleConfig(statusmessage = ModuleConfig.StatusMessageConfig(node_status = "Active")) + val config = + ModuleConfig.Builder() + .also { wb -> + wb.statusmessage = + ModuleConfig.StatusMessageConfig.Builder().also { wb -> wb.node_status = "Active" }.build() + } + .build() every { nodeManager.myNodeNum } returns MutableStateFlow(myNum) everySuspend { nodeManager.updateNodeStatusAndPersist(myNum, "Active") } calls { @@ -286,7 +350,13 @@ class MeshConfigHandlerImplTest { val statusStarted = CompletableDeferred() val releaseStatus = CompletableDeferred() val myNum = 123 - val config = ModuleConfig(statusmessage = ModuleConfig.StatusMessageConfig(node_status = "Active")) + val config = + ModuleConfig.Builder() + .also { wb -> + wb.statusmessage = + ModuleConfig.StatusMessageConfig.Builder().also { wb -> wb.node_status = "Active" }.build() + } + .build() every { nodeManager.myNodeNum } returns MutableStateFlow(myNum) everySuspend { nodeManager.updateNodeStatusAndPersist(myNum, "Active") } calls { @@ -312,7 +382,7 @@ class MeshConfigHandlerImplTest { @Test fun `handleChannel persists channel settings`() = runTest(testDispatcher) { handler = createHandler(backgroundScope) - val channel = Channel(index = 0) + val channel = Channel.Builder().also { wb -> wb.index = 0 }.build() handler.handleChannel(channel, session) advanceUntilIdle() @@ -340,7 +410,7 @@ class MeshConfigHandlerImplTest { deviceId = null, ) - val channel = Channel(index = 2) + val channel = Channel.Builder().also { wb -> wb.index = 2 }.build() handler.handleChannel(channel, session) advanceUntilIdle() @@ -352,7 +422,7 @@ class MeshConfigHandlerImplTest { handler = createHandler(backgroundScope) every { nodeManager.getMyNodeInfo() } returns null - val channel = Channel(index = 0) + val channel = Channel.Builder().also { wb -> wb.index = 0 }.build() handler.handleChannel(channel, session) advanceUntilIdle() @@ -364,7 +434,7 @@ class MeshConfigHandlerImplTest { @Test fun `handleDeviceUIConfig persists config`() = runTest(testDispatcher) { handler = createHandler(backgroundScope) - val config = DeviceUIConfig() + val config = DeviceUIConfig.Builder().build() handler.handleDeviceUIConfig(config, session) advanceUntilIdle() @@ -376,7 +446,15 @@ class MeshConfigHandlerImplTest { @Test fun `handleDeviceConfig calls onHandshakeProgress`() = runTest(testDispatcher) { handler = createHandler(backgroundScope) - val config = Config(device = Config.DeviceConfig(role = Config.DeviceConfig.Role.CLIENT)) + val config = + Config.Builder() + .also { wb -> + wb.device = + Config.DeviceConfig.Builder() + .also { wb -> wb.role = Config.DeviceConfig.Role.CLIENT } + .build() + } + .build() handler.handleDeviceConfig(config, session) advanceUntilIdle() @@ -386,7 +464,10 @@ class MeshConfigHandlerImplTest { @Test fun `handleModuleConfig calls onHandshakeProgress`() = runTest(testDispatcher) { handler = createHandler(backgroundScope) - val config = ModuleConfig(mqtt = ModuleConfig.MQTTConfig(enabled = true)) + val config = + ModuleConfig.Builder() + .also { wb -> wb.mqtt = ModuleConfig.MQTTConfig.Builder().also { wb -> wb.enabled = true }.build() } + .build() handler.handleModuleConfig(config, session) advanceUntilIdle() @@ -398,7 +479,7 @@ class MeshConfigHandlerImplTest { handler = createHandler(backgroundScope) every { nodeManager.getMyNodeInfo() } returns null - val channel = Channel(index = 0) + val channel = Channel.Builder().also { wb -> wb.index = 0 }.build() handler.handleChannel(channel, session) advanceUntilIdle() @@ -408,7 +489,7 @@ class MeshConfigHandlerImplTest { @Test fun `handleDeviceUIConfig calls onHandshakeProgress`() = runTest(testDispatcher) { handler = createHandler(backgroundScope) - handler.handleDeviceUIConfig(DeviceUIConfig(), session) + handler.handleDeviceUIConfig(DeviceUIConfig.Builder().build(), session) advanceUntilIdle() verify { connectionManager.onHandshakeProgress() } @@ -419,7 +500,7 @@ class MeshConfigHandlerImplTest { @Test fun `handleRegionPresets persists map`() = runTest(testDispatcher) { handler = createHandler(backgroundScope) - val map = LoRaRegionPresetMap() + val map = LoRaRegionPresetMap.Builder().build() handler.handleRegionPresets(map, session) advanceUntilIdle() diff --git a/core/data/src/commonTest/kotlin/org/meshtastic/core/data/manager/MeshConnectionManagerImplTest.kt b/core/data/src/commonTest/kotlin/org/meshtastic/core/data/manager/MeshConnectionManagerImplTest.kt index 6384e7b99d..7301f64414 100644 --- a/core/data/src/commonTest/kotlin/org/meshtastic/core/data/manager/MeshConnectionManagerImplTest.kt +++ b/core/data/src/commonTest/kotlin/org/meshtastic/core/data/manager/MeshConnectionManagerImplTest.kt @@ -103,8 +103,8 @@ class MeshConnectionManagerImplTest { private val radioConnectionState = MutableStateFlow(ConnectionState.Disconnected) private lateinit var connectionStateHolder: ConnectionStateHolder - private val localConfigFlow = MutableStateFlow(LocalConfig()) - private val moduleConfigFlow = MutableStateFlow(LocalModuleConfig()) + private val localConfigFlow = MutableStateFlow(LocalConfig.Builder().build()) + private val moduleConfigFlow = MutableStateFlow(LocalModuleConfig.Builder().build()) private lateinit var testDispatcher: TestDispatcher @@ -134,8 +134,8 @@ class MeshConnectionManagerImplTest { testDispatcher = UnconfinedTestDispatcher() radioConnectionState.value = ConnectionState.Disconnected connectionStateHolder = ConnectionStateHolder() - localConfigFlow.value = LocalConfig() - moduleConfigFlow.value = LocalModuleConfig() + localConfigFlow.value = LocalConfig.Builder().build() + moduleConfigFlow.value = LocalModuleConfig.Builder().build() every { radioInterfaceService.connectionState } returns radioConnectionState every { radioConfigRepository.localConfigFlow } returns localConfigFlow @@ -290,22 +290,31 @@ class MeshConnectionManagerImplTest { manager = createManager(backgroundScope) radioConnectionState.value = ConnectionState.Connected advanceUntilIdle() - nodeRepository.updateLocalStats(LocalStats(noise_floor = -70)) + nodeRepository.updateLocalStats(LocalStats.Builder().also { wb -> wb.noise_floor = -70 }.build()) radioConnectionState.value = ConnectionState.Disconnected advanceUntilIdle() - assertEquals(LocalStats(), nodeRepository.localStats.value, "Disconnect should reset to \"no reading yet\"") + assertEquals( + LocalStats.Builder().build(), + nodeRepository.localStats.value, + "Disconnect should reset to \"no reading yet\"", + ) } @Test fun `DeviceSleep behavior when power saving is off maps to Disconnected`() = runTest(testDispatcher) { // Power saving disabled + Role CLIENT val config = - LocalConfig( - power = Config.PowerConfig(is_power_saving = false), - device = Config.DeviceConfig(role = Config.DeviceConfig.Role.CLIENT), - ) + LocalConfig.Builder() + .also { wb -> + wb.power = Config.PowerConfig.Builder().also { wb -> wb.is_power_saving = false }.build() + wb.device = + Config.DeviceConfig.Builder() + .also { wb -> wb.role = Config.DeviceConfig.Role.CLIENT } + .build() + } + .build() every { radioConfigRepository.localConfigFlow } returns flowOf(config) every { nodeManager.nodeDBbyNodeNum } returns emptyMap() @@ -325,7 +334,12 @@ class MeshConnectionManagerImplTest { @Test fun `DeviceSleep behavior when power saving is on stays in DeviceSleep`() = runTest(testDispatcher) { // Power saving enabled - val config = LocalConfig(power = Config.PowerConfig(is_power_saving = true)) + val config = + LocalConfig.Builder() + .also { wb -> + wb.power = Config.PowerConfig.Builder().also { wb -> wb.is_power_saving = true }.build() + } + .build() every { radioConfigRepository.localConfigFlow } returns flowOf(config) manager = createManager(backgroundScope) @@ -357,10 +371,19 @@ class MeshConnectionManagerImplTest { @Test fun `onNodeDbReady starts MQTT and requests history`() = runTest(testDispatcher) { val moduleConfig = - LocalModuleConfig( - mqtt = ModuleConfig.MQTTConfig(enabled = true, proxy_to_client_enabled = true), - store_forward = ModuleConfig.StoreForwardConfig(enabled = true), - ) + LocalModuleConfig.Builder() + .also { wb -> + wb.mqtt = + ModuleConfig.MQTTConfig.Builder() + .also { wb -> + wb.enabled = true + wb.proxy_to_client_enabled = true + } + .build() + wb.store_forward = + ModuleConfig.StoreForwardConfig.Builder().also { wb -> wb.enabled = true }.build() + } + .build() moduleConfigFlow.value = moduleConfig everySuspend { commandSender.requestTelemetry(any(), any(), any()) } returns Unit every { nodeManager.myNodeNum } returns MutableStateFlow(123) @@ -394,7 +417,14 @@ class MeshConnectionManagerImplTest { "MQTT and history collectors must be active; additional collectors are allowed", ) serviceRepository.setConnectionState(ConnectionState.Disconnected) - delayedModuleConfig.emit(LocalModuleConfig(store_forward = ModuleConfig.StoreForwardConfig(enabled = true))) + delayedModuleConfig.emit( + LocalModuleConfig.Builder() + .also { wb -> + wb.store_forward = + ModuleConfig.StoreForwardConfig.Builder().also { wb -> wb.enabled = true }.build() + } + .build(), + ) runCurrent() verifySuspend(exactly(0)) { historyManager.requestHistoryReplay(any(), any(), any(), any(), any()) } @@ -406,7 +436,13 @@ class MeshConnectionManagerImplTest { var historyAttempts = 0 val telemetryAttempts = mutableMapOf() val admissionVersions = mutableListOf() - moduleConfigFlow.value = LocalModuleConfig(store_forward = ModuleConfig.StoreForwardConfig(enabled = true)) + moduleConfigFlow.value = + LocalModuleConfig.Builder() + .also { wb -> + wb.store_forward = + ModuleConfig.StoreForwardConfig.Builder().also { wb -> wb.enabled = true }.build() + } + .build() everySuspend { commandSender.sendAdminForConnection(any(), any(), any(), any(), any()) } calls { call -> admissionVersions += call.arg(1) @@ -515,10 +551,21 @@ class MeshConnectionManagerImplTest { // Router with ls_secs=3600 — previously this created a 3630s timeout. // With the cap, it should be clamped to 300s. val config = - LocalConfig( - power = Config.PowerConfig(is_power_saving = true, ls_secs = 3600), - device = Config.DeviceConfig(role = Config.DeviceConfig.Role.ROUTER), - ) + LocalConfig.Builder() + .also { wb -> + wb.power = + Config.PowerConfig.Builder() + .also { wb -> + wb.is_power_saving = true + wb.ls_secs = 3600 + } + .build() + wb.device = + Config.DeviceConfig.Builder() + .also { wb -> wb.role = Config.DeviceConfig.Role.ROUTER } + .build() + } + .build() every { radioConfigRepository.localConfigFlow } returns flowOf(config) every { nodeManager.nodeDBbyNodeNum } returns emptyMap() @@ -550,7 +597,12 @@ class MeshConnectionManagerImplTest { @Test fun `rapid state transitions are serialized by connectionMutex`() = runTest(testDispatcher) { // Power saving enabled so DeviceSleep is preserved (not mapped to Disconnected) - val config = LocalConfig(power = Config.PowerConfig(is_power_saving = true)) + val config = + LocalConfig.Builder() + .also { wb -> + wb.power = Config.PowerConfig.Builder().also { wb -> wb.is_power_saving = true }.build() + } + .build() every { radioConfigRepository.localConfigFlow } returns flowOf(config) every { nodeManager.nodeDBbyNodeNum } returns emptyMap() @@ -596,7 +648,18 @@ class MeshConnectionManagerImplTest { runTest(standardDispatcher) { // Power saving enabled with ls_secs=0 so the sleep timeout boundary is just before the // Stage 1 handshake watchdog. That keeps this test isolated to sleep-timeout behavior. - val config = LocalConfig(power = Config.PowerConfig(is_power_saving = true, ls_secs = 0)) + val config = + LocalConfig.Builder() + .also { wb -> + wb.power = + Config.PowerConfig.Builder() + .also { wb -> + wb.is_power_saving = true + wb.ls_secs = 0 + } + .build() + } + .build() every { radioConfigRepository.localConfigFlow } returns flowOf(config) every { nodeManager.nodeDBbyNodeNum } returns emptyMap() diff --git a/core/data/src/commonTest/kotlin/org/meshtastic/core/data/manager/MeshDataHandlerTest.kt b/core/data/src/commonTest/kotlin/org/meshtastic/core/data/manager/MeshDataHandlerTest.kt index e9297f5af5..cab3d82810 100644 --- a/core/data/src/commonTest/kotlin/org/meshtastic/core/data/manager/MeshDataHandlerTest.kt +++ b/core/data/src/commonTest/kotlin/org/meshtastic/core/data/manager/MeshDataHandlerTest.kt @@ -199,7 +199,7 @@ class MeshDataHandlerTest { // Stub commonly accessed properties to avoid NPE from autofill every { nodeManager.getNodeById(any()) } returns null every { nodeManager.nodeDBbyNodeNum } returns emptyMap() - every { radioConfigRepository.channelSetFlow } returns MutableStateFlow(ChannelSet()) + every { radioConfigRepository.channelSetFlow } returns MutableStateFlow(ChannelSet.Builder().build()) // GeofenceMonitor collects this on init; stub it so the launched collector doesn't NPE on the test scope. every { packetRepository.getWaypoints() } returns emptyFlow() everySuspend { packetRepository.findPacketsWithId(any()) } returns emptyList() @@ -213,7 +213,7 @@ class MeshDataHandlerTest { @Test fun `handleReceivedData returns early when dataMapper returns null`() { - val packet = MeshPacket() + val packet = MeshPacket.Builder().build() every { dataMapper.toDataPacket(packet) } returns null handler.handleReceivedData(packet, 123) @@ -224,12 +224,26 @@ class MeshDataHandlerTest { @Test fun `handleReceivedData does not broadcast for position from local node`() { val myNodeNum = 123 - val position = Position(latitude_i = 450000000, longitude_i = 900000000) + val position = + Position.Builder() + .also { wb -> + wb.latitude_i = 450000000 + wb.longitude_i = 900000000 + } + .build() val packet = - MeshPacket( - from = myNodeNum, - decoded = Data(portnum = PortNum.POSITION_APP, payload = position.encode().toByteString()), - ) + MeshPacket.Builder() + .also { wb -> + wb.from = myNodeNum + wb.decoded = + Data.Builder() + .also { wb -> + wb.portnum = PortNum.POSITION_APP + wb.payload = position.encode().toByteString() + } + .build() + } + .build() val dataPacket = DataPacket( from = NodeAddress.numToDefaultId(myNodeNum), @@ -249,7 +263,13 @@ class MeshDataHandlerTest { fun `handleReceivedData broadcasts for remote packets`() { val myNodeNum = 123 val remoteNum = 456 - val packet = MeshPacket(from = remoteNum, decoded = Data(portnum = PortNum.PRIVATE_APP)) + val packet = + MeshPacket.Builder() + .also { wb -> + wb.from = remoteNum + wb.decoded = Data.Builder().also { wb -> wb.portnum = PortNum.PRIVATE_APP }.build() + } + .build() val dataPacket = DataPacket( from = NodeAddress.numToDefaultId(remoteNum), @@ -264,7 +284,13 @@ class MeshDataHandlerTest { @Test fun `handleReceivedData tracks analytics`() { - val packet = MeshPacket(from = 456, decoded = Data(portnum = PortNum.PRIVATE_APP)) + val packet = + MeshPacket.Builder() + .also { wb -> + wb.from = 456 + wb.decoded = Data.Builder().also { wb -> wb.portnum = PortNum.PRIVATE_APP }.build() + } + .build() val dataPacket = DataPacket( from = "!other", @@ -285,12 +311,26 @@ class MeshDataHandlerTest { fun `position packet delegates to nodeManager`() { val myNodeNum = 123 val remoteNum = 456 - val position = Position(latitude_i = 450000000, longitude_i = 900000000) + val position = + Position.Builder() + .also { wb -> + wb.latitude_i = 450000000 + wb.longitude_i = 900000000 + } + .build() val packet = - MeshPacket( - from = remoteNum, - decoded = Data(portnum = PortNum.POSITION_APP, payload = position.encode().toByteString()), - ) + MeshPacket.Builder() + .also { wb -> + wb.from = remoteNum + wb.decoded = + Data.Builder() + .also { wb -> + wb.portnum = PortNum.POSITION_APP + wb.payload = position.encode().toByteString() + } + .build() + } + .build() val dataPacket = DataPacket( from = "!remote", @@ -312,12 +352,27 @@ class MeshDataHandlerTest { fun `nodeinfo packet from remote delegates to handleReceivedUser`() { val myNodeNum = 123 val remoteNum = 456 - val user = User(id = "!remote", long_name = "Remote", short_name = "R") + val user = + User.Builder() + .also { wb -> + wb.id = "!remote" + wb.long_name = "Remote" + wb.short_name = "R" + } + .build() val packet = - MeshPacket( - from = remoteNum, - decoded = Data(portnum = PortNum.NODEINFO_APP, payload = user.encode().toByteString()), - ) + MeshPacket.Builder() + .also { wb -> + wb.from = remoteNum + wb.decoded = + Data.Builder() + .also { wb -> + wb.portnum = PortNum.NODEINFO_APP + wb.payload = user.encode().toByteString() + } + .build() + } + .build() val dataPacket = DataPacket( from = "!remote", @@ -335,12 +390,27 @@ class MeshDataHandlerTest { @Test fun `nodeinfo packet from local node is ignored`() { val myNodeNum = 123 - val user = User(id = "!local", long_name = "Local", short_name = "L") + val user = + User.Builder() + .also { wb -> + wb.id = "!local" + wb.long_name = "Local" + wb.short_name = "L" + } + .build() val packet = - MeshPacket( - from = myNodeNum, - decoded = Data(portnum = PortNum.NODEINFO_APP, payload = user.encode().toByteString()), - ) + MeshPacket.Builder() + .also { wb -> + wb.from = myNodeNum + wb.decoded = + Data.Builder() + .also { wb -> + wb.portnum = PortNum.NODEINFO_APP + wb.payload = user.encode().toByteString() + } + .build() + } + .build() val dataPacket = DataPacket( from = "!local", @@ -362,12 +432,27 @@ class MeshDataHandlerTest { @Test fun `paxcounter packet delegates to nodeManager`() { val remoteNum = 456 - val pax = Paxcount(wifi = 10, ble = 5, uptime = 1000) + val pax = + Paxcount.Builder() + .also { wb -> + wb.wifi = 10 + wb.ble = 5 + wb.uptime = 1000 + } + .build() val packet = - MeshPacket( - from = remoteNum, - decoded = Data(portnum = PortNum.PAXCOUNTER_APP, payload = pax.encode().toByteString()), - ) + MeshPacket.Builder() + .also { wb -> + wb.from = remoteNum + wb.decoded = + Data.Builder() + .also { wb -> + wb.portnum = PortNum.PAXCOUNTER_APP + wb.payload = pax.encode().toByteString() + } + .build() + } + .build() val dataPacket = DataPacket( from = "!remote", @@ -387,10 +472,18 @@ class MeshDataHandlerTest { @Test fun `traceroute packet delegates to tracerouteHandler and suppresses broadcast`() { val packet = - MeshPacket( - from = 456, - decoded = Data(portnum = PortNum.TRACEROUTE_APP, payload = byteArrayOf().toByteString()), - ) + MeshPacket.Builder() + .also { wb -> + wb.from = 456 + wb.decoded = + Data.Builder() + .also { wb -> + wb.portnum = PortNum.TRACEROUTE_APP + wb.payload = byteArrayOf().toByteString() + } + .build() + } + .build() val dataPacket = DataPacket( from = "!remote", @@ -409,12 +502,20 @@ class MeshDataHandlerTest { @Test fun `neighborinfo packet delegates to neighborInfoHandler and broadcasts`() { - val ni = NeighborInfo(node_id = 456) + val ni = NeighborInfo.Builder().also { wb -> wb.node_id = 456 }.build() val packet = - MeshPacket( - from = 456, - decoded = Data(portnum = PortNum.NEIGHBORINFO_APP, payload = ni.encode().toByteString()), - ) + MeshPacket.Builder() + .also { wb -> + wb.from = 456 + wb.decoded = + Data.Builder() + .also { wb -> + wb.portnum = PortNum.NEIGHBORINFO_APP + wb.payload = ni.encode().toByteString() + } + .build() + } + .build() val dataPacket = DataPacket( from = "!remote", @@ -433,12 +534,26 @@ class MeshDataHandlerTest { @Test fun `mesh beacon with a join offer is recorded as an invitation`() { - val beacon = MeshBeacon(message = "Join us", offer_channel = ChannelSettings(name = "PartyNet")) + val beacon = + MeshBeacon.Builder() + .also { wb -> + wb.message = "Join us" + wb.offer_channel = ChannelSettings.Builder().also { wb -> wb.name = "PartyNet" }.build() + } + .build() val packet = - MeshPacket( - from = 456, - decoded = Data(portnum = PortNum.MESH_BEACON_APP, payload = beacon.encode().toByteString()), - ) + MeshPacket.Builder() + .also { wb -> + wb.from = 456 + wb.decoded = + Data.Builder() + .also { wb -> + wb.portnum = PortNum.MESH_BEACON_APP + wb.payload = beacon.encode().toByteString() + } + .build() + } + .build() every { dataMapper.toDataPacket(packet) } returns DataPacket( from = "!remote", @@ -456,12 +571,23 @@ class MeshDataHandlerTest { @Test fun `mesh beacon without a join offer is ignored`() { - val beacon = MeshBeacon(message = "Just saying hi") // no offer_channel → not actionable + val beacon = + MeshBeacon.Builder() + .also { wb -> wb.message = "Just saying hi" } + .build() // no offer_channel → not actionable val packet = - MeshPacket( - from = 456, - decoded = Data(portnum = PortNum.MESH_BEACON_APP, payload = beacon.encode().toByteString()), - ) + MeshPacket.Builder() + .also { wb -> + wb.from = 456 + wb.decoded = + Data.Builder() + .also { wb -> + wb.portnum = PortNum.MESH_BEACON_APP + wb.payload = beacon.encode().toByteString() + } + .build() + } + .build() every { dataMapper.toDataPacket(packet) } returns DataPacket( from = "!remote", @@ -477,12 +603,27 @@ class MeshDataHandlerTest { @Test fun `our own mesh beacon is ignored`() { // Spec FR-001: ignore beacons from the scanning node itself (else a listen+broadcast node self-notifies). - val beacon = MeshBeacon(message = "Join us", offer_channel = ChannelSettings(name = "PartyNet")) + val beacon = + MeshBeacon.Builder() + .also { wb -> + wb.message = "Join us" + wb.offer_channel = ChannelSettings.Builder().also { wb -> wb.name = "PartyNet" }.build() + } + .build() val packet = - MeshPacket( - from = 123, // == myNodeNum below - decoded = Data(portnum = PortNum.MESH_BEACON_APP, payload = beacon.encode().toByteString()), - ) + MeshPacket.Builder() + .also { wb -> + wb.from = 123 + // == myNodeNum below + wb.decoded = + Data.Builder() + .also { wb -> + wb.portnum = PortNum.MESH_BEACON_APP + wb.payload = beacon.encode().toByteString() + } + .build() + } + .build() every { dataMapper.toDataPacket(packet) } returns DataPacket(from = "!self", bytes = beacon.encode().toByteString(), dataType = PortNum.MESH_BEACON_APP.value) @@ -496,22 +637,40 @@ class MeshDataHandlerTest { // design#140 behavior 10: the radio already has "PartyNet" configured, so this offer is redundant. every { radioConfigRepository.channelSetFlow } returns MutableStateFlow( - ChannelSet( - settings = listOf(ChannelSettings(name = "PartyNet")), - lora_config = Config.LoRaConfig(use_preset = true, modem_preset = ModemPreset.LONG_FAST), - ), + ChannelSet.Builder() + .also { wb -> + wb.settings = listOf(ChannelSettings.Builder().also { wb -> wb.name = "PartyNet" }.build()) + wb.lora_config = + Config.LoRaConfig.Builder() + .also { wb -> + wb.use_preset = true + wb.modem_preset = ModemPreset.LONG_FAST + } + .build() + } + .build(), ) val beacon = - MeshBeacon( - message = "Join us", - offer_channel = ChannelSettings(name = "PartyNet"), - offer_preset = ModemPreset.LONG_FAST, - ) + MeshBeacon.Builder() + .also { wb -> + wb.message = "Join us" + wb.offer_channel = ChannelSettings.Builder().also { wb -> wb.name = "PartyNet" }.build() + wb.offer_preset = ModemPreset.LONG_FAST + } + .build() val packet = - MeshPacket( - from = 456, - decoded = Data(portnum = PortNum.MESH_BEACON_APP, payload = beacon.encode().toByteString()), - ) + MeshPacket.Builder() + .also { wb -> + wb.from = 456 + wb.decoded = + Data.Builder() + .also { wb -> + wb.portnum = PortNum.MESH_BEACON_APP + wb.payload = beacon.encode().toByteString() + } + .build() + } + .build() every { dataMapper.toDataPacket(packet) } returns DataPacket( from = "!remote", @@ -532,10 +691,18 @@ class MeshDataHandlerTest { @Test fun `store forward packet delegates to storeForwardHandler`() { val packet = - MeshPacket( - from = 456, - decoded = Data(portnum = PortNum.STORE_FORWARD_APP, payload = byteArrayOf().toByteString()), - ) + MeshPacket.Builder() + .also { wb -> + wb.from = 456 + wb.decoded = + Data.Builder() + .also { wb -> + wb.portnum = PortNum.STORE_FORWARD_APP + wb.payload = byteArrayOf().toByteString() + } + .build() + } + .build() val dataPacket = DataPacket( from = "!remote", @@ -553,13 +720,21 @@ class MeshDataHandlerTest { // --- Routing/ACK-NAK handling --- private fun routingPacket(error: Routing.Error): MeshPacket { - val routing = Routing(error_reason = error) + val routing = Routing.Builder().also { wb -> wb.error_reason = error }.build() val packet = - MeshPacket( - from = 456, - decoded = - Data(portnum = PortNum.ROUTING_APP, payload = routing.encode().toByteString(), request_id = 99), - ) + MeshPacket.Builder() + .also { wb -> + wb.from = 456 + wb.decoded = + Data.Builder() + .also { wb -> + wb.portnum = PortNum.ROUTING_APP + wb.payload = routing.encode().toByteString() + wb.request_id = 99 + } + .build() + } + .build() every { dataMapper.toDataPacket(packet) } returns DataPacket( from = "!remote", @@ -590,13 +765,21 @@ class MeshDataHandlerTest { @Test fun `routing ack from a retired generation cannot update the replacement database`() = testScope.runTest { - val routing = Routing(error_reason = Routing.Error.NONE) + val routing = Routing.Builder().also { wb -> wb.error_reason = Routing.Error.NONE }.build() val packet = - MeshPacket( - from = 456, - decoded = - Data(portnum = PortNum.ROUTING_APP, payload = routing.encode().toByteString(), request_id = 99), - ) + MeshPacket.Builder() + .also { wb -> + wb.from = 456 + wb.decoded = + Data.Builder() + .also { wb -> + wb.portnum = PortNum.ROUTING_APP + wb.payload = routing.encode().toByteString() + wb.request_id = 99 + } + .build() + } + .build() val dataPacket = DataPacket( from = "!remote", @@ -619,13 +802,21 @@ class MeshDataHandlerTest { @Test fun `routing packet always broadcasts`() { - val routing = Routing(error_reason = Routing.Error.NONE) + val routing = Routing.Builder().also { wb -> wb.error_reason = Routing.Error.NONE }.build() val packet = - MeshPacket( - from = 456, - decoded = - Data(portnum = PortNum.ROUTING_APP, payload = routing.encode().toByteString(), request_id = 99), - ) + MeshPacket.Builder() + .also { wb -> + wb.from = 456 + wb.decoded = + Data.Builder() + .also { wb -> + wb.portnum = PortNum.ROUTING_APP + wb.payload = routing.encode().toByteString() + wb.request_id = 99 + } + .build() + } + .build() val dataPacket = DataPacket( from = "!remote", @@ -644,15 +835,31 @@ class MeshDataHandlerTest { @Test fun `telemetry packet delegates to telemetryHandler`() { val telemetry = - Telemetry( - time = 2000, - device_metrics = org.meshtastic.proto.DeviceMetrics(battery_level = 80, voltage = 4.0f), - ) + Telemetry.Builder() + .also { wb -> + wb.time = 2000 + wb.device_metrics = + org.meshtastic.proto.DeviceMetrics.Builder() + .also { wb -> + wb.battery_level = 80 + wb.voltage = 4.0f + } + .build() + } + .build() val packet = - MeshPacket( - from = 456, - decoded = Data(portnum = PortNum.TELEMETRY_APP, payload = telemetry.encode().toByteString()), - ) + MeshPacket.Builder() + .also { wb -> + wb.from = 456 + wb.decoded = + Data.Builder() + .also { wb -> + wb.portnum = PortNum.TELEMETRY_APP + wb.payload = telemetry.encode().toByteString() + } + .build() + } + .build() val dataPacket = DataPacket( from = "!remote", @@ -672,15 +879,31 @@ class MeshDataHandlerTest { fun `telemetry from local node delegates to telemetryHandler`() { val myNodeNum = 123 val telemetry = - Telemetry( - time = 2000, - device_metrics = org.meshtastic.proto.DeviceMetrics(battery_level = 80, voltage = 4.0f), - ) + Telemetry.Builder() + .also { wb -> + wb.time = 2000 + wb.device_metrics = + org.meshtastic.proto.DeviceMetrics.Builder() + .also { wb -> + wb.battery_level = 80 + wb.voltage = 4.0f + } + .build() + } + .build() val packet = - MeshPacket( - from = myNodeNum, - decoded = Data(portnum = PortNum.TELEMETRY_APP, payload = telemetry.encode().toByteString()), - ) + MeshPacket.Builder() + .also { wb -> + wb.from = myNodeNum + wb.decoded = + Data.Builder() + .also { wb -> + wb.portnum = PortNum.TELEMETRY_APP + wb.payload = telemetry.encode().toByteString() + } + .build() + } + .build() val dataPacket = DataPacket( from = "!local", @@ -701,12 +924,19 @@ class MeshDataHandlerTest { @Test fun `text message is persisted via rememberDataPacket`() = testScope.runTest { val packet = - MeshPacket( - id = 42, - from = 456, - decoded = - Data(portnum = PortNum.TEXT_MESSAGE_APP, payload = "hello".encodeToByteArray().toByteString()), - ) + MeshPacket.Builder() + .also { wb -> + wb.id = 42 + wb.from = 456 + wb.decoded = + Data.Builder() + .also { wb -> + wb.portnum = PortNum.TEXT_MESSAGE_APP + wb.payload = "hello".encodeToByteArray().toByteString() + } + .build() + } + .build() val dataPacket = DataPacket( id = 42, @@ -721,7 +951,17 @@ class MeshDataHandlerTest { every { messageFilter.shouldFilter(any(), any()) } returns false // Provide sender node so getSenderName() doesn't fall back to getString (requires Skiko) every { nodeManager.getNodeById("!remote") } returns - Node(num = 456, user = User(id = "!remote", long_name = "Remote User", short_name = "RU")) + Node( + num = 456, + user = + User.Builder() + .also { wb -> + wb.id = "!remote" + wb.long_name = "Remote User" + wb.short_name = "RU" + } + .build(), + ) handler.handleReceivedData(packet, 123) advanceUntilIdle() @@ -732,12 +972,19 @@ class MeshDataHandlerTest { @Test fun `text persistence from a retired session is rejected before database access`() = testScope.runTest { val packet = - MeshPacket( - id = 45, - from = 456, - decoded = - Data(portnum = PortNum.TEXT_MESSAGE_APP, payload = "late".encodeToByteArray().toByteString()), - ) + MeshPacket.Builder() + .also { wb -> + wb.id = 45 + wb.from = 456 + wb.decoded = + Data.Builder() + .also { wb -> + wb.portnum = PortNum.TEXT_MESSAGE_APP + wb.payload = "late".encodeToByteArray().toByteString() + } + .build() + } + .build() val dataPacket = DataPacket( id = 45, @@ -759,12 +1006,19 @@ class MeshDataHandlerTest { @Test fun `duplicate text message is not inserted again`() = testScope.runTest { val packet = - MeshPacket( - id = 42, - from = 456, - decoded = - Data(portnum = PortNum.TEXT_MESSAGE_APP, payload = "hello".encodeToByteArray().toByteString()), - ) + MeshPacket.Builder() + .also { wb -> + wb.id = 42 + wb.from = 456 + wb.decoded = + Data.Builder() + .also { wb -> + wb.portnum = PortNum.TEXT_MESSAGE_APP + wb.payload = "hello".encodeToByteArray().toByteString() + } + .build() + } + .build() val dataPacket = DataPacket( id = 42, @@ -788,12 +1042,19 @@ class MeshDataHandlerTest { @Test fun `same packet id from a different sender is persisted`() = testScope.runTest { val packet = - MeshPacket( - id = 42, - from = 789, - decoded = - Data(portnum = PortNum.TEXT_MESSAGE_APP, payload = "second".encodeToByteArray().toByteString()), - ) + MeshPacket.Builder() + .also { wb -> + wb.id = 42 + wb.from = 789 + wb.decoded = + Data.Builder() + .also { wb -> + wb.portnum = PortNum.TEXT_MESSAGE_APP + wb.payload = "second".encodeToByteArray().toByteString() + } + .build() + } + .build() val existing = DataPacket( id = 42, @@ -830,18 +1091,22 @@ class MeshDataHandlerTest { fun `text with reply_id and emoji is treated as reaction`() = testScope.runTest { val emojiBytes = "👍".encodeToByteArray() val packet = - MeshPacket( - id = 99, - from = 456, - to = 123, - decoded = - Data( - portnum = PortNum.TEXT_MESSAGE_APP, - payload = emojiBytes.toByteString(), - reply_id = 42, - emoji = 1, - ), - ) + MeshPacket.Builder() + .also { wb -> + wb.id = 99 + wb.from = 456 + wb.to = 123 + wb.decoded = + Data.Builder() + .also { wb -> + wb.portnum = PortNum.TEXT_MESSAGE_APP + wb.payload = emojiBytes.toByteString() + wb.reply_id = 42 + wb.emoji = 1 + } + .build() + } + .build() val dataPacket = DataPacket( id = 99, @@ -853,8 +1118,8 @@ class MeshDataHandlerTest { every { dataMapper.toDataPacket(packet) } returns dataPacket every { nodeManager.nodeDBbyNodeNum } returns mapOf( - 456 to Node(num = 456, user = User(id = "!remote")), - 123 to Node(num = 123, user = User(id = "!local")), + 456 to Node(num = 456, user = User.Builder().also { wb -> wb.id = "!remote" }.build()), + 123 to Node(num = 123, user = User.Builder().also { wb -> wb.id = "!local" }.build()), ) every { nodeManager.toNodeID(456) } returns "!remote" every { nodeManager.toNodeID(123) } returns "!local" @@ -872,20 +1137,24 @@ class MeshDataHandlerTest { fun `PKI reaction persists normalized channel and finds its notification parent`() = testScope.runTest { val emoji = "+1" val packet = - MeshPacket( - id = 99, - from = 456, - to = 123, - channel = 0, - pki_encrypted = true, - decoded = - Data( - portnum = PortNum.TEXT_MESSAGE_APP, - payload = emoji.encodeToByteArray().toByteString(), - reply_id = 42, - emoji = 1, - ), - ) + MeshPacket.Builder() + .also { wb -> + wb.id = 99 + wb.from = 456 + wb.to = 123 + wb.channel = 0 + wb.pki_encrypted = true + wb.decoded = + Data.Builder() + .also { wb -> + wb.portnum = PortNum.TEXT_MESSAGE_APP + wb.payload = emoji.encodeToByteArray().toByteString() + wb.reply_id = 42 + wb.emoji = 1 + } + .build() + } + .build() val dataPacket = DataPacket( id = 99, @@ -910,14 +1179,33 @@ class MeshDataHandlerTest { every { dataMapper.toDataPacket(packet) } returns dataPacket every { nodeManager.nodeDBbyNodeNum } returns mapOf( - 456 to Node(num = 456, user = User(id = "!remote", long_name = "Remote User")), - 123 to Node(num = 123, user = User(id = NodeAddress.ID_LOCAL)), + 456 to + Node( + num = 456, + user = + User.Builder() + .also { wb -> + wb.id = "!remote" + wb.long_name = "Remote User" + } + .build(), + ), + 123 to Node(num = 123, user = User.Builder().also { wb -> wb.id = NodeAddress.ID_LOCAL }.build()), ) every { nodeManager.toNodeID(456) } returns "!remote" every { nodeManager.toNodeID(123) } returns NodeAddress.ID_LOCAL every { nodeManager.myNodeNum } returns MutableStateFlow(123) every { nodeManager.getNodeById("!remote") } returns - Node(num = 456, user = User(id = "!remote", long_name = "Remote User")) + Node( + num = 456, + user = + User.Builder() + .also { wb -> + wb.id = "!remote" + wb.long_name = "Remote User" + } + .build(), + ) everySuspend { packetRepository.findReactionsWithId(99) } returns emptyList() everySuspend { packetRepository.findPacketsWithId(42) } returns listOf(otherConversationParent, parent) everySuspend { packetRepository.getContactSettings("8!remote") } returns @@ -947,18 +1235,22 @@ class MeshDataHandlerTest { fun `same reaction packet id from a different sender is persisted`() = testScope.runTest { val emojiBytes = "👍".encodeToByteArray() val packet = - MeshPacket( - id = 99, - from = 456, - to = 123, - decoded = - Data( - portnum = PortNum.TEXT_MESSAGE_APP, - payload = emojiBytes.toByteString(), - reply_id = 42, - emoji = 1, - ), - ) + MeshPacket.Builder() + .also { wb -> + wb.id = 99 + wb.from = 456 + wb.to = 123 + wb.decoded = + Data.Builder() + .also { wb -> + wb.portnum = PortNum.TEXT_MESSAGE_APP + wb.payload = emojiBytes.toByteString() + wb.reply_id = 42 + wb.emoji = 1 + } + .build() + } + .build() val dataPacket = DataPacket( id = 99, @@ -970,7 +1262,7 @@ class MeshDataHandlerTest { val existing = Reaction( replyId = 42, - user = User(id = "!other"), + user = User.Builder().also { wb -> wb.id = "!other" }.build(), emoji = "👍", timestamp = 1L, snr = null, @@ -981,8 +1273,8 @@ class MeshDataHandlerTest { every { dataMapper.toDataPacket(packet) } returns dataPacket every { nodeManager.nodeDBbyNodeNum } returns mapOf( - 456 to Node(num = 456, user = User(id = "!remote")), - 123 to Node(num = 123, user = User(id = "!local")), + 456 to Node(num = 456, user = User.Builder().also { wb -> wb.id = "!remote" }.build()), + 123 to Node(num = 123, user = User.Builder().also { wb -> wb.id = "!local" }.build()), ) every { nodeManager.toNodeID(456) } returns "!remote" every { nodeManager.toNodeID(123) } returns "!local" @@ -1002,12 +1294,19 @@ class MeshDataHandlerTest { @Test fun `range test packet is remembered as text message type`() = testScope.runTest { val packet = - MeshPacket( - id = 55, - from = 456, - decoded = - Data(portnum = PortNum.RANGE_TEST_APP, payload = "test".encodeToByteArray().toByteString()), - ) + MeshPacket.Builder() + .also { wb -> + wb.id = 55 + wb.from = 456 + wb.decoded = + Data.Builder() + .also { wb -> + wb.portnum = PortNum.RANGE_TEST_APP + wb.payload = "test".encodeToByteArray().toByteString() + } + .build() + } + .build() val dataPacket = DataPacket( id = 55, @@ -1021,7 +1320,17 @@ class MeshDataHandlerTest { everySuspend { packetRepository.getContactSettings(any()) } returns ContactSettings(contactKey = "test") every { messageFilter.shouldFilter(any(), any()) } returns false every { nodeManager.getNodeById("!remote") } returns - Node(num = 456, user = User(id = "!remote", long_name = "Remote User", short_name = "RU")) + Node( + num = 456, + user = + User.Builder() + .also { wb -> + wb.id = "!remote" + wb.long_name = "Remote User" + wb.short_name = "RU" + } + .build(), + ) handler.handleReceivedData(packet, 123) advanceUntilIdle() @@ -1034,9 +1343,23 @@ class MeshDataHandlerTest { @Test fun `admin message delegates to adminPacketHandler`() { - val admin = org.meshtastic.proto.AdminMessage(session_passkey = okio.ByteString.of(1, 2, 3)) + val admin = + org.meshtastic.proto.AdminMessage.Builder() + .also { wb -> wb.session_passkey = okio.ByteString.of(1, 2, 3) } + .build() val packet = - MeshPacket(from = 123, decoded = Data(portnum = PortNum.ADMIN_APP, payload = admin.encode().toByteString())) + MeshPacket.Builder() + .also { wb -> + wb.from = 123 + wb.decoded = + Data.Builder() + .also { wb -> + wb.portnum = PortNum.ADMIN_APP + wb.payload = admin.encode().toByteString() + } + .build() + } + .build() val dataPacket = DataPacket( from = "!local", @@ -1056,15 +1379,19 @@ class MeshDataHandlerTest { @Test fun `filtered message is inserted with filtered flag`() = testScope.runTest { val packet = - MeshPacket( - id = 77, - from = 456, - decoded = - Data( - portnum = PortNum.TEXT_MESSAGE_APP, - payload = "spam content".encodeToByteArray().toByteString(), - ), - ) + MeshPacket.Builder() + .also { wb -> + wb.id = 77 + wb.from = 456 + wb.decoded = + Data.Builder() + .also { wb -> + wb.portnum = PortNum.TEXT_MESSAGE_APP + wb.payload = "spam content".encodeToByteArray().toByteString() + } + .build() + } + .build() val dataPacket = DataPacket( id = 77, @@ -1089,12 +1416,19 @@ class MeshDataHandlerTest { @Test fun `message from ignored node is filtered`() = testScope.runTest { val packet = - MeshPacket( - id = 88, - from = 456, - decoded = - Data(portnum = PortNum.TEXT_MESSAGE_APP, payload = "hello".encodeToByteArray().toByteString()), - ) + MeshPacket.Builder() + .also { wb -> + wb.id = 88 + wb.from = 456 + wb.decoded = + Data.Builder() + .also { wb -> + wb.portnum = PortNum.TEXT_MESSAGE_APP + wb.payload = "hello".encodeToByteArray().toByteString() + } + .build() + } + .build() val dataPacket = DataPacket( id = 88, @@ -1106,7 +1440,7 @@ class MeshDataHandlerTest { every { dataMapper.toDataPacket(packet) } returns dataPacket everySuspend { packetRepository.findPacketsWithId(88) } returns emptyList() every { nodeManager.getNodeById("!remote") } returns - Node(num = 456, user = User(id = "!remote"), isIgnored = true) + Node(num = 456, user = User.Builder().also { wb -> wb.id = "!remote" }.build(), isIgnored = true) everySuspend { packetRepository.getContactSettings(any()) } returns ContactSettings(contactKey = "test") handler.handleReceivedData(packet, 123) @@ -1119,12 +1453,19 @@ class MeshDataHandlerTest { private val myId = "!abcd1234" - private fun mentionPacket() = MeshPacket( - id = 101, - from = 456, - decoded = - Data(portnum = PortNum.TEXT_MESSAGE_APP, payload = "hey @$myId".encodeToByteArray().toByteString()), - ) + private fun mentionPacket() = MeshPacket.Builder() + .also { wb -> + wb.id = 101 + wb.from = 456 + wb.decoded = + Data.Builder() + .also { wb -> + wb.portnum = PortNum.TEXT_MESSAGE_APP + wb.payload = "hey @$myId".encodeToByteArray().toByteString() + } + .build() + } + .build() private fun mentionDataPacket() = DataPacket( id = 101, @@ -1144,7 +1485,17 @@ class MeshDataHandlerTest { every { nodeManager.getMyId() } returns myId // Node mute is authoritative: a mention must NOT break through it. every { nodeManager.getNodeById("!remote") } returns - Node(num = 456, user = User(id = "!remote", long_name = "Remote User"), isMuted = true) + Node( + num = 456, + user = + User.Builder() + .also { wb -> + wb.id = "!remote" + wb.long_name = "Remote User" + } + .build(), + isMuted = true, + ) handler.handleReceivedData(packet, 123) advanceUntilIdle() @@ -1165,7 +1516,16 @@ class MeshDataHandlerTest { every { messageFilter.shouldFilter(any(), any()) } returns false every { nodeManager.getMyId() } returns myId every { nodeManager.getNodeById("!remote") } returns - Node(num = 456, user = User(id = "!remote", long_name = "Remote User")) + Node( + num = 456, + user = + User.Builder() + .also { wb -> + wb.id = "!remote" + wb.long_name = "Remote User" + } + .build(), + ) handler.handleReceivedData(packet, 123) advanceUntilIdle() @@ -1187,7 +1547,16 @@ class MeshDataHandlerTest { every { messageFilter.shouldFilter(any(), any()) } returns false every { nodeManager.getMyId() } returns myId every { nodeManager.getNodeById("!remote") } returns - Node(num = 456, user = User(id = "!remote", long_name = "Remote User")) + Node( + num = 456, + user = + User.Builder() + .also { wb -> + wb.id = "!remote" + wb.long_name = "Remote User" + } + .build(), + ) } @Test @@ -1243,7 +1612,19 @@ class MeshDataHandlerTest { private fun waypointPacket(txId: Int, from: Int, waypoint: Waypoint): MeshPacket { val payload = waypoint.encode().toByteString() val packet = - MeshPacket(id = txId, from = from, decoded = Data(portnum = PortNum.WAYPOINT_APP, payload = payload)) + MeshPacket.Builder() + .also { wb -> + wb.id = txId + wb.from = from + wb.decoded = + Data.Builder() + .also { wb -> + wb.portnum = PortNum.WAYPOINT_APP + wb.payload = payload + } + .build() + } + .build() val dataPacket = DataPacket( id = txId, @@ -1259,7 +1640,17 @@ class MeshDataHandlerTest { /** Persist a single stored waypoint (via the getWaypoints firehose) so handleWaypoint can read its owner. */ private fun storeWaypoint(id: Int, lockedTo: Int) { val stored = - DataPacket(to = NodeAddress.ID_BROADCAST, channel = 0, waypoint = Waypoint(id = id, locked_to = lockedTo)) + DataPacket( + to = NodeAddress.ID_BROADCAST, + channel = 0, + waypoint = + Waypoint.Builder() + .also { wb -> + wb.id = id + wb.locked_to = lockedTo + } + .build(), + ) every { packetRepository.getWaypoints() } returns flowOf(listOf(stored)) } @@ -1274,7 +1665,18 @@ class MeshDataHandlerTest { storeWaypoint(id = 42, lockedTo = 111) // Mallory (999) replays waypoint 42 with locked_to = 0 (unlock). The inbound check passes, so only the // stored-owner check can drop it. - val packet = waypointPacket(txId = 500, from = 999, waypoint = Waypoint(id = 42, locked_to = 0)) + val packet = + waypointPacket( + txId = 500, + from = 999, + waypoint = + Waypoint.Builder() + .also { wb -> + wb.id = 42 + wb.locked_to = 0 + } + .build(), + ) stubWaypointPersistDependencies(500) handler.handleReceivedData(packet, 123) @@ -1288,7 +1690,18 @@ class MeshDataHandlerTest { storeWaypoint(id = 42, lockedTo = 111) // Mallory (999) locks waypoint 42 to herself. The inbound check passes (locked_to == from), so only the // stored-owner check can catch this. - val packet = waypointPacket(txId = 501, from = 999, waypoint = Waypoint(id = 42, locked_to = 999)) + val packet = + waypointPacket( + txId = 501, + from = 999, + waypoint = + Waypoint.Builder() + .also { wb -> + wb.id = 42 + wb.locked_to = 999 + } + .build(), + ) stubWaypointPersistDependencies(501) handler.handleReceivedData(packet, 123) @@ -1300,7 +1713,18 @@ class MeshDataHandlerTest { @Test fun `owner unlock of their own locked waypoint is accepted`() = testScope.runTest { storeWaypoint(id = 42, lockedTo = 111) - val packet = waypointPacket(txId = 502, from = 111, waypoint = Waypoint(id = 42, locked_to = 0)) + val packet = + waypointPacket( + txId = 502, + from = 111, + waypoint = + Waypoint.Builder() + .also { wb -> + wb.id = 42 + wb.locked_to = 0 + } + .build(), + ) stubWaypointPersistDependencies(502) handler.handleReceivedData(packet, 123) @@ -1312,7 +1736,18 @@ class MeshDataHandlerTest { @Test fun `owner edit of their own locked waypoint is accepted`() = testScope.runTest { storeWaypoint(id = 42, lockedTo = 111) - val packet = waypointPacket(txId = 503, from = 111, waypoint = Waypoint(id = 42, locked_to = 111)) + val packet = + waypointPacket( + txId = 503, + from = 111, + waypoint = + Waypoint.Builder() + .also { wb -> + wb.id = 42 + wb.locked_to = 111 + } + .build(), + ) stubWaypointPersistDependencies(503) handler.handleReceivedData(packet, 123) @@ -1325,7 +1760,18 @@ class MeshDataHandlerTest { fun `new waypoint from a non-owner is accepted when none is stored`() = testScope.runTest { // Nothing persisted yet: getWaypoints() emits an empty list (as Room does). A creation, not a hijack. every { packetRepository.getWaypoints() } returns flowOf(emptyList()) - val packet = waypointPacket(txId = 504, from = 999, waypoint = Waypoint(id = 42, locked_to = 0)) + val packet = + waypointPacket( + txId = 504, + from = 999, + waypoint = + Waypoint.Builder() + .also { wb -> + wb.id = 42 + wb.locked_to = 0 + } + .build(), + ) stubWaypointPersistDependencies(504) handler.handleReceivedData(packet, 123) @@ -1337,7 +1783,18 @@ class MeshDataHandlerTest { @Test fun `non-owner update to an unlocked waypoint is accepted`() = testScope.runTest { storeWaypoint(id = 42, lockedTo = 0) - val packet = waypointPacket(txId = 505, from = 999, waypoint = Waypoint(id = 42, locked_to = 0)) + val packet = + waypointPacket( + txId = 505, + from = 999, + waypoint = + Waypoint.Builder() + .also { wb -> + wb.id = 42 + wb.locked_to = 0 + } + .build(), + ) stubWaypointPersistDependencies(505) handler.handleReceivedData(packet, 123) @@ -1350,7 +1807,18 @@ class MeshDataHandlerTest { fun `waypoint locked to someone other than the sender is dropped`() = testScope.runTest { // Pre-existing inbound-payload rule: a node can only lock a waypoint to itself. Nothing stored here — the // payload itself is invalid, so it is rejected before any repository read. - val packet = waypointPacket(txId = 506, from = 999, waypoint = Waypoint(id = 42, locked_to = 111)) + val packet = + waypointPacket( + txId = 506, + from = 999, + waypoint = + Waypoint.Builder() + .also { wb -> + wb.id = 42 + wb.locked_to = 111 + } + .build(), + ) stubWaypointPersistDependencies(506) handler.handleReceivedData(packet, 123) diff --git a/core/data/src/commonTest/kotlin/org/meshtastic/core/data/manager/MeshMessageProcessorImplTest.kt b/core/data/src/commonTest/kotlin/org/meshtastic/core/data/manager/MeshMessageProcessorImplTest.kt index 797e0d6f75..791f6c34a0 100644 --- a/core/data/src/commonTest/kotlin/org/meshtastic/core/data/manager/MeshMessageProcessorImplTest.kt +++ b/core/data/src/commonTest/kotlin/org/meshtastic/core/data/manager/MeshMessageProcessorImplTest.kt @@ -158,8 +158,8 @@ class MeshMessageProcessorImplTest { @Test fun `handleFromRadio dispatches non-packet variants to fromRadioDispatcher`() = runTest(testDispatcher) { processor = createProcessor(backgroundScope) - val logRecord = LogRecord(message = "test log") - val fromRadio = FromRadio(log_record = logRecord) + val logRecord = LogRecord.Builder().also { wb -> wb.message = "test log" }.build() + val fromRadio = FromRadio.Builder().also { wb -> wb.log_record = logRecord }.build() val bytes = fromRadio.encode() processor.handleFromRadio(frame(bytes), myNodeNum) @@ -173,7 +173,7 @@ class MeshMessageProcessorImplTest { processor = createProcessor(backgroundScope) // Encode a raw LogRecord (not wrapped in FromRadio) — first decode as FromRadio fails, // fallback decode as LogRecord succeeds - val logRecord = LogRecord(message = "fallback log") + val logRecord = LogRecord.Builder().also { wb -> wb.message = "fallback log" }.build() val bytes = logRecord.encode() processor.handleFromRadio(frame(bytes), myNodeNum) @@ -198,7 +198,10 @@ class MeshMessageProcessorImplTest { fun `stale session frame is dropped before dispatch or persistence`() = runTest(testDispatcher) { processor = createProcessor(backgroundScope) activeSession.value = RadioSessionContext(generation = 4L, address = session.address) - val fromRadio = FromRadio(log_record = LogRecord(message = "stale")) + val fromRadio = + FromRadio.Builder() + .also { wb -> wb.log_record = LogRecord.Builder().also { wb -> wb.message = "stale" }.build() } + .build() processor.handleFromRadio(frame(fromRadio.encode()), myNodeNum) advanceUntilIdle() @@ -211,7 +214,10 @@ class MeshMessageProcessorImplTest { @Test fun `session revoked after decode cannot reach packet handlers`() = runTest(testDispatcher) { processor = createProcessor(backgroundScope) - val fromRadio = FromRadio(log_record = LogRecord(message = "revoked")) + val fromRadio = + FromRadio.Builder() + .also { wb -> wb.log_record = LogRecord.Builder().also { wb -> wb.message = "revoked" }.build() } + .build() everySuspend { radioInterfaceService.runWhileSessionActive(session, any()) } returns false processor.handleFromRadio(frame(fromRadio.encode()), myNodeNum) @@ -249,14 +255,27 @@ class MeshMessageProcessorImplTest { releasePersistence.await() } val packet = - MeshPacket( - id = 9, - from = 999, - decoded = Data(portnum = PortNum.TEXT_MESSAGE_APP, payload = ByteString.EMPTY), - rx_time = 1000, - ) + MeshPacket.Builder() + .also { wb -> + wb.id = 9 + wb.from = 999 + wb.decoded = + Data.Builder() + .also { wb -> + wb.portnum = PortNum.TEXT_MESSAGE_APP + wb.payload = ByteString.EMPTY + } + .build() + wb.rx_time = 1000 + } + .build() - val processing = async { processor.handleFromRadio(frame(FromRadio(packet = packet).encode()), myNodeNum) } + val processing = async { + processor.handleFromRadio( + frame(FromRadio.Builder().also { wb -> wb.packet = packet }.build().encode()), + myNodeNum, + ) + } persistenceStarted.await() assertFalse(processing.isCompleted, "session authority must remain held until node persistence finishes") @@ -270,14 +289,25 @@ class MeshMessageProcessorImplTest { every { nodeManager.myNodeNum } returns MutableStateFlow(myNodeNum) processor = createProcessor(backgroundScope) val packet = - MeshPacket( - id = 10, - from = myNodeNum, - decoded = Data(portnum = PortNum.TEXT_MESSAGE_APP, payload = ByteString.EMPTY), - rx_time = 1, - ) + MeshPacket.Builder() + .also { wb -> + wb.id = 10 + wb.from = myNodeNum + wb.decoded = + Data.Builder() + .also { wb -> + wb.portnum = PortNum.TEXT_MESSAGE_APP + wb.payload = ByteString.EMPTY + } + .build() + wb.rx_time = 1 + } + .build() - processor.handleFromRadio(frame(FromRadio(packet = packet).encode()), myNodeNum) + processor.handleFromRadio( + frame(FromRadio.Builder().also { wb -> wb.packet = packet }.build().encode()), + myNodeNum, + ) advanceUntilIdle() verifySuspend(mode = VerifyMode.exactly(1)) { nodeManager.updateNodeAndPersist(myNodeNum, any(), any()) } @@ -287,7 +317,10 @@ class MeshMessageProcessorImplTest { fun `replacement generation refreshes the local node without waiting for the prior throttle`() = runTest(testDispatcher) { processor = createProcessor(backgroundScope) - val fromRadio = FromRadio(log_record = LogRecord(message = "alive")) + val fromRadio = + FromRadio.Builder() + .also { wb -> wb.log_record = LogRecord.Builder().also { wb -> wb.message = "alive" }.build() } + .build() processor.handleFromRadio(frame(fromRadio.encode()), myNodeNum) advanceUntilIdle() @@ -306,7 +339,10 @@ class MeshMessageProcessorImplTest { // receivedData collector, which would cancel the collection and silently deafen the radio (a remote DoS). // Regression guard for the safeCatching in processFromRadio: before that fix this call rethrew and failed. every { fromRadioDispatcher.handleFromRadio(any(), any()) } throws RuntimeException("hostile packet") - val fromRadio = FromRadio(log_record = LogRecord(message = "boom")) // routes to fromRadioDispatcher + val fromRadio = + FromRadio.Builder() + .also { wb -> wb.log_record = LogRecord.Builder().also { wb -> wb.message = "boom" }.build() } + .build() // routes to fromRadioDispatcher processor.handleFromRadio(frame(fromRadio.encode()), myNodeNum) // must return normally, not throw advanceUntilIdle() @@ -324,12 +360,20 @@ class MeshMessageProcessorImplTest { isNodeDbReady.value = false val packet = - MeshPacket( - id = 1, - from = 999, - decoded = Data(portnum = PortNum.TEXT_MESSAGE_APP, payload = ByteString.EMPTY), - rx_time = 1000, - ) + MeshPacket.Builder() + .also { wb -> + wb.id = 1 + wb.from = 999 + wb.decoded = + Data.Builder() + .also { wb -> + wb.portnum = PortNum.TEXT_MESSAGE_APP + wb.payload = ByteString.EMPTY + } + .build() + wb.rx_time = 1000 + } + .build() processor.handleReceivedMeshPacket(packet, myNodeNum) advanceUntilIdle() @@ -344,12 +388,20 @@ class MeshMessageProcessorImplTest { isNodeDbReady.value = false val packet = - MeshPacket( - id = 1, - from = 999, - decoded = Data(portnum = PortNum.TEXT_MESSAGE_APP, payload = ByteString.EMPTY), - rx_time = 1000, - ) + MeshPacket.Builder() + .also { wb -> + wb.id = 1 + wb.from = 999 + wb.decoded = + Data.Builder() + .also { wb -> + wb.portnum = PortNum.TEXT_MESSAGE_APP + wb.payload = ByteString.EMPTY + } + .build() + wb.rx_time = 1000 + } + .build() processor.handleReceivedMeshPacket(packet, myNodeNum) advanceUntilIdle() @@ -376,12 +428,20 @@ class MeshMessageProcessorImplTest { listOf(1, 2).forEach { id -> processor.handleReceivedMeshPacket( - MeshPacket( - id = id, - from = 999, - decoded = Data(portnum = PortNum.TEXT_MESSAGE_APP, payload = ByteString.EMPTY), - rx_time = 1000 + id, - ), + MeshPacket.Builder() + .also { wb -> + wb.id = id + wb.from = 999 + wb.decoded = + Data.Builder() + .also { wb -> + wb.portnum = PortNum.TEXT_MESSAGE_APP + wb.payload = ByteString.EMPTY + } + .build() + wb.rx_time = 1000 + id + } + .build(), myNodeNum, ) } @@ -403,14 +463,25 @@ class MeshMessageProcessorImplTest { isNodeDbReady.value = true everySuspend { radioInterfaceService.runWithSessionLease(session, any()) } returns false val packet = - MeshPacket( - id = 6, - from = 999, - decoded = Data(portnum = PortNum.TEXT_MESSAGE_APP, payload = ByteString.EMPTY), - rx_time = 1000, - ) + MeshPacket.Builder() + .also { wb -> + wb.id = 6 + wb.from = 999 + wb.decoded = + Data.Builder() + .also { wb -> + wb.portnum = PortNum.TEXT_MESSAGE_APP + wb.payload = ByteString.EMPTY + } + .build() + wb.rx_time = 1000 + } + .build() - processor.handleFromRadio(frame(FromRadio(packet = packet).encode()), myNodeNum) + processor.handleFromRadio( + frame(FromRadio.Builder().also { wb -> wb.packet = packet }.build().encode()), + myNodeNum, + ) advanceUntilIdle() verifySuspend(mode = VerifyMode.exactly(0)) { meshLogRepository.insert(any()) } @@ -427,21 +498,42 @@ class MeshMessageProcessorImplTest { processedIds += (it.args[0] as MeshPacket).id } val oldPacket = - MeshPacket( - id = 7, - from = 999, - decoded = Data(portnum = PortNum.TEXT_MESSAGE_APP, payload = ByteString.EMPTY), - rx_time = 1000, - ) + MeshPacket.Builder() + .also { wb -> + wb.id = 7 + wb.from = 999 + wb.decoded = + Data.Builder() + .also { wb -> + wb.portnum = PortNum.TEXT_MESSAGE_APP + wb.payload = ByteString.EMPTY + } + .build() + wb.rx_time = 1000 + } + .build() - processor.handleFromRadio(frame(FromRadio(packet = oldPacket).encode()), myNodeNum) + processor.handleFromRadio( + frame(FromRadio.Builder().also { wb -> wb.packet = oldPacket }.build().encode()), + myNodeNum, + ) advanceUntilIdle() val replacementSession = RadioSessionContext(generation = 4L, address = session.address) activeSession.value = replacementSession - val replacementPacket = oldPacket.copy(id = 8, rx_time = 1001) + val replacementPacket = + oldPacket + .newBuilder() + .also { wb -> + wb.id = 8 + wb.rx_time = 1001 + } + .build() processor.handleFromRadio( - frame(FromRadio(packet = replacementPacket).encode(), replacementSession), + frame( + FromRadio.Builder().also { wb -> wb.packet = replacementPacket }.build().encode(), + replacementSession, + ), myNodeNum, ) advanceUntilIdle() @@ -462,12 +554,20 @@ class MeshMessageProcessorImplTest { // packets are accumulated properly. repeat(5) { i -> val packet = - MeshPacket( - id = i, - from = 999, - decoded = Data(portnum = PortNum.TEXT_MESSAGE_APP, payload = ByteString.EMPTY), - rx_time = 1000 + i, - ) + MeshPacket.Builder() + .also { wb -> + wb.id = i + wb.from = 999 + wb.decoded = + Data.Builder() + .also { wb -> + wb.portnum = PortNum.TEXT_MESSAGE_APP + wb.payload = ByteString.EMPTY + } + .build() + wb.rx_time = 1000 + i + } + .build() processor.handleReceivedMeshPacket(packet, myNodeNum) } advanceUntilIdle() @@ -498,19 +598,35 @@ class MeshMessageProcessorImplTest { RuntimeException("corrupted NodeInfo") val poisoned = - MeshPacket( - id = 1, - from = 999, - decoded = Data(portnum = PortNum.NODEINFO_APP, payload = ByteString.EMPTY), - rx_time = 1000, - ) + MeshPacket.Builder() + .also { wb -> + wb.id = 1 + wb.from = 999 + wb.decoded = + Data.Builder() + .also { wb -> + wb.portnum = PortNum.NODEINFO_APP + wb.payload = ByteString.EMPTY + } + .build() + wb.rx_time = 1000 + } + .build() val healthy = - MeshPacket( - id = 2, - from = 998, - decoded = Data(portnum = PortNum.TEXT_MESSAGE_APP, payload = ByteString.EMPTY), - rx_time = 1001, - ) + MeshPacket.Builder() + .also { wb -> + wb.id = 2 + wb.from = 998 + wb.decoded = + Data.Builder() + .also { wb -> + wb.portnum = PortNum.TEXT_MESSAGE_APP + wb.payload = ByteString.EMPTY + } + .build() + wb.rx_time = 1001 + } + .build() processor.handleReceivedMeshPacket(poisoned, myNodeNum) processor.handleReceivedMeshPacket(healthy, myNodeNum) @@ -532,12 +648,20 @@ class MeshMessageProcessorImplTest { isNodeDbReady.value = true val packet = - MeshPacket( - id = 1, - from = myNodeNum, - decoded = Data(portnum = PortNum.TEXT_MESSAGE_APP, payload = ByteString.EMPTY), - rx_time = 0, // should be replaced with current time - ) + MeshPacket.Builder() + .also { wb -> + wb.id = 1 + wb.from = myNodeNum + wb.decoded = + Data.Builder() + .also { wb -> + wb.portnum = PortNum.TEXT_MESSAGE_APP + wb.payload = ByteString.EMPTY + } + .build() + wb.rx_time = 0 // should be replaced with current time + } + .build() processor.handleReceivedMeshPacket(packet, myNodeNum) advanceUntilIdle() @@ -551,12 +675,20 @@ class MeshMessageProcessorImplTest { isNodeDbReady.value = true val packet = - MeshPacket( - id = 2, - from = myNodeNum, - decoded = Data(portnum = PortNum.TEXT_MESSAGE_APP, payload = ByteString.EMPTY), - rx_time = 1700000000, - ) + MeshPacket.Builder() + .also { wb -> + wb.id = 2 + wb.from = myNodeNum + wb.decoded = + Data.Builder() + .also { wb -> + wb.portnum = PortNum.TEXT_MESSAGE_APP + wb.payload = ByteString.EMPTY + } + .build() + wb.rx_time = 1700000000 + } + .build() processor.handleReceivedMeshPacket(packet, myNodeNum) advanceUntilIdle() @@ -570,12 +702,20 @@ class MeshMessageProcessorImplTest { isNodeDbReady.value = true val packet = - MeshPacket( - id = 3, - from = myNodeNum, - decoded = Data(portnum = PortNum.TEXT_MESSAGE_APP, payload = ByteString.EMPTY), - rx_time = null, // radio had no clock at reception - ) + MeshPacket.Builder() + .also { wb -> + wb.id = 3 + wb.from = myNodeNum + wb.decoded = + Data.Builder() + .also { wb -> + wb.portnum = PortNum.TEXT_MESSAGE_APP + wb.payload = ByteString.EMPTY + } + .build() + wb.rx_time = null // radio had no clock at reception + } + .build() processor.handleReceivedMeshPacket(packet, myNodeNum) advanceUntilIdle() @@ -593,12 +733,20 @@ class MeshMessageProcessorImplTest { isNodeDbReady.value = true val packet = - MeshPacket( - id = 10, - from = 999, - decoded = Data(portnum = PortNum.TEXT_MESSAGE_APP, payload = ByteString.EMPTY), - rx_time = 1700000000, - ) + MeshPacket.Builder() + .also { wb -> + wb.id = 10 + wb.from = 999 + wb.decoded = + Data.Builder() + .also { wb -> + wb.portnum = PortNum.TEXT_MESSAGE_APP + wb.payload = ByteString.EMPTY + } + .build() + wb.rx_time = 1700000000 + } + .build() processor.handleReceivedMeshPacket(packet, myNodeNum) advanceUntilIdle() @@ -614,13 +762,21 @@ class MeshMessageProcessorImplTest { val senderNode = 999 val packet = - MeshPacket( - id = 10, - from = senderNode, - decoded = Data(portnum = PortNum.TEXT_MESSAGE_APP, payload = ByteString.EMPTY), - rx_time = 1700000000, - channel = 1, - ) + MeshPacket.Builder() + .also { wb -> + wb.id = 10 + wb.from = senderNode + wb.decoded = + Data.Builder() + .also { wb -> + wb.portnum = PortNum.TEXT_MESSAGE_APP + wb.payload = ByteString.EMPTY + } + .build() + wb.rx_time = 1700000000 + wb.channel = 1 + } + .build() processor.handleReceivedMeshPacket(packet, myNodeNum) advanceUntilIdle() @@ -636,7 +792,14 @@ class MeshMessageProcessorImplTest { processor = createProcessor(backgroundScope) isNodeDbReady.value = true - val packet = MeshPacket(id = 1, from = 999, decoded = null) + val packet = + MeshPacket.Builder() + .also { wb -> + wb.id = 1 + wb.from = 999 + wb.decoded = null + } + .build() processor.handleReceivedMeshPacket(packet, myNodeNum) advanceUntilIdle() @@ -654,12 +817,20 @@ class MeshMessageProcessorImplTest { isNodeDbReady.value = true val packet = - MeshPacket( - id = 10, - from = 999, - decoded = Data(portnum = PortNum.TEXT_MESSAGE_APP, payload = ByteString.EMPTY), - rx_time = 1700000000, - ) + MeshPacket.Builder() + .also { wb -> + wb.id = 10 + wb.from = 999 + wb.decoded = + Data.Builder() + .also { wb -> + wb.portnum = PortNum.TEXT_MESSAGE_APP + wb.payload = ByteString.EMPTY + } + .build() + wb.rx_time = 1700000000 + } + .build() processor.handleReceivedMeshPacket(packet, null) advanceUntilIdle() @@ -684,12 +855,20 @@ class MeshMessageProcessorImplTest { isNodeDbReady.value = true // DB ready, our node number still unknown val packet = - MeshPacket( - id = 11, - from = 999, - decoded = Data(portnum = PortNum.TEXT_MESSAGE_APP, payload = ByteString.EMPTY), - rx_time = 1700000000, - ) + MeshPacket.Builder() + .also { wb -> + wb.id = 11 + wb.from = 999 + wb.decoded = + Data.Builder() + .also { wb -> + wb.portnum = PortNum.TEXT_MESSAGE_APP + wb.payload = ByteString.EMPTY + } + .build() + wb.rx_time = 1700000000 + } + .build() processor.handleReceivedMeshPacket(packet, null) advanceUntilIdle() verifySuspend(mode = VerifyMode.not) { serviceRepository.emitMeshPacket(any()) } @@ -717,12 +896,20 @@ class MeshMessageProcessorImplTest { isNodeDbReady.value = false val packet = - MeshPacket( - id = 1, - from = 999, - decoded = Data(portnum = PortNum.TEXT_MESSAGE_APP, payload = ByteString.EMPTY), - rx_time = 1000, - ) + MeshPacket.Builder() + .also { wb -> + wb.id = 1 + wb.from = 999 + wb.decoded = + Data.Builder() + .also { wb -> + wb.portnum = PortNum.TEXT_MESSAGE_APP + wb.payload = ByteString.EMPTY + } + .build() + wb.rx_time = 1000 + } + .build() processor.handleReceivedMeshPacket(packet, myNodeNum) advanceUntilIdle() @@ -741,8 +928,8 @@ class MeshMessageProcessorImplTest { @Test fun `FromRadio log_record variant is logged as MeshLog`() = runTest(testDispatcher) { processor = createProcessor(backgroundScope) - val logRecord = LogRecord(message = "device log") - val fromRadio = FromRadio(log_record = logRecord) + val logRecord = LogRecord.Builder().also { wb -> wb.message = "device log" }.build() + val fromRadio = FromRadio.Builder().also { wb -> wb.log_record = logRecord }.build() val bytes = fromRadio.encode() processor.handleFromRadio(frame(bytes), myNodeNum) diff --git a/core/data/src/commonTest/kotlin/org/meshtastic/core/data/manager/NeighborInfoHandlerImplTest.kt b/core/data/src/commonTest/kotlin/org/meshtastic/core/data/manager/NeighborInfoHandlerImplTest.kt index 75981e190f..3b273c6cd7 100644 --- a/core/data/src/commonTest/kotlin/org/meshtastic/core/data/manager/NeighborInfoHandlerImplTest.kt +++ b/core/data/src/commonTest/kotlin/org/meshtastic/core/data/manager/NeighborInfoHandlerImplTest.kt @@ -55,11 +55,37 @@ class NeighborInfoHandlerImplTest { @Test fun `handleNeighborInfo stores lastNeighborInfo when from own node`() { - val ni = NeighborInfo(node_id = myNodeNum, neighbors = listOf(Neighbor(node_id = 100, snr = 5.0f))) + val ni = + NeighborInfo.Builder() + .also { wb -> + wb.node_id = myNodeNum + wb.neighbors = + listOf( + Neighbor.Builder() + .also { wb -> + wb.node_id = 100 + wb.snr = 5.0f + } + .build(), + ) + } + .build() val packet = createPacketWithNeighborInfo(from = myNodeNum, ni = ni) - every { nodeRepository.getUser(100) } returns User(long_name = "Alice", short_name = "AL") - every { nodeRepository.getUser(myNodeNum) } returns User(long_name = "Me", short_name = "ME") + every { nodeRepository.getUser(100) } returns + User.Builder() + .also { wb -> + wb.long_name = "Alice" + wb.short_name = "AL" + } + .build() + every { nodeRepository.getUser(myNodeNum) } returns + User.Builder() + .also { wb -> + wb.long_name = "Me" + wb.short_name = "ME" + } + .build() handler.handleNeighborInfo(packet) @@ -69,11 +95,37 @@ class NeighborInfoHandlerImplTest { @Test fun `handleNeighborInfo does not store lastNeighborInfo when from remote node`() { val remoteNode = 99999 - val ni = NeighborInfo(node_id = remoteNode, neighbors = listOf(Neighbor(node_id = 200, snr = 3.0f))) + val ni = + NeighborInfo.Builder() + .also { wb -> + wb.node_id = remoteNode + wb.neighbors = + listOf( + Neighbor.Builder() + .also { wb -> + wb.node_id = 200 + wb.snr = 3.0f + } + .build(), + ) + } + .build() val packet = createPacketWithNeighborInfo(from = remoteNode, ni = ni) - every { nodeRepository.getUser(200) } returns User(long_name = "Bob", short_name = "BO") - every { nodeRepository.getUser(remoteNode) } returns User(long_name = "Remote", short_name = "RM") + every { nodeRepository.getUser(200) } returns + User.Builder() + .also { wb -> + wb.long_name = "Bob" + wb.short_name = "BO" + } + .build() + every { nodeRepository.getUser(remoteNode) } returns + User.Builder() + .also { wb -> + wb.long_name = "Remote" + wb.short_name = "RM" + } + .build() handler.handleNeighborInfo(packet) @@ -83,15 +135,49 @@ class NeighborInfoHandlerImplTest { @Test fun `handleNeighborInfo sets response on serviceRepository`() { val ni = - NeighborInfo( - node_id = myNodeNum, - neighbors = listOf(Neighbor(node_id = 100, snr = 5.5f), Neighbor(node_id = 200, snr = -2.0f)), - ) + NeighborInfo.Builder() + .also { wb -> + wb.node_id = myNodeNum + wb.neighbors = + listOf( + Neighbor.Builder() + .also { wb -> + wb.node_id = 100 + wb.snr = 5.5f + } + .build(), + Neighbor.Builder() + .also { wb -> + wb.node_id = 200 + wb.snr = -2.0f + } + .build(), + ) + } + .build() val packet = createPacketWithNeighborInfo(from = myNodeNum, ni = ni) - every { nodeRepository.getUser(100) } returns User(long_name = "Alice", short_name = "AL") - every { nodeRepository.getUser(200) } returns User(long_name = "Bob", short_name = "BO") - every { nodeRepository.getUser(myNodeNum) } returns User(long_name = "Me", short_name = "ME") + every { nodeRepository.getUser(100) } returns + User.Builder() + .also { wb -> + wb.long_name = "Alice" + wb.short_name = "AL" + } + .build() + every { nodeRepository.getUser(200) } returns + User.Builder() + .also { wb -> + wb.long_name = "Bob" + wb.short_name = "BO" + } + .build() + every { nodeRepository.getUser(myNodeNum) } returns + User.Builder() + .also { wb -> + wb.long_name = "Me" + wb.short_name = "ME" + } + .build() handler.handleNeighborInfo(packet) @@ -100,7 +186,7 @@ class NeighborInfoHandlerImplTest { @Test fun `handleNeighborInfo ignores packet with null decoded`() { - val packet = MeshPacket(from = myNodeNum) + val packet = MeshPacket.Builder().also { wb -> wb.from = myNodeNum }.build() handler.handleNeighborInfo(packet) assertNull(handler.lastNeighborInfo) } @@ -108,11 +194,37 @@ class NeighborInfoHandlerImplTest { @Test fun `recordStartTime and handleNeighborInfo includes duration`() { val requestId = 42 - val ni = NeighborInfo(node_id = myNodeNum, neighbors = listOf(Neighbor(node_id = 100, snr = 1.0f))) + val ni = + NeighborInfo.Builder() + .also { wb -> + wb.node_id = myNodeNum + wb.neighbors = + listOf( + Neighbor.Builder() + .also { wb -> + wb.node_id = 100 + wb.snr = 1.0f + } + .build(), + ) + } + .build() val packet = createPacketWithNeighborInfo(from = myNodeNum, ni = ni, requestId = requestId) - every { nodeRepository.getUser(100) } returns User(long_name = "Alice", short_name = "AL") - every { nodeRepository.getUser(myNodeNum) } returns User(long_name = "Me", short_name = "ME") + every { nodeRepository.getUser(100) } returns + User.Builder() + .also { wb -> + wb.long_name = "Alice" + wb.short_name = "AL" + } + .build() + every { nodeRepository.getUser(myNodeNum) } returns + User.Builder() + .also { wb -> + wb.long_name = "Me" + wb.short_name = "ME" + } + .build() handler.recordStartTime(requestId) handler.handleNeighborInfo(packet) @@ -122,6 +234,17 @@ class NeighborInfoHandlerImplTest { private fun createPacketWithNeighborInfo(from: Int, ni: NeighborInfo, requestId: Int = 0): MeshPacket { val encoded = NeighborInfo.ADAPTER.encode(ni).toByteString() - return MeshPacket(from = from, decoded = Data(payload = encoded, request_id = requestId)) + return MeshPacket.Builder() + .also { wb -> + wb.from = from + wb.decoded = + Data.Builder() + .also { wb -> + wb.payload = encoded + wb.request_id = requestId + } + .build() + } + .build() } } diff --git a/core/data/src/commonTest/kotlin/org/meshtastic/core/data/manager/NodeManagerImplTest.kt b/core/data/src/commonTest/kotlin/org/meshtastic/core/data/manager/NodeManagerImplTest.kt index b6f64a84ad..741928bf98 100644 --- a/core/data/src/commonTest/kotlin/org/meshtastic/core/data/manager/NodeManagerImplTest.kt +++ b/core/data/src/commonTest/kotlin/org/meshtastic/core/data/manager/NodeManagerImplTest.kt @@ -53,6 +53,7 @@ import org.meshtastic.proto.DeviceMetrics import org.meshtastic.proto.EnvironmentMetrics import org.meshtastic.proto.HardwareModel import org.meshtastic.proto.Paxcount +import org.meshtastic.proto.SoilWaterMetrics import org.meshtastic.proto.StatusMessage import org.meshtastic.proto.Telemetry import org.meshtastic.proto.User @@ -126,12 +127,14 @@ class NodeManagerImplTest { val num = 200_000 + i nodeManager.handleReceivedUser( num, - User( - id = NodeAddress.numToDefaultId(num), - long_name = "Flood $i", - short_name = "F$i", - hw_model = HardwareModel.TLORA_V2, - ), + User.Builder() + .also { wb -> + wb.id = NodeAddress.numToDefaultId(num) + wb.long_name = "Flood $i" + wb.short_name = "F$i" + wb.hw_model = HardwareModel.TLORA_V2 + } + .build(), ) } @@ -172,7 +175,16 @@ class NodeManagerImplTest { fun `eviction prefers placeholders over nodes with a real identity`() { val identified = 7777 nodeManager.updateNode(identified) { - it.copy(user = it.user.copy(long_name = "Real Node", hw_model = HardwareModel.TLORA_V2)) + it.copy( + user = + it.user + .newBuilder() + .also { wb -> + wb.long_name = "Real Node" + wb.hw_model = HardwareModel.TLORA_V2 + } + .build(), + ) } floodPastCapAndAssertEvicted() @@ -293,13 +305,27 @@ class NodeManagerImplTest { fun `handleReceivedUser preserves existing user if incoming is default`() { val nodeNum = 1234 val existingUser = - User(id = "!12345678", long_name = "My Custom Name", short_name = "MCN", hw_model = HardwareModel.TLORA_V2) + User.Builder() + .also { wb -> + wb.id = "!12345678" + wb.long_name = "My Custom Name" + wb.short_name = "MCN" + wb.hw_model = HardwareModel.TLORA_V2 + } + .build() // Setup existing node nodeManager.updateNode(nodeNum) { it.copy(user = existingUser) } val incomingDefaultUser = - User(id = "!12345678", long_name = "Meshtastic 5678", short_name = "5678", hw_model = HardwareModel.UNSET) + User.Builder() + .also { wb -> + wb.id = "!12345678" + wb.long_name = "Meshtastic 5678" + wb.short_name = "5678" + wb.hw_model = HardwareModel.UNSET + } + .build() nodeManager.handleReceivedUser(nodeNum, incomingDefaultUser) @@ -313,12 +339,26 @@ class NodeManagerImplTest { val nodeNum = 1234 // Use a non-UNSET hw_model so isUnknownUser=false (avoids new-node notification + getString) val existingUser = - User(id = "!12345678", long_name = "Old Name", short_name = "ON", hw_model = HardwareModel.TLORA_V2) + User.Builder() + .also { wb -> + wb.id = "!12345678" + wb.long_name = "Old Name" + wb.short_name = "ON" + wb.hw_model = HardwareModel.TLORA_V2 + } + .build() nodeManager.updateNode(nodeNum) { it.copy(user = existingUser) } val incomingDetailedUser = - User(id = "!12345678", long_name = "Real User", short_name = "RU", hw_model = HardwareModel.TLORA_V1) + User.Builder() + .also { wb -> + wb.id = "!12345678" + wb.long_name = "Real User" + wb.short_name = "RU" + wb.hw_model = HardwareModel.TLORA_V1 + } + .build() nodeManager.handleReceivedUser(nodeNum, incomingDetailedUser) @@ -330,7 +370,13 @@ class NodeManagerImplTest { @Test fun `handleReceivedPosition updates node position`() { val nodeNum = 1234 - val position = ProtoPosition(latitude_i = 450000000, longitude_i = 900000000) + val position = + ProtoPosition.Builder() + .also { wb -> + wb.latitude_i = 450000000 + wb.longitude_i = 900000000 + } + .build() nodeManager.handleReceivedPosition(nodeNum, 9999, position, 0) @@ -344,11 +390,26 @@ class NodeManagerImplTest { @Test fun `handleReceivedPosition with zero coordinates preserves last known location but updates satellites`() { val nodeNum = 1234 - val initialPosition = ProtoPosition(latitude_i = 450000000, longitude_i = 900000000, sats_in_view = 10) + val initialPosition = + ProtoPosition.Builder() + .also { wb -> + wb.latitude_i = 450000000 + wb.longitude_i = 900000000 + wb.sats_in_view = 10 + } + .build() nodeManager.handleReceivedPosition(nodeNum, 9999, initialPosition, 1000000L) // Receive "zero" position with new satellite count - val zeroPosition = ProtoPosition(latitude_i = 0, longitude_i = 0, sats_in_view = 5, time = 1001) + val zeroPosition = + ProtoPosition.Builder() + .also { wb -> + wb.latitude_i = 0 + wb.longitude_i = 0 + wb.sats_in_view = 5 + wb.time = 1001 + } + .build() nodeManager.handleReceivedPosition(nodeNum, 9999, zeroPosition, 1001000L) val result = nodeManager.nodeDBbyNodeNum[nodeNum] @@ -369,7 +430,14 @@ class NodeManagerImplTest { nodeManager.handleReceivedPosition( nodeNum, 9999, - ProtoPosition(latitude_i = exactLatI, longitude_i = exactLonI, altitude = 135, time = 1000), + ProtoPosition.Builder() + .also { wb -> + wb.latitude_i = exactLatI + wb.longitude_i = exactLonI + wb.altitude = 135 + wb.time = 1000 + } + .build(), 1000000L, ) @@ -379,14 +447,16 @@ class NodeManagerImplTest { nodeManager.handleReceivedPosition( nodeNum, 9999, - ProtoPosition( - latitude_i = coarseLatI, - longitude_i = coarseLonI, - altitude = 210, - sats_in_view = 7, - time = 2000, - precision_bits = 13, - ), + ProtoPosition.Builder() + .also { wb -> + wb.latitude_i = coarseLatI + wb.longitude_i = coarseLonI + wb.altitude = 210 + wb.sats_in_view = 7 + wb.time = 2000 + wb.precision_bits = 13 + } + .build(), 2000000L, ) @@ -410,7 +480,13 @@ class NodeManagerImplTest { nodeManager.handleReceivedPosition( nodeNum, 9999, - ProtoPosition(latitude_i = 524595000, longitude_i = 310255000, time = 1000), + ProtoPosition.Builder() + .also { wb -> + wb.latitude_i = 524595000 + wb.longitude_i = 310255000 + wb.time = 1000 + } + .build(), 1000000L, ) @@ -420,7 +496,14 @@ class NodeManagerImplTest { nodeManager.handleReceivedPosition( nodeNum, 9999, - ProtoPosition(latitude_i = movedLatI, longitude_i = movedLonI, time = 2000, precision_bits = 13), + ProtoPosition.Builder() + .also { wb -> + wb.latitude_i = movedLatI + wb.longitude_i = movedLonI + wb.time = 2000 + wb.precision_bits = 13 + } + .build(), 2000000L, ) @@ -439,7 +522,14 @@ class NodeManagerImplTest { nodeManager.handleReceivedPosition( nodeNum, 9999, - ProtoPosition(latitude_i = coarseLatI, longitude_i = coarseLonI, time = 1000, precision_bits = 11), + ProtoPosition.Builder() + .also { wb -> + wb.latitude_i = coarseLatI + wb.longitude_i = coarseLonI + wb.time = 1000 + wb.precision_bits = 11 + } + .build(), 1000000L, ) @@ -448,7 +538,14 @@ class NodeManagerImplTest { nodeManager.handleReceivedPosition( nodeNum, 9999, - ProtoPosition(latitude_i = finerLatI, longitude_i = finerLonI, time = 2000, precision_bits = 13), + ProtoPosition.Builder() + .also { wb -> + wb.latitude_i = finerLatI + wb.longitude_i = finerLonI + wb.time = 2000 + wb.precision_bits = 13 + } + .build(), 2000000L, ) @@ -462,7 +559,15 @@ class NodeManagerImplTest { @Test fun `handleReceivedPosition for local node ignores purely empty packets`() { val myNum = 1111 - val emptyPos = ProtoPosition(latitude_i = 0, longitude_i = 0, sats_in_view = 0, time = 0) + val emptyPos = + ProtoPosition.Builder() + .also { wb -> + wb.latitude_i = 0 + wb.longitude_i = 0 + wb.sats_in_view = 0 + wb.time = 0 + } + .build() nodeManager.handleReceivedPosition(myNum, myNum, emptyPos, 0) @@ -476,7 +581,13 @@ class NodeManagerImplTest { val nodeNum = 1234 nodeManager.updateNode(nodeNum) { it.copy(lastHeard = 1000) } - val telemetry = Telemetry(time = 2000, device_metrics = DeviceMetrics(battery_level = 50)) + val telemetry = + Telemetry.Builder() + .also { wb -> + wb.time = 2000 + wb.device_metrics = DeviceMetrics.Builder().also { wb -> wb.battery_level = 50 }.build() + } + .build() nodeManager.handleReceivedTelemetry(nodeNum, telemetry) @@ -487,7 +598,18 @@ class NodeManagerImplTest { @Test fun `handleReceivedTelemetry updates device metrics`() { val nodeNum = 1234 - val telemetry = Telemetry(device_metrics = DeviceMetrics(battery_level = 75, voltage = 3.8f)) + val telemetry = + Telemetry.Builder() + .also { wb -> + wb.device_metrics = + DeviceMetrics.Builder() + .also { wb -> + wb.battery_level = 75 + wb.voltage = 3.8f + } + .build() + } + .build() nodeManager.handleReceivedTelemetry(nodeNum, telemetry) @@ -501,7 +623,17 @@ class NodeManagerImplTest { fun `handleReceivedTelemetry updates environment metrics`() { val nodeNum = 1234 val telemetry = - Telemetry(environment_metrics = EnvironmentMetrics(temperature = 22.5f, relative_humidity = 45.0f)) + Telemetry.Builder() + .also { wb -> + wb.environment_metrics = + EnvironmentMetrics.Builder() + .also { wb -> + wb.temperature = 22.5f + wb.relative_humidity = 45.0f + } + .build() + } + .build() nodeManager.handleReceivedTelemetry(nodeNum, telemetry) @@ -511,9 +643,36 @@ class NodeManagerImplTest { assertEquals(45.0f, result.environmentMetrics.relative_humidity) } + @Test + fun `handleReceivedTelemetry updates soil water metrics`() { + val nodeNum = 1234 + val telemetry = + Telemetry.Builder() + .also { wb -> + wb.soil_water_metrics = + SoilWaterMetrics.Builder() + .also { wb -> + wb.soil_ph = 6.5f + wb.nitrogen = 0f + } + .build() + } + .build() + + nodeManager.handleReceivedTelemetry(nodeNum, telemetry) + + val result = nodeManager.nodeDBbyNodeNum[nodeNum] + assertEquals(6.5f, result!!.soilWaterMetrics.soil_ph) + // A reported 0 mg/kg is a reading, not absence. + assertEquals(0f, result.soilWaterMetrics.nitrogen) + assertNull(result.soilWaterMetrics.potassium) + } + @Test fun `clear resets internal state`() { - nodeManager.updateNode(1234) { it.copy(user = it.user.copy(long_name = "Test")) } + nodeManager.updateNode(1234) { + it.copy(user = it.user.newBuilder().also { wb -> wb.long_name = "Test" }.build()) + } nodeManager.clear() assertTrue(nodeManager.nodeDBbyNodeNum.isEmpty()) @@ -537,7 +696,7 @@ class NodeManagerImplTest { fun `toNodeID returns user ID for known node`() { val nodeNum = 5678 val userId = "!customid" - nodeManager.updateNode(nodeNum) { it.copy(user = it.user.copy(id = userId)) } + nodeManager.updateNode(nodeNum) { it.copy(user = it.user.newBuilder().also { wb -> wb.id = userId }.build()) } val result = nodeManager.toNodeID(nodeNum) assertEquals(userId, result) } @@ -546,7 +705,17 @@ class NodeManagerImplTest { fun `removeByNodenum removes node from map`() { val nodeNum = 1234 nodeManager.updateNode(nodeNum) { - Node(num = nodeNum, user = User(id = "!testnode", long_name = "Test", short_name = "T")) + Node( + num = nodeNum, + user = + User.Builder() + .also { wb -> + wb.id = "!testnode" + wb.long_name = "Test" + wb.short_name = "T" + } + .build(), + ) } assertTrue(nodeManager.nodeDBbyNodeNum.containsKey(nodeNum)) assertNotNull(nodeManager.getNodeById("!testnode")) @@ -562,17 +731,26 @@ class NodeManagerImplTest { val nodeNum = 1234 val pk = ByteArray(32) { (it + 1).toByte() }.toByteString() val existingUser = - User(id = "!12345678", long_name = "Existing", short_name = "EX", hw_model = HardwareModel.TLORA_V2) + User.Builder() + .also { wb -> + wb.id = "!12345678" + wb.long_name = "Existing" + wb.short_name = "EX" + wb.hw_model = HardwareModel.TLORA_V2 + } + .build() nodeManager.updateNode(nodeNum) { it.copy(user = existingUser) } val incomingUser = - User( - id = "!12345678", - long_name = "Updated", - short_name = "UP", - hw_model = HardwareModel.TLORA_V2, - public_key = pk, - ) + User.Builder() + .also { wb -> + wb.id = "!12345678" + wb.long_name = "Updated" + wb.short_name = "UP" + wb.hw_model = HardwareModel.TLORA_V2 + wb.public_key = pk + } + .build() nodeManager.handleReceivedUser(nodeNum, incomingUser) val result = nodeManager.nodeDBbyNodeNum[nodeNum]!! @@ -582,34 +760,43 @@ class NodeManagerImplTest { } @Test - fun `handleReceivedUser sets empty publicKey when key mismatch clears user key`() { + fun `handleReceivedUser keeps the stored key when a different one arrives`() { val nodeNum = 1234 val existingPk = ByteArray(32) { (it + 1).toByte() }.toByteString() val existingUser = - User( - id = "!12345678", - long_name = "Existing", - short_name = "EX", - hw_model = HardwareModel.TLORA_V2, - public_key = existingPk, - ) + User.Builder() + .also { wb -> + wb.id = "!12345678" + wb.long_name = "Existing" + wb.short_name = "EX" + wb.hw_model = HardwareModel.TLORA_V2 + wb.public_key = existingPk + } + .build() nodeManager.updateNode(nodeNum) { it.copy(user = existingUser, publicKey = existingPk) } val differentPk = ByteArray(32) { (it + 10).toByte() }.toByteString() val incomingUser = - User( - id = "!12345678", - long_name = "Updated", - short_name = "UP", - hw_model = HardwareModel.TLORA_V2, - public_key = differentPk, - ) + User.Builder() + .also { wb -> + wb.id = "!12345678" + wb.long_name = "Updated" + wb.short_name = "UP" + wb.hw_model = HardwareModel.TLORA_V2 + wb.public_key = differentPk + } + .build() nodeManager.handleReceivedUser(nodeNum, incomingUser) val result = nodeManager.nodeDBbyNodeNum[nodeNum]!! - // Key mismatch: newUser gets public_key cleared to EMPTY, and publicKey should match - assertEquals(ByteString.EMPTY, result.publicKey) - assertEquals(ByteString.EMPTY, result.user.public_key) + // First-wins, matching firmware: anyone can broadcast a NodeInfo under this node's number, so the substitute + // is refused rather than applied. Clearing the key here would break PKC direct messages to the contact on the + // word of whoever sent it. + assertEquals(existingPk, result.publicKey) + assertEquals(existingPk, result.user.public_key) + // The refusal is still surfaced — the row reads as a mismatch without the key having been destroyed. + assertFalse(result.keyMatch) + assertTrue(result.mismatchKey) } @Test @@ -617,14 +804,24 @@ class NodeManagerImplTest { val nodeNum = 5678 val pk = ByteArray(32) { (it + 1).toByte() }.toByteString() val user = - User( - id = "!abcd1234", - long_name = "Remote Node", - short_name = "RN", - hw_model = HardwareModel.HELTEC_V3, - public_key = pk, - ) - val info = ProtoNodeInfo(num = nodeNum, user = user, last_heard = 1000, channel = 0) + User.Builder() + .also { wb -> + wb.id = "!abcd1234" + wb.long_name = "Remote Node" + wb.short_name = "RN" + wb.hw_model = HardwareModel.HELTEC_V3 + wb.public_key = pk + } + .build() + val info = + ProtoNodeInfo.Builder() + .also { wb -> + wb.num = nodeNum + wb.user = user + wb.last_heard = 1000 + wb.channel = 0 + } + .build() nodeManager.installNodeInfo(info) @@ -642,18 +839,35 @@ class NodeManagerImplTest { nodeManager.handleReceivedPosition( nodeNum, 9999, - ProtoPosition(latitude_i = exactLatI, longitude_i = exactLonI, time = 1000), + ProtoPosition.Builder() + .also { wb -> + wb.latitude_i = exactLatI + wb.longitude_i = exactLonI + wb.time = 1000 + } + .build(), 1000000L, ) val coarse = - ProtoPosition( - latitude_i = (exactLatI and (-1 shl 19)) + (1 shl 18), - longitude_i = (exactLonI and (-1 shl 19)) + (1 shl 18), - time = 2000, - precision_bits = 13, - ) - nodeManager.installNodeInfo(ProtoNodeInfo(num = nodeNum, position = coarse, last_heard = 2000, channel = 0)) + ProtoPosition.Builder() + .also { wb -> + wb.latitude_i = (exactLatI and (-1 shl 19)) + (1 shl 18) + wb.longitude_i = (exactLonI and (-1 shl 19)) + (1 shl 18) + wb.time = 2000 + wb.precision_bits = 13 + } + .build() + nodeManager.installNodeInfo( + ProtoNodeInfo.Builder() + .also { wb -> + wb.num = nodeNum + wb.position = coarse + wb.last_heard = 2000 + wb.channel = 0 + } + .build(), + ) val result = nodeManager.nodeDBbyNodeNum[nodeNum]!! assertEquals(exactLatI, result.position.latitude_i) @@ -670,15 +884,25 @@ class NodeManagerImplTest { val nodeNum = 5678 val pk = ByteArray(32) { (it + 1).toByte() }.toByteString() val user = - User( - id = "!abcd1234", - long_name = "Licensed Op", - short_name = "LO", - hw_model = HardwareModel.HELTEC_V3, - public_key = pk, - is_licensed = true, - ) - val info = ProtoNodeInfo(num = nodeNum, user = user, last_heard = 1000, channel = 0) + User.Builder() + .also { wb -> + wb.id = "!abcd1234" + wb.long_name = "Licensed Op" + wb.short_name = "LO" + wb.hw_model = HardwareModel.HELTEC_V3 + wb.public_key = pk + wb.is_licensed = true + } + .build() + val info = + ProtoNodeInfo.Builder() + .also { wb -> + wb.num = nodeNum + wb.user = user + wb.last_heard = 1000 + wb.channel = 0 + } + .build() nodeManager.installNodeInfo(info) @@ -719,8 +943,24 @@ class NodeManagerImplTest { every { nodeRepository.myNodeInfo } returns MutableStateFlow(repoInfo) // Add node with position (non-zero lat → hasGPS = true) - nodeManager.handleReceivedPosition(myNum, myNum, ProtoPosition(latitude_i = 100), 0) - nodeManager.updateNode(myNum) { it.copy(user = it.user.copy(id = "!mydevice", hw_model = HardwareModel.TBEAM)) } + nodeManager.handleReceivedPosition( + myNum, + myNum, + ProtoPosition.Builder().also { wb -> wb.latitude_i = 100 }.build(), + 0, + ) + nodeManager.updateNode(myNum) { + it.copy( + user = + it.user + .newBuilder() + .also { wb -> + wb.id = "!mydevice" + wb.hw_model = HardwareModel.TBEAM + } + .build(), + ) + } val result = nodeManager.getMyNodeInfo() @@ -753,7 +993,9 @@ class NodeManagerImplTest { ) every { nodeRepository.myNodeInfo } returns MutableStateFlow(repoInfo) - nodeManager.updateNode(myNum) { it.copy(user = it.user.copy(hw_model = HardwareModel.HELTEC_V3)) } + nodeManager.updateNode(myNum) { + it.copy(user = it.user.newBuilder().also { wb -> wb.hw_model = HardwareModel.HELTEC_V3 }.build()) + } val result = nodeManager.getMyNodeInfo() @@ -767,7 +1009,7 @@ class NodeManagerImplTest { nodeManager.updateNode(nodeNum) { it.copy(lastHeard = 1000) } // Telemetry with no metrics at all - val telemetry = Telemetry(time = 3000) + val telemetry = Telemetry.Builder().also { wb -> wb.time = 3000 }.build() nodeManager.handleReceivedTelemetry(nodeNum, telemetry) @@ -788,7 +1030,9 @@ class NodeManagerImplTest { fun `getMyId returns user ID when connected`() { val myNum = 1234 nodeManager.setMyNodeNum(myNum) - nodeManager.updateNode(myNum) { it.copy(user = it.user.copy(id = "!mynode42")) } + nodeManager.updateNode(myNum) { + it.copy(user = it.user.newBuilder().also { wb -> wb.id = "!mynode42" }.build()) + } val result = nodeManager.getMyId() assertEquals("!mynode42", result) @@ -809,13 +1053,15 @@ class NodeManagerImplTest { private fun makeKnownNode(num: Int, pk: ByteString, name: String = "Known"): Node = Node( num = num, user = - User( - id = "!${num.toString(16)}", - long_name = name, - short_name = name.take(3), - hw_model = HardwareModel.TLORA_V2, - public_key = pk, - ), + User.Builder() + .also { wb -> + wb.id = "!${num.toString(16)}" + wb.long_name = name + wb.short_name = name.take(3) + wb.hw_model = HardwareModel.TLORA_V2 + wb.public_key = pk + } + .build(), publicKey = pk, ) @@ -838,29 +1084,59 @@ class NodeManagerImplTest { nodeManager.applyTrustedIdentityMigrations(listOf(retiredNum)) enableDbWrites() - nodeManager.handleReceivedTelemetry(retiredNum, Telemetry(device_metrics = DeviceMetrics(battery_level = 50))) + nodeManager.handleReceivedTelemetry( + retiredNum, + Telemetry.Builder() + .also { wb -> wb.device_metrics = DeviceMetrics.Builder().also { wb -> wb.battery_level = 50 }.build() } + .build(), + ) nodeManager.handleReceivedPosition( retiredNum, myNodeNum = 9999, - p = ProtoPosition(latitude_i = 123, longitude_i = 456), + p = + ProtoPosition.Builder() + .also { wb -> + wb.latitude_i = 123 + wb.longitude_i = 456 + } + .build(), defaultTime = 1000L, ) - nodeManager.handleReceivedPaxcounter(retiredNum, Paxcount(wifi = 10, ble = 5, uptime = 1000)) - nodeManager.handleReceivedNodeStatus(retiredNum, StatusMessage(status = "stale")) - nodeManager.installNodeInfo( - ProtoNodeInfo( - num = retiredNum, - user = - User( - id = "!retired", - long_name = "Retired", - short_name = "RET", - hw_model = HardwareModel.TLORA_V2, - public_key = validPk, - ), - ), + nodeManager.handleReceivedPaxcounter( + retiredNum, + Paxcount.Builder() + .also { wb -> + wb.wifi = 10 + wb.ble = 5 + wb.uptime = 1000 + } + .build(), + ) + nodeManager.handleReceivedNodeStatus( + retiredNum, + StatusMessage.Builder().also { wb -> wb.status = "stale" }.build(), + ) + nodeManager.installNodeInfo( + ProtoNodeInfo.Builder() + .also { wb -> + wb.num = retiredNum + wb.user = + User.Builder() + .also { wb -> + wb.id = "!retired" + wb.long_name = "Retired" + wb.short_name = "RET" + wb.hw_model = HardwareModel.TLORA_V2 + wb.public_key = validPk + } + .build() + } + .build(), + ) + nodeManager.insertMetadata( + retiredNum, + DeviceMetadata.Builder().also { wb -> wb.firmware_version = "2.7.0" }.build(), ) - nodeManager.insertMetadata(retiredNum, DeviceMetadata(firmware_version = "2.7.0")) testScope.advanceUntilIdle() assertNull(nodeManager.nodeDBbyNodeNum[retiredNum]) @@ -880,27 +1156,38 @@ class NodeManagerImplTest { nodeManager.handleReceivedUser( retiredNum, - User(id = "!keyless", long_name = "Keyless replay", short_name = "KEY", hw_model = HardwareModel.TLORA_V2), + User.Builder() + .also { wb -> + wb.id = "!keyless" + wb.long_name = "Keyless replay" + wb.short_name = "KEY" + wb.hw_model = HardwareModel.TLORA_V2 + } + .build(), ) nodeManager.handleReceivedUser( retiredNum, - User( - id = "!invalid", - long_name = "Invalid-key replay", - short_name = "INV", - hw_model = HardwareModel.TLORA_V2, - public_key = byteArrayOf(1, 2, 3).toByteString(), - ), + User.Builder() + .also { wb -> + wb.id = "!invalid" + wb.long_name = "Invalid-key replay" + wb.short_name = "INV" + wb.hw_model = HardwareModel.TLORA_V2 + wb.public_key = byteArrayOf(1, 2, 3).toByteString() + } + .build(), ) nodeManager.handleReceivedUser( retiredNum, - User( - id = "!represented", - long_name = "Represented replay", - short_name = "REP", - hw_model = HardwareModel.TLORA_V2, - public_key = validPk, - ), + User.Builder() + .also { wb -> + wb.id = "!represented" + wb.long_name = "Represented replay" + wb.short_name = "REP" + wb.hw_model = HardwareModel.TLORA_V2 + wb.public_key = validPk + } + .build(), ) testScope.advanceUntilIdle() @@ -924,13 +1211,15 @@ class NodeManagerImplTest { nodeManager.handleReceivedUser( retiredNum, - User( - id = "!replacement", - long_name = "Replacement", - short_name = "NEW", - hw_model = HardwareModel.TLORA_V2, - public_key = replacementKey, - ), + User.Builder() + .also { wb -> + wb.id = "!replacement" + wb.long_name = "Replacement" + wb.short_name = "NEW" + wb.hw_model = HardwareModel.TLORA_V2 + wb.public_key = replacementKey + } + .build(), ) testScope.advanceUntilIdle() @@ -964,7 +1253,14 @@ class NodeManagerImplTest { // A stale keyless replay at the retired number is still suppressed after the reload. nodeManager.handleReceivedUser( retiredNum, - User(id = "!stale", long_name = "Stale replay", short_name = "STL", hw_model = HardwareModel.TLORA_V2), + User.Builder() + .also { wb -> + wb.id = "!stale" + wb.long_name = "Stale replay" + wb.short_name = "STL" + wb.hw_model = HardwareModel.TLORA_V2 + } + .build(), ) testScope.advanceUntilIdle() @@ -977,11 +1273,21 @@ class NodeManagerImplTest { fun `clear starts a new session without retired-number suppression`() { val retiredNum = validPk.noncanonicalNum(4400) nodeManager.applyTrustedIdentityMigrations(listOf(retiredNum)) - nodeManager.handleReceivedTelemetry(retiredNum, Telemetry(device_metrics = DeviceMetrics(battery_level = 10))) + nodeManager.handleReceivedTelemetry( + retiredNum, + Telemetry.Builder() + .also { wb -> wb.device_metrics = DeviceMetrics.Builder().also { wb -> wb.battery_level = 10 }.build() } + .build(), + ) assertNull(nodeManager.nodeDBbyNodeNum[retiredNum]) nodeManager.clear() - nodeManager.handleReceivedTelemetry(retiredNum, Telemetry(device_metrics = DeviceMetrics(battery_level = 90))) + nodeManager.handleReceivedTelemetry( + retiredNum, + Telemetry.Builder() + .also { wb -> wb.device_metrics = DeviceMetrics.Builder().also { wb -> wb.battery_level = 90 }.build() } + .build(), + ) assertEquals(90, nodeManager.nodeDBbyNodeNum[retiredNum]?.deviceMetrics?.battery_level) } @@ -997,13 +1303,15 @@ class NodeManagerImplTest { enableDbWrites() val staleUser = - User( - id = "!old", - long_name = "Stale", - short_name = "STL", - hw_model = HardwareModel.TLORA_V2, - public_key = validPk, - ) + User.Builder() + .also { wb -> + wb.id = "!old" + wb.long_name = "Stale" + wb.short_name = "STL" + wb.hw_model = HardwareModel.TLORA_V2 + wb.public_key = validPk + } + .build() nodeManager.handleReceivedUser(oldNum, staleUser) testScope.advanceUntilIdle() @@ -1030,28 +1338,38 @@ class NodeManagerImplTest { Node( num = oldNum, user = - User( - id = defaultId, - long_name = "Meshtastic ${defaultId.takeLast(4)}", - short_name = defaultId.takeLast(4), - hw_model = HardwareModel.UNSET, - public_key = placeholderKey, - ), + User.Builder() + .also { wb -> + wb.id = defaultId + wb.long_name = "Meshtastic ${defaultId.takeLast(4)}" + wb.short_name = defaultId.takeLast(4) + wb.hw_model = HardwareModel.UNSET + wb.public_key = placeholderKey + } + .build(), publicKey = placeholderKey, - position = ProtoPosition(latitude_i = 123, longitude_i = 456), + position = + ProtoPosition.Builder() + .also { wb -> + wb.latitude_i = 123 + wb.longitude_i = 456 + } + .build(), channel = 7, ) } enableDbWrites() val staleUser = - User( - id = "!old", - long_name = "Stale", - short_name = "STL", - hw_model = HardwareModel.TLORA_V2, - public_key = validPk, - ) + User.Builder() + .also { wb -> + wb.id = "!old" + wb.long_name = "Stale" + wb.short_name = "STL" + wb.hw_model = HardwareModel.TLORA_V2 + wb.public_key = validPk + } + .build() nodeManager.handleReceivedUser(oldNum, staleUser) testScope.advanceUntilIdle() @@ -1081,13 +1399,15 @@ class NodeManagerImplTest { enableDbWrites() val incomingUser = - User( - id = "!stale", - long_name = "Stale", - short_name = "STL", - hw_model = HardwareModel.TLORA_V2, - public_key = validPk, - ) + User.Builder() + .also { wb -> + wb.id = "!stale" + wb.long_name = "Stale" + wb.short_name = "STL" + wb.hw_model = HardwareModel.TLORA_V2 + wb.public_key = validPk + } + .build() nodeManager.handleReceivedUser(fromNum, incomingUser) testScope.advanceUntilIdle() @@ -1116,13 +1436,15 @@ class NodeManagerImplTest { enableDbWrites() val incomingUser = - User( - id = "!incoming", - long_name = "Incoming", - short_name = "INC", - hw_model = HardwareModel.TLORA_V2, - public_key = validPk, - ) + User.Builder() + .also { wb -> + wb.id = "!incoming" + wb.long_name = "Incoming" + wb.short_name = "INC" + wb.hw_model = HardwareModel.TLORA_V2 + wb.public_key = validPk + } + .build() nodeManager.handleReceivedUser(fromNum, incomingUser) testScope.advanceUntilIdle() @@ -1144,13 +1466,15 @@ class NodeManagerImplTest { nodeManager.handleReceivedUser( fromNum, - User( - id = "!incoming", - long_name = "Incoming", - short_name = "INC", - hw_model = HardwareModel.TLORA_V2, - public_key = validPk, - ), + User.Builder() + .also { wb -> + wb.id = "!incoming" + wb.long_name = "Incoming" + wb.short_name = "INC" + wb.hw_model = HardwareModel.TLORA_V2 + wb.public_key = validPk + } + .build(), ) testScope.advanceUntilIdle() @@ -1171,30 +1495,40 @@ class NodeManagerImplTest { nodeManager.handleReceivedPosition( fromNum = fromNum, myNodeNum = 9999, - p = ProtoPosition(latitude_i = 123, longitude_i = 456), + p = + ProtoPosition.Builder() + .also { wb -> + wb.latitude_i = 123 + wb.longitude_i = 456 + } + .build(), defaultTime = 1000L, ) enableDbWrites() nodeManager.handleReceivedUser( fromNum, - User( - id = "!incoming", - long_name = "First Identity", - short_name = "ONE", - hw_model = HardwareModel.TLORA_V2, - public_key = validPk, - ), + User.Builder() + .also { wb -> + wb.id = "!incoming" + wb.long_name = "First Identity" + wb.short_name = "ONE" + wb.hw_model = HardwareModel.TLORA_V2 + wb.public_key = validPk + } + .build(), ) nodeManager.handleReceivedUser( fromNum, - User( - id = "!incoming", - long_name = "Updated Identity", - short_name = "TWO", - hw_model = HardwareModel.TLORA_V2, - public_key = validPk, - ), + User.Builder() + .also { wb -> + wb.id = "!incoming" + wb.long_name = "Updated Identity" + wb.short_name = "TWO" + wb.hw_model = HardwareModel.TLORA_V2 + wb.public_key = validPk + } + .build(), ) testScope.advanceUntilIdle() @@ -1220,13 +1554,15 @@ class NodeManagerImplTest { enableDbWrites() val localUser = - User( - id = "!local", - long_name = "Local Node", - short_name = "LCL", - hw_model = HardwareModel.TLORA_V2, - public_key = validPk, - ) + User.Builder() + .also { wb -> + wb.id = "!local" + wb.long_name = "Local Node" + wb.short_name = "LCL" + wb.hw_model = HardwareModel.TLORA_V2 + wb.public_key = validPk + } + .build() nodeManager.handleReceivedUser(localNum, localUser) testScope.advanceUntilIdle() @@ -1247,13 +1583,15 @@ class NodeManagerImplTest { val replayedNodeNum = 2999 val replayedKey = ByteArray(32) { (it + 90).toByte() }.toByteString() val replayedUser = - User( - id = "!replayed", - long_name = "Replayed Node", - short_name = "RPL", - hw_model = HardwareModel.TLORA_V2, - public_key = replayedKey, - ) + User.Builder() + .also { wb -> + wb.id = "!replayed" + wb.long_name = "Replayed Node" + wb.short_name = "RPL" + wb.hw_model = HardwareModel.TLORA_V2 + wb.public_key = replayedKey + } + .build() nodeManager.setAllowNodeDbWrites(true) nodeManager.handleReceivedUser(replayedNodeNum, replayedUser) @@ -1270,13 +1608,15 @@ class NodeManagerImplTest { fun `pre-ready user packet stays notification-suppressed across a CAS retry`() { val replayedNodeNum = 2998 val replayedUser = - User( - id = "!retry", - long_name = "Retried Baseline Node", - short_name = "RTY", - hw_model = HardwareModel.TLORA_V2, - public_key = ByteArray(32) { (it + 91).toByte() }.toByteString(), - ) + User.Builder() + .also { wb -> + wb.id = "!retry" + wb.long_name = "Retried Baseline Node" + wb.short_name = "RTY" + wb.hw_model = HardwareModel.TLORA_V2 + wb.public_key = ByteArray(32) { (it + 91).toByte() }.toByteString() + } + .build() var reductionCount = 0 nodeManager.receivedUserReductionHook = { reductionCount += 1 @@ -1300,13 +1640,15 @@ class NodeManagerImplTest { val newNodeNum = 3000 val newPk = ByteArray(32) { (it + 100).toByte() }.toByteString() val newUser = - User( - id = "!newnode", - long_name = "New Node", - short_name = "NEW", - hw_model = HardwareModel.TLORA_V2, - public_key = newPk, - ) + User.Builder() + .also { wb -> + wb.id = "!newnode" + wb.long_name = "New Node" + wb.short_name = "NEW" + wb.hw_model = HardwareModel.TLORA_V2 + wb.public_key = newPk + } + .build() enableDbWrites() val captured = mutableListOf() @@ -1346,33 +1688,44 @@ class NodeManagerImplTest { // (a) null key nodeManager.handleReceivedUser( targetNum, - User(id = "!a", long_name = "A", short_name = "A", hw_model = HardwareModel.TLORA_V2), + User.Builder() + .also { wb -> + wb.id = "!a" + wb.long_name = "A" + wb.short_name = "A" + wb.hw_model = HardwareModel.TLORA_V2 + } + .build(), ) testScope.advanceUntilIdle() // (b) empty key nodeManager.handleReceivedUser( targetNum, - User( - id = "!b", - long_name = "B", - short_name = "B", - hw_model = HardwareModel.TLORA_V2, - public_key = ByteString.EMPTY, - ), + User.Builder() + .also { wb -> + wb.id = "!b" + wb.long_name = "B" + wb.short_name = "B" + wb.hw_model = HardwareModel.TLORA_V2 + wb.public_key = ByteString.EMPTY + } + .build(), ) testScope.advanceUntilIdle() // (c) ERROR key nodeManager.handleReceivedUser( targetNum, - User( - id = "!c", - long_name = "C", - short_name = "C", - hw_model = HardwareModel.TLORA_V2, - public_key = Node.ERROR_BYTE_STRING, - ), + User.Builder() + .also { wb -> + wb.id = "!c" + wb.long_name = "C" + wb.short_name = "C" + wb.hw_model = HardwareModel.TLORA_V2 + wb.public_key = Node.ERROR_BYTE_STRING + } + .build(), ) testScope.advanceUntilIdle() @@ -1389,13 +1742,15 @@ class NodeManagerImplTest { enableDbWrites() val updatedUser = - User( - id = "!${nodeNum.toString(16)}", - long_name = "Updated", - short_name = "UPD", - hw_model = HardwareModel.TLORA_V2, - public_key = validPk, - ) + User.Builder() + .also { wb -> + wb.id = "!${nodeNum.toString(16)}" + wb.long_name = "Updated" + wb.short_name = "UPD" + wb.hw_model = HardwareModel.TLORA_V2 + wb.public_key = validPk + } + .build() nodeManager.handleReceivedUser(nodeNum, updatedUser) testScope.advanceUntilIdle() @@ -1418,13 +1773,15 @@ class NodeManagerImplTest { enableDbWrites() val staleUser = - User( - id = "!old", - long_name = "Stale Packet", - short_name = "STL", - hw_model = HardwareModel.TLORA_V2, - public_key = validPk, - ) + User.Builder() + .also { wb -> + wb.id = "!old" + wb.long_name = "Stale Packet" + wb.short_name = "STL" + wb.hw_model = HardwareModel.TLORA_V2 + wb.public_key = validPk + } + .build() nodeManager.handleReceivedUser(oldNum, staleUser) testScope.advanceUntilIdle() @@ -1444,7 +1801,15 @@ class NodeManagerImplTest { nodeManager.updateNode(staleOld) { makeKnownNode(staleOld, validPk, "Old") } nodeManager.handleReceivedUser( staleOld, - User(id = "!s", long_name = "S", short_name = "S", hw_model = HardwareModel.TLORA_V2, public_key = validPk), + User.Builder() + .also { wb -> + wb.id = "!s" + wb.long_name = "S" + wb.short_name = "S" + wb.hw_model = HardwareModel.TLORA_V2 + wb.public_key = validPk + } + .build(), ) // Conflict: different established identity at fromNum. @@ -1455,7 +1820,15 @@ class NodeManagerImplTest { nodeManager.updateNode(conflictFrom) { makeKnownNode(conflictFrom, conflictPk, "Established") } nodeManager.handleReceivedUser( conflictFrom, - User(id = "!c", long_name = "C", short_name = "C", hw_model = HardwareModel.TLORA_V2, public_key = validPk), + User.Builder() + .also { wb -> + wb.id = "!c" + wb.long_name = "C" + wb.short_name = "C" + wb.hw_model = HardwareModel.TLORA_V2 + wb.public_key = validPk + } + .build(), ) // Local: fromNum == myNodeNum, local-link authoritative update. @@ -1465,7 +1838,15 @@ class NodeManagerImplTest { nodeManager.updateNode(localGhost) { makeKnownNode(localGhost, validPk, "Ghost") } nodeManager.handleReceivedUser( localNum, - User(id = "!l", long_name = "L", short_name = "L", hw_model = HardwareModel.TLORA_V2, public_key = validPk), + User.Builder() + .also { wb -> + wb.id = "!l" + wb.long_name = "L" + wb.short_name = "L" + wb.hw_model = HardwareModel.TLORA_V2 + wb.public_key = validPk + } + .build(), ) testScope.advanceUntilIdle() @@ -1486,13 +1867,15 @@ class NodeManagerImplTest { Node( num = newNum, user = - User( - id = sharedUserId, - long_name = "Canonical", - short_name = "CAN", - hw_model = HardwareModel.TLORA_V2, - public_key = validPk, - ), + User.Builder() + .also { wb -> + wb.id = sharedUserId + wb.long_name = "Canonical" + wb.short_name = "CAN" + wb.hw_model = HardwareModel.TLORA_V2 + wb.public_key = validPk + } + .build(), publicKey = validPk, ) } @@ -1500,26 +1883,30 @@ class NodeManagerImplTest { Node( num = oldNum, user = - User( - id = sharedUserId, - long_name = "Stale", - short_name = "STL", - hw_model = HardwareModel.TLORA_V2, - public_key = validPk, - ), + User.Builder() + .also { wb -> + wb.id = sharedUserId + wb.long_name = "Stale" + wb.short_name = "STL" + wb.hw_model = HardwareModel.TLORA_V2 + wb.public_key = validPk + } + .build(), publicKey = validPk, ) } enableDbWrites() val staleUser = - User( - id = sharedUserId, - long_name = "Stale", - short_name = "STL", - hw_model = HardwareModel.TLORA_V2, - public_key = validPk, - ) + User.Builder() + .also { wb -> + wb.id = sharedUserId + wb.long_name = "Stale" + wb.short_name = "STL" + wb.hw_model = HardwareModel.TLORA_V2 + wb.public_key = validPk + } + .build() nodeManager.handleReceivedUser(oldNum, staleUser) testScope.advanceUntilIdle() @@ -1543,39 +1930,45 @@ class NodeManagerImplTest { // All malformed keys should be treated as no-key (normal update path). nodeManager.handleReceivedUser( nodeNum, - User( - id = "!short", - long_name = "S", - short_name = "S", - hw_model = HardwareModel.TLORA_V2, - public_key = shortKey, - ), + User.Builder() + .also { wb -> + wb.id = "!short" + wb.long_name = "S" + wb.short_name = "S" + wb.hw_model = HardwareModel.TLORA_V2 + wb.public_key = shortKey + } + .build(), ) var result = nodeManager.nodeDBbyNodeNum[nodeNum] assertEquals(ByteString.EMPTY, result!!.publicKey) nodeManager.handleReceivedUser( nodeNum, - User( - id = "!almost", - long_name = "A", - short_name = "A", - hw_model = HardwareModel.TLORA_V2, - public_key = almostKey, - ), + User.Builder() + .also { wb -> + wb.id = "!almost" + wb.long_name = "A" + wb.short_name = "A" + wb.hw_model = HardwareModel.TLORA_V2 + wb.public_key = almostKey + } + .build(), ) result = nodeManager.nodeDBbyNodeNum[nodeNum] assertEquals(ByteString.EMPTY, result!!.publicKey) nodeManager.handleReceivedUser( nodeNum, - User( - id = "!long", - long_name = "L", - short_name = "L", - hw_model = HardwareModel.TLORA_V2, - public_key = longKey, - ), + User.Builder() + .also { wb -> + wb.id = "!long" + wb.long_name = "L" + wb.short_name = "L" + wb.hw_model = HardwareModel.TLORA_V2 + wb.public_key = longKey + } + .build(), ) result = nodeManager.nodeDBbyNodeNum[nodeNum] assertEquals(ByteString.EMPTY, result!!.publicKey) @@ -1591,13 +1984,15 @@ class NodeManagerImplTest { nodeManager.handleReceivedUser( nodeNum, - User( - id = "!test", - long_name = "Test", - short_name = "T", - hw_model = HardwareModel.TLORA_V2, - public_key = validUserKey, - ), + User.Builder() + .also { wb -> + wb.id = "!test" + wb.long_name = "Test" + wb.short_name = "T" + wb.hw_model = HardwareModel.TLORA_V2 + wb.public_key = validUserKey + } + .build(), ) // Manually set an invalid Node.publicKey nodeManager.updateNode(nodeNum) { it.copy(publicKey = invalidNodeKey) } @@ -1625,13 +2020,15 @@ class NodeManagerImplTest { Node( num = conflictNum, user = - User( - id = "!conflict", - long_name = "Conflict", - short_name = "CON", - hw_model = HardwareModel.UNSET, - public_key = conflictKey, - ), + User.Builder() + .also { wb -> + wb.id = "!conflict" + wb.long_name = "Conflict" + wb.short_name = "CON" + wb.hw_model = HardwareModel.UNSET + wb.public_key = conflictKey + } + .build(), publicKey = conflictKey, ) } @@ -1639,13 +2036,15 @@ class NodeManagerImplTest { // Packet at conflictNum with canonicalKey should be treated as conflict (preserve both) nodeManager.handleReceivedUser( conflictNum, - User( - id = "!c", - long_name = "C", - short_name = "C", - hw_model = HardwareModel.TLORA_V2, - public_key = canonicalKey, - ), + User.Builder() + .also { wb -> + wb.id = "!c" + wb.long_name = "C" + wb.short_name = "C" + wb.hw_model = HardwareModel.TLORA_V2 + wb.public_key = canonicalKey + } + .build(), ) // Both nodes should still exist @@ -1661,22 +2060,26 @@ class NodeManagerImplTest { fun `custom incomplete identity is preserved over default incoming`() { val nodeNum = 1234 val customUser = - User( - id = "!custom", - long_name = "My Custom Name", - short_name = "MCN", - hw_model = HardwareModel.UNSET, // Incomplete hardware - ) + User.Builder() + .also { wb -> + wb.id = "!custom" + wb.long_name = "My Custom Name" + wb.short_name = "MCN" + wb.hw_model = HardwareModel.UNSET // Incomplete hardware + } + .build() nodeManager.updateNode(nodeNum) { it.copy(user = customUser) } // Incoming default user should not overwrite custom identity val defaultUser = - User( - id = NodeAddress.numToDefaultId(nodeNum), - long_name = "Meshtastic ${nodeNum.toHex().takeLast(4)}", - short_name = nodeNum.toHex().takeLast(4), - hw_model = HardwareModel.UNSET, - ) + User.Builder() + .also { wb -> + wb.id = NodeAddress.numToDefaultId(nodeNum) + wb.long_name = "Meshtastic ${nodeNum.toHex().takeLast(4)}" + wb.short_name = nodeNum.toHex().takeLast(4) + wb.hw_model = HardwareModel.UNSET + } + .build() nodeManager.handleReceivedUser(nodeNum, defaultUser) val result = nodeManager.nodeDBbyNodeNum[nodeNum] @@ -1697,19 +2100,43 @@ class NodeManagerImplTest { nodeManager.updateNode(node1) { Node( num = node1, - user = User(id = userId, long_name = "N1", short_name = "N1", hw_model = HardwareModel.UNSET), + user = + User.Builder() + .also { wb -> + wb.id = userId + wb.long_name = "N1" + wb.short_name = "N1" + wb.hw_model = HardwareModel.UNSET + } + .build(), ) } nodeManager.updateNode(node2) { Node( num = node2, - user = User(id = userId, long_name = "N2", short_name = "N2", hw_model = HardwareModel.TLORA_V2), + user = + User.Builder() + .also { wb -> + wb.id = userId + wb.long_name = "N2" + wb.short_name = "N2" + wb.hw_model = HardwareModel.TLORA_V2 + } + .build(), ) } nodeManager.updateNode(node3) { Node( num = node3, - user = User(id = userId, long_name = "N3", short_name = "N3", hw_model = HardwareModel.UNSET), + user = + User.Builder() + .also { wb -> + wb.id = userId + wb.long_name = "N3" + wb.short_name = "N3" + wb.hw_model = HardwareModel.UNSET + } + .build(), ) } @@ -1732,20 +2159,45 @@ class NodeManagerImplTest { nodeManager.updateNode(nodeNum) { Node( num = nodeNum, - user = User(id = oldUserId, long_name = "Old", short_name = "OLD", hw_model = HardwareModel.TLORA_V2), + user = + User.Builder() + .also { wb -> + wb.id = oldUserId + wb.long_name = "Old" + wb.short_name = "OLD" + wb.hw_model = HardwareModel.TLORA_V2 + } + .build(), ) } nodeManager.updateNode(survivorNum) { Node( num = survivorNum, user = - User(id = oldUserId, long_name = "Survivor", short_name = "SUR", hw_model = HardwareModel.TLORA_V2), + User.Builder() + .also { wb -> + wb.id = oldUserId + wb.long_name = "Survivor" + wb.short_name = "SUR" + wb.hw_model = HardwareModel.TLORA_V2 + } + .build(), ) } // Change nodeNum's user ID nodeManager.updateNode(nodeNum) { - it.copy(user = it.user.copy(id = newUserId, long_name = "New", short_name = "NEW")) + it.copy( + user = + it.user + .newBuilder() + .also { wb -> + wb.id = newUserId + wb.long_name = "New" + wb.short_name = "NEW" + } + .build(), + ) } // byId for oldUserId should now point to survivorNum @@ -1772,17 +2224,28 @@ class NodeManagerImplTest { node1 to Node( num = node1, - user = User(id = userId, long_name = "N1", short_name = "N1", hw_model = HardwareModel.UNSET), + user = + User.Builder() + .also { wb -> + wb.id = userId + wb.long_name = "N1" + wb.short_name = "N1" + wb.hw_model = HardwareModel.UNSET + } + .build(), ), node2 to Node( num = node2, - user = User( - id = userId, - long_name = "N2", - short_name = "N2", - hw_model = HardwareModel.TLORA_V2, - ), + user = + User.Builder() + .also { wb -> + wb.id = userId + wb.long_name = "N2" + wb.short_name = "N2" + wb.hw_model = HardwareModel.TLORA_V2 + } + .build(), ), ) val nodes21 = @@ -1790,17 +2253,28 @@ class NodeManagerImplTest { node2 to Node( num = node2, - user = User( - id = userId, - long_name = "N2", - short_name = "N2", - hw_model = HardwareModel.TLORA_V2, - ), + user = + User.Builder() + .also { wb -> + wb.id = userId + wb.long_name = "N2" + wb.short_name = "N2" + wb.hw_model = HardwareModel.TLORA_V2 + } + .build(), ), node1 to Node( num = node1, - user = User(id = userId, long_name = "N1", short_name = "N1", hw_model = HardwareModel.UNSET), + user = + User.Builder() + .also { wb -> + wb.id = userId + wb.long_name = "N1" + wb.short_name = "N1" + wb.hw_model = HardwareModel.UNSET + } + .build(), ), ) @@ -1827,13 +2301,15 @@ class NodeManagerImplTest { Node( num = competingNum, user = - User( - id = userId, - long_name = "Fallback Winner", - short_name = "FBK", - hw_model = HardwareModel.TLORA_V2, - public_key = competingKey, - ), + User.Builder() + .also { wb -> + wb.id = userId + wb.long_name = "Fallback Winner" + wb.short_name = "FBK" + wb.hw_model = HardwareModel.TLORA_V2 + wb.public_key = competingKey + } + .build(), publicKey = competingKey, ) } @@ -1841,13 +2317,15 @@ class NodeManagerImplTest { Node( num = preferredNum, user = - User( - id = userId, - long_name = "Preferred", - short_name = "PRE", - hw_model = HardwareModel.TLORA_V2, - public_key = canonicalKey, - ), + User.Builder() + .also { wb -> + wb.id = userId + wb.long_name = "Preferred" + wb.short_name = "PRE" + wb.hw_model = HardwareModel.TLORA_V2 + wb.public_key = canonicalKey + } + .build(), publicKey = canonicalKey, ) } @@ -1855,26 +2333,30 @@ class NodeManagerImplTest { Node( num = staleNum, user = - User( - id = userId, - long_name = "Stale", - short_name = "STL", - hw_model = HardwareModel.TLORA_V2, - public_key = canonicalKey, - ), + User.Builder() + .also { wb -> + wb.id = userId + wb.long_name = "Stale" + wb.short_name = "STL" + wb.hw_model = HardwareModel.TLORA_V2 + wb.public_key = canonicalKey + } + .build(), publicKey = canonicalKey, ) } nodeManager.handleReceivedUser( staleNum, - User( - id = userId, - long_name = "Stale Replay", - short_name = "STL", - hw_model = HardwareModel.TLORA_V2, - public_key = canonicalKey, - ), + User.Builder() + .also { wb -> + wb.id = userId + wb.long_name = "Stale Replay" + wb.short_name = "STL" + wb.hw_model = HardwareModel.TLORA_V2 + wb.public_key = canonicalKey + } + .build(), ) assertNull(nodeManager.nodeDBbyNodeNum[staleNum]) @@ -1897,20 +2379,43 @@ class NodeManagerImplTest { nodeManager.updateNode(localNum) { Node( num = localNum, - user = User(id = userId, long_name = "Local", short_name = "LOC", hw_model = HardwareModel.TLORA_V2), + user = + User.Builder() + .also { wb -> + wb.id = userId + wb.long_name = "Local" + wb.short_name = "LOC" + wb.hw_model = HardwareModel.TLORA_V2 + } + .build(), ) } nodeManager.updateNode(remoteNum) { Node( num = remoteNum, - user = User(id = userId, long_name = "Remote", short_name = "REM", hw_model = HardwareModel.TLORA_V2), + user = + User.Builder() + .also { wb -> + wb.id = userId + wb.long_name = "Remote" + wb.short_name = "REM" + wb.hw_model = HardwareModel.TLORA_V2 + } + .build(), ) } assertEquals(remoteNum, nodeManager.getNodeById(userId)!!.num) nodeManager.handleReceivedUser( localNum, - User(id = userId, long_name = "Local Updated", short_name = "LOC", hw_model = HardwareModel.TLORA_V2), + User.Builder() + .also { wb -> + wb.id = userId + wb.long_name = "Local Updated" + wb.short_name = "LOC" + wb.hw_model = HardwareModel.TLORA_V2 + } + .build(), ) assertNotNull(nodeManager.nodeDBbyNodeNum[remoteNum]) @@ -1925,7 +2430,15 @@ class NodeManagerImplTest { fun `notification has exact title message ID category and deep link`() { enableDbWrites() val nodeNum = 1234 - val user = User(id = "!test", long_name = "Test User", short_name = "TST", hw_model = HardwareModel.TLORA_V2) + val user = + User.Builder() + .also { wb -> + wb.id = "!test" + wb.long_name = "Test User" + wb.short_name = "TST" + wb.hw_model = HardwareModel.TLORA_V2 + } + .build() nodeManager.handleReceivedUser(nodeNum, user) testScope.advanceUntilIdle() @@ -1958,14 +2471,22 @@ class NodeManagerImplTest { Node( num = canonicalNum, user = - User( - id = placeholderId, - long_name = "Meshtastic ${placeholderId.takeLast(4)}", - short_name = placeholderId.takeLast(4), - hw_model = HardwareModel.UNSET, - ), + User.Builder() + .also { wb -> + wb.id = placeholderId + wb.long_name = "Meshtastic ${placeholderId.takeLast(4)}" + wb.short_name = placeholderId.takeLast(4) + wb.hw_model = HardwareModel.UNSET + } + .build(), publicKey = ByteString.EMPTY, - position = ProtoPosition(latitude_i = 111, longitude_i = 222), + position = + ProtoPosition.Builder() + .also { wb -> + wb.latitude_i = 111 + wb.longitude_i = 222 + } + .build(), channel = 4, ) } @@ -1973,13 +2494,15 @@ class NodeManagerImplTest { nodeManager.handleReceivedUser( canonicalNum, - User( - id = "!canonical", - long_name = "Canonical", - short_name = "CAN", - hw_model = HardwareModel.TLORA_V2, - public_key = canonicalKey, - ), + User.Builder() + .also { wb -> + wb.id = "!canonical" + wb.long_name = "Canonical" + wb.short_name = "CAN" + wb.hw_model = HardwareModel.TLORA_V2 + wb.public_key = canonicalKey + } + .build(), ) testScope.advanceUntilIdle() @@ -2008,13 +2531,15 @@ class NodeManagerImplTest { nodeManager.handleReceivedUser( canonicalNum, - User( - id = "!fresh", - long_name = "Fresh Canonical", - short_name = "FRS", - hw_model = HardwareModel.TLORA_V2, - public_key = canonicalKey, - ), + User.Builder() + .also { wb -> + wb.id = "!fresh" + wb.long_name = "Fresh Canonical" + wb.short_name = "FRS" + wb.hw_model = HardwareModel.TLORA_V2 + wb.public_key = canonicalKey + } + .build(), ) testScope.advanceUntilIdle() @@ -2044,13 +2569,15 @@ class NodeManagerImplTest { nodeManager.handleReceivedUser( a, - User( - id = "!a", - long_name = "Alpha Packet", - short_name = "ALA", - hw_model = HardwareModel.TLORA_V2, - public_key = key, - ), + User.Builder() + .also { wb -> + wb.id = "!a" + wb.long_name = "Alpha Packet" + wb.short_name = "ALA" + wb.hw_model = HardwareModel.TLORA_V2 + wb.public_key = key + } + .build(), ) testScope.advanceUntilIdle() @@ -2081,13 +2608,15 @@ class NodeManagerImplTest { nodeManager.handleReceivedUser( canonicalNum, - User( - id = "!c", - long_name = "Claimant", - short_name = "CLM", - hw_model = HardwareModel.TLORA_V2, - public_key = canonicalKey, - ), + User.Builder() + .also { wb -> + wb.id = "!c" + wb.long_name = "Claimant" + wb.short_name = "CLM" + wb.hw_model = HardwareModel.TLORA_V2 + wb.public_key = canonicalKey + } + .build(), ) testScope.advanceUntilIdle() @@ -2120,13 +2649,15 @@ class NodeManagerImplTest { nodeManager.handleReceivedUser( newLocalNum, - User( - id = "!local9999", - long_name = "Current Local", - short_name = "LOC", - hw_model = HardwareModel.TLORA_V2, - public_key = key, - ), + User.Builder() + .also { wb -> + wb.id = "!local9999" + wb.long_name = "Current Local" + wb.short_name = "LOC" + wb.hw_model = HardwareModel.TLORA_V2 + wb.public_key = key + } + .build(), ) testScope.advanceUntilIdle() @@ -2150,13 +2681,15 @@ class NodeManagerImplTest { nodeManager.handleReceivedUser( num, - User( - id = "!session-reset", - long_name = "Previous Session", - short_name = "OLD", - hw_model = HardwareModel.TLORA_V2, - public_key = key, - ), + User.Builder() + .also { wb -> + wb.id = "!session-reset" + wb.long_name = "Previous Session" + wb.short_name = "OLD" + wb.hw_model = HardwareModel.TLORA_V2 + wb.public_key = key + } + .build(), ) testScope.advanceUntilIdle() @@ -2173,14 +2706,26 @@ class NodeManagerImplTest { val sharedId = "!shared" val replacementId = "!replacement" var index = NodeManagerImpl.NodeIndex() - index = index.put(1, makeKnownNode(1, keyA).copy(user = makeKnownNode(1, keyA).user.copy(id = sharedId))) - index = index.put(2, makeKnownNode(2, keyA).copy(user = makeKnownNode(2, keyA).user.copy(id = sharedId))) + index = + index.put( + 1, + makeKnownNode(1, keyA) + .copy(user = makeKnownNode(1, keyA).user.newBuilder().also { wb -> wb.id = sharedId }.build()), + ) + index = + index.put( + 2, + makeKnownNode(2, keyA) + .copy(user = makeKnownNode(2, keyA).user.newBuilder().also { wb -> wb.id = sharedId }.build()), + ) assertEquals?>(setOf(1, 2), index.candidateNumsById[sharedId]) assertEquals?>(setOf(1, 2), index.candidateNumsByPublicKey[keyA]) assertEquals(1, index.byId[sharedId]?.num) - val replacement = makeKnownNode(1, keyB).copy(user = makeKnownNode(1, keyB).user.copy(id = replacementId)) + val replacement = + makeKnownNode(1, keyB) + .copy(user = makeKnownNode(1, keyB).user.newBuilder().also { wb -> wb.id = replacementId }.build()) index = index.put(1, replacement) assertEquals?>(setOf(2), index.candidateNumsById[sharedId]) assertEquals?>(setOf(1), index.candidateNumsById[replacementId]) @@ -2198,13 +2743,15 @@ class NodeManagerImplTest { private fun Int.toHex(): String = this.toString(16).padStart(8, '0') - private fun userWithKey(key: ByteString, longName: String, shortName: String): User = User( - id = key.hex(), - long_name = longName, - short_name = shortName, - hw_model = HardwareModel.TLORA_V2, - public_key = key, - ) + private fun userWithKey(key: ByteString, longName: String, shortName: String): User = User.Builder() + .also { wb -> + wb.id = key.hex() + wb.long_name = longName + wb.short_name = shortName + wb.hw_model = HardwareModel.TLORA_V2 + wb.public_key = key + } + .build() // ---------- Retired-node notification behavior ---------- diff --git a/core/data/src/commonTest/kotlin/org/meshtastic/core/data/manager/PacketHandlerImplTest.kt b/core/data/src/commonTest/kotlin/org/meshtastic/core/data/manager/PacketHandlerImplTest.kt index 4a5ef0d87c..6f28c88d8e 100644 --- a/core/data/src/commonTest/kotlin/org/meshtastic/core/data/manager/PacketHandlerImplTest.kt +++ b/core/data/src/commonTest/kotlin/org/meshtastic/core/data/manager/PacketHandlerImplTest.kt @@ -134,7 +134,8 @@ class PacketHandlerImplTest { @Test fun `sendToRadio with ToRadio sends immediately`() { - val toRadio = ToRadio(packet = MeshPacket(id = 123)) + val toRadio = + ToRadio.Builder().also { wb -> wb.packet = MeshPacket.Builder().also { wb -> wb.id = 123 }.build() }.build() handler.sendToRadio(toRadio) @@ -143,7 +144,8 @@ class PacketHandlerImplTest { @Test fun `trySendToRadio reports direct transport admission`() { - val toRadio = ToRadio(packet = MeshPacket(id = 124)) + val toRadio = + ToRadio.Builder().also { wb -> wb.packet = MeshPacket.Builder().also { wb -> wb.id = 124 }.build() }.build() assertTrue(handler.trySendToRadio(toRadio)) @@ -154,17 +156,19 @@ class PacketHandlerImplTest { @Test fun `sendToRadio updates status using the full outgoing packet identity`() = runTest(testDispatcher) { val packet = - MeshPacket( - from = 0x11111111, - to = 0x22222222, - id = 123, - decoded = Data(portnum = PortNum.TEXT_MESSAGE_APP), - ) + MeshPacket.Builder() + .also { wb -> + wb.from = 0x11111111 + wb.to = 0x22222222 + wb.id = 123 + wb.decoded = Data.Builder().also { wb -> wb.portnum = PortNum.TEXT_MESSAGE_APP }.build() + } + .build() everySuspend { packetRepository.applyOutgoingQueueStatus(packet, MessageStatus.ENROUTE) } returns PersistedPacket(PERSISTED_ID, storedTextPacket(id = 123, status = MessageStatus.QUEUED)) - handler.sendToRadio(ToRadio(packet = packet)) + handler.sendToRadio(ToRadio.Builder().also { wb -> wb.packet = packet }.build()) testScheduler.runCurrent() verifySuspend { packetRepository.applyOutgoingQueueStatus(packet, MessageStatus.ENROUTE) } @@ -172,7 +176,7 @@ class PacketHandlerImplTest { @Test fun `sendToRadio with MeshPacket queues and sends when connected`() = runTest(testDispatcher) { - val packet = MeshPacket(id = 456) + val packet = MeshPacket.Builder().also { wb -> wb.id = 456 }.build() connectionStateFlow.value = ConnectionState.Connected handler.sendToRadio(packet) @@ -191,7 +195,11 @@ class PacketHandlerImplTest { lifecycleFlow.value = ConnectionLifecycle(version = 13, state = ConnectionState.Connected) - val accepted = ownedHandler.sendToRadioForConnection(MeshPacket(id = 460), capturedVersion) + val accepted = + ownedHandler.sendToRadioForConnection( + MeshPacket.Builder().also { wb -> wb.id = 460 }.build(), + capturedVersion, + ) assertFalse(accepted) verify(exactly(0)) { radioInterfaceService.trySendToRadio(any()) } @@ -205,12 +213,25 @@ class PacketHandlerImplTest { val ownedHandler = handlerWithScope(testScope) val capturedVersion = lifecycleFlow.value.version - assertTrue(ownedHandler.sendToRadio(MeshPacket(id = 460))) + assertTrue(ownedHandler.sendToRadio(MeshPacket.Builder().also { wb -> wb.id = 460 }.build())) testScheduler.runCurrent() - assertTrue(ownedHandler.sendToRadioForConnection(MeshPacket(id = 461), capturedVersion)) + assertTrue( + ownedHandler.sendToRadioForConnection( + MeshPacket.Builder().also { wb -> wb.id = 461 }.build(), + capturedVersion, + ), + ) lifecycleFlow.value = ConnectionLifecycle(version = 13, state = ConnectionState.Connected) - ownedHandler.handleQueueStatus(QueueStatus(mesh_packet_id = 460, res = 0, free = 16)) + ownedHandler.handleQueueStatus( + QueueStatus.Builder() + .also { wb -> + wb.mesh_packet_id = 460 + wb.res = 0 + wb.free = 16 + } + .build(), + ) testScheduler.runCurrent() verify(exactly(1)) { radioInterfaceService.trySendToRadio(any()) } @@ -222,7 +243,8 @@ class PacketHandlerImplTest { stoppedScope.cancel() val stoppedHandler = handlerWithScope(stoppedScope) - val result = stoppedHandler.sendToRadioAndAwaitResult(MeshPacket(id = 457)) + val result = + stoppedHandler.sendToRadioAndAwaitResult(MeshPacket.Builder().also { wb -> wb.id = 457 }.build()) assertEquals(AwaitedSendStatus.TRANSPORT_STOPPED, result.status) assertFalse(result.dispatched) @@ -234,7 +256,7 @@ class PacketHandlerImplTest { stoppedScope.cancel() val stoppedHandler = handlerWithScope(stoppedScope) - val accepted = stoppedHandler.sendToRadio(MeshPacket(id = 459)) + val accepted = stoppedHandler.sendToRadio(MeshPacket.Builder().also { wb -> wb.id = 459 }.build()) assertFalse(accepted) verify(exactly(0)) { radioInterfaceService.trySendToRadio(any()) } @@ -248,7 +270,7 @@ class PacketHandlerImplTest { val result = async(start = CoroutineStart.UNDISPATCHED) { - stoppedHandler.sendToRadioAndAwaitResult(MeshPacket(id = 458)) + stoppedHandler.sendToRadioAndAwaitResult(MeshPacket.Builder().also { wb -> wb.id = 458 }.build()) } serviceScope.cancel() testScheduler.runCurrent() @@ -260,18 +282,21 @@ class PacketHandlerImplTest { @Test fun `handleQueueStatus completes deferred`() = runTest(testDispatcher) { - val packet = MeshPacket(id = 789) + val packet = MeshPacket.Builder().also { wb -> wb.id = 789 }.build() connectionStateFlow.value = ConnectionState.Connected handler.sendToRadio(packet) testScheduler.runCurrent() val status = - QueueStatus( - mesh_packet_id = 789, - res = 0, // Success - free = 1, - ) + QueueStatus.Builder() + .also { wb -> + wb.mesh_packet_id = 789 + wb.res = 0 + // Success + wb.free = 1 + } + .build() handler.handleQueueStatus(status) testScheduler.runCurrent() @@ -283,10 +308,18 @@ class PacketHandlerImplTest { // through the synchronous local loopback — a success, not a queue failure. connectionStateFlow.value = ConnectionState.Connected - val result = async { handler.sendToRadioAndAwait(MeshPacket(id = 790)) } + val result = async { handler.sendToRadioAndAwait(MeshPacket.Builder().also { wb -> wb.id = 790 }.build()) } testScheduler.runCurrent() - handler.handleQueueStatus(QueueStatus(mesh_packet_id = 790, res = 35, free = 16)) + handler.handleQueueStatus( + QueueStatus.Builder() + .also { wb -> + wb.mesh_packet_id = 790 + wb.res = 35 + wb.free = 16 + } + .build(), + ) testScheduler.runCurrent() assertTrue(result.await()) @@ -299,10 +332,18 @@ class PacketHandlerImplTest { // this fix targets). Only the plain res=0 "accepted, now full" echo should be skipped. connectionStateFlow.value = ConnectionState.Connected - val result = async { handler.sendToRadioAndAwait(MeshPacket(id = 792)) } + val result = async { handler.sendToRadioAndAwait(MeshPacket.Builder().also { wb -> wb.id = 792 }.build()) } testScheduler.runCurrent() - handler.handleQueueStatus(QueueStatus(mesh_packet_id = 792, res = 35, free = 0)) + handler.handleQueueStatus( + QueueStatus.Builder() + .also { wb -> + wb.mesh_packet_id = 792 + wb.res = 35 + wb.free = 0 + } + .build(), + ) testScheduler.runCurrent() assertTrue(result.await()) @@ -311,10 +352,20 @@ class PacketHandlerImplTest { @Test fun `packet-specific accepted full queue status completes the matching queue stage`() = runTest(testDispatcher) { connectionStateFlow.value = ConnectionState.Connected - val result = async { handler.sendToRadioAndAwaitResult(MeshPacket(id = 793)) } + val result = async { + handler.sendToRadioAndAwaitResult(MeshPacket.Builder().also { wb -> wb.id = 793 }.build()) + } testScheduler.runCurrent() - handler.handleQueueStatus(QueueStatus(mesh_packet_id = 793, res = 0, free = 0)) + handler.handleQueueStatus( + QueueStatus.Builder() + .also { wb -> + wb.mesh_packet_id = 793 + wb.res = 0 + wb.free = 0 + } + .build(), + ) testScheduler.runCurrent() assertFalse(result.isCompleted, "queue admission must not satisfy the strict routing waiter") @@ -326,10 +377,20 @@ class PacketHandlerImplTest { fun `strict await treats queue rejection as failure`() = runTest(testDispatcher) { connectionStateFlow.value = ConnectionState.Connected - val result = async { handler.sendToRadioAndAwaitResult(MeshPacket(id = 791)) } + val result = async { + handler.sendToRadioAndAwaitResult(MeshPacket.Builder().also { wb -> wb.id = 791 }.build()) + } testScheduler.runCurrent() - handler.handleQueueStatus(QueueStatus(mesh_packet_id = 791, res = 33, free = 16)) + handler.handleQueueStatus( + QueueStatus.Builder() + .also { wb -> + wb.mesh_packet_id = 791 + wb.res = 33 + wb.free = 16 + } + .build(), + ) testScheduler.runCurrent() val rejected = result.await() @@ -341,9 +402,11 @@ class PacketHandlerImplTest { @Test fun `await response timeout starts after earlier queued packets`() = runTest(testDispatcher) { connectionStateFlow.value = ConnectionState.Connected - handler.sendToRadio(MeshPacket(id = 800)) - handler.sendToRadio(MeshPacket(id = 801)) - val result = async { handler.sendToRadioAndAwaitResult(MeshPacket(id = 802)) } + handler.sendToRadio(MeshPacket.Builder().also { wb -> wb.id = 800 }.build()) + handler.sendToRadio(MeshPacket.Builder().also { wb -> wb.id = 801 }.build()) + val result = async { + handler.sendToRadioAndAwaitResult(MeshPacket.Builder().also { wb -> wb.id = 802 }.build()) + } testScheduler.runCurrent() // Let both earlier packets consume their full response windows. The awaited packet has not timed out @@ -356,7 +419,15 @@ class PacketHandlerImplTest { testScheduler.runCurrent() assertFalse(result.isCompleted) - handler.handleQueueStatus(QueueStatus(mesh_packet_id = 802, res = 0, free = 16)) + handler.handleQueueStatus( + QueueStatus.Builder() + .also { wb -> + wb.mesh_packet_id = 802 + wb.res = 0 + wb.free = 16 + } + .build(), + ) testScheduler.runCurrent() assertFalse(result.isCompleted, "QueueStatus must not satisfy the strict routing waiter") handler.completeDispatchedResponse(dataRequestId = 802, complete = true) @@ -371,10 +442,15 @@ class PacketHandlerImplTest { val storedPacket = storedTextPacket(id = 804, status = MessageStatus.QUEUED) everySuspend { packetRepository.applyOutgoingQueueStatus(any(), any()) } returns PersistedPacket(PERSISTED_ID, storedPacket) - handler.sendToRadio(MeshPacket(id = 803)) + handler.sendToRadio(MeshPacket.Builder().also { wb -> wb.id = 803 }.build()) val result = async { handler.sendToRadioAndAwaitResult( - MeshPacket(id = 804, decoded = Data(portnum = PortNum.TEXT_MESSAGE_APP)), + MeshPacket.Builder() + .also { wb -> + wb.id = 804 + wb.decoded = Data.Builder().also { wb -> wb.portnum = PortNum.TEXT_MESSAGE_APP }.build() + } + .build(), ) } testScheduler.runCurrent() @@ -391,8 +467,10 @@ class PacketHandlerImplTest { @Test fun `queue stop skips persistence lookup for a non-persisted packet`() = runTest(testDispatcher) { connectionStateFlow.value = ConnectionState.Connected - handler.sendToRadio(MeshPacket(id = 820)) - val queued = async { handler.sendToRadioAndAwaitResult(MeshPacket(id = 821)) } + handler.sendToRadio(MeshPacket.Builder().also { wb -> wb.id = 820 }.build()) + val queued = async { + handler.sendToRadioAndAwaitResult(MeshPacket.Builder().also { wb -> wb.id = 821 }.build()) + } testScheduler.runCurrent() handler.stopPacketQueue() @@ -412,10 +490,21 @@ class PacketHandlerImplTest { lookups++ if (lookups == 1) null else PersistedPacket(PERSISTED_ID, storedPacket) } - handler.sendToRadio(MeshPacket(id = 820)) + handler.sendToRadio(MeshPacket.Builder().also { wb -> wb.id = 820 }.build()) val queued = async { handler.sendToRadioAndAwaitResult( - MeshPacket(id = 822, decoded = Data(portnum = PortNum.TEXT_MESSAGE_APP, emoji = 1)), + MeshPacket.Builder() + .also { wb -> + wb.id = 822 + wb.decoded = + Data.Builder() + .also { wb -> + wb.portnum = PortNum.TEXT_MESSAGE_APP + wb.emoji = 1 + } + .build() + } + .build(), ) } testScheduler.runCurrent() @@ -437,7 +526,7 @@ class PacketHandlerImplTest { val storedReaction = Reaction( replyId = 1, - user = User(id = "!00000001"), + user = User.Builder().also { wb -> wb.id = "!00000001" }.build(), emoji = "👍", timestamp = 0, snr = null, @@ -453,10 +542,22 @@ class PacketHandlerImplTest { if (lookups == 1) null else PersistedReaction(PERSISTED_REACTION_ID, storedReaction) } - handler.sendToRadio(MeshPacket(id = 820)) + handler.sendToRadio(MeshPacket.Builder().also { wb -> wb.id = 820 }.build()) val queued = async { handler.sendToRadioAndAwaitResult( - MeshPacket(id = 825, decoded = Data(portnum = PortNum.TEXT_MESSAGE_APP, reply_id = 1, emoji = 1)), + MeshPacket.Builder() + .also { wb -> + wb.id = 825 + wb.decoded = + Data.Builder() + .also { wb -> + wb.portnum = PortNum.TEXT_MESSAGE_APP + wb.reply_id = 1 + wb.emoji = 1 + } + .build() + } + .build(), ) } testScheduler.runCurrent() @@ -477,9 +578,24 @@ class PacketHandlerImplTest { everySuspend { packetRepository.applyOutgoingQueueStatus(any(), any()) } returns PersistedPacket(PERSISTED_ID, storedPacket) - handler.sendToRadio(MeshPacket(id = 823, decoded = Data(portnum = PortNum.TEXT_MESSAGE_APP))) + handler.sendToRadio( + MeshPacket.Builder() + .also { wb -> + wb.id = 823 + wb.decoded = Data.Builder().also { wb -> wb.portnum = PortNum.TEXT_MESSAGE_APP }.build() + } + .build(), + ) testScheduler.runCurrent() - handler.handleQueueStatus(QueueStatus(mesh_packet_id = 823, res = 0, free = 16)) + handler.handleQueueStatus( + QueueStatus.Builder() + .also { wb -> + wb.mesh_packet_id = 823 + wb.res = 0 + wb.free = 16 + } + .build(), + ) testScheduler.runCurrent() testScheduler.advanceTimeBy(PacketHandlerImpl.SEND_ACK_TIMEOUT + 1.seconds) @@ -494,10 +610,15 @@ class PacketHandlerImplTest { everySuspend { packetRepository.applyOutgoingQueueStatus(any(), any()) } returns PersistedPacket(PERSISTED_ID, storedPacket) - handler.sendToRadio(MeshPacket(id = 820)) + handler.sendToRadio(MeshPacket.Builder().also { wb -> wb.id = 820 }.build()) val queued = async { handler.sendToRadioAndAwaitResult( - MeshPacket(id = 824, decoded = Data(portnum = PortNum.TEXT_MESSAGE_APP)), + MeshPacket.Builder() + .also { wb -> + wb.id = 824 + wb.decoded = Data.Builder().also { wb -> wb.portnum = PortNum.TEXT_MESSAGE_APP }.build() + } + .build(), ) } testScheduler.runCurrent() @@ -520,7 +641,12 @@ class PacketHandlerImplTest { PersistedPacket(PERSISTED_ID, storedPacket) val result = async { handler.sendToRadioAndAwaitResult( - MeshPacket(id = 807, decoded = Data(portnum = PortNum.TEXT_MESSAGE_APP)), + MeshPacket.Builder() + .also { wb -> + wb.id = 807 + wb.decoded = Data.Builder().also { wb -> wb.portnum = PortNum.TEXT_MESSAGE_APP }.build() + } + .build(), ) } testScheduler.runCurrent() @@ -542,12 +668,25 @@ class PacketHandlerImplTest { PersistedPacket(PERSISTED_ID, storedPacket) val result = async { handler.sendToRadioAndAwaitResult( - MeshPacket(id = 817, decoded = Data(portnum = PortNum.TEXT_MESSAGE_APP)), + MeshPacket.Builder() + .also { wb -> + wb.id = 817 + wb.decoded = Data.Builder().also { wb -> wb.portnum = PortNum.TEXT_MESSAGE_APP }.build() + } + .build(), ) } testScheduler.runCurrent() - handler.handleQueueStatus(QueueStatus(mesh_packet_id = 817, res = 0, free = 16)) + handler.handleQueueStatus( + QueueStatus.Builder() + .also { wb -> + wb.mesh_packet_id = 817 + wb.res = 0 + wb.free = 16 + } + .build(), + ) handler.stopPacketQueue() testScheduler.runCurrent() @@ -560,12 +699,24 @@ class PacketHandlerImplTest { @Test fun `disconnect drains queued responses without restarting the processor`() = runTest(testDispatcher) { connectionStateFlow.value = ConnectionState.Connected - val first = async { handler.sendToRadioAndAwaitResult(MeshPacket(id = 813)) } - val queued = async { handler.sendToRadioAndAwaitResult(MeshPacket(id = 814)) } + val first = async { + handler.sendToRadioAndAwaitResult(MeshPacket.Builder().also { wb -> wb.id = 813 }.build()) + } + val queued = async { + handler.sendToRadioAndAwaitResult(MeshPacket.Builder().also { wb -> wb.id = 814 }.build()) + } testScheduler.runCurrent() connectionStateFlow.value = ConnectionState.Disconnected - handler.handleQueueStatus(QueueStatus(mesh_packet_id = 813, res = 0, free = 16)) + handler.handleQueueStatus( + QueueStatus.Builder() + .also { wb -> + wb.mesh_packet_id = 813 + wb.res = 0 + wb.free = 16 + } + .build(), + ) testScheduler.runCurrent() val firstStopped = first.await() @@ -580,7 +731,9 @@ class PacketHandlerImplTest { @Test fun `missing queue status does not terminate an admitted routing waiter`() = runTest(testDispatcher) { connectionStateFlow.value = ConnectionState.Connected - val result = async { handler.sendToRadioAndAwaitResult(MeshPacket(id = 805)) } + val result = async { + handler.sendToRadioAndAwaitResult(MeshPacket.Builder().also { wb -> wb.id = 805 }.build()) + } testScheduler.runCurrent() testScheduler.advanceTimeBy(responseTimeoutCrossingMillis) @@ -594,14 +747,24 @@ class PacketHandlerImplTest { @Test fun `late queue rejection still terminates a strict waiter after confirmation timeout`() = runTest(testDispatcher) { connectionStateFlow.value = ConnectionState.Connected - val result = async { handler.sendToRadioAndAwaitResult(MeshPacket(id = 824)) } + val result = async { + handler.sendToRadioAndAwaitResult(MeshPacket.Builder().also { wb -> wb.id = 824 }.build()) + } testScheduler.runCurrent() testScheduler.advanceTimeBy(responseTimeoutCrossingMillis) testScheduler.runCurrent() assertFalse(result.isCompleted) - handler.handleQueueStatus(QueueStatus(mesh_packet_id = 824, res = 33, free = 16)) + handler.handleQueueStatus( + QueueStatus.Builder() + .also { wb -> + wb.mesh_packet_id = 824 + wb.res = 33 + wb.free = 16 + } + .build(), + ) testScheduler.runCurrent() val rejected = result.await() @@ -612,7 +775,13 @@ class PacketHandlerImplTest { @Test fun `missing queue status does not fail a dispatched persisted packet`() = runTest(testDispatcher) { connectionStateFlow.value = ConnectionState.Connected - val packet = MeshPacket(id = 806, decoded = Data(portnum = PortNum.TEXT_MESSAGE_APP)) + val packet = + MeshPacket.Builder() + .also { wb -> + wb.id = 806 + wb.decoded = Data.Builder().also { wb -> wb.portnum = PortNum.TEXT_MESSAGE_APP }.build() + } + .build() everySuspend { packetRepository.applyOutgoingQueueStatus(packet, MessageStatus.ENROUTE) } returns PersistedPacket(PERSISTED_ID, storedTextPacket(id = 806, status = MessageStatus.QUEUED)) @@ -627,7 +796,9 @@ class PacketHandlerImplTest { @Test fun `disconnected queue admission reports transport stopped`() = runTest(testDispatcher) { - val result = async { handler.sendToRadioAndAwaitResult(MeshPacket(id = 805)) } + val result = async { + handler.sendToRadioAndAwaitResult(MeshPacket.Builder().also { wb -> wb.id = 805 }.build()) + } testScheduler.runCurrent() val stopped = result.await() @@ -638,7 +809,7 @@ class PacketHandlerImplTest { @Test fun `disconnected fire and forget admission is rejected`() = runTest(testDispatcher) { - assertFalse(handler.sendToRadio(MeshPacket(id = 806))) + assertFalse(handler.sendToRadio(MeshPacket.Builder().also { wb -> wb.id = 806 }.build())) verify(exactly(0)) { radioInterfaceService.trySendToRadio(any()) } } @@ -652,7 +823,12 @@ class PacketHandlerImplTest { PersistedPacket(PERSISTED_ID, storedPacket) val result = async { handler.sendToRadioAndAwaitResult( - MeshPacket(id = 808, decoded = Data(portnum = PortNum.TEXT_MESSAGE_APP)), + MeshPacket.Builder() + .also { wb -> + wb.id = 808 + wb.decoded = Data.Builder().also { wb -> wb.portnum = PortNum.TEXT_MESSAGE_APP }.build() + } + .build(), ) } testScheduler.runCurrent() @@ -668,7 +844,9 @@ class PacketHandlerImplTest { every { radioInterfaceService.trySendToRadio(any()) } throws IllegalStateException("test send failure") connectionStateFlow.value = ConnectionState.Connected - val result = async { handler.sendToRadioAndAwaitResult(MeshPacket(id = 806)) } + val result = async { + handler.sendToRadioAndAwaitResult(MeshPacket.Builder().also { wb -> wb.id = 806 }.build()) + } testScheduler.runCurrent() val failed = result.await() @@ -679,18 +857,38 @@ class PacketHandlerImplTest { @Test fun `queue status without a packet id does not satisfy strict routing waiters`() = runTest(testDispatcher) { connectionStateFlow.value = ConnectionState.Connected - val first = async { handler.sendToRadioAndAwaitResult(MeshPacket(id = 820)) } - val second = async { handler.sendToRadioAndAwaitResult(MeshPacket(id = 821)) } + val first = async { + handler.sendToRadioAndAwaitResult(MeshPacket.Builder().also { wb -> wb.id = 820 }.build()) + } + val second = async { + handler.sendToRadioAndAwaitResult(MeshPacket.Builder().also { wb -> wb.id = 821 }.build()) + } testScheduler.runCurrent() - handler.handleQueueStatus(QueueStatus(mesh_packet_id = 0, res = 0, free = 16)) + handler.handleQueueStatus( + QueueStatus.Builder() + .also { wb -> + wb.mesh_packet_id = 0 + wb.res = 0 + wb.free = 16 + } + .build(), + ) testScheduler.runCurrent() assertFalse(first.isCompleted, "QueueStatus must not satisfy the first routing waiter") assertFalse(second.isCompleted) verify(exactly(2)) { radioInterfaceService.trySendToRadio(any()) } - handler.handleQueueStatus(QueueStatus(mesh_packet_id = 0, res = 0, free = 16)) + handler.handleQueueStatus( + QueueStatus.Builder() + .also { wb -> + wb.mesh_packet_id = 0 + wb.res = 0 + wb.free = 16 + } + .build(), + ) testScheduler.runCurrent() assertFalse(second.isCompleted, "QueueStatus must not satisfy the second routing waiter") @@ -707,11 +905,19 @@ class PacketHandlerImplTest { handler.stopPacketQueue() testScheduler.runCurrent() - assertTrue(handler.sendToRadio(MeshPacket(id = 822))) + assertTrue(handler.sendToRadio(MeshPacket.Builder().also { wb -> wb.id = 822 }.build())) testScheduler.runCurrent() verify(exactly(1)) { radioInterfaceService.trySendToRadio(any()) } - handler.handleQueueStatus(QueueStatus(mesh_packet_id = 822, res = 0, free = 16)) + handler.handleQueueStatus( + QueueStatus.Builder() + .also { wb -> + wb.mesh_packet_id = 822 + wb.res = 0 + wb.free = 16 + } + .build(), + ) testScheduler.runCurrent() } @@ -731,10 +937,17 @@ class PacketHandlerImplTest { val interrupted = async { handler.sendToRadioAndAwaitResult( - MeshPacket(id = 815, decoded = Data(portnum = PortNum.TEXT_MESSAGE_APP)), + MeshPacket.Builder() + .also { wb -> + wb.id = 815 + wb.decoded = Data.Builder().also { wb -> wb.portnum = PortNum.TEXT_MESSAGE_APP }.build() + } + .build(), ) } - val queued = async { handler.sendToRadioAndAwaitResult(MeshPacket(id = 816)) } + val queued = async { + handler.sendToRadioAndAwaitResult(MeshPacket.Builder().also { wb -> wb.id = 816 }.build()) + } testScheduler.runCurrent() val stopped = interrupted.await() @@ -742,17 +955,33 @@ class PacketHandlerImplTest { assertFalse(stopped.dispatched) verifySuspend { packetRepository.applyOutgoingQueueStatus(any(), MessageStatus.ERROR) } - handler.handleQueueStatus(QueueStatus(mesh_packet_id = 816, res = 0, free = 16)) + handler.handleQueueStatus( + QueueStatus.Builder() + .also { wb -> + wb.mesh_packet_id = 816 + wb.res = 0 + wb.free = 16 + } + .build(), + ) testScheduler.runCurrent() handler.completeDispatchedResponse(dataRequestId = 816, complete = true) val accepted = queued.await() assertEquals(AwaitedSendStatus.ACCEPTED, accepted.status) assertTrue(accepted.dispatched) - assertTrue(handler.sendToRadio(MeshPacket(id = 815))) + assertTrue(handler.sendToRadio(MeshPacket.Builder().also { wb -> wb.id = 815 }.build())) testScheduler.runCurrent() verify(exactly(3)) { radioInterfaceService.trySendToRadio(any()) } - handler.handleQueueStatus(QueueStatus(mesh_packet_id = 815, res = 0, free = 16)) + handler.handleQueueStatus( + QueueStatus.Builder() + .also { wb -> + wb.mesh_packet_id = 815 + wb.res = 0 + wb.free = 16 + } + .build(), + ) testScheduler.runCurrent() } @@ -760,27 +989,40 @@ class PacketHandlerImplTest { fun `awaited packet without an id is rejected before dispatch`() = runTest(testDispatcher) { connectionStateFlow.value = ConnectionState.Connected - val result = handler.sendToRadioAndAwaitResult(MeshPacket()) + val result = handler.sendToRadioAndAwaitResult(MeshPacket.Builder().build()) assertEquals(AwaitedSendStatus.REJECTED, result.status) assertFalse(result.dispatched) - assertFalse(handler.sendToRadioAndAwait(MeshPacket()), "the Boolean compatibility API must map rejection") + assertFalse( + handler.sendToRadioAndAwait(MeshPacket.Builder().build()), + "the Boolean compatibility API must map rejection", + ) verify(exactly(0)) { radioInterfaceService.trySendToRadio(any()) } } @Test fun `duplicate awaited packet id is rejected without replacing the original waiter`() = runTest(testDispatcher) { connectionStateFlow.value = ConnectionState.Connected - val original = async { handler.sendToRadioAndAwaitResult(MeshPacket(id = 809)) } + val original = async { + handler.sendToRadioAndAwaitResult(MeshPacket.Builder().also { wb -> wb.id = 809 }.build()) + } testScheduler.runCurrent() - val duplicate = handler.sendToRadioAndAwaitResult(MeshPacket(id = 809)) + val duplicate = handler.sendToRadioAndAwaitResult(MeshPacket.Builder().also { wb -> wb.id = 809 }.build()) assertEquals(AwaitedSendStatus.REJECTED, duplicate.status) assertFalse(duplicate.dispatched) assertFalse(original.isCompleted) - handler.handleQueueStatus(QueueStatus(mesh_packet_id = 809, res = 0, free = 16)) + handler.handleQueueStatus( + QueueStatus.Builder() + .also { wb -> + wb.mesh_packet_id = 809 + wb.res = 0 + wb.free = 16 + } + .build(), + ) testScheduler.runCurrent() assertFalse(original.isCompleted) handler.completeDispatchedResponse(dataRequestId = 809, complete = true) @@ -793,24 +1035,50 @@ class PacketHandlerImplTest { @Test fun `response received before dispatch is ignored and retains the queued id`() = runTest(testDispatcher) { connectionStateFlow.value = ConnectionState.Connected - handler.sendToRadio(MeshPacket(id = 816)) - val queued = async { handler.sendToRadioAndAwaitResult(MeshPacket(id = 817)) } + handler.sendToRadio(MeshPacket.Builder().also { wb -> wb.id = 816 }.build()) + val queued = async { + handler.sendToRadioAndAwaitResult(MeshPacket.Builder().also { wb -> wb.id = 817 }.build()) + } testScheduler.runCurrent() - handler.handleQueueStatus(QueueStatus(mesh_packet_id = 817, res = 0, free = 16)) + handler.handleQueueStatus( + QueueStatus.Builder() + .also { wb -> + wb.mesh_packet_id = 817 + wb.res = 0 + wb.free = 16 + } + .build(), + ) handler.completeDispatchedResponse(dataRequestId = 817, complete = true) testScheduler.runCurrent() assertFalse(queued.isCompleted) - val duplicate = handler.sendToRadioAndAwaitResult(MeshPacket(id = 817)) + val duplicate = handler.sendToRadioAndAwaitResult(MeshPacket.Builder().also { wb -> wb.id = 817 }.build()) assertEquals(AwaitedSendStatus.REJECTED, duplicate.status) - handler.handleQueueStatus(QueueStatus(mesh_packet_id = 816, res = 0, free = 16)) + handler.handleQueueStatus( + QueueStatus.Builder() + .also { wb -> + wb.mesh_packet_id = 816 + wb.res = 0 + wb.free = 16 + } + .build(), + ) testScheduler.runCurrent() verify(exactly(2)) { radioInterfaceService.trySendToRadio(any()) } - handler.handleQueueStatus(QueueStatus(mesh_packet_id = 817, res = 0, free = 16)) + handler.handleQueueStatus( + QueueStatus.Builder() + .also { wb -> + wb.mesh_packet_id = 817 + wb.res = 0 + wb.free = 16 + } + .build(), + ) testScheduler.runCurrent() assertFalse(queued.isCompleted) handler.completeDispatchedResponse(dataRequestId = 817, complete = true) @@ -818,19 +1086,37 @@ class PacketHandlerImplTest { val accepted = queued.await() assertEquals(AwaitedSendStatus.ACCEPTED, accepted.status) assertTrue(accepted.dispatched) - assertTrue(handler.sendToRadio(MeshPacket(id = 817))) + assertTrue(handler.sendToRadio(MeshPacket.Builder().also { wb -> wb.id = 817 }.build())) testScheduler.runCurrent() - handler.handleQueueStatus(QueueStatus(mesh_packet_id = 817, res = 0, free = 16)) + handler.handleQueueStatus( + QueueStatus.Builder() + .also { wb -> + wb.mesh_packet_id = 817 + wb.res = 0 + wb.free = 16 + } + .build(), + ) testScheduler.runCurrent() } @Test fun `routing rejection after dispatch completes awaited response as radio rejected`() = runTest(testDispatcher) { connectionStateFlow.value = ConnectionState.Connected - val result = async { handler.sendToRadioAndAwaitResult(MeshPacket(id = 818)) } + val result = async { + handler.sendToRadioAndAwaitResult(MeshPacket.Builder().also { wb -> wb.id = 818 }.build()) + } testScheduler.runCurrent() - handler.handleQueueStatus(QueueStatus(mesh_packet_id = 818, res = 0, free = 16)) + handler.handleQueueStatus( + QueueStatus.Builder() + .also { wb -> + wb.mesh_packet_id = 818 + wb.res = 0 + wb.free = 16 + } + .build(), + ) testScheduler.runCurrent() handler.completeDispatchedResponse(dataRequestId = 818, complete = false) testScheduler.runCurrent() @@ -843,11 +1129,21 @@ class PacketHandlerImplTest { @Test fun `late queue status cannot replace an already completed response`() = runTest(testDispatcher) { connectionStateFlow.value = ConnectionState.Connected - val result = async { handler.sendToRadioAndAwaitResult(MeshPacket(id = 819)) } + val result = async { + handler.sendToRadioAndAwaitResult(MeshPacket.Builder().also { wb -> wb.id = 819 }.build()) + } testScheduler.runCurrent() handler.completeDispatchedResponse(dataRequestId = 819, complete = true) - handler.handleQueueStatus(QueueStatus(mesh_packet_id = 819, res = 33, free = 16)) + handler.handleQueueStatus( + QueueStatus.Builder() + .also { wb -> + wb.mesh_packet_id = 819 + wb.res = 33 + wb.free = 16 + } + .build(), + ) testScheduler.runCurrent() val accepted = result.await() @@ -858,12 +1154,14 @@ class PacketHandlerImplTest { @Test fun `cancelling an awaiting caller does not release its queued packet id`() = runTest(testDispatcher) { connectionStateFlow.value = ConnectionState.Connected - handler.sendToRadio(MeshPacket(id = 811)) - val awaiting = async { handler.sendToRadioAndAwaitResult(MeshPacket(id = 812)) } + handler.sendToRadio(MeshPacket.Builder().also { wb -> wb.id = 811 }.build()) + val awaiting = async { + handler.sendToRadioAndAwaitResult(MeshPacket.Builder().also { wb -> wb.id = 812 }.build()) + } testScheduler.runCurrent() awaiting.cancelAndJoin() - val duplicate = handler.sendToRadioAndAwaitResult(MeshPacket(id = 812)) + val duplicate = handler.sendToRadioAndAwaitResult(MeshPacket.Builder().also { wb -> wb.id = 812 }.build()) assertEquals(AwaitedSendStatus.REJECTED, duplicate.status) assertFalse(duplicate.dispatched) @@ -875,10 +1173,20 @@ class PacketHandlerImplTest { @Test fun `service owned routing expiry times out an active strict waiter`() = runTest(testDispatcher) { connectionStateFlow.value = ConnectionState.Connected - val result = async { handler.sendToRadioAndAwaitResult(MeshPacket(id = 812)) } + val result = async { + handler.sendToRadioAndAwaitResult(MeshPacket.Builder().also { wb -> wb.id = 812 }.build()) + } testScheduler.runCurrent() - handler.handleQueueStatus(QueueStatus(mesh_packet_id = 812, res = 0, free = 16)) + handler.handleQueueStatus( + QueueStatus.Builder() + .also { wb -> + wb.mesh_packet_id = 812 + wb.res = 0 + wb.free = 16 + } + .build(), + ) testScheduler.runCurrent() testScheduler.advanceTimeBy(PacketHandlerImpl.ROUTING_RESPONSE_TIMEOUT.inWholeMilliseconds) testScheduler.runCurrent() @@ -891,23 +1199,43 @@ class PacketHandlerImplTest { @Test fun `cancelled strict await releases its packet id after service owned routing expiry`() = runTest(testDispatcher) { connectionStateFlow.value = ConnectionState.Connected - val awaiting = async { handler.sendToRadioAndAwaitResult(MeshPacket(id = 812)) } + val awaiting = async { + handler.sendToRadioAndAwaitResult(MeshPacket.Builder().also { wb -> wb.id = 812 }.build()) + } testScheduler.runCurrent() - handler.handleQueueStatus(QueueStatus(mesh_packet_id = 812, res = 0, free = 16)) + handler.handleQueueStatus( + QueueStatus.Builder() + .also { wb -> + wb.mesh_packet_id = 812 + wb.res = 0 + wb.free = 16 + } + .build(), + ) testScheduler.runCurrent() awaiting.cancelAndJoin() - val duplicate = handler.sendToRadioAndAwaitResult(MeshPacket(id = 812)) + val duplicate = handler.sendToRadioAndAwaitResult(MeshPacket.Builder().also { wb -> wb.id = 812 }.build()) assertEquals(AwaitedSendStatus.REJECTED, duplicate.status) assertFalse(duplicate.dispatched) testScheduler.advanceTimeBy(PacketHandlerImpl.ROUTING_RESPONSE_TIMEOUT.inWholeMilliseconds) testScheduler.runCurrent() - val retry = async { handler.sendToRadioAndAwaitResult(MeshPacket(id = 812)) } + val retry = async { + handler.sendToRadioAndAwaitResult(MeshPacket.Builder().also { wb -> wb.id = 812 }.build()) + } testScheduler.runCurrent() - handler.handleQueueStatus(QueueStatus(mesh_packet_id = 812, res = 0, free = 16)) + handler.handleQueueStatus( + QueueStatus.Builder() + .also { wb -> + wb.mesh_packet_id = 812 + wb.res = 0 + wb.free = 16 + } + .build(), + ) handler.completeDispatchedResponse(dataRequestId = 812, complete = true) testScheduler.runCurrent() @@ -920,11 +1248,11 @@ class PacketHandlerImplTest { fun `fire and forget rejects invalid packet ids without throwing or replacing queued work`() = runTest(testDispatcher) { connectionStateFlow.value = ConnectionState.Connected - assertTrue(handler.sendToRadio(MeshPacket(id = 810))) + assertTrue(handler.sendToRadio(MeshPacket.Builder().also { wb -> wb.id = 810 }.build())) testScheduler.runCurrent() - assertFalse(handler.sendToRadio(MeshPacket(id = 810))) - assertFalse(handler.sendToRadio(MeshPacket())) + assertFalse(handler.sendToRadio(MeshPacket.Builder().also { wb -> wb.id = 810 }.build())) + assertFalse(handler.sendToRadio(MeshPacket.Builder().build())) testScheduler.runCurrent() verify(exactly(1)) { radioInterfaceService.trySendToRadio(any()) } @@ -936,13 +1264,21 @@ class PacketHandlerImplTest { @Test fun `completed packet id can be reused by a later retry`() = runTest(testDispatcher) { connectionStateFlow.value = ConnectionState.Connected - assertTrue(handler.sendToRadio(MeshPacket(id = 810))) + assertTrue(handler.sendToRadio(MeshPacket.Builder().also { wb -> wb.id = 810 }.build())) testScheduler.runCurrent() - handler.handleQueueStatus(QueueStatus(mesh_packet_id = 810, res = 0, free = 16)) + handler.handleQueueStatus( + QueueStatus.Builder() + .also { wb -> + wb.mesh_packet_id = 810 + wb.res = 0 + wb.free = 16 + } + .build(), + ) testScheduler.runCurrent() - assertTrue(handler.sendToRadio(MeshPacket(id = 810))) + assertTrue(handler.sendToRadio(MeshPacket.Builder().also { wb -> wb.id = 810 }.build())) testScheduler.runCurrent() verify(exactly(2)) { radioInterfaceService.trySendToRadio(any()) } @@ -953,7 +1289,7 @@ class PacketHandlerImplTest { @Test fun `strict await fails immediately while disconnected`() = runTest(testDispatcher) { - val result = handler.sendToRadioAndAwait(MeshPacket(id = 796)) + val result = handler.sendToRadioAndAwait(MeshPacket.Builder().also { wb -> wb.id = 796 }.build()) assertFalse(result) assertEquals(0, testScheduler.currentTime) @@ -964,7 +1300,7 @@ class PacketHandlerImplTest { connectionStateFlow.value = ConnectionState.Connected every { radioInterfaceService.trySendToRadio(any()) } throws IllegalStateException("transport failed") - val result = async { handler.sendToRadioAndAwait(MeshPacket(id = 797)) } + val result = async { handler.sendToRadioAndAwait(MeshPacket.Builder().also { wb -> wb.id = 797 }.build()) } testScheduler.runCurrent() assertTrue(result.isCompleted) @@ -975,10 +1311,18 @@ class PacketHandlerImplTest { fun `strict await does not complete on ordinary queue acceptance`() = runTest(testDispatcher) { connectionStateFlow.value = ConnectionState.Connected - val result = async { handler.sendToRadioAndAwait(MeshPacket(id = 793)) } + val result = async { handler.sendToRadioAndAwait(MeshPacket.Builder().also { wb -> wb.id = 793 }.build()) } testScheduler.runCurrent() - handler.handleQueueStatus(QueueStatus(mesh_packet_id = 793, res = 0, free = 16)) + handler.handleQueueStatus( + QueueStatus.Builder() + .also { wb -> + wb.mesh_packet_id = 793 + wb.res = 0 + wb.free = 16 + } + .build(), + ) testScheduler.runCurrent() assertFalse(result.isCompleted) @@ -990,10 +1334,20 @@ class PacketHandlerImplTest { @Test fun `strict routing wait outlives the local queue response window`() = runTest(testDispatcher) { connectionStateFlow.value = ConnectionState.Connected - val result = async { handler.sendToRadioAndAwaitResult(MeshPacket(id = 822)) } + val result = async { + handler.sendToRadioAndAwaitResult(MeshPacket.Builder().also { wb -> wb.id = 822 }.build()) + } testScheduler.runCurrent() - handler.handleQueueStatus(QueueStatus(mesh_packet_id = 822, res = 0, free = 16)) + handler.handleQueueStatus( + QueueStatus.Builder() + .also { wb -> + wb.mesh_packet_id = 822 + wb.res = 0 + wb.free = 16 + } + .build(), + ) testScheduler.runCurrent() testScheduler.advanceTimeBy(PacketHandlerImpl.RESPONSE_TIMEOUT + 1.seconds) testScheduler.runCurrent() @@ -1007,9 +1361,17 @@ class PacketHandlerImplTest { fun `strict await succeeds on routing ack`() = runTest(testDispatcher) { connectionStateFlow.value = ConnectionState.Connected - val result = async { handler.sendToRadioAndAwait(MeshPacket(id = 794)) } + val result = async { handler.sendToRadioAndAwait(MeshPacket.Builder().also { wb -> wb.id = 794 }.build()) } testScheduler.runCurrent() - handler.handleQueueStatus(QueueStatus(mesh_packet_id = 794, res = 0, free = 16)) + handler.handleQueueStatus( + QueueStatus.Builder() + .also { wb -> + wb.mesh_packet_id = 794 + wb.res = 0 + wb.free = 16 + } + .build(), + ) testScheduler.runCurrent() assertFalse(result.isCompleted) @@ -1023,15 +1385,35 @@ class PacketHandlerImplTest { fun `zero id synchronous loopback completes only the active queue entry`() = runTest(testDispatcher) { connectionStateFlow.value = ConnectionState.Connected - val awaitingRoutingAck = async { handler.sendToRadioAndAwait(MeshPacket(id = 798)) } + val awaitingRoutingAck = async { + handler.sendToRadioAndAwait(MeshPacket.Builder().also { wb -> wb.id = 798 }.build()) + } testScheduler.runCurrent() - handler.handleQueueStatus(QueueStatus(mesh_packet_id = 798, res = 0, free = 16)) + handler.handleQueueStatus( + QueueStatus.Builder() + .also { wb -> + wb.mesh_packet_id = 798 + wb.res = 0 + wb.free = 16 + } + .build(), + ) testScheduler.runCurrent() assertFalse(awaitingRoutingAck.isCompleted) - val synchronousLoopback = async { handler.sendToRadioAndAwait(MeshPacket(id = 799)) } + val synchronousLoopback = async { + handler.sendToRadioAndAwait(MeshPacket.Builder().also { wb -> wb.id = 799 }.build()) + } testScheduler.runCurrent() - handler.handleQueueStatus(QueueStatus(mesh_packet_id = 0, res = 35, free = 16)) + handler.handleQueueStatus( + QueueStatus.Builder() + .also { wb -> + wb.mesh_packet_id = 0 + wb.res = 35 + wb.free = 16 + } + .build(), + ) testScheduler.runCurrent() assertTrue(synchronousLoopback.await()) @@ -1045,9 +1427,17 @@ class PacketHandlerImplTest { fun `strict await fails on routing nak`() = runTest(testDispatcher) { connectionStateFlow.value = ConnectionState.Connected - val result = async { handler.sendToRadioAndAwait(MeshPacket(id = 795)) } + val result = async { handler.sendToRadioAndAwait(MeshPacket.Builder().also { wb -> wb.id = 795 }.build()) } testScheduler.runCurrent() - handler.handleQueueStatus(QueueStatus(mesh_packet_id = 795, res = 0, free = 16)) + handler.handleQueueStatus( + QueueStatus.Builder() + .also { wb -> + wb.mesh_packet_id = 795 + wb.res = 0 + wb.free = 16 + } + .build(), + ) testScheduler.runCurrent() handler.completeDispatchedResponse(795, complete = false) @@ -1058,7 +1448,14 @@ class PacketHandlerImplTest { @Test fun `handleQueueStatus property test`() = runTest(testDispatcher) { checkAll(Arb.int(0, 10), Arb.int(0, 32), Arb.int(0, 100000)) { res, free, packetId -> - val status = QueueStatus(res = res, free = free, mesh_packet_id = packetId) + val status = + QueueStatus.Builder() + .also { wb -> + wb.res = res + wb.free = free + wb.mesh_packet_id = packetId + } + .build() // Ensure it doesn't crash on any input handler.handleQueueStatus(status) @@ -1068,8 +1465,14 @@ class PacketHandlerImplTest { @Test fun `outgoing packets are logged with NODE_NUM_LOCAL`() = runTest(testDispatcher) { - val packet = MeshPacket(id = 123, decoded = Data(portnum = PortNum.TEXT_MESSAGE_APP)) - val toRadio = ToRadio(packet = packet) + val packet = + MeshPacket.Builder() + .also { wb -> + wb.id = 123 + wb.decoded = Data.Builder().also { wb -> wb.portnum = PortNum.TEXT_MESSAGE_APP }.build() + } + .build() + val toRadio = ToRadio.Builder().also { wb -> wb.packet = packet }.build() handler.sendToRadio(toRadio) testScheduler.runCurrent() @@ -1085,7 +1488,7 @@ class PacketHandlerImplTest { private fun reaction(id: Int, timestamp: Long = 0L, status: MessageStatus = MessageStatus.ENROUTE) = Reaction( replyId = 1, - user = User(id = "!00000001"), + user = User.Builder().also { wb -> wb.id = "!00000001" }.build(), emoji = "👍", timestamp = timestamp, snr = null, @@ -1095,10 +1498,26 @@ class PacketHandlerImplTest { status = status, ) - private fun outboundDataPacket(id: Int) = MeshPacket(id = id, decoded = Data(portnum = PortNum.TEXT_MESSAGE_APP)) + private fun outboundDataPacket(id: Int) = MeshPacket.Builder() + .also { wb -> + wb.id = id + wb.decoded = Data.Builder().also { wb -> wb.portnum = PortNum.TEXT_MESSAGE_APP }.build() + } + .build() - private fun outboundReaction(id: Int) = - MeshPacket(id = id, decoded = Data(portnum = PortNum.TEXT_MESSAGE_APP, reply_id = 1, emoji = 1)) + private fun outboundReaction(id: Int) = MeshPacket.Builder() + .also { wb -> + wb.id = id + wb.decoded = + Data.Builder() + .also { wb -> + wb.portnum = PortNum.TEXT_MESSAGE_APP + wb.reply_id = 1 + wb.emoji = 1 + } + .build() + } + .build() @Test fun `unacked ENROUTE send times out to a retryable ERROR TIMEOUT`() = runTest(testDispatcher) { @@ -1106,7 +1525,7 @@ class PacketHandlerImplTest { everySuspend { packetRepository.applyOutgoingQueueStatus(any(), any()) } returns PersistedPacket(PERSISTED_ID, enrouteDataPacket(123, status = MessageStatus.QUEUED)) - handler.sendToRadio(ToRadio(packet = outboundDataPacket(123))) + handler.sendToRadio(ToRadio.Builder().also { wb -> wb.packet = outboundDataPacket(123) }.build()) testScheduler.advanceTimeBy(PacketHandlerImpl.SEND_ACK_TIMEOUT + 1.seconds) testScheduler.runCurrent() @@ -1119,7 +1538,7 @@ class PacketHandlerImplTest { everySuspend { packetRepository.applyOutgoingQueueStatus(any(), any()) } returns PersistedPacket(PERSISTED_ID, enrouteDataPacket(124, status = MessageStatus.QUEUED)) - handler.sendToRadio(ToRadio(packet = outboundDataPacket(124))) + handler.sendToRadio(ToRadio.Builder().also { wb -> wb.packet = outboundDataPacket(124) }.build()) testScheduler.advanceTimeBy(PacketHandlerImpl.SEND_ACK_TIMEOUT - 1.seconds) testScheduler.runCurrent() @@ -1133,7 +1552,7 @@ class PacketHandlerImplTest { everySuspend { packetRepository.applyOutgoingReactionQueueStatus(125, MessageStatus.ENROUTE) } returns PersistedReaction(PERSISTED_REACTION_ID, queued) - handler.sendToRadio(ToRadio(packet = outboundReaction(125))) + handler.sendToRadio(ToRadio.Builder().also { wb -> wb.packet = outboundReaction(125) }.build()) testScheduler.runCurrent() testScheduler.advanceTimeBy(PacketHandlerImpl.SEND_ACK_TIMEOUT + 1.seconds) testScheduler.runCurrent() @@ -1217,7 +1636,7 @@ class PacketHandlerImplTest { everySuspend { packetRepository.getEnroutePackets() } returns listOf(PersistedPacket(PERSISTED_ID, packet)) everySuspend { packetRepository.getEnrouteReactions() } returns emptyList() - handler.sendToRadio(ToRadio(packet = outboundDataPacket(325))) + handler.sendToRadio(ToRadio.Builder().also { wb -> wb.packet = outboundDataPacket(325) }.build()) testScheduler.runCurrent() repeat(3) { handler.rearmSendAckTimeouts() diff --git a/core/data/src/commonTest/kotlin/org/meshtastic/core/data/manager/TelemetryPacketHandlerImplTest.kt b/core/data/src/commonTest/kotlin/org/meshtastic/core/data/manager/TelemetryPacketHandlerImplTest.kt index a072f79e9a..9199358a3d 100644 --- a/core/data/src/commonTest/kotlin/org/meshtastic/core/data/manager/TelemetryPacketHandlerImplTest.kt +++ b/core/data/src/commonTest/kotlin/org/meshtastic/core/data/manager/TelemetryPacketHandlerImplTest.kt @@ -46,9 +46,12 @@ import org.meshtastic.proto.EnvironmentMetrics import org.meshtastic.proto.MeshPacket import org.meshtastic.proto.PortNum import org.meshtastic.proto.PowerMetrics +import org.meshtastic.proto.SoilWaterMetrics import org.meshtastic.proto.Telemetry import kotlin.test.BeforeTest import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertNull @OptIn(ExperimentalCoroutinesApi::class) class TelemetryPacketHandlerImplTest { @@ -96,11 +99,19 @@ class TelemetryPacketHandlerImplTest { private fun makeTelemetryPacket(from: Int, telemetry: Telemetry): MeshPacket { val payload = telemetry.encode().toByteString() - return MeshPacket( - from = from, - decoded = Data(portnum = PortNum.TELEMETRY_APP, payload = payload), - rx_time = 1700000000, - ) + return MeshPacket.Builder() + .also { wb -> + wb.from = from + wb.decoded = + Data.Builder() + .also { wb -> + wb.portnum = PortNum.TELEMETRY_APP + wb.payload = payload + } + .build() + wb.rx_time = 1700000000 + } + .build() } private fun makeDataPacket(from: Int): DataPacket = DataPacket( @@ -117,7 +128,18 @@ class TelemetryPacketHandlerImplTest { @Test fun `local device metrics updates telemetry on connectionManager`() = testScope.runTest { val telemetry = - Telemetry(time = 1700000000, device_metrics = DeviceMetrics(battery_level = 80, voltage = 4.1f)) + Telemetry.Builder() + .also { wb -> + wb.time = 1700000000 + wb.device_metrics = + DeviceMetrics.Builder() + .also { wb -> + wb.battery_level = 80 + wb.voltage = 4.1f + } + .build() + } + .build() val packet = makeTelemetryPacket(myNodeNum, telemetry) val dataPacket = makeDataPacket(myNodeNum) @@ -133,7 +155,18 @@ class TelemetryPacketHandlerImplTest { @Test fun `remote device metrics updates node but not connectionManager`() = testScope.runTest { val telemetry = - Telemetry(time = 1700000000, device_metrics = DeviceMetrics(battery_level = 90, voltage = 4.2f)) + Telemetry.Builder() + .also { wb -> + wb.time = 1700000000 + wb.device_metrics = + DeviceMetrics.Builder() + .also { wb -> + wb.battery_level = 90 + wb.voltage = 4.2f + } + .build() + } + .build() val packet = makeTelemetryPacket(remoteNodeNum, telemetry) val dataPacket = makeDataPacket(remoteNodeNum) @@ -148,10 +181,18 @@ class TelemetryPacketHandlerImplTest { @Test fun `environment metrics updates node with environment data`() = testScope.runTest { val telemetry = - Telemetry( - time = 1700000000, - environment_metrics = EnvironmentMetrics(temperature = 25.5f, relative_humidity = 60.0f), - ) + Telemetry.Builder() + .also { wb -> + wb.time = 1700000000 + wb.environment_metrics = + EnvironmentMetrics.Builder() + .also { wb -> + wb.temperature = 25.5f + wb.relative_humidity = 60.0f + } + .build() + } + .build() val packet = makeTelemetryPacket(remoteNodeNum, telemetry) val dataPacket = makeDataPacket(remoteNodeNum) @@ -161,11 +202,43 @@ class TelemetryPacketHandlerImplTest { verify { nodeManager.updateNodeForSession(remoteNodeNum, radioSession, any(), any()) } } + @Test + fun `soil water metrics updates node with probe data`() = testScope.runTest { + val telemetry = + Telemetry.Builder() + .also { wb -> + wb.time = 1700000000 + wb.soil_water_metrics = SoilWaterMetrics.Builder().also { wb -> wb.soil_ph = 6.8f }.build() + } + .build() + val packet = makeTelemetryPacket(remoteNodeNum, telemetry) + val dataPacket = makeDataPacket(remoteNodeNum) + // Run the handler's transform ourselves: the mock would otherwise accept any lambda, including one that + // never copies the probe data across. + var updated: Node? = null + every { nodeManager.updateNodeForSession(remoteNodeNum, radioSession, any(), any()) } calls + { args -> + updated = args.arg<(Node) -> Node>(3)(Node(num = remoteNodeNum)) + } + + handler.handleTelemetry(packet, dataPacket, myNodeNum, radioSession) + advanceUntilIdle() + + assertEquals(6.8f, updated?.soilWaterMetrics?.soil_ph) + assertNull(updated?.soilWaterMetrics?.nitrogen) + } + // ---------- Power metrics ---------- @Test fun `power metrics updates node with power data`() = testScope.runTest { - val telemetry = Telemetry(time = 1700000000, power_metrics = PowerMetrics(ch1_voltage = 3.3f)) + val telemetry = + Telemetry.Builder() + .also { wb -> + wb.time = 1700000000 + wb.power_metrics = PowerMetrics.Builder().also { wb -> wb.ch1_voltage = 3.3f }.build() + } + .build() val packet = makeTelemetryPacket(remoteNodeNum, telemetry) val dataPacket = makeDataPacket(remoteNodeNum) @@ -179,7 +252,19 @@ class TelemetryPacketHandlerImplTest { @Test fun `telemetry with time 0 gets time from dataPacket`() = testScope.runTest { - val telemetry = Telemetry(time = 0, device_metrics = DeviceMetrics(battery_level = 50, voltage = 3.8f)) + val telemetry = + Telemetry.Builder() + .also { wb -> + wb.time = 0 + wb.device_metrics = + DeviceMetrics.Builder() + .also { wb -> + wb.battery_level = 50 + wb.voltage = 3.8f + } + .build() + } + .build() val packet = makeTelemetryPacket(myNodeNum, telemetry) val dataPacket = makeDataPacket(myNodeNum) @@ -193,7 +278,13 @@ class TelemetryPacketHandlerImplTest { @Test fun `handleTelemetry with null decoded payload returns early`() = testScope.runTest { - val packet = MeshPacket(from = myNodeNum, decoded = null) + val packet = + MeshPacket.Builder() + .also { wb -> + wb.from = myNodeNum + wb.decoded = null + } + .build() val dataPacket = makeDataPacket(myNodeNum) handler.handleTelemetry(packet, dataPacket, myNodeNum, radioSession) @@ -204,10 +295,18 @@ class TelemetryPacketHandlerImplTest { @Test fun `handleTelemetry with empty payload bytes returns early`() = testScope.runTest { val packet = - MeshPacket( - from = myNodeNum, - decoded = Data(portnum = PortNum.TELEMETRY_APP, payload = okio.ByteString.EMPTY), - ) + MeshPacket.Builder() + .also { wb -> + wb.from = myNodeNum + wb.decoded = + Data.Builder() + .also { wb -> + wb.portnum = PortNum.TELEMETRY_APP + wb.payload = okio.ByteString.EMPTY + } + .build() + } + .build() val dataPacket = makeDataPacket(myNodeNum) handler.handleTelemetry(packet, dataPacket, myNodeNum, radioSession) @@ -220,7 +319,18 @@ class TelemetryPacketHandlerImplTest { @Test fun `healthy battery level does not trigger low battery notification`() = testScope.runTest { val telemetry = - Telemetry(time = 1700000000, device_metrics = DeviceMetrics(battery_level = 80, voltage = 4.0f)) + Telemetry.Builder() + .also { wb -> + wb.time = 1700000000 + wb.device_metrics = + DeviceMetrics.Builder() + .also { wb -> + wb.battery_level = 80 + wb.voltage = 4.0f + } + .build() + } + .build() val packet = makeTelemetryPacket(myNodeNum, telemetry) val dataPacket = makeDataPacket(myNodeNum) diff --git a/core/data/src/commonTest/kotlin/org/meshtastic/core/data/manager/XModemManagerImplTest.kt b/core/data/src/commonTest/kotlin/org/meshtastic/core/data/manager/XModemManagerImplTest.kt index 830d2dac39..146b41a049 100644 --- a/core/data/src/commonTest/kotlin/org/meshtastic/core/data/manager/XModemManagerImplTest.kt +++ b/core/data/src/commonTest/kotlin/org/meshtastic/core/data/manager/XModemManagerImplTest.kt @@ -62,26 +62,30 @@ class XModemManagerImplTest { xmodemManager.fileTransferFlow.test { // Send Block 1 xmodemManager.handleIncomingXModem( - XModem( - control = XModem.Control.SOH, - seq = 1, - crc16 = calculateExpectedCrc(payload1), - buffer = payload1.toByteString(), - ), + XModem.Builder() + .also { wb -> + wb.control = XModem.Control.SOH + wb.seq = 1 + wb.crc16 = calculateExpectedCrc(payload1) + wb.buffer = payload1.toByteString() + } + .build(), ) // Send Block 2 xmodemManager.handleIncomingXModem( - XModem( - control = XModem.Control.SOH, - seq = 2, - crc16 = calculateExpectedCrc(payload2), - buffer = payload2.toByteString(), - ), + XModem.Builder() + .also { wb -> + wb.control = XModem.Control.SOH + wb.seq = 2 + wb.crc16 = calculateExpectedCrc(payload2) + wb.buffer = payload2.toByteString() + } + .build(), ) // EOT - xmodemManager.handleIncomingXModem(XModem(control = XModem.Control.EOT)) + xmodemManager.handleIncomingXModem(XModem.Builder().also { wb -> wb.control = XModem.Control.EOT }.build()) val file = awaitItem() assertEquals("test.txt", file.name) @@ -96,12 +100,15 @@ class XModemManagerImplTest { val payload1 = "Bad CRC payload".encodeToByteArray() xmodemManager.handleIncomingXModem( - XModem( - control = XModem.Control.SOH, - seq = 1, - crc16 = 0xBAD, // intentionally bad - buffer = payload1.toByteString(), - ), + XModem.Builder() + .also { wb -> + wb.control = XModem.Control.SOH + wb.seq = 1 + wb.crc16 = 0xBAD + // intentionally bad + wb.buffer = payload1.toByteString() + } + .build(), ) verify(exactly(1)) { packetHandler.sendToRadio(any()) } @@ -111,7 +118,7 @@ class XModemManagerImplTest { fun `handles CAN and resets state`() = runTest { xmodemManager.setTransferName("bad.txt") - xmodemManager.handleIncomingXModem(XModem(control = XModem.Control.CAN)) + xmodemManager.handleIncomingXModem(XModem.Builder().also { wb -> wb.control = XModem.Control.CAN }.build()) // No control sent back for CAN by the device, just resets. // If we cancel locally, we send CAN. Wait, the test is for receiving CAN. @@ -127,14 +134,16 @@ class XModemManagerImplTest { xmodemManager.fileTransferFlow.test { xmodemManager.handleIncomingXModem( - XModem( - control = XModem.Control.SOH, - seq = 1, - crc16 = calculateExpectedCrc(payload), - buffer = payload.toByteString(), - ), + XModem.Builder() + .also { wb -> + wb.control = XModem.Control.SOH + wb.seq = 1 + wb.crc16 = calculateExpectedCrc(payload) + wb.buffer = payload.toByteString() + } + .build(), ) - xmodemManager.handleIncomingXModem(XModem(control = XModem.Control.EOT)) + xmodemManager.handleIncomingXModem(XModem.Builder().also { wb -> wb.control = XModem.Control.EOT }.build()) val file = awaitItem() val expected = byteArrayOf(0x48, 0x69) // "Hi" diff --git a/core/data/src/commonTest/kotlin/org/meshtastic/core/data/repository/CommonMeshLogRepositoryTest.kt b/core/data/src/commonTest/kotlin/org/meshtastic/core/data/repository/CommonMeshLogRepositoryTest.kt index e3428dd117..1c98380bef 100644 --- a/core/data/src/commonTest/kotlin/org/meshtastic/core/data/repository/CommonMeshLogRepositoryTest.kt +++ b/core/data/src/commonTest/kotlin/org/meshtastic/core/data/repository/CommonMeshLogRepositoryTest.kt @@ -86,10 +86,26 @@ abstract class CommonMeshLogRepositoryTest { @Test fun `parseTelemetryLog preserves zero temperature`() = runTest(testDispatcher) { val zeroTemp = 0.0f - val telemetry = Telemetry(environment_metrics = EnvironmentMetrics(temperature = zeroTemp)) + val telemetry = + Telemetry.Builder() + .also { wb -> + wb.environment_metrics = + EnvironmentMetrics.Builder().also { wb -> wb.temperature = zeroTemp }.build() + } + .build() val meshPacket = - MeshPacket(decoded = Data(payload = telemetry.encode().toByteString(), portnum = PortNum.TELEMETRY_APP)) + MeshPacket.Builder() + .also { wb -> + wb.decoded = + Data.Builder() + .also { wb -> + wb.payload = telemetry.encode().toByteString() + wb.portnum = PortNum.TELEMETRY_APP + } + .build() + } + .build() val meshLog = MeshLog( @@ -99,7 +115,7 @@ abstract class CommonMeshLogRepositoryTest { raw_message = "", fromNum = 0, portNum = PortNum.TELEMETRY_APP.value, - fromRadio = FromRadio(packet = meshPacket), + fromRadio = FromRadio.Builder().also { wb -> wb.packet = meshPacket }.build(), ) repository.insert(meshLog) @@ -142,9 +158,20 @@ abstract class CommonMeshLogRepositoryTest { 0, // asEntity will map it if we pass localNodeNum to asEntity, but here we set it manually portNum = port, fromRadio = - FromRadio( - packet = MeshPacket(from = localNodeNum, decoded = Data(portnum = PortNum.TEXT_MESSAGE_APP)), - ), + FromRadio.Builder() + .also { wb -> + wb.packet = + MeshPacket.Builder() + .also { wb -> + wb.from = localNodeNum + wb.decoded = + Data.Builder() + .also { wb -> wb.portnum = PortNum.TEXT_MESSAGE_APP } + .build() + } + .build() + } + .build(), ) repository.insert(log) @@ -164,28 +191,45 @@ abstract class CommonMeshLogRepositoryTest { telemetryLog( uuid = "local-stats", nodeNum = nodeNum, - telemetry = Telemetry(local_stats = LocalStats(noise_floor = -112)), + telemetry = + Telemetry.Builder() + .also { wb -> + wb.local_stats = LocalStats.Builder().also { wb -> wb.noise_floor = -112 }.build() + } + .build(), receivedDate = nowMillis + 3, ) val deviceLog = telemetryLog( uuid = "device", nodeNum = nodeNum, - telemetry = Telemetry(device_metrics = DeviceMetrics(battery_level = 80)), + telemetry = + Telemetry.Builder() + .also { wb -> + wb.device_metrics = DeviceMetrics.Builder().also { wb -> wb.battery_level = 80 }.build() + } + .build(), receivedDate = nowMillis + 2, ) val environmentLog = telemetryLog( uuid = "environment", nodeNum = nodeNum, - telemetry = Telemetry(environment_metrics = EnvironmentMetrics(temperature = 21f)), + telemetry = + Telemetry.Builder() + .also { wb -> + wb.environment_metrics = + EnvironmentMetrics.Builder().also { wb -> wb.temperature = 21f }.build() + } + .build(), receivedDate = nowMillis + 1, ) val localStatsRequestLog = telemetryLog( uuid = "local-stats-request", nodeNum = nodeNum, - telemetry = Telemetry(local_stats = LocalStats()), + telemetry = + Telemetry.Builder().also { wb -> wb.local_stats = LocalStats.Builder().build() }.build(), receivedDate = nowMillis, wantResponse = true, ) @@ -241,7 +285,14 @@ abstract class CommonMeshLogRepositoryTest { // Firmware before 2.8 emitted the repeated field; stored logs must still chart after the repoint. @Suppress("DEPRECATION") val telemetry = - Telemetry(environment_metrics = EnvironmentMetrics(one_wire_temperature = listOf(11f, 0f, 33f))) + Telemetry.Builder() + .also { wb -> + wb.environment_metrics = + EnvironmentMetrics.Builder() + .also { wb -> wb.one_wire_temperature = listOf(11f, 0f, 33f) } + .build() + } + .build() repository.insert(telemetryLog("legacy-one-wire", 0, telemetry, nowMillis)) val metrics = repository.getTelemetryFrom(0).first().single().environment_metrics @@ -257,7 +308,13 @@ abstract class CommonMeshLogRepositoryTest { @Test fun `parseTelemetryLog normalizes absent per-channel readings to NaN`() = runTest(testDispatcher) { - val telemetry = Telemetry(environment_metrics = EnvironmentMetrics(temperature = 21f)) + val telemetry = + Telemetry.Builder() + .also { wb -> + wb.environment_metrics = + EnvironmentMetrics.Builder().also { wb -> wb.temperature = 21f }.build() + } + .build() repository.insert(telemetryLog("absent-channels", 0, telemetry, nowMillis)) val metrics = repository.getTelemetryFrom(0).first().single().environment_metrics @@ -273,7 +330,17 @@ abstract class CommonMeshLogRepositoryTest { fun `parseTelemetryLog preserves zero per-channel readings`() = runTest(testDispatcher) { // 0 V on an unloaded ADC input and 0°C on a probe are measurements, not "no sensor" sentinels. val telemetry = - Telemetry(environment_metrics = EnvironmentMetrics(one_wire_temperature_ch0 = 0f, adc_voltage_ch0 = 0f)) + Telemetry.Builder() + .also { wb -> + wb.environment_metrics = + EnvironmentMetrics.Builder() + .also { wb -> + wb.one_wire_temperature_ch0 = 0f + wb.adc_voltage_ch0 = 0f + } + .build() + } + .build() repository.insert(telemetryLog("zero-channels", 0, telemetry, nowMillis)) val metrics = repository.getTelemetryFrom(0).first().single().environment_metrics @@ -297,17 +364,23 @@ abstract class CommonMeshLogRepositoryTest { fromNum = nodeNum, portNum = PortNum.TELEMETRY_APP.value, fromRadio = - FromRadio( - packet = - MeshPacket( - from = nodeNum, - decoded = - Data( - payload = telemetry.encode().toByteString(), - portnum = PortNum.TELEMETRY_APP, - want_response = wantResponse, - ), - ), - ), + FromRadio.Builder() + .also { wb -> + wb.packet = + MeshPacket.Builder() + .also { wb -> + wb.from = nodeNum + wb.decoded = + Data.Builder() + .also { wb -> + wb.payload = telemetry.encode().toByteString() + wb.portnum = PortNum.TELEMETRY_APP + wb.want_response = wantResponse + } + .build() + } + .build() + } + .build(), ) } diff --git a/core/data/src/commonTest/kotlin/org/meshtastic/core/data/repository/CommonPacketRepositoryTest.kt b/core/data/src/commonTest/kotlin/org/meshtastic/core/data/repository/CommonPacketRepositoryTest.kt index aebf094842..825b9ca4b6 100644 --- a/core/data/src/commonTest/kotlin/org/meshtastic/core/data/repository/CommonPacketRepositoryTest.kt +++ b/core/data/src/commonTest/kotlin/org/meshtastic/core/data/repository/CommonPacketRepositoryTest.kt @@ -124,12 +124,14 @@ abstract class CommonPacketRepositoryTest { val firstId = repository.savePacket(0, "0!aaaa0001", first, 1L) val secondId = repository.savePacket(0, "0!aaaa0001", second, 2L) val meshPacket = - MeshPacket( - from = 1, - to = 0xAAAA0001.toInt(), - id = 77, - decoded = Data(portnum = PortNum.TEXT_MESSAGE_APP), - ) + MeshPacket.Builder() + .also { wb -> + wb.from = 1 + wb.to = 0xAAAA0001.toInt() + wb.id = 77 + wb.decoded = Data.Builder().also { wb -> wb.portnum = PortNum.TEXT_MESSAGE_APP }.build() + } + .build() assertNotEquals(firstId, secondId) assertEquals("first", repository.getPacketByPersistedId(firstId)?.text) @@ -309,12 +311,14 @@ abstract class CommonPacketRepositoryTest { val updated = repository.updateOutgoingMessageStatus( - MeshPacket( - from = 0x11111111, - to = 0x22222222, - id = packetId, - decoded = Data(portnum = PortNum.TEXT_MESSAGE_APP), - ), + MeshPacket.Builder() + .also { wb -> + wb.from = 0x11111111 + wb.to = 0x22222222 + wb.id = packetId + wb.decoded = Data.Builder().also { wb -> wb.portnum = PortNum.TEXT_MESSAGE_APP }.build() + } + .build(), MessageStatus.ENROUTE, ) @@ -402,5 +406,6 @@ abstract class CommonPacketRepositoryTest { assertNull(pagedCandidates.singleOrNull()) } - private fun testNode(id: String?): Node = Node(num = 0, user = User(id = id.orEmpty())) + private fun testNode(id: String?): Node = + Node(num = 0, user = User.Builder().also { wb -> wb.id = id.orEmpty() }.build()) } diff --git a/core/data/src/commonTest/kotlin/org/meshtastic/core/data/repository/RadioConfigRepositoryImplTest.kt b/core/data/src/commonTest/kotlin/org/meshtastic/core/data/repository/RadioConfigRepositoryImplTest.kt index f107a387cd..c6566f07e4 100644 --- a/core/data/src/commonTest/kotlin/org/meshtastic/core/data/repository/RadioConfigRepositoryImplTest.kt +++ b/core/data/src/commonTest/kotlin/org/meshtastic/core/data/repository/RadioConfigRepositoryImplTest.kt @@ -82,7 +82,16 @@ class RadioConfigRepositoryImplTest { // under test (that it caps at all, deterministically, is). val cap = 4096 - repeat(cap + 500) { i -> repository.addFileInfo(FileInfo(file_name = "file$i.bin", size_bytes = i)) } + repeat(cap + 500) { i -> + repository.addFileInfo( + FileInfo.Builder() + .also { wb -> + wb.file_name = "file$i.bin" + wb.size_bytes = i + } + .build(), + ) + } val manifest = repository.fileManifestFlow.first() assertEquals(cap, manifest.size) @@ -102,7 +111,18 @@ class RadioConfigRepositoryImplTest { withContext(Dispatchers.Default) { coroutineScope { (0 until n) - .map { i -> async { repository.addFileInfo(FileInfo(file_name = "f$i.bin", size_bytes = i)) } } + .map { i -> + async { + repository.addFileInfo( + FileInfo.Builder() + .also { wb -> + wb.file_name = "f$i.bin" + wb.size_bytes = i + } + .build(), + ) + } + } .awaitAll() } } @@ -113,14 +133,35 @@ class RadioConfigRepositoryImplTest { @Test fun `clearFileManifest empties the manifest so a new handshake can accumulate again`() = runTest { val repository = createRepository() - repository.addFileInfo(FileInfo(file_name = "a.bin", size_bytes = 1)) - repository.addFileInfo(FileInfo(file_name = "b.bin", size_bytes = 2)) + repository.addFileInfo( + FileInfo.Builder() + .also { wb -> + wb.file_name = "a.bin" + wb.size_bytes = 1 + } + .build(), + ) + repository.addFileInfo( + FileInfo.Builder() + .also { wb -> + wb.file_name = "b.bin" + wb.size_bytes = 2 + } + .build(), + ) assertEquals(2, repository.fileManifestFlow.first().size) repository.clearFileManifest() assertEquals(0, repository.fileManifestFlow.first().size) - repository.addFileInfo(FileInfo(file_name = "c.bin", size_bytes = 3)) + repository.addFileInfo( + FileInfo.Builder() + .also { wb -> + wb.file_name = "c.bin" + wb.size_bytes = 3 + } + .build(), + ) assertEquals(1, repository.fileManifestFlow.first().size) } } diff --git a/core/data/src/jvmTest/kotlin/org/meshtastic/core/data/manager/StoreForwardPacketHandlerImplTest.kt b/core/data/src/jvmTest/kotlin/org/meshtastic/core/data/manager/StoreForwardPacketHandlerImplTest.kt index b72b92be1c..9c6d53e60c 100644 --- a/core/data/src/jvmTest/kotlin/org/meshtastic/core/data/manager/StoreForwardPacketHandlerImplTest.kt +++ b/core/data/src/jvmTest/kotlin/org/meshtastic/core/data/manager/StoreForwardPacketHandlerImplTest.kt @@ -21,6 +21,7 @@ import dev.mokkery.answering.returns import dev.mokkery.every import dev.mokkery.everySuspend import dev.mokkery.matcher.any +import dev.mokkery.matcher.capture.capture import dev.mokkery.mock import dev.mokkery.verify import dev.mokkery.verify.VerifyMode @@ -49,6 +50,7 @@ import org.meshtastic.proto.StoreAndForward import org.meshtastic.proto.StoreForwardPlusPlus import kotlin.test.BeforeTest import kotlin.test.Test +import kotlin.test.assertEquals @OptIn(ExperimentalCoroutinesApi::class) class StoreForwardPacketHandlerImplTest { @@ -83,12 +85,34 @@ class StoreForwardPacketHandlerImplTest { private fun makeSfPacket(from: Int, sf: StoreAndForward): MeshPacket { val payload = sf.encode().toByteString() - return MeshPacket(from = from, decoded = Data(portnum = PortNum.STORE_FORWARD_APP, payload = payload)) + return MeshPacket.Builder() + .also { wb -> + wb.from = from + wb.decoded = + Data.Builder() + .also { wb -> + wb.portnum = PortNum.STORE_FORWARD_APP + wb.payload = payload + } + .build() + } + .build() } private fun makeSfppPacket(from: Int, sfpp: StoreForwardPlusPlus): MeshPacket { val payload = sfpp.encode().toByteString() - return MeshPacket(from = from, decoded = Data(portnum = PortNum.STORE_FORWARD_APP, payload = payload)) + return MeshPacket.Builder() + .also { wb -> + wb.from = from + wb.decoded = + Data.Builder() + .also { wb -> + wb.portnum = PortNum.STORE_FORWARD_APP + wb.payload = payload + } + .build() + } + .build() } private fun makeDataPacket(from: Int): DataPacket = DataPacket( @@ -105,9 +129,18 @@ class StoreForwardPacketHandlerImplTest { @Test fun `handleStoreAndForward stats creates text data packet`() = testScope.runTest { val sf = - StoreAndForward( - stats = StoreAndForward.Statistics(messages_total = 100, messages_saved = 50, messages_max = 200), - ) + StoreAndForward.Builder() + .also { wb -> + wb.stats = + StoreAndForward.Statistics.Builder() + .also { wb -> + wb.messages_total = 100 + wb.messages_saved = 50 + wb.messages_max = 200 + } + .build() + } + .build() val packet = makeSfPacket(999, sf) val dataPacket = makeDataPacket(999) @@ -122,10 +155,18 @@ class StoreForwardPacketHandlerImplTest { @Test fun `handleStoreAndForward history creates text packet and updates last request`() = testScope.runTest { val sf = - StoreAndForward( - history = - StoreAndForward.History(history_messages = 42, window = 3600000, last_request = 1700000000), - ) + StoreAndForward.Builder() + .also { wb -> + wb.history = + StoreAndForward.History.Builder() + .also { wb -> + wb.history_messages = 42 + wb.window = 3600000 + wb.last_request = 1700000000 + } + .build() + } + .build() val packet = makeSfPacket(999, sf) val dataPacket = makeDataPacket(999) @@ -140,7 +181,18 @@ class StoreForwardPacketHandlerImplTest { @Test fun `handleStoreAndForward heartbeat does not crash`() = testScope.runTest { - val sf = StoreAndForward(heartbeat = StoreAndForward.Heartbeat(period = 900, secondary = 1)) + val sf = + StoreAndForward.Builder() + .also { wb -> + wb.heartbeat = + StoreAndForward.Heartbeat.Builder() + .also { wb -> + wb.period = 900 + wb.secondary = 1 + } + .build() + } + .build() val packet = makeSfPacket(999, sf) val dataPacket = makeDataPacket(999) @@ -154,10 +206,12 @@ class StoreForwardPacketHandlerImplTest { @Test fun `handleStoreAndForward text with broadcast rr sets to broadcast`() = testScope.runTest { val sf = - StoreAndForward( - text = "Hello from router".encodeToByteArray().toByteString(), - rr = StoreAndForward.RequestResponse.ROUTER_TEXT_BROADCAST, - ) + StoreAndForward.Builder() + .also { wb -> + wb.text = "Hello from router".encodeToByteArray().toByteString() + wb.rr = StoreAndForward.RequestResponse.ROUTER_TEXT_BROADCAST + } + .build() val packet = makeSfPacket(999, sf) val dataPacket = makeDataPacket(999) @@ -170,10 +224,12 @@ class StoreForwardPacketHandlerImplTest { @Test fun `handleStoreAndForward text without broadcast rr preserves destination`() = testScope.runTest { val sf = - StoreAndForward( - text = "Direct message".encodeToByteArray().toByteString(), - rr = StoreAndForward.RequestResponse.ROUTER_TEXT_DIRECT, - ) + StoreAndForward.Builder() + .also { wb -> + wb.text = "Direct message".encodeToByteArray().toByteString() + wb.rr = StoreAndForward.RequestResponse.ROUTER_TEXT_DIRECT + } + .build() val packet = makeSfPacket(999, sf) val dataPacket = makeDataPacket(999) @@ -187,7 +243,13 @@ class StoreForwardPacketHandlerImplTest { @Test fun `handleStoreAndForward with null payload returns early`() = testScope.runTest { - val packet = MeshPacket(from = 999, decoded = null) + val packet = + MeshPacket.Builder() + .also { wb -> + wb.from = 999 + wb.decoded = null + } + .build() val dataPacket = makeDataPacket(999) handler.handleStoreAndForward(packet, dataPacket, myNodeNum) @@ -199,7 +261,7 @@ class StoreForwardPacketHandlerImplTest { @Test fun `handleStoreAndForward with no fields set does not crash`() = testScope.runTest { - val sf = StoreAndForward() + val sf = StoreAndForward.Builder().build() val packet = makeSfPacket(999, sf) val dataPacket = makeDataPacket(999) @@ -213,14 +275,16 @@ class StoreForwardPacketHandlerImplTest { @Test fun `handleStoreForwardPlusPlus LINK_PROVIDE with message_hash updates status`() = testScope.runTest { val sfpp = - StoreForwardPlusPlus( - sfpp_message_type = StoreForwardPlusPlus.SFPP_message_type.LINK_PROVIDE, - encapsulated_id = 42, - encapsulated_from = 1000, - encapsulated_to = 2000, - message_hash = ByteString.of(0x01, 0x02, 0x03, 0x04), - commit_hash = ByteString.EMPTY, - ) + StoreForwardPlusPlus.Builder() + .also { wb -> + wb.sfpp_message_type = StoreForwardPlusPlus.SFPP_message_type.LINK_PROVIDE + wb.encapsulated_id = 42 + wb.encapsulated_from = 1000 + wb.encapsulated_to = 2000 + wb.message_hash = ByteString.of(0x01, 0x02, 0x03, 0x04) + wb.commit_hash = ByteString.EMPTY + } + .build() val packet = makeSfppPacket(999, sfpp) handler.handleStoreForwardPlusPlus(packet) @@ -233,13 +297,15 @@ class StoreForwardPacketHandlerImplTest { fun `SFPP update from a retired same-address generation is rejected`() = testScope.runTest { val oldSession = RadioSessionContext(generation = 4L, address = "ble:same") val sfpp = - StoreForwardPlusPlus( - sfpp_message_type = StoreForwardPlusPlus.SFPP_message_type.LINK_PROVIDE, - encapsulated_id = 42, - encapsulated_from = 1000, - encapsulated_to = 2000, - message_hash = ByteString.of(0x01, 0x02, 0x03, 0x04), - ) + StoreForwardPlusPlus.Builder() + .also { wb -> + wb.sfpp_message_type = StoreForwardPlusPlus.SFPP_message_type.LINK_PROVIDE + wb.encapsulated_id = 42 + wb.encapsulated_from = 1000 + wb.encapsulated_to = 2000 + wb.message_hash = ByteString.of(0x01, 0x02, 0x03, 0x04) + } + .build() val packet = makeSfppPacket(999, sfpp) everySuspend { radioInterfaceService.runWithSessionLease(oldSession, any()) } returns false @@ -256,11 +322,13 @@ class StoreForwardPacketHandlerImplTest { @Test fun `handleStoreForwardPlusPlus CANON_ANNOUNCE updates status by hash`() = testScope.runTest { val sfpp = - StoreForwardPlusPlus( - sfpp_message_type = StoreForwardPlusPlus.SFPP_message_type.CANON_ANNOUNCE, - message_hash = ByteString.of(0xAA.toByte(), 0xBB.toByte()), - encapsulated_rxtime = 1700000000, - ) + StoreForwardPlusPlus.Builder() + .also { wb -> + wb.sfpp_message_type = StoreForwardPlusPlus.SFPP_message_type.CANON_ANNOUNCE + wb.message_hash = ByteString.of(0xAA.toByte(), 0xBB.toByte()) + wb.encapsulated_rxtime = 1700000000 + } + .build() val packet = makeSfppPacket(999, sfpp) handler.handleStoreForwardPlusPlus(packet) @@ -273,7 +341,10 @@ class StoreForwardPacketHandlerImplTest { @Test fun `handleStoreForwardPlusPlus CHAIN_QUERY logs info without crash`() = testScope.runTest { - val sfpp = StoreForwardPlusPlus(sfpp_message_type = StoreForwardPlusPlus.SFPP_message_type.CHAIN_QUERY) + val sfpp = + StoreForwardPlusPlus.Builder() + .also { wb -> wb.sfpp_message_type = StoreForwardPlusPlus.SFPP_message_type.CHAIN_QUERY } + .build() val packet = makeSfppPacket(999, sfpp) handler.handleStoreForwardPlusPlus(packet) @@ -285,7 +356,10 @@ class StoreForwardPacketHandlerImplTest { @Test fun `handleStoreForwardPlusPlus LINK_REQUEST logs info without crash`() = testScope.runTest { - val sfpp = StoreForwardPlusPlus(sfpp_message_type = StoreForwardPlusPlus.SFPP_message_type.LINK_REQUEST) + val sfpp = + StoreForwardPlusPlus.Builder() + .also { wb -> wb.sfpp_message_type = StoreForwardPlusPlus.SFPP_message_type.LINK_REQUEST } + .build() val packet = makeSfppPacket(999, sfpp) handler.handleStoreForwardPlusPlus(packet) @@ -297,7 +371,13 @@ class StoreForwardPacketHandlerImplTest { @Test fun `handleStoreForwardPlusPlus with null payload returns early`() = testScope.runTest { - val packet = MeshPacket(from = 999, decoded = null) + val packet = + MeshPacket.Builder() + .also { wb -> + wb.from = 999 + wb.decoded = null + } + .build() handler.handleStoreForwardPlusPlus(packet) advanceUntilIdle() @@ -309,14 +389,16 @@ class StoreForwardPacketHandlerImplTest { @Test fun `handleStoreForwardPlusPlus LINK_PROVIDE_FIRSTHALF handled as link provide`() = testScope.runTest { val sfpp = - StoreForwardPlusPlus( - sfpp_message_type = StoreForwardPlusPlus.SFPP_message_type.LINK_PROVIDE_FIRSTHALF, - encapsulated_id = 55, - encapsulated_from = 1000, - encapsulated_to = 2000, - message_hash = ByteString.of(0x01, 0x02), - commit_hash = ByteString.EMPTY, - ) + StoreForwardPlusPlus.Builder() + .also { wb -> + wb.sfpp_message_type = StoreForwardPlusPlus.SFPP_message_type.LINK_PROVIDE_FIRSTHALF + wb.encapsulated_id = 55 + wb.encapsulated_from = 1000 + wb.encapsulated_to = 2000 + wb.message_hash = ByteString.of(0x01, 0x02) + wb.commit_hash = ByteString.EMPTY + } + .build() val packet = makeSfppPacket(999, sfpp) handler.handleStoreForwardPlusPlus(packet) @@ -328,14 +410,16 @@ class StoreForwardPacketHandlerImplTest { @Test fun `handleStoreForwardPlusPlus LINK_PROVIDE_SECONDHALF handled as link provide`() = testScope.runTest { val sfpp = - StoreForwardPlusPlus( - sfpp_message_type = StoreForwardPlusPlus.SFPP_message_type.LINK_PROVIDE_SECONDHALF, - encapsulated_id = 56, - encapsulated_from = 1000, - encapsulated_to = 2000, - message_hash = ByteString.of(0x03, 0x04), - commit_hash = ByteString.EMPTY, - ) + StoreForwardPlusPlus.Builder() + .also { wb -> + wb.sfpp_message_type = StoreForwardPlusPlus.SFPP_message_type.LINK_PROVIDE_SECONDHALF + wb.encapsulated_id = 56 + wb.encapsulated_from = 1000 + wb.encapsulated_to = 2000 + wb.message_hash = ByteString.of(0x03, 0x04) + wb.commit_hash = ByteString.EMPTY + } + .build() val packet = makeSfppPacket(999, sfpp) handler.handleStoreForwardPlusPlus(packet) @@ -349,14 +433,16 @@ class StoreForwardPacketHandlerImplTest { @Test fun `handleStoreForwardPlusPlus LINK_PROVIDE with commit_hash sets SFPP_CONFIRMED`() = testScope.runTest { val sfpp = - StoreForwardPlusPlus( - sfpp_message_type = StoreForwardPlusPlus.SFPP_message_type.LINK_PROVIDE, - encapsulated_id = 77, - encapsulated_from = 1000, - encapsulated_to = 2000, - message_hash = ByteString.of(0x01, 0x02), - commit_hash = ByteString.of(0xAA.toByte()), // non-empty - ) + StoreForwardPlusPlus.Builder() + .also { wb -> + wb.sfpp_message_type = StoreForwardPlusPlus.SFPP_message_type.LINK_PROVIDE + wb.encapsulated_id = 77 + wb.encapsulated_from = 1000 + wb.encapsulated_to = 2000 + wb.message_hash = ByteString.of(0x01, 0x02) + wb.commit_hash = ByteString.of(0xAA.toByte()) // non-empty + } + .build() val packet = makeSfppPacket(999, sfpp) handler.handleStoreForwardPlusPlus(packet) @@ -371,11 +457,113 @@ class StoreForwardPacketHandlerImplTest { fun `handleStoreAndForward with malformed payload does not crash`() = testScope.runTest { val malformedPayload = ByteString.of(0xFF.toByte(), 0xFE.toByte(), 0x07, 0x0E) val packet = - MeshPacket(from = 999, decoded = Data(portnum = PortNum.STORE_FORWARD_APP, payload = malformedPayload)) + MeshPacket.Builder() + .also { wb -> + wb.from = 999 + wb.decoded = + Data.Builder() + .also { wb -> + wb.portnum = PortNum.STORE_FORWARD_APP + wb.payload = malformedPayload + } + .build() + } + .build() val dataPacket = makeDataPacket(999) // Should not throw — the handler catches the IOException from proto decoding handler.handleStoreAndForward(packet, dataPacket, myNodeNum) advanceUntilIdle() } + + // ---------- Legacy S&F: original_id ---------- + + @Test + fun `handleStoreAndForward text adopts original_id as the packet id`() = testScope.runTest { + val captured = mutableListOf() + every { dataHandler.rememberDataPacket(capture(captured), any(), any(), any()) } returns Unit + val sf = + StoreAndForward.Builder() + .also { wb -> + wb.text = "Replayed by a router".encodeToByteArray().toByteString() + wb.rr = StoreAndForward.RequestResponse.ROUTER_TEXT_DIRECT + wb.original_id = 0x4321 + } + .build() + val packet = makeSfPacket(999, sf) + val dataPacket = makeDataPacket(999) + + handler.handleStoreAndForward(packet, dataPacket, myNodeNum) + advanceUntilIdle() + + assertEquals(0x4321, captured.single().id) + } + + @Test + fun `handleStoreAndForward text keeps the header id when original_id is absent`() = testScope.runTest { + val captured = mutableListOf() + every { dataHandler.rememberDataPacket(capture(captured), any(), any(), any()) } returns Unit + val sf = + StoreAndForward.Builder() + .also { wb -> + wb.text = "Replayed by an older router".encodeToByteArray().toByteString() + wb.rr = StoreAndForward.RequestResponse.ROUTER_TEXT_DIRECT + } + .build() + val packet = makeSfPacket(999, sf) + val dataPacket = makeDataPacket(999) + + handler.handleStoreAndForward(packet, dataPacket, myNodeNum) + advanceUntilIdle() + + assertEquals(dataPacket.id, captured.single().id) + } + + @Test + fun `handleStoreAndForward text ignores original_id for a non-router-text rr`() = testScope.runTest { + val captured = mutableListOf() + every { dataHandler.rememberDataPacket(capture(captured), any(), any(), any()) } returns Unit + val sf = + StoreAndForward.Builder() + .also { wb -> + wb.text = "Text under an unexpected response type".encodeToByteArray().toByteString() + wb.rr = StoreAndForward.RequestResponse.ROUTER_ERROR + wb.original_id = 0x4321 + } + .build() + val packet = makeSfPacket(999, sf) + val dataPacket = makeDataPacket(999) + + handler.handleStoreAndForward(packet, dataPacket, myNodeNum) + advanceUntilIdle() + + assertEquals(dataPacket.id, captured.single().id) + } + + @Test + fun `handleStoreAndForward stats ignores original_id`() = testScope.runTest { + val captured = mutableListOf() + every { dataHandler.rememberDataPacket(capture(captured), any(), any(), any()) } returns Unit + val sf = + StoreAndForward.Builder() + .also { wb -> + wb.stats = + StoreAndForward.Statistics.Builder() + .also { wb -> + wb.messages_total = 100 + wb.messages_saved = 50 + wb.messages_max = 200 + } + .build() + wb.original_id = 0x4321 + } + .build() + val packet = makeSfPacket(999, sf) + val dataPacket = makeDataPacket(999) + + handler.handleStoreAndForward(packet, dataPacket, myNodeNum) + advanceUntilIdle() + + assertEquals(dataPacket.id, captured.single().id) + } } diff --git a/core/data/src/jvmTest/kotlin/org/meshtastic/core/data/repository/AirQualityChartReproTest.kt b/core/data/src/jvmTest/kotlin/org/meshtastic/core/data/repository/AirQualityChartReproTest.kt index 4ef83638ad..3f314eb2f8 100644 --- a/core/data/src/jvmTest/kotlin/org/meshtastic/core/data/repository/AirQualityChartReproTest.kt +++ b/core/data/src/jvmTest/kotlin/org/meshtastic/core/data/repository/AirQualityChartReproTest.kt @@ -113,24 +113,35 @@ class AirQualityChartReproTest { ) /** Brian's real reading: pm10_standard=1, pm25_standard=2, pm100_standard=2. */ - private fun brianAirQualityTelemetry() = Telemetry( - air_quality_metrics = - AirQualityMetrics( - pm10_standard = 1, - pm25_standard = 2, - pm100_standard = 2, - pm10_environmental = 1, - pm25_environmental = 2, - pm100_environmental = 2, - ), - ) + private fun brianAirQualityTelemetry() = Telemetry.Builder() + .also { wb -> + wb.air_quality_metrics = + AirQualityMetrics.Builder() + .also { wb -> + wb.pm10_standard = 1 + wb.pm25_standard = 2 + wb.pm100_standard = 2 + wb.pm10_environmental = 1 + wb.pm25_environmental = 2 + wb.pm100_environmental = 2 + } + .build() + } + .build() - private fun airQualityPacket() = MeshPacket( - from = localNum, - rx_time = 1_700_000_000, - decoded = - Data(payload = brianAirQualityTelemetry().encode().toByteString(), portnum = PortNum.TELEMETRY_APP), - ) + private fun airQualityPacket() = MeshPacket.Builder() + .also { wb -> + wb.from = localNum + wb.rx_time = 1_700_000_000 + wb.decoded = + Data.Builder() + .also { wb -> + wb.payload = brianAirQualityTelemetry().encode().toByteString() + wb.portnum = PortNum.TELEMETRY_APP + } + .build() + } + .build() private fun airQualityLog(fromNum: Int) = MeshLog( uuid = "aq-$fromNum", @@ -139,7 +150,7 @@ class AirQualityChartReproTest { raw_message = "", fromNum = fromNum, portNum = PortNum.TELEMETRY_APP.value, - fromRadio = FromRadio(packet = airQualityPacket()), + fromRadio = FromRadio.Builder().also { wb -> wb.packet = airQualityPacket() }.build(), ) /** Checkpoint 1: the parse + query round-trip preserves the air-quality payload (rules out content loss). */ diff --git a/core/database/schemas/org.meshtastic.core.database.MeshtasticDatabase/59.json b/core/database/schemas/org.meshtastic.core.database.MeshtasticDatabase/59.json new file mode 100644 index 0000000000..af4479201d --- /dev/null +++ b/core/database/schemas/org.meshtastic.core.database.MeshtasticDatabase/59.json @@ -0,0 +1,1833 @@ +{ + "formatVersion": 1, + "database": { + "version": 59, + "identityHash": "3a701cc22f7c57b09cac889f64cd9360", + "entities": [ + { + "tableName": "my_node", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`myNodeNum` INTEGER NOT NULL, `model` TEXT, `firmwareVersion` TEXT, `couldUpdate` INTEGER NOT NULL, `shouldUpdate` INTEGER NOT NULL, `currentPacketId` INTEGER NOT NULL, `messageTimeoutMsec` INTEGER NOT NULL, `minAppVersion` INTEGER NOT NULL, `maxChannels` INTEGER NOT NULL, `hasWifi` INTEGER NOT NULL, `deviceId` TEXT, `pioEnv` TEXT, PRIMARY KEY(`myNodeNum`))", + "fields": [ + { + "fieldPath": "myNodeNum", + "columnName": "myNodeNum", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "model", + "columnName": "model", + "affinity": "TEXT" + }, + { + "fieldPath": "firmwareVersion", + "columnName": "firmwareVersion", + "affinity": "TEXT" + }, + { + "fieldPath": "couldUpdate", + "columnName": "couldUpdate", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "shouldUpdate", + "columnName": "shouldUpdate", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "currentPacketId", + "columnName": "currentPacketId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "messageTimeoutMsec", + "columnName": "messageTimeoutMsec", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "minAppVersion", + "columnName": "minAppVersion", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "maxChannels", + "columnName": "maxChannels", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "hasWifi", + "columnName": "hasWifi", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "deviceId", + "columnName": "deviceId", + "affinity": "TEXT" + }, + { + "fieldPath": "pioEnv", + "columnName": "pioEnv", + "affinity": "TEXT" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "myNodeNum" + ] + } + }, + { + "tableName": "nodes", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`num` INTEGER NOT NULL, `user` BLOB NOT NULL, `long_name` TEXT, `short_name` TEXT, `position` BLOB NOT NULL, `latitude` REAL NOT NULL, `longitude` REAL NOT NULL, `snr` REAL NOT NULL, `rssi` INTEGER NOT NULL, `last_heard` INTEGER NOT NULL, `device_metrics` BLOB NOT NULL, `channel` INTEGER NOT NULL, `via_mqtt` INTEGER NOT NULL, `hops_away` INTEGER NOT NULL, `is_favorite` INTEGER NOT NULL, `is_ignored` INTEGER NOT NULL DEFAULT 0, `is_muted` INTEGER NOT NULL DEFAULT 0, `environment_metrics` BLOB NOT NULL, `power_metrics` BLOB NOT NULL, `air_quality_metrics` BLOB NOT NULL DEFAULT x'', `paxcounter` BLOB NOT NULL, `public_key` BLOB, `notes` TEXT NOT NULL DEFAULT '', `power_channel_labels` TEXT NOT NULL DEFAULT '[]', `manually_verified` INTEGER NOT NULL DEFAULT 0, `node_status` TEXT, `last_transport` INTEGER NOT NULL DEFAULT 0, `has_xeddsa_signed` INTEGER NOT NULL DEFAULT 0, `heard_on_current_lora` INTEGER NOT NULL DEFAULT 1, `key_match` INTEGER NOT NULL DEFAULT 1, `new_public_key` BLOB, PRIMARY KEY(`num`))", + "fields": [ + { + "fieldPath": "num", + "columnName": "num", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "user", + "columnName": "user", + "affinity": "BLOB", + "notNull": true + }, + { + "fieldPath": "longName", + "columnName": "long_name", + "affinity": "TEXT" + }, + { + "fieldPath": "shortName", + "columnName": "short_name", + "affinity": "TEXT" + }, + { + "fieldPath": "position", + "columnName": "position", + "affinity": "BLOB", + "notNull": true + }, + { + "fieldPath": "latitude", + "columnName": "latitude", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "longitude", + "columnName": "longitude", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "snr", + "columnName": "snr", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "rssi", + "columnName": "rssi", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastHeard", + "columnName": "last_heard", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "deviceTelemetry", + "columnName": "device_metrics", + "affinity": "BLOB", + "notNull": true + }, + { + "fieldPath": "channel", + "columnName": "channel", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "viaMqtt", + "columnName": "via_mqtt", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "hopsAway", + "columnName": "hops_away", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isFavorite", + "columnName": "is_favorite", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isIgnored", + "columnName": "is_ignored", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "isMuted", + "columnName": "is_muted", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "environmentTelemetry", + "columnName": "environment_metrics", + "affinity": "BLOB", + "notNull": true + }, + { + "fieldPath": "powerTelemetry", + "columnName": "power_metrics", + "affinity": "BLOB", + "notNull": true + }, + { + "fieldPath": "airQualityTelemetry", + "columnName": "air_quality_metrics", + "affinity": "BLOB", + "notNull": true, + "defaultValue": "x''" + }, + { + "fieldPath": "paxcounter", + "columnName": "paxcounter", + "affinity": "BLOB", + "notNull": true + }, + { + "fieldPath": "publicKey", + "columnName": "public_key", + "affinity": "BLOB" + }, + { + "fieldPath": "notes", + "columnName": "notes", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "powerChannelLabels", + "columnName": "power_channel_labels", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "'[]'" + }, + { + "fieldPath": "manuallyVerified", + "columnName": "manually_verified", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "nodeStatus", + "columnName": "node_status", + "affinity": "TEXT" + }, + { + "fieldPath": "lastTransport", + "columnName": "last_transport", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "signsPackets", + "columnName": "has_xeddsa_signed", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "heardOnCurrentLora", + "columnName": "heard_on_current_lora", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "1" + }, + { + "fieldPath": "keyMatch", + "columnName": "key_match", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "1" + }, + { + "fieldPath": "newPublicKey", + "columnName": "new_public_key", + "affinity": "BLOB" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "num" + ] + }, + "indices": [ + { + "name": "index_nodes_last_heard", + "unique": false, + "columnNames": [ + "last_heard" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_nodes_last_heard` ON `${TABLE_NAME}` (`last_heard`)" + }, + { + "name": "index_nodes_short_name", + "unique": false, + "columnNames": [ + "short_name" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_nodes_short_name` ON `${TABLE_NAME}` (`short_name`)" + }, + { + "name": "index_nodes_long_name", + "unique": false, + "columnNames": [ + "long_name" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_nodes_long_name` ON `${TABLE_NAME}` (`long_name`)" + }, + { + "name": "index_nodes_hops_away", + "unique": false, + "columnNames": [ + "hops_away" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_nodes_hops_away` ON `${TABLE_NAME}` (`hops_away`)" + }, + { + "name": "index_nodes_is_favorite", + "unique": false, + "columnNames": [ + "is_favorite" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_nodes_is_favorite` ON `${TABLE_NAME}` (`is_favorite`)" + }, + { + "name": "index_nodes_last_heard_is_favorite", + "unique": false, + "columnNames": [ + "last_heard", + "is_favorite" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_nodes_last_heard_is_favorite` ON `${TABLE_NAME}` (`last_heard`, `is_favorite`)" + }, + { + "name": "index_nodes_public_key", + "unique": false, + "columnNames": [ + "public_key" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_nodes_public_key` ON `${TABLE_NAME}` (`public_key`)" + } + ] + }, + { + "tableName": "packet", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uuid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `myNodeNum` INTEGER NOT NULL DEFAULT 0, `port_num` INTEGER NOT NULL, `contact_key` TEXT NOT NULL, `received_time` INTEGER NOT NULL, `read` INTEGER NOT NULL DEFAULT 1, `data` TEXT NOT NULL, `packet_id` INTEGER NOT NULL DEFAULT 0, `routing_error` INTEGER NOT NULL DEFAULT -1, `snr` REAL, `rssi` INTEGER, `hopsAway` INTEGER NOT NULL DEFAULT -1, `sfpp_hash` BLOB, `filtered` INTEGER NOT NULL DEFAULT 0, `message_text` TEXT NOT NULL DEFAULT '', `translated_text` TEXT, `show_translated` INTEGER NOT NULL DEFAULT 0)", + "fields": [ + { + "fieldPath": "uuid", + "columnName": "uuid", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "myNodeNum", + "columnName": "myNodeNum", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "port_num", + "columnName": "port_num", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "contact_key", + "columnName": "contact_key", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "received_time", + "columnName": "received_time", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "read", + "columnName": "read", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "1" + }, + { + "fieldPath": "data", + "columnName": "data", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "packetId", + "columnName": "packet_id", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "routingError", + "columnName": "routing_error", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "-1" + }, + { + "fieldPath": "snr", + "columnName": "snr", + "affinity": "REAL" + }, + { + "fieldPath": "rssi", + "columnName": "rssi", + "affinity": "INTEGER" + }, + { + "fieldPath": "hopsAway", + "columnName": "hopsAway", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "-1" + }, + { + "fieldPath": "sfpp_hash", + "columnName": "sfpp_hash", + "affinity": "BLOB" + }, + { + "fieldPath": "filtered", + "columnName": "filtered", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "messageText", + "columnName": "message_text", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "translatedText", + "columnName": "translated_text", + "affinity": "TEXT" + }, + { + "fieldPath": "showTranslated", + "columnName": "show_translated", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "uuid" + ] + }, + "indices": [ + { + "name": "index_packet_myNodeNum", + "unique": false, + "columnNames": [ + "myNodeNum" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_packet_myNodeNum` ON `${TABLE_NAME}` (`myNodeNum`)" + }, + { + "name": "index_packet_port_num", + "unique": false, + "columnNames": [ + "port_num" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_packet_port_num` ON `${TABLE_NAME}` (`port_num`)" + }, + { + "name": "index_packet_contact_key", + "unique": false, + "columnNames": [ + "contact_key" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_packet_contact_key` ON `${TABLE_NAME}` (`contact_key`)" + }, + { + "name": "index_packet_contact_key_port_num_received_time", + "unique": false, + "columnNames": [ + "contact_key", + "port_num", + "received_time" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_packet_contact_key_port_num_received_time` ON `${TABLE_NAME}` (`contact_key`, `port_num`, `received_time`)" + }, + { + "name": "index_packet_packet_id", + "unique": false, + "columnNames": [ + "packet_id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_packet_packet_id` ON `${TABLE_NAME}` (`packet_id`)" + }, + { + "name": "index_packet_received_time", + "unique": false, + "columnNames": [ + "received_time" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_packet_received_time` ON `${TABLE_NAME}` (`received_time`)" + }, + { + "name": "index_packet_filtered", + "unique": false, + "columnNames": [ + "filtered" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_packet_filtered` ON `${TABLE_NAME}` (`filtered`)" + }, + { + "name": "index_packet_read", + "unique": false, + "columnNames": [ + "read" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_packet_read` ON `${TABLE_NAME}` (`read`)" + } + ] + }, + { + "tableName": "packet_fts", + "createSql": "CREATE VIRTUAL TABLE IF NOT EXISTS `${TABLE_NAME}` USING FTS5(`message_text`, tokenize=`unicode61`, content=`packet`)", + "fields": [ + { + "fieldPath": "messageText", + "columnName": "message_text", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [] + }, + "ftsVersion": "FTS5", + "ftsOptions": { + "tokenizer": "unicode61", + "tokenizerArgs": [], + "contentTable": "packet", + "languageIdColumnName": "", + "matchInfo": "FTS4", + "notIndexedColumns": [], + "prefixSizes": [], + "preferredOrder": "ASC", + "contentRowId": "", + "columnSize": true, + "detail": "FULL" + }, + "contentSyncTriggers": [ + "CREATE TRIGGER IF NOT EXISTS room_fts_content_sync_packet_fts_BEFORE_UPDATE BEFORE UPDATE ON `packet` BEGIN DELETE FROM `packet_fts` WHERE `rowid`=OLD.`rowid`; END", + "CREATE TRIGGER IF NOT EXISTS room_fts_content_sync_packet_fts_BEFORE_DELETE BEFORE DELETE ON `packet` BEGIN DELETE FROM `packet_fts` WHERE `rowid`=OLD.`rowid`; END", + "CREATE TRIGGER IF NOT EXISTS room_fts_content_sync_packet_fts_AFTER_UPDATE AFTER UPDATE ON `packet` BEGIN INSERT INTO `packet_fts`(`rowid`, `message_text`) VALUES (NEW.`rowid`, NEW.`message_text`); END", + "CREATE TRIGGER IF NOT EXISTS room_fts_content_sync_packet_fts_AFTER_INSERT AFTER INSERT ON `packet` BEGIN INSERT INTO `packet_fts`(`rowid`, `message_text`) VALUES (NEW.`rowid`, NEW.`message_text`); END" + ] + }, + { + "tableName": "contact_settings", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`contact_key` TEXT NOT NULL, `muteUntil` INTEGER NOT NULL, `last_read_message_uuid` INTEGER, `last_read_message_timestamp` INTEGER, `filtering_disabled` INTEGER NOT NULL DEFAULT 0, `draft` TEXT NOT NULL DEFAULT '', `pinned` INTEGER NOT NULL DEFAULT 0, PRIMARY KEY(`contact_key`))", + "fields": [ + { + "fieldPath": "contact_key", + "columnName": "contact_key", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "muteUntil", + "columnName": "muteUntil", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastReadMessageUuid", + "columnName": "last_read_message_uuid", + "affinity": "INTEGER" + }, + { + "fieldPath": "lastReadMessageTimestamp", + "columnName": "last_read_message_timestamp", + "affinity": "INTEGER" + }, + { + "fieldPath": "filteringDisabled", + "columnName": "filtering_disabled", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "draft", + "columnName": "draft", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "pinned", + "columnName": "pinned", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "contact_key" + ] + } + }, + { + "tableName": "log", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uuid` TEXT NOT NULL, `type` TEXT NOT NULL, `received_date` INTEGER NOT NULL, `message` TEXT NOT NULL, `from_num` INTEGER NOT NULL DEFAULT 0, `port_num` INTEGER NOT NULL DEFAULT 0, `from_radio` BLOB NOT NULL DEFAULT x'', PRIMARY KEY(`uuid`))", + "fields": [ + { + "fieldPath": "uuid", + "columnName": "uuid", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "message_type", + "columnName": "type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "received_date", + "columnName": "received_date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "raw_message", + "columnName": "message", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "fromNum", + "columnName": "from_num", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "portNum", + "columnName": "port_num", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "fromRadio", + "columnName": "from_radio", + "affinity": "BLOB", + "notNull": true, + "defaultValue": "x''" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "uuid" + ] + }, + "indices": [ + { + "name": "index_log_from_num", + "unique": false, + "columnNames": [ + "from_num" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_log_from_num` ON `${TABLE_NAME}` (`from_num`)" + }, + { + "name": "index_log_port_num", + "unique": false, + "columnNames": [ + "port_num" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_log_port_num` ON `${TABLE_NAME}` (`port_num`)" + } + ] + }, + { + "tableName": "quick_chat", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uuid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `message` TEXT NOT NULL, `mode` TEXT NOT NULL, `position` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "uuid", + "columnName": "uuid", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "message", + "columnName": "message", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "mode", + "columnName": "mode", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "position", + "columnName": "position", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "uuid" + ] + } + }, + { + "tableName": "reactions", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`myNodeNum` INTEGER NOT NULL DEFAULT 0, `reply_id` INTEGER NOT NULL, `user_id` TEXT NOT NULL, `emoji` TEXT NOT NULL, `timestamp` INTEGER NOT NULL, `snr` REAL, `rssi` INTEGER, `hopsAway` INTEGER NOT NULL DEFAULT -1, `packet_id` INTEGER NOT NULL DEFAULT 0, `status` INTEGER NOT NULL DEFAULT 0, `routing_error` INTEGER NOT NULL DEFAULT 0, `relays` INTEGER NOT NULL DEFAULT 0, `relay_node` INTEGER, `to` TEXT, `channel` INTEGER NOT NULL DEFAULT 0, `sfpp_hash` BLOB, PRIMARY KEY(`myNodeNum`, `reply_id`, `user_id`, `emoji`))", + "fields": [ + { + "fieldPath": "myNodeNum", + "columnName": "myNodeNum", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "replyId", + "columnName": "reply_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "userId", + "columnName": "user_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "emoji", + "columnName": "emoji", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "timestamp", + "columnName": "timestamp", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "snr", + "columnName": "snr", + "affinity": "REAL" + }, + { + "fieldPath": "rssi", + "columnName": "rssi", + "affinity": "INTEGER" + }, + { + "fieldPath": "hopsAway", + "columnName": "hopsAway", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "-1" + }, + { + "fieldPath": "packetId", + "columnName": "packet_id", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "status", + "columnName": "status", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "routingError", + "columnName": "routing_error", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "relays", + "columnName": "relays", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "relayNode", + "columnName": "relay_node", + "affinity": "INTEGER" + }, + { + "fieldPath": "to", + "columnName": "to", + "affinity": "TEXT" + }, + { + "fieldPath": "channel", + "columnName": "channel", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "sfpp_hash", + "columnName": "sfpp_hash", + "affinity": "BLOB" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "myNodeNum", + "reply_id", + "user_id", + "emoji" + ] + }, + "indices": [ + { + "name": "index_reactions_reply_id", + "unique": false, + "columnNames": [ + "reply_id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_reactions_reply_id` ON `${TABLE_NAME}` (`reply_id`)" + }, + { + "name": "index_reactions_packet_id", + "unique": false, + "columnNames": [ + "packet_id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_reactions_packet_id` ON `${TABLE_NAME}` (`packet_id`)" + } + ] + }, + { + "tableName": "metadata", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`num` INTEGER NOT NULL, `proto` BLOB NOT NULL, `timestamp` INTEGER NOT NULL, PRIMARY KEY(`num`))", + "fields": [ + { + "fieldPath": "num", + "columnName": "num", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "proto", + "columnName": "proto", + "affinity": "BLOB", + "notNull": true + }, + { + "fieldPath": "timestamp", + "columnName": "timestamp", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "num" + ] + }, + "indices": [ + { + "name": "index_metadata_num", + "unique": false, + "columnNames": [ + "num" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_metadata_num` ON `${TABLE_NAME}` (`num`)" + } + ] + }, + { + "tableName": "device_hardware", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`actively_supported` INTEGER NOT NULL, `architecture` TEXT NOT NULL, `display_name` TEXT NOT NULL, `has_ink_hud` INTEGER, `has_mui` INTEGER, `hwModel` INTEGER NOT NULL, `hw_model_slug` TEXT NOT NULL, `images` TEXT, `last_updated` INTEGER NOT NULL, `partition_scheme` TEXT, `platformio_target` TEXT NOT NULL, `requires_dfu` INTEGER, `support_level` INTEGER, `tags` TEXT, PRIMARY KEY(`platformio_target`))", + "fields": [ + { + "fieldPath": "activelySupported", + "columnName": "actively_supported", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "architecture", + "columnName": "architecture", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "displayName", + "columnName": "display_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "hasInkHud", + "columnName": "has_ink_hud", + "affinity": "INTEGER" + }, + { + "fieldPath": "hasMui", + "columnName": "has_mui", + "affinity": "INTEGER" + }, + { + "fieldPath": "hwModel", + "columnName": "hwModel", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "hwModelSlug", + "columnName": "hw_model_slug", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "images", + "columnName": "images", + "affinity": "TEXT" + }, + { + "fieldPath": "lastUpdated", + "columnName": "last_updated", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "partitionScheme", + "columnName": "partition_scheme", + "affinity": "TEXT" + }, + { + "fieldPath": "platformioTarget", + "columnName": "platformio_target", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "requiresDfu", + "columnName": "requires_dfu", + "affinity": "INTEGER" + }, + { + "fieldPath": "supportLevel", + "columnName": "support_level", + "affinity": "INTEGER" + }, + { + "fieldPath": "tags", + "columnName": "tags", + "affinity": "TEXT" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "platformio_target" + ] + } + }, + { + "tableName": "device_link", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`short_code` TEXT NOT NULL, `link_description` TEXT, `is_vendor` INTEGER NOT NULL, `regions` TEXT, `targets` TEXT, PRIMARY KEY(`short_code`))", + "fields": [ + { + "fieldPath": "shortCode", + "columnName": "short_code", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "linkDescription", + "columnName": "link_description", + "affinity": "TEXT" + }, + { + "fieldPath": "isVendor", + "columnName": "is_vendor", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "regions", + "columnName": "regions", + "affinity": "TEXT" + }, + { + "fieldPath": "targets", + "columnName": "targets", + "affinity": "TEXT" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "short_code" + ] + } + }, + { + "tableName": "firmware_release", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `page_url` TEXT NOT NULL, `release_notes` TEXT NOT NULL, `title` TEXT NOT NULL, `zip_url` TEXT NOT NULL, `last_updated` INTEGER NOT NULL, `release_type` TEXT NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "pageUrl", + "columnName": "page_url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "releaseNotes", + "columnName": "release_notes", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "zipUrl", + "columnName": "zip_url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastUpdated", + "columnName": "last_updated", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "releaseType", + "columnName": "release_type", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + } + }, + { + "tableName": "traceroute_node_position", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`log_uuid` TEXT NOT NULL, `request_id` INTEGER NOT NULL, `node_num` INTEGER NOT NULL, `position` BLOB NOT NULL, PRIMARY KEY(`log_uuid`, `node_num`), FOREIGN KEY(`log_uuid`) REFERENCES `log`(`uuid`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "logUuid", + "columnName": "log_uuid", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "requestId", + "columnName": "request_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "nodeNum", + "columnName": "node_num", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "position", + "columnName": "position", + "affinity": "BLOB", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "log_uuid", + "node_num" + ] + }, + "indices": [ + { + "name": "index_traceroute_node_position_log_uuid", + "unique": false, + "columnNames": [ + "log_uuid" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_traceroute_node_position_log_uuid` ON `${TABLE_NAME}` (`log_uuid`)" + }, + { + "name": "index_traceroute_node_position_request_id", + "unique": false, + "columnNames": [ + "request_id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_traceroute_node_position_request_id` ON `${TABLE_NAME}` (`request_id`)" + } + ], + "foreignKeys": [ + { + "table": "log", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "log_uuid" + ], + "referencedColumns": [ + "uuid" + ] + } + ] + }, + { + "tableName": "discovery_session", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `timestamp` INTEGER NOT NULL, `presets_scanned` TEXT NOT NULL, `home_preset` TEXT NOT NULL, `total_unique_nodes` INTEGER NOT NULL DEFAULT 0, `avg_channel_utilization` REAL NOT NULL DEFAULT 0.0, `total_messages` INTEGER NOT NULL DEFAULT 0, `total_sensor_packets` INTEGER NOT NULL DEFAULT 0, `furthest_node_distance` REAL NOT NULL DEFAULT 0.0, `completion_status` TEXT NOT NULL DEFAULT 'complete', `ai_summary` TEXT, `user_latitude` REAL NOT NULL DEFAULT 0.0, `user_longitude` REAL NOT NULL DEFAULT 0.0, `total_dwell_seconds` INTEGER NOT NULL DEFAULT 0, `device_address` TEXT, `home_lora_config` BLOB, `home_primary_channel` BLOB)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "timestamp", + "columnName": "timestamp", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "presetsScanned", + "columnName": "presets_scanned", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "homePreset", + "columnName": "home_preset", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "totalUniqueNodes", + "columnName": "total_unique_nodes", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "avgChannelUtilization", + "columnName": "avg_channel_utilization", + "affinity": "REAL", + "notNull": true, + "defaultValue": "0.0" + }, + { + "fieldPath": "totalMessages", + "columnName": "total_messages", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "totalSensorPackets", + "columnName": "total_sensor_packets", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "furthestNodeDistance", + "columnName": "furthest_node_distance", + "affinity": "REAL", + "notNull": true, + "defaultValue": "0.0" + }, + { + "fieldPath": "completionStatus", + "columnName": "completion_status", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "'complete'" + }, + { + "fieldPath": "aiSummary", + "columnName": "ai_summary", + "affinity": "TEXT" + }, + { + "fieldPath": "userLatitude", + "columnName": "user_latitude", + "affinity": "REAL", + "notNull": true, + "defaultValue": "0.0" + }, + { + "fieldPath": "userLongitude", + "columnName": "user_longitude", + "affinity": "REAL", + "notNull": true, + "defaultValue": "0.0" + }, + { + "fieldPath": "totalDwellSeconds", + "columnName": "total_dwell_seconds", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "deviceAddress", + "columnName": "device_address", + "affinity": "TEXT" + }, + { + "fieldPath": "homeLoraConfig", + "columnName": "home_lora_config", + "affinity": "BLOB" + }, + { + "fieldPath": "homePrimaryChannel", + "columnName": "home_primary_channel", + "affinity": "BLOB" + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + } + }, + { + "tableName": "discovery_preset_result", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `session_id` INTEGER NOT NULL, `preset_name` TEXT NOT NULL, `dwell_duration_seconds` INTEGER NOT NULL DEFAULT 0, `unique_nodes` INTEGER NOT NULL DEFAULT 0, `direct_neighbor_count` INTEGER NOT NULL DEFAULT 0, `mesh_neighbor_count` INTEGER NOT NULL DEFAULT 0, `infrastructure_node_count` INTEGER NOT NULL DEFAULT 0, `message_count` INTEGER NOT NULL DEFAULT 0, `sensor_packet_count` INTEGER NOT NULL DEFAULT 0, `avg_channel_utilization` REAL NOT NULL DEFAULT 0.0, `avg_airtime_rate` REAL NOT NULL DEFAULT 0.0, `packet_success_rate` REAL NOT NULL DEFAULT 0.0, `packet_failure_rate` REAL NOT NULL DEFAULT 0.0, `ai_summary` TEXT, `num_packets_tx` INTEGER NOT NULL DEFAULT 0, `num_packets_rx` INTEGER NOT NULL DEFAULT 0, `num_packets_rx_bad` INTEGER NOT NULL DEFAULT 0, `num_rx_dupe` INTEGER NOT NULL DEFAULT 0, `num_tx_relay` INTEGER NOT NULL DEFAULT 0, `num_tx_relay_canceled` INTEGER NOT NULL DEFAULT 0, `num_online_nodes` INTEGER NOT NULL DEFAULT 0, `num_total_nodes` INTEGER NOT NULL DEFAULT 0, `uptime_seconds` INTEGER NOT NULL DEFAULT 0, FOREIGN KEY(`session_id`) REFERENCES `discovery_session`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "sessionId", + "columnName": "session_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "presetName", + "columnName": "preset_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "dwellDurationSeconds", + "columnName": "dwell_duration_seconds", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "uniqueNodes", + "columnName": "unique_nodes", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "directNeighborCount", + "columnName": "direct_neighbor_count", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "meshNeighborCount", + "columnName": "mesh_neighbor_count", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "infrastructureNodeCount", + "columnName": "infrastructure_node_count", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "messageCount", + "columnName": "message_count", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "sensorPacketCount", + "columnName": "sensor_packet_count", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "avgChannelUtilization", + "columnName": "avg_channel_utilization", + "affinity": "REAL", + "notNull": true, + "defaultValue": "0.0" + }, + { + "fieldPath": "avgAirtimeRate", + "columnName": "avg_airtime_rate", + "affinity": "REAL", + "notNull": true, + "defaultValue": "0.0" + }, + { + "fieldPath": "packetSuccessRate", + "columnName": "packet_success_rate", + "affinity": "REAL", + "notNull": true, + "defaultValue": "0.0" + }, + { + "fieldPath": "packetFailureRate", + "columnName": "packet_failure_rate", + "affinity": "REAL", + "notNull": true, + "defaultValue": "0.0" + }, + { + "fieldPath": "aiSummary", + "columnName": "ai_summary", + "affinity": "TEXT" + }, + { + "fieldPath": "numPacketsTx", + "columnName": "num_packets_tx", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "numPacketsRx", + "columnName": "num_packets_rx", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "numPacketsRxBad", + "columnName": "num_packets_rx_bad", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "numRxDupe", + "columnName": "num_rx_dupe", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "numTxRelay", + "columnName": "num_tx_relay", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "numTxRelayCanceled", + "columnName": "num_tx_relay_canceled", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "numOnlineNodes", + "columnName": "num_online_nodes", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "numTotalNodes", + "columnName": "num_total_nodes", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "uptimeSeconds", + "columnName": "uptime_seconds", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_discovery_preset_result_session_id", + "unique": false, + "columnNames": [ + "session_id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_discovery_preset_result_session_id` ON `${TABLE_NAME}` (`session_id`)" + } + ], + "foreignKeys": [ + { + "table": "discovery_session", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "session_id" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "discovered_node", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `preset_result_id` INTEGER NOT NULL, `node_num` INTEGER NOT NULL, `short_name` TEXT, `long_name` TEXT, `neighbor_type` TEXT NOT NULL DEFAULT 'direct', `latitude` REAL, `longitude` REAL, `distance_from_user` REAL, `hop_count` INTEGER NOT NULL DEFAULT 0, `snr` REAL NOT NULL DEFAULT 0, `rssi` INTEGER, `message_count` INTEGER NOT NULL DEFAULT 0, `sensor_packet_count` INTEGER NOT NULL DEFAULT 0, `is_infrastructure` INTEGER NOT NULL DEFAULT 0, FOREIGN KEY(`preset_result_id`) REFERENCES `discovery_preset_result`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "presetResultId", + "columnName": "preset_result_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "nodeNum", + "columnName": "node_num", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "shortName", + "columnName": "short_name", + "affinity": "TEXT" + }, + { + "fieldPath": "longName", + "columnName": "long_name", + "affinity": "TEXT" + }, + { + "fieldPath": "neighborType", + "columnName": "neighbor_type", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "'direct'" + }, + { + "fieldPath": "latitude", + "columnName": "latitude", + "affinity": "REAL" + }, + { + "fieldPath": "longitude", + "columnName": "longitude", + "affinity": "REAL" + }, + { + "fieldPath": "distanceFromUser", + "columnName": "distance_from_user", + "affinity": "REAL" + }, + { + "fieldPath": "hopCount", + "columnName": "hop_count", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "snr", + "columnName": "snr", + "affinity": "REAL", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "rssi", + "columnName": "rssi", + "affinity": "INTEGER" + }, + { + "fieldPath": "messageCount", + "columnName": "message_count", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "sensorPacketCount", + "columnName": "sensor_packet_count", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "isInfrastructure", + "columnName": "is_infrastructure", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_discovered_node_preset_result_id", + "unique": false, + "columnNames": [ + "preset_result_id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_discovered_node_preset_result_id` ON `${TABLE_NAME}` (`preset_result_id`)" + }, + { + "name": "index_discovered_node_node_num", + "unique": false, + "columnNames": [ + "node_num" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_discovered_node_node_num` ON `${TABLE_NAME}` (`node_num`)" + } + ], + "foreignKeys": [ + { + "table": "discovery_preset_result", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "preset_result_id" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "event_firmware_edition", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`edition` TEXT NOT NULL, `display_name` TEXT NOT NULL, `welcome_message` TEXT NOT NULL, `event_start` TEXT, `event_end` TEXT, `time_zone` TEXT, `location` TEXT, `icon_url` TEXT, `accent_color` TEXT, `tag` TEXT, `domain` TEXT, `theme_json` TEXT, `firmware_json` TEXT, `links_json` TEXT NOT NULL, PRIMARY KEY(`edition`))", + "fields": [ + { + "fieldPath": "edition", + "columnName": "edition", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "displayName", + "columnName": "display_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "welcomeMessage", + "columnName": "welcome_message", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "eventStart", + "columnName": "event_start", + "affinity": "TEXT" + }, + { + "fieldPath": "eventEnd", + "columnName": "event_end", + "affinity": "TEXT" + }, + { + "fieldPath": "timeZone", + "columnName": "time_zone", + "affinity": "TEXT" + }, + { + "fieldPath": "location", + "columnName": "location", + "affinity": "TEXT" + }, + { + "fieldPath": "iconUrl", + "columnName": "icon_url", + "affinity": "TEXT" + }, + { + "fieldPath": "accentColor", + "columnName": "accent_color", + "affinity": "TEXT" + }, + { + "fieldPath": "tag", + "columnName": "tag", + "affinity": "TEXT" + }, + { + "fieldPath": "domain", + "columnName": "domain", + "affinity": "TEXT" + }, + { + "fieldPath": "themeJson", + "columnName": "theme_json", + "affinity": "TEXT" + }, + { + "fieldPath": "firmwareJson", + "columnName": "firmware_json", + "affinity": "TEXT" + }, + { + "fieldPath": "linksJson", + "columnName": "links_json", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "edition" + ] + } + }, + { + "tableName": "merge_marker", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`source_db_name` TEXT NOT NULL, `merged_at` INTEGER NOT NULL, PRIMARY KEY(`source_db_name`))", + "fields": [ + { + "fieldPath": "sourceDbName", + "columnName": "source_db_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "mergedAt", + "columnName": "merged_at", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "source_db_name" + ] + } + }, + { + "tableName": "channel_set", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `channel_set` BLOB NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "channelSet", + "columnName": "channel_set", + "affinity": "BLOB", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + } + }, + { + "tableName": "bootloader_ota_quirks_cache", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `devices_json` TEXT NOT NULL, `soft_device_variants_json` TEXT NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "devicesJson", + "columnName": "devices_json", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "softDeviceVariantsJson", + "columnName": "soft_device_variants_json", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + } + }, + { + "tableName": "maintenance_uf2_cache", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `manifest_json` TEXT NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "manifestJson", + "columnName": "manifest_json", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + } + } + ], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '3a701cc22f7c57b09cac889f64cd9360')" + ] + } +} \ No newline at end of file diff --git a/core/database/schemas/org.meshtastic.core.database.MeshtasticDatabase/60.json b/core/database/schemas/org.meshtastic.core.database.MeshtasticDatabase/60.json new file mode 100644 index 0000000000..c93a69d5ff --- /dev/null +++ b/core/database/schemas/org.meshtastic.core.database.MeshtasticDatabase/60.json @@ -0,0 +1,1845 @@ +{ + "formatVersion": 1, + "database": { + "version": 60, + "identityHash": "ddb50cdd4162940a03ae5c8b778ba4ee", + "entities": [ + { + "tableName": "my_node", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`myNodeNum` INTEGER NOT NULL, `model` TEXT, `firmwareVersion` TEXT, `couldUpdate` INTEGER NOT NULL, `shouldUpdate` INTEGER NOT NULL, `currentPacketId` INTEGER NOT NULL, `messageTimeoutMsec` INTEGER NOT NULL, `minAppVersion` INTEGER NOT NULL, `maxChannels` INTEGER NOT NULL, `hasWifi` INTEGER NOT NULL, `deviceId` TEXT, `pioEnv` TEXT, PRIMARY KEY(`myNodeNum`))", + "fields": [ + { + "fieldPath": "myNodeNum", + "columnName": "myNodeNum", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "model", + "columnName": "model", + "affinity": "TEXT" + }, + { + "fieldPath": "firmwareVersion", + "columnName": "firmwareVersion", + "affinity": "TEXT" + }, + { + "fieldPath": "couldUpdate", + "columnName": "couldUpdate", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "shouldUpdate", + "columnName": "shouldUpdate", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "currentPacketId", + "columnName": "currentPacketId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "messageTimeoutMsec", + "columnName": "messageTimeoutMsec", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "minAppVersion", + "columnName": "minAppVersion", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "maxChannels", + "columnName": "maxChannels", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "hasWifi", + "columnName": "hasWifi", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "deviceId", + "columnName": "deviceId", + "affinity": "TEXT" + }, + { + "fieldPath": "pioEnv", + "columnName": "pioEnv", + "affinity": "TEXT" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "myNodeNum" + ] + } + }, + { + "tableName": "nodes", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`num` INTEGER NOT NULL, `user` BLOB NOT NULL, `long_name` TEXT, `short_name` TEXT, `position` BLOB NOT NULL, `latitude` REAL NOT NULL, `longitude` REAL NOT NULL, `snr` REAL NOT NULL, `rssi` INTEGER NOT NULL, `last_heard` INTEGER NOT NULL, `device_metrics` BLOB NOT NULL, `channel` INTEGER NOT NULL, `via_mqtt` INTEGER NOT NULL, `hops_away` INTEGER NOT NULL, `is_favorite` INTEGER NOT NULL, `is_ignored` INTEGER NOT NULL DEFAULT 0, `is_muted` INTEGER NOT NULL DEFAULT 0, `environment_metrics` BLOB NOT NULL, `power_metrics` BLOB NOT NULL, `air_quality_metrics` BLOB NOT NULL DEFAULT x'', `paxcounter` BLOB NOT NULL, `public_key` BLOB, `notes` TEXT NOT NULL DEFAULT '', `power_channel_labels` TEXT NOT NULL DEFAULT '[]', `manually_verified` INTEGER NOT NULL DEFAULT 0, `node_status` TEXT, `last_transport` INTEGER NOT NULL DEFAULT 0, `has_xeddsa_signed` INTEGER NOT NULL DEFAULT 0, `heard_on_current_lora` INTEGER NOT NULL DEFAULT 1, `key_match` INTEGER NOT NULL DEFAULT 1, `new_public_key` BLOB, PRIMARY KEY(`num`))", + "fields": [ + { + "fieldPath": "num", + "columnName": "num", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "user", + "columnName": "user", + "affinity": "BLOB", + "notNull": true + }, + { + "fieldPath": "longName", + "columnName": "long_name", + "affinity": "TEXT" + }, + { + "fieldPath": "shortName", + "columnName": "short_name", + "affinity": "TEXT" + }, + { + "fieldPath": "position", + "columnName": "position", + "affinity": "BLOB", + "notNull": true + }, + { + "fieldPath": "latitude", + "columnName": "latitude", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "longitude", + "columnName": "longitude", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "snr", + "columnName": "snr", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "rssi", + "columnName": "rssi", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastHeard", + "columnName": "last_heard", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "deviceTelemetry", + "columnName": "device_metrics", + "affinity": "BLOB", + "notNull": true + }, + { + "fieldPath": "channel", + "columnName": "channel", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "viaMqtt", + "columnName": "via_mqtt", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "hopsAway", + "columnName": "hops_away", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isFavorite", + "columnName": "is_favorite", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isIgnored", + "columnName": "is_ignored", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "isMuted", + "columnName": "is_muted", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "environmentTelemetry", + "columnName": "environment_metrics", + "affinity": "BLOB", + "notNull": true + }, + { + "fieldPath": "powerTelemetry", + "columnName": "power_metrics", + "affinity": "BLOB", + "notNull": true + }, + { + "fieldPath": "airQualityTelemetry", + "columnName": "air_quality_metrics", + "affinity": "BLOB", + "notNull": true, + "defaultValue": "x''" + }, + { + "fieldPath": "paxcounter", + "columnName": "paxcounter", + "affinity": "BLOB", + "notNull": true + }, + { + "fieldPath": "publicKey", + "columnName": "public_key", + "affinity": "BLOB" + }, + { + "fieldPath": "notes", + "columnName": "notes", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "powerChannelLabels", + "columnName": "power_channel_labels", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "'[]'" + }, + { + "fieldPath": "manuallyVerified", + "columnName": "manually_verified", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "nodeStatus", + "columnName": "node_status", + "affinity": "TEXT" + }, + { + "fieldPath": "lastTransport", + "columnName": "last_transport", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "signsPackets", + "columnName": "has_xeddsa_signed", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "heardOnCurrentLora", + "columnName": "heard_on_current_lora", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "1" + }, + { + "fieldPath": "keyMatch", + "columnName": "key_match", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "1" + }, + { + "fieldPath": "newPublicKey", + "columnName": "new_public_key", + "affinity": "BLOB" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "num" + ] + }, + "indices": [ + { + "name": "index_nodes_last_heard", + "unique": false, + "columnNames": [ + "last_heard" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_nodes_last_heard` ON `${TABLE_NAME}` (`last_heard`)" + }, + { + "name": "index_nodes_short_name", + "unique": false, + "columnNames": [ + "short_name" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_nodes_short_name` ON `${TABLE_NAME}` (`short_name`)" + }, + { + "name": "index_nodes_long_name", + "unique": false, + "columnNames": [ + "long_name" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_nodes_long_name` ON `${TABLE_NAME}` (`long_name`)" + }, + { + "name": "index_nodes_hops_away", + "unique": false, + "columnNames": [ + "hops_away" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_nodes_hops_away` ON `${TABLE_NAME}` (`hops_away`)" + }, + { + "name": "index_nodes_is_favorite", + "unique": false, + "columnNames": [ + "is_favorite" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_nodes_is_favorite` ON `${TABLE_NAME}` (`is_favorite`)" + }, + { + "name": "index_nodes_last_heard_is_favorite", + "unique": false, + "columnNames": [ + "last_heard", + "is_favorite" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_nodes_last_heard_is_favorite` ON `${TABLE_NAME}` (`last_heard`, `is_favorite`)" + }, + { + "name": "index_nodes_public_key", + "unique": false, + "columnNames": [ + "public_key" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_nodes_public_key` ON `${TABLE_NAME}` (`public_key`)" + } + ] + }, + { + "tableName": "packet", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uuid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `myNodeNum` INTEGER NOT NULL DEFAULT 0, `port_num` INTEGER NOT NULL, `contact_key` TEXT NOT NULL, `received_time` INTEGER NOT NULL, `read` INTEGER NOT NULL DEFAULT 1, `data` TEXT NOT NULL, `packet_id` INTEGER NOT NULL DEFAULT 0, `routing_error` INTEGER NOT NULL DEFAULT -1, `snr` REAL, `rssi` INTEGER, `hopsAway` INTEGER NOT NULL DEFAULT -1, `sfpp_hash` BLOB, `filtered` INTEGER NOT NULL DEFAULT 0, `message_text` TEXT NOT NULL DEFAULT '', `translated_text` TEXT, `show_translated` INTEGER NOT NULL DEFAULT 0)", + "fields": [ + { + "fieldPath": "uuid", + "columnName": "uuid", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "myNodeNum", + "columnName": "myNodeNum", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "port_num", + "columnName": "port_num", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "contact_key", + "columnName": "contact_key", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "received_time", + "columnName": "received_time", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "read", + "columnName": "read", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "1" + }, + { + "fieldPath": "data", + "columnName": "data", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "packetId", + "columnName": "packet_id", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "routingError", + "columnName": "routing_error", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "-1" + }, + { + "fieldPath": "snr", + "columnName": "snr", + "affinity": "REAL" + }, + { + "fieldPath": "rssi", + "columnName": "rssi", + "affinity": "INTEGER" + }, + { + "fieldPath": "hopsAway", + "columnName": "hopsAway", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "-1" + }, + { + "fieldPath": "sfpp_hash", + "columnName": "sfpp_hash", + "affinity": "BLOB" + }, + { + "fieldPath": "filtered", + "columnName": "filtered", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "messageText", + "columnName": "message_text", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "translatedText", + "columnName": "translated_text", + "affinity": "TEXT" + }, + { + "fieldPath": "showTranslated", + "columnName": "show_translated", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "uuid" + ] + }, + "indices": [ + { + "name": "index_packet_myNodeNum", + "unique": false, + "columnNames": [ + "myNodeNum" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_packet_myNodeNum` ON `${TABLE_NAME}` (`myNodeNum`)" + }, + { + "name": "index_packet_port_num", + "unique": false, + "columnNames": [ + "port_num" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_packet_port_num` ON `${TABLE_NAME}` (`port_num`)" + }, + { + "name": "index_packet_contact_key", + "unique": false, + "columnNames": [ + "contact_key" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_packet_contact_key` ON `${TABLE_NAME}` (`contact_key`)" + }, + { + "name": "index_packet_contact_key_port_num_received_time", + "unique": false, + "columnNames": [ + "contact_key", + "port_num", + "received_time" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_packet_contact_key_port_num_received_time` ON `${TABLE_NAME}` (`contact_key`, `port_num`, `received_time`)" + }, + { + "name": "index_packet_packet_id", + "unique": false, + "columnNames": [ + "packet_id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_packet_packet_id` ON `${TABLE_NAME}` (`packet_id`)" + }, + { + "name": "index_packet_received_time", + "unique": false, + "columnNames": [ + "received_time" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_packet_received_time` ON `${TABLE_NAME}` (`received_time`)" + }, + { + "name": "index_packet_filtered", + "unique": false, + "columnNames": [ + "filtered" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_packet_filtered` ON `${TABLE_NAME}` (`filtered`)" + }, + { + "name": "index_packet_read", + "unique": false, + "columnNames": [ + "read" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_packet_read` ON `${TABLE_NAME}` (`read`)" + } + ] + }, + { + "tableName": "packet_fts", + "createSql": "CREATE VIRTUAL TABLE IF NOT EXISTS `${TABLE_NAME}` USING FTS5(`message_text`, tokenize=`unicode61`, content=`packet`)", + "fields": [ + { + "fieldPath": "messageText", + "columnName": "message_text", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [] + }, + "ftsVersion": "FTS5", + "ftsOptions": { + "tokenizer": "unicode61", + "tokenizerArgs": [], + "contentTable": "packet", + "languageIdColumnName": "", + "matchInfo": "FTS4", + "notIndexedColumns": [], + "prefixSizes": [], + "preferredOrder": "ASC", + "contentRowId": "", + "columnSize": true, + "detail": "FULL" + }, + "contentSyncTriggers": [ + "CREATE TRIGGER IF NOT EXISTS room_fts_content_sync_packet_fts_BEFORE_UPDATE BEFORE UPDATE ON `packet` BEGIN DELETE FROM `packet_fts` WHERE `rowid`=OLD.`rowid`; END", + "CREATE TRIGGER IF NOT EXISTS room_fts_content_sync_packet_fts_BEFORE_DELETE BEFORE DELETE ON `packet` BEGIN DELETE FROM `packet_fts` WHERE `rowid`=OLD.`rowid`; END", + "CREATE TRIGGER IF NOT EXISTS room_fts_content_sync_packet_fts_AFTER_UPDATE AFTER UPDATE ON `packet` BEGIN INSERT INTO `packet_fts`(`rowid`, `message_text`) VALUES (NEW.`rowid`, NEW.`message_text`); END", + "CREATE TRIGGER IF NOT EXISTS room_fts_content_sync_packet_fts_AFTER_INSERT AFTER INSERT ON `packet` BEGIN INSERT INTO `packet_fts`(`rowid`, `message_text`) VALUES (NEW.`rowid`, NEW.`message_text`); END" + ] + }, + { + "tableName": "contact_settings", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`contact_key` TEXT NOT NULL, `muteUntil` INTEGER NOT NULL, `last_read_message_uuid` INTEGER, `last_read_message_timestamp` INTEGER, `filtering_disabled` INTEGER NOT NULL DEFAULT 0, `draft` TEXT NOT NULL DEFAULT '', `pinned` INTEGER NOT NULL DEFAULT 0, `display_name` TEXT NOT NULL DEFAULT '', PRIMARY KEY(`contact_key`))", + "fields": [ + { + "fieldPath": "contact_key", + "columnName": "contact_key", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "muteUntil", + "columnName": "muteUntil", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastReadMessageUuid", + "columnName": "last_read_message_uuid", + "affinity": "INTEGER" + }, + { + "fieldPath": "lastReadMessageTimestamp", + "columnName": "last_read_message_timestamp", + "affinity": "INTEGER" + }, + { + "fieldPath": "filteringDisabled", + "columnName": "filtering_disabled", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "draft", + "columnName": "draft", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "pinned", + "columnName": "pinned", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "displayName", + "columnName": "display_name", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "contact_key" + ] + } + }, + { + "tableName": "log", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uuid` TEXT NOT NULL, `type` TEXT NOT NULL, `received_date` INTEGER NOT NULL, `message` TEXT NOT NULL, `from_num` INTEGER NOT NULL DEFAULT 0, `port_num` INTEGER NOT NULL DEFAULT 0, `from_radio` BLOB NOT NULL DEFAULT x'', PRIMARY KEY(`uuid`))", + "fields": [ + { + "fieldPath": "uuid", + "columnName": "uuid", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "message_type", + "columnName": "type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "received_date", + "columnName": "received_date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "raw_message", + "columnName": "message", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "fromNum", + "columnName": "from_num", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "portNum", + "columnName": "port_num", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "fromRadio", + "columnName": "from_radio", + "affinity": "BLOB", + "notNull": true, + "defaultValue": "x''" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "uuid" + ] + }, + "indices": [ + { + "name": "index_log_from_num", + "unique": false, + "columnNames": [ + "from_num" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_log_from_num` ON `${TABLE_NAME}` (`from_num`)" + }, + { + "name": "index_log_port_num", + "unique": false, + "columnNames": [ + "port_num" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_log_port_num` ON `${TABLE_NAME}` (`port_num`)" + } + ] + }, + { + "tableName": "quick_chat", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uuid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `message` TEXT NOT NULL, `mode` TEXT NOT NULL, `position` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "uuid", + "columnName": "uuid", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "message", + "columnName": "message", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "mode", + "columnName": "mode", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "position", + "columnName": "position", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "uuid" + ] + } + }, + { + "tableName": "reactions", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`myNodeNum` INTEGER NOT NULL DEFAULT 0, `reply_id` INTEGER NOT NULL, `user_id` TEXT NOT NULL, `emoji` TEXT NOT NULL, `timestamp` INTEGER NOT NULL, `snr` REAL, `rssi` INTEGER, `hopsAway` INTEGER NOT NULL DEFAULT -1, `packet_id` INTEGER NOT NULL DEFAULT 0, `status` INTEGER NOT NULL DEFAULT 0, `routing_error` INTEGER NOT NULL DEFAULT 0, `relays` INTEGER NOT NULL DEFAULT 0, `relay_node` INTEGER, `to` TEXT, `channel` INTEGER NOT NULL DEFAULT 0, `sfpp_hash` BLOB, PRIMARY KEY(`myNodeNum`, `reply_id`, `user_id`, `emoji`))", + "fields": [ + { + "fieldPath": "myNodeNum", + "columnName": "myNodeNum", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "replyId", + "columnName": "reply_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "userId", + "columnName": "user_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "emoji", + "columnName": "emoji", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "timestamp", + "columnName": "timestamp", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "snr", + "columnName": "snr", + "affinity": "REAL" + }, + { + "fieldPath": "rssi", + "columnName": "rssi", + "affinity": "INTEGER" + }, + { + "fieldPath": "hopsAway", + "columnName": "hopsAway", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "-1" + }, + { + "fieldPath": "packetId", + "columnName": "packet_id", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "status", + "columnName": "status", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "routingError", + "columnName": "routing_error", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "relays", + "columnName": "relays", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "relayNode", + "columnName": "relay_node", + "affinity": "INTEGER" + }, + { + "fieldPath": "to", + "columnName": "to", + "affinity": "TEXT" + }, + { + "fieldPath": "channel", + "columnName": "channel", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "sfpp_hash", + "columnName": "sfpp_hash", + "affinity": "BLOB" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "myNodeNum", + "reply_id", + "user_id", + "emoji" + ] + }, + "indices": [ + { + "name": "index_reactions_reply_id", + "unique": false, + "columnNames": [ + "reply_id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_reactions_reply_id` ON `${TABLE_NAME}` (`reply_id`)" + }, + { + "name": "index_reactions_packet_id", + "unique": false, + "columnNames": [ + "packet_id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_reactions_packet_id` ON `${TABLE_NAME}` (`packet_id`)" + } + ] + }, + { + "tableName": "metadata", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`num` INTEGER NOT NULL, `proto` BLOB NOT NULL, `timestamp` INTEGER NOT NULL, PRIMARY KEY(`num`))", + "fields": [ + { + "fieldPath": "num", + "columnName": "num", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "proto", + "columnName": "proto", + "affinity": "BLOB", + "notNull": true + }, + { + "fieldPath": "timestamp", + "columnName": "timestamp", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "num" + ] + }, + "indices": [ + { + "name": "index_metadata_num", + "unique": false, + "columnNames": [ + "num" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_metadata_num` ON `${TABLE_NAME}` (`num`)" + } + ] + }, + { + "tableName": "device_hardware", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`actively_supported` INTEGER NOT NULL, `architecture` TEXT NOT NULL, `display_name` TEXT NOT NULL, `has_ink_hud` INTEGER, `has_mui` INTEGER, `hwModel` INTEGER NOT NULL, `hw_model_slug` TEXT NOT NULL, `images` TEXT, `last_updated` INTEGER NOT NULL, `partition_scheme` TEXT, `platformio_target` TEXT NOT NULL, `requires_dfu` INTEGER, `support_level` INTEGER, `tags` TEXT, PRIMARY KEY(`platformio_target`))", + "fields": [ + { + "fieldPath": "activelySupported", + "columnName": "actively_supported", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "architecture", + "columnName": "architecture", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "displayName", + "columnName": "display_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "hasInkHud", + "columnName": "has_ink_hud", + "affinity": "INTEGER" + }, + { + "fieldPath": "hasMui", + "columnName": "has_mui", + "affinity": "INTEGER" + }, + { + "fieldPath": "hwModel", + "columnName": "hwModel", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "hwModelSlug", + "columnName": "hw_model_slug", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "images", + "columnName": "images", + "affinity": "TEXT" + }, + { + "fieldPath": "lastUpdated", + "columnName": "last_updated", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "partitionScheme", + "columnName": "partition_scheme", + "affinity": "TEXT" + }, + { + "fieldPath": "platformioTarget", + "columnName": "platformio_target", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "requiresDfu", + "columnName": "requires_dfu", + "affinity": "INTEGER" + }, + { + "fieldPath": "supportLevel", + "columnName": "support_level", + "affinity": "INTEGER" + }, + { + "fieldPath": "tags", + "columnName": "tags", + "affinity": "TEXT" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "platformio_target" + ] + } + }, + { + "tableName": "device_link", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`short_code` TEXT NOT NULL, `link_description` TEXT, `is_vendor` INTEGER NOT NULL, `regions` TEXT, `targets` TEXT, PRIMARY KEY(`short_code`))", + "fields": [ + { + "fieldPath": "shortCode", + "columnName": "short_code", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "linkDescription", + "columnName": "link_description", + "affinity": "TEXT" + }, + { + "fieldPath": "isVendor", + "columnName": "is_vendor", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "regions", + "columnName": "regions", + "affinity": "TEXT" + }, + { + "fieldPath": "targets", + "columnName": "targets", + "affinity": "TEXT" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "short_code" + ] + } + }, + { + "tableName": "firmware_release", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `page_url` TEXT NOT NULL, `release_notes` TEXT NOT NULL, `title` TEXT NOT NULL, `zip_url` TEXT NOT NULL, `last_updated` INTEGER NOT NULL, `release_type` TEXT NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "pageUrl", + "columnName": "page_url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "releaseNotes", + "columnName": "release_notes", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "zipUrl", + "columnName": "zip_url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastUpdated", + "columnName": "last_updated", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "releaseType", + "columnName": "release_type", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + } + }, + { + "tableName": "traceroute_node_position", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`log_uuid` TEXT NOT NULL, `request_id` INTEGER NOT NULL, `node_num` INTEGER NOT NULL, `position` BLOB NOT NULL, PRIMARY KEY(`log_uuid`, `node_num`), FOREIGN KEY(`log_uuid`) REFERENCES `log`(`uuid`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "logUuid", + "columnName": "log_uuid", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "requestId", + "columnName": "request_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "nodeNum", + "columnName": "node_num", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "position", + "columnName": "position", + "affinity": "BLOB", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "log_uuid", + "node_num" + ] + }, + "indices": [ + { + "name": "index_traceroute_node_position_log_uuid", + "unique": false, + "columnNames": [ + "log_uuid" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_traceroute_node_position_log_uuid` ON `${TABLE_NAME}` (`log_uuid`)" + }, + { + "name": "index_traceroute_node_position_request_id", + "unique": false, + "columnNames": [ + "request_id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_traceroute_node_position_request_id` ON `${TABLE_NAME}` (`request_id`)" + } + ], + "foreignKeys": [ + { + "table": "log", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "log_uuid" + ], + "referencedColumns": [ + "uuid" + ] + } + ] + }, + { + "tableName": "discovery_session", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `timestamp` INTEGER NOT NULL, `presets_scanned` TEXT NOT NULL, `home_preset` TEXT NOT NULL, `total_unique_nodes` INTEGER NOT NULL DEFAULT 0, `avg_channel_utilization` REAL NOT NULL DEFAULT 0.0, `total_messages` INTEGER NOT NULL DEFAULT 0, `total_sensor_packets` INTEGER NOT NULL DEFAULT 0, `furthest_node_distance` REAL NOT NULL DEFAULT 0.0, `completion_status` TEXT NOT NULL DEFAULT 'complete', `ai_summary` TEXT, `user_latitude` REAL NOT NULL DEFAULT 0.0, `user_longitude` REAL NOT NULL DEFAULT 0.0, `total_dwell_seconds` INTEGER NOT NULL DEFAULT 0, `device_address` TEXT, `home_lora_config` BLOB, `home_primary_channel` BLOB)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "timestamp", + "columnName": "timestamp", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "presetsScanned", + "columnName": "presets_scanned", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "homePreset", + "columnName": "home_preset", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "totalUniqueNodes", + "columnName": "total_unique_nodes", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "avgChannelUtilization", + "columnName": "avg_channel_utilization", + "affinity": "REAL", + "notNull": true, + "defaultValue": "0.0" + }, + { + "fieldPath": "totalMessages", + "columnName": "total_messages", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "totalSensorPackets", + "columnName": "total_sensor_packets", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "furthestNodeDistance", + "columnName": "furthest_node_distance", + "affinity": "REAL", + "notNull": true, + "defaultValue": "0.0" + }, + { + "fieldPath": "completionStatus", + "columnName": "completion_status", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "'complete'" + }, + { + "fieldPath": "aiSummary", + "columnName": "ai_summary", + "affinity": "TEXT" + }, + { + "fieldPath": "userLatitude", + "columnName": "user_latitude", + "affinity": "REAL", + "notNull": true, + "defaultValue": "0.0" + }, + { + "fieldPath": "userLongitude", + "columnName": "user_longitude", + "affinity": "REAL", + "notNull": true, + "defaultValue": "0.0" + }, + { + "fieldPath": "totalDwellSeconds", + "columnName": "total_dwell_seconds", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "deviceAddress", + "columnName": "device_address", + "affinity": "TEXT" + }, + { + "fieldPath": "homeLoraConfig", + "columnName": "home_lora_config", + "affinity": "BLOB" + }, + { + "fieldPath": "homePrimaryChannel", + "columnName": "home_primary_channel", + "affinity": "BLOB" + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + } + }, + { + "tableName": "discovery_preset_result", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `session_id` INTEGER NOT NULL, `preset_name` TEXT NOT NULL, `dwell_duration_seconds` INTEGER NOT NULL DEFAULT 0, `unique_nodes` INTEGER NOT NULL DEFAULT 0, `direct_neighbor_count` INTEGER NOT NULL DEFAULT 0, `mesh_neighbor_count` INTEGER NOT NULL DEFAULT 0, `infrastructure_node_count` INTEGER NOT NULL DEFAULT 0, `message_count` INTEGER NOT NULL DEFAULT 0, `sensor_packet_count` INTEGER NOT NULL DEFAULT 0, `avg_channel_utilization` REAL NOT NULL DEFAULT 0.0, `avg_airtime_rate` REAL NOT NULL DEFAULT 0.0, `packet_success_rate` REAL NOT NULL DEFAULT 0.0, `packet_failure_rate` REAL NOT NULL DEFAULT 0.0, `ai_summary` TEXT, `num_packets_tx` INTEGER NOT NULL DEFAULT 0, `num_packets_rx` INTEGER NOT NULL DEFAULT 0, `num_packets_rx_bad` INTEGER NOT NULL DEFAULT 0, `num_rx_dupe` INTEGER NOT NULL DEFAULT 0, `num_tx_relay` INTEGER NOT NULL DEFAULT 0, `num_tx_relay_canceled` INTEGER NOT NULL DEFAULT 0, `num_online_nodes` INTEGER NOT NULL DEFAULT 0, `num_total_nodes` INTEGER NOT NULL DEFAULT 0, `uptime_seconds` INTEGER NOT NULL DEFAULT 0, FOREIGN KEY(`session_id`) REFERENCES `discovery_session`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "sessionId", + "columnName": "session_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "presetName", + "columnName": "preset_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "dwellDurationSeconds", + "columnName": "dwell_duration_seconds", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "uniqueNodes", + "columnName": "unique_nodes", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "directNeighborCount", + "columnName": "direct_neighbor_count", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "meshNeighborCount", + "columnName": "mesh_neighbor_count", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "infrastructureNodeCount", + "columnName": "infrastructure_node_count", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "messageCount", + "columnName": "message_count", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "sensorPacketCount", + "columnName": "sensor_packet_count", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "avgChannelUtilization", + "columnName": "avg_channel_utilization", + "affinity": "REAL", + "notNull": true, + "defaultValue": "0.0" + }, + { + "fieldPath": "avgAirtimeRate", + "columnName": "avg_airtime_rate", + "affinity": "REAL", + "notNull": true, + "defaultValue": "0.0" + }, + { + "fieldPath": "packetSuccessRate", + "columnName": "packet_success_rate", + "affinity": "REAL", + "notNull": true, + "defaultValue": "0.0" + }, + { + "fieldPath": "packetFailureRate", + "columnName": "packet_failure_rate", + "affinity": "REAL", + "notNull": true, + "defaultValue": "0.0" + }, + { + "fieldPath": "aiSummary", + "columnName": "ai_summary", + "affinity": "TEXT" + }, + { + "fieldPath": "numPacketsTx", + "columnName": "num_packets_tx", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "numPacketsRx", + "columnName": "num_packets_rx", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "numPacketsRxBad", + "columnName": "num_packets_rx_bad", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "numRxDupe", + "columnName": "num_rx_dupe", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "numTxRelay", + "columnName": "num_tx_relay", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "numTxRelayCanceled", + "columnName": "num_tx_relay_canceled", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "numOnlineNodes", + "columnName": "num_online_nodes", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "numTotalNodes", + "columnName": "num_total_nodes", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "uptimeSeconds", + "columnName": "uptime_seconds", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_discovery_preset_result_session_id", + "unique": false, + "columnNames": [ + "session_id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_discovery_preset_result_session_id` ON `${TABLE_NAME}` (`session_id`)" + } + ], + "foreignKeys": [ + { + "table": "discovery_session", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "session_id" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "discovered_node", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `preset_result_id` INTEGER NOT NULL, `node_num` INTEGER NOT NULL, `short_name` TEXT, `long_name` TEXT, `neighbor_type` TEXT NOT NULL DEFAULT 'direct', `latitude` REAL, `longitude` REAL, `distance_from_user` REAL, `hop_count` INTEGER NOT NULL DEFAULT 0, `snr` REAL NOT NULL DEFAULT 0, `rssi` INTEGER, `message_count` INTEGER NOT NULL DEFAULT 0, `sensor_packet_count` INTEGER NOT NULL DEFAULT 0, `is_infrastructure` INTEGER NOT NULL DEFAULT 0, FOREIGN KEY(`preset_result_id`) REFERENCES `discovery_preset_result`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "presetResultId", + "columnName": "preset_result_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "nodeNum", + "columnName": "node_num", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "shortName", + "columnName": "short_name", + "affinity": "TEXT" + }, + { + "fieldPath": "longName", + "columnName": "long_name", + "affinity": "TEXT" + }, + { + "fieldPath": "neighborType", + "columnName": "neighbor_type", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "'direct'" + }, + { + "fieldPath": "latitude", + "columnName": "latitude", + "affinity": "REAL" + }, + { + "fieldPath": "longitude", + "columnName": "longitude", + "affinity": "REAL" + }, + { + "fieldPath": "distanceFromUser", + "columnName": "distance_from_user", + "affinity": "REAL" + }, + { + "fieldPath": "hopCount", + "columnName": "hop_count", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "snr", + "columnName": "snr", + "affinity": "REAL", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "rssi", + "columnName": "rssi", + "affinity": "INTEGER" + }, + { + "fieldPath": "messageCount", + "columnName": "message_count", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "sensorPacketCount", + "columnName": "sensor_packet_count", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "isInfrastructure", + "columnName": "is_infrastructure", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_discovered_node_preset_result_id", + "unique": false, + "columnNames": [ + "preset_result_id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_discovered_node_preset_result_id` ON `${TABLE_NAME}` (`preset_result_id`)" + }, + { + "name": "index_discovered_node_node_num", + "unique": false, + "columnNames": [ + "node_num" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_discovered_node_node_num` ON `${TABLE_NAME}` (`node_num`)" + } + ], + "foreignKeys": [ + { + "table": "discovery_preset_result", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "preset_result_id" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "event_firmware_edition", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`edition` TEXT NOT NULL, `display_name` TEXT NOT NULL, `welcome_message` TEXT NOT NULL, `event_start` TEXT, `event_end` TEXT, `time_zone` TEXT, `location` TEXT, `icon_url` TEXT, `accent_color` TEXT, `tag` TEXT, `domain` TEXT, `theme_json` TEXT, `firmware_json` TEXT, `links_json` TEXT NOT NULL, PRIMARY KEY(`edition`))", + "fields": [ + { + "fieldPath": "edition", + "columnName": "edition", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "displayName", + "columnName": "display_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "welcomeMessage", + "columnName": "welcome_message", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "eventStart", + "columnName": "event_start", + "affinity": "TEXT" + }, + { + "fieldPath": "eventEnd", + "columnName": "event_end", + "affinity": "TEXT" + }, + { + "fieldPath": "timeZone", + "columnName": "time_zone", + "affinity": "TEXT" + }, + { + "fieldPath": "location", + "columnName": "location", + "affinity": "TEXT" + }, + { + "fieldPath": "iconUrl", + "columnName": "icon_url", + "affinity": "TEXT" + }, + { + "fieldPath": "accentColor", + "columnName": "accent_color", + "affinity": "TEXT" + }, + { + "fieldPath": "tag", + "columnName": "tag", + "affinity": "TEXT" + }, + { + "fieldPath": "domain", + "columnName": "domain", + "affinity": "TEXT" + }, + { + "fieldPath": "themeJson", + "columnName": "theme_json", + "affinity": "TEXT" + }, + { + "fieldPath": "firmwareJson", + "columnName": "firmware_json", + "affinity": "TEXT" + }, + { + "fieldPath": "linksJson", + "columnName": "links_json", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "edition" + ] + } + }, + { + "tableName": "merge_marker", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`source_db_name` TEXT NOT NULL, `merged_at` INTEGER NOT NULL, PRIMARY KEY(`source_db_name`))", + "fields": [ + { + "fieldPath": "sourceDbName", + "columnName": "source_db_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "mergedAt", + "columnName": "merged_at", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "source_db_name" + ] + } + }, + { + "tableName": "channel_set", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `channel_set` BLOB NOT NULL, `last_reconciled` BLOB, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "channelSet", + "columnName": "channel_set", + "affinity": "BLOB", + "notNull": true + }, + { + "fieldPath": "lastReconciled", + "columnName": "last_reconciled", + "affinity": "BLOB" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + } + }, + { + "tableName": "bootloader_ota_quirks_cache", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `devices_json` TEXT NOT NULL, `soft_device_variants_json` TEXT NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "devicesJson", + "columnName": "devices_json", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "softDeviceVariantsJson", + "columnName": "soft_device_variants_json", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + } + }, + { + "tableName": "maintenance_uf2_cache", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `manifest_json` TEXT NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "manifestJson", + "columnName": "manifest_json", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + } + } + ], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'ddb50cdd4162940a03ae5c8b778ba4ee')" + ] + } +} \ No newline at end of file diff --git a/core/database/schemas/org.meshtastic.core.database.MeshtasticDatabase/61.json b/core/database/schemas/org.meshtastic.core.database.MeshtasticDatabase/61.json new file mode 100644 index 0000000000..0a246dcfd6 --- /dev/null +++ b/core/database/schemas/org.meshtastic.core.database.MeshtasticDatabase/61.json @@ -0,0 +1,1852 @@ +{ + "formatVersion": 1, + "database": { + "version": 61, + "identityHash": "c0060fc2597e2f9c13762e40de774ea5", + "entities": [ + { + "tableName": "my_node", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`myNodeNum` INTEGER NOT NULL, `model` TEXT, `firmwareVersion` TEXT, `couldUpdate` INTEGER NOT NULL, `shouldUpdate` INTEGER NOT NULL, `currentPacketId` INTEGER NOT NULL, `messageTimeoutMsec` INTEGER NOT NULL, `minAppVersion` INTEGER NOT NULL, `maxChannels` INTEGER NOT NULL, `hasWifi` INTEGER NOT NULL, `deviceId` TEXT, `pioEnv` TEXT, PRIMARY KEY(`myNodeNum`))", + "fields": [ + { + "fieldPath": "myNodeNum", + "columnName": "myNodeNum", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "model", + "columnName": "model", + "affinity": "TEXT" + }, + { + "fieldPath": "firmwareVersion", + "columnName": "firmwareVersion", + "affinity": "TEXT" + }, + { + "fieldPath": "couldUpdate", + "columnName": "couldUpdate", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "shouldUpdate", + "columnName": "shouldUpdate", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "currentPacketId", + "columnName": "currentPacketId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "messageTimeoutMsec", + "columnName": "messageTimeoutMsec", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "minAppVersion", + "columnName": "minAppVersion", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "maxChannels", + "columnName": "maxChannels", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "hasWifi", + "columnName": "hasWifi", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "deviceId", + "columnName": "deviceId", + "affinity": "TEXT" + }, + { + "fieldPath": "pioEnv", + "columnName": "pioEnv", + "affinity": "TEXT" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "myNodeNum" + ] + } + }, + { + "tableName": "nodes", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`num` INTEGER NOT NULL, `user` BLOB NOT NULL, `long_name` TEXT, `short_name` TEXT, `position` BLOB NOT NULL, `latitude` REAL NOT NULL, `longitude` REAL NOT NULL, `snr` REAL NOT NULL, `rssi` INTEGER NOT NULL, `last_heard` INTEGER NOT NULL, `device_metrics` BLOB NOT NULL, `channel` INTEGER NOT NULL, `via_mqtt` INTEGER NOT NULL, `hops_away` INTEGER NOT NULL, `is_favorite` INTEGER NOT NULL, `is_ignored` INTEGER NOT NULL DEFAULT 0, `is_muted` INTEGER NOT NULL DEFAULT 0, `environment_metrics` BLOB NOT NULL, `power_metrics` BLOB NOT NULL, `air_quality_metrics` BLOB NOT NULL DEFAULT x'', `soil_water_metrics` BLOB NOT NULL DEFAULT x'', `paxcounter` BLOB NOT NULL, `public_key` BLOB, `notes` TEXT NOT NULL DEFAULT '', `power_channel_labels` TEXT NOT NULL DEFAULT '[]', `manually_verified` INTEGER NOT NULL DEFAULT 0, `node_status` TEXT, `last_transport` INTEGER NOT NULL DEFAULT 0, `has_xeddsa_signed` INTEGER NOT NULL DEFAULT 0, `heard_on_current_lora` INTEGER NOT NULL DEFAULT 1, `key_match` INTEGER NOT NULL DEFAULT 1, `new_public_key` BLOB, PRIMARY KEY(`num`))", + "fields": [ + { + "fieldPath": "num", + "columnName": "num", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "user", + "columnName": "user", + "affinity": "BLOB", + "notNull": true + }, + { + "fieldPath": "longName", + "columnName": "long_name", + "affinity": "TEXT" + }, + { + "fieldPath": "shortName", + "columnName": "short_name", + "affinity": "TEXT" + }, + { + "fieldPath": "position", + "columnName": "position", + "affinity": "BLOB", + "notNull": true + }, + { + "fieldPath": "latitude", + "columnName": "latitude", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "longitude", + "columnName": "longitude", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "snr", + "columnName": "snr", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "rssi", + "columnName": "rssi", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastHeard", + "columnName": "last_heard", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "deviceTelemetry", + "columnName": "device_metrics", + "affinity": "BLOB", + "notNull": true + }, + { + "fieldPath": "channel", + "columnName": "channel", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "viaMqtt", + "columnName": "via_mqtt", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "hopsAway", + "columnName": "hops_away", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isFavorite", + "columnName": "is_favorite", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isIgnored", + "columnName": "is_ignored", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "isMuted", + "columnName": "is_muted", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "environmentTelemetry", + "columnName": "environment_metrics", + "affinity": "BLOB", + "notNull": true + }, + { + "fieldPath": "powerTelemetry", + "columnName": "power_metrics", + "affinity": "BLOB", + "notNull": true + }, + { + "fieldPath": "airQualityTelemetry", + "columnName": "air_quality_metrics", + "affinity": "BLOB", + "notNull": true, + "defaultValue": "x''" + }, + { + "fieldPath": "soilWaterTelemetry", + "columnName": "soil_water_metrics", + "affinity": "BLOB", + "notNull": true, + "defaultValue": "x''" + }, + { + "fieldPath": "paxcounter", + "columnName": "paxcounter", + "affinity": "BLOB", + "notNull": true + }, + { + "fieldPath": "publicKey", + "columnName": "public_key", + "affinity": "BLOB" + }, + { + "fieldPath": "notes", + "columnName": "notes", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "powerChannelLabels", + "columnName": "power_channel_labels", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "'[]'" + }, + { + "fieldPath": "manuallyVerified", + "columnName": "manually_verified", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "nodeStatus", + "columnName": "node_status", + "affinity": "TEXT" + }, + { + "fieldPath": "lastTransport", + "columnName": "last_transport", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "signsPackets", + "columnName": "has_xeddsa_signed", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "heardOnCurrentLora", + "columnName": "heard_on_current_lora", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "1" + }, + { + "fieldPath": "keyMatch", + "columnName": "key_match", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "1" + }, + { + "fieldPath": "newPublicKey", + "columnName": "new_public_key", + "affinity": "BLOB" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "num" + ] + }, + "indices": [ + { + "name": "index_nodes_last_heard", + "unique": false, + "columnNames": [ + "last_heard" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_nodes_last_heard` ON `${TABLE_NAME}` (`last_heard`)" + }, + { + "name": "index_nodes_short_name", + "unique": false, + "columnNames": [ + "short_name" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_nodes_short_name` ON `${TABLE_NAME}` (`short_name`)" + }, + { + "name": "index_nodes_long_name", + "unique": false, + "columnNames": [ + "long_name" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_nodes_long_name` ON `${TABLE_NAME}` (`long_name`)" + }, + { + "name": "index_nodes_hops_away", + "unique": false, + "columnNames": [ + "hops_away" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_nodes_hops_away` ON `${TABLE_NAME}` (`hops_away`)" + }, + { + "name": "index_nodes_is_favorite", + "unique": false, + "columnNames": [ + "is_favorite" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_nodes_is_favorite` ON `${TABLE_NAME}` (`is_favorite`)" + }, + { + "name": "index_nodes_last_heard_is_favorite", + "unique": false, + "columnNames": [ + "last_heard", + "is_favorite" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_nodes_last_heard_is_favorite` ON `${TABLE_NAME}` (`last_heard`, `is_favorite`)" + }, + { + "name": "index_nodes_public_key", + "unique": false, + "columnNames": [ + "public_key" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_nodes_public_key` ON `${TABLE_NAME}` (`public_key`)" + } + ] + }, + { + "tableName": "packet", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uuid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `myNodeNum` INTEGER NOT NULL DEFAULT 0, `port_num` INTEGER NOT NULL, `contact_key` TEXT NOT NULL, `received_time` INTEGER NOT NULL, `read` INTEGER NOT NULL DEFAULT 1, `data` TEXT NOT NULL, `packet_id` INTEGER NOT NULL DEFAULT 0, `routing_error` INTEGER NOT NULL DEFAULT -1, `snr` REAL, `rssi` INTEGER, `hopsAway` INTEGER NOT NULL DEFAULT -1, `sfpp_hash` BLOB, `filtered` INTEGER NOT NULL DEFAULT 0, `message_text` TEXT NOT NULL DEFAULT '', `translated_text` TEXT, `show_translated` INTEGER NOT NULL DEFAULT 0)", + "fields": [ + { + "fieldPath": "uuid", + "columnName": "uuid", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "myNodeNum", + "columnName": "myNodeNum", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "port_num", + "columnName": "port_num", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "contact_key", + "columnName": "contact_key", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "received_time", + "columnName": "received_time", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "read", + "columnName": "read", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "1" + }, + { + "fieldPath": "data", + "columnName": "data", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "packetId", + "columnName": "packet_id", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "routingError", + "columnName": "routing_error", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "-1" + }, + { + "fieldPath": "snr", + "columnName": "snr", + "affinity": "REAL" + }, + { + "fieldPath": "rssi", + "columnName": "rssi", + "affinity": "INTEGER" + }, + { + "fieldPath": "hopsAway", + "columnName": "hopsAway", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "-1" + }, + { + "fieldPath": "sfpp_hash", + "columnName": "sfpp_hash", + "affinity": "BLOB" + }, + { + "fieldPath": "filtered", + "columnName": "filtered", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "messageText", + "columnName": "message_text", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "translatedText", + "columnName": "translated_text", + "affinity": "TEXT" + }, + { + "fieldPath": "showTranslated", + "columnName": "show_translated", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "uuid" + ] + }, + "indices": [ + { + "name": "index_packet_myNodeNum", + "unique": false, + "columnNames": [ + "myNodeNum" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_packet_myNodeNum` ON `${TABLE_NAME}` (`myNodeNum`)" + }, + { + "name": "index_packet_port_num", + "unique": false, + "columnNames": [ + "port_num" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_packet_port_num` ON `${TABLE_NAME}` (`port_num`)" + }, + { + "name": "index_packet_contact_key", + "unique": false, + "columnNames": [ + "contact_key" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_packet_contact_key` ON `${TABLE_NAME}` (`contact_key`)" + }, + { + "name": "index_packet_contact_key_port_num_received_time", + "unique": false, + "columnNames": [ + "contact_key", + "port_num", + "received_time" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_packet_contact_key_port_num_received_time` ON `${TABLE_NAME}` (`contact_key`, `port_num`, `received_time`)" + }, + { + "name": "index_packet_packet_id", + "unique": false, + "columnNames": [ + "packet_id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_packet_packet_id` ON `${TABLE_NAME}` (`packet_id`)" + }, + { + "name": "index_packet_received_time", + "unique": false, + "columnNames": [ + "received_time" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_packet_received_time` ON `${TABLE_NAME}` (`received_time`)" + }, + { + "name": "index_packet_filtered", + "unique": false, + "columnNames": [ + "filtered" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_packet_filtered` ON `${TABLE_NAME}` (`filtered`)" + }, + { + "name": "index_packet_read", + "unique": false, + "columnNames": [ + "read" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_packet_read` ON `${TABLE_NAME}` (`read`)" + } + ] + }, + { + "tableName": "packet_fts", + "createSql": "CREATE VIRTUAL TABLE IF NOT EXISTS `${TABLE_NAME}` USING FTS5(`message_text`, tokenize=`unicode61`, content=`packet`)", + "fields": [ + { + "fieldPath": "messageText", + "columnName": "message_text", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [] + }, + "ftsVersion": "FTS5", + "ftsOptions": { + "tokenizer": "unicode61", + "tokenizerArgs": [], + "contentTable": "packet", + "languageIdColumnName": "", + "matchInfo": "FTS4", + "notIndexedColumns": [], + "prefixSizes": [], + "preferredOrder": "ASC", + "contentRowId": "", + "columnSize": true, + "detail": "FULL" + }, + "contentSyncTriggers": [ + "CREATE TRIGGER IF NOT EXISTS room_fts_content_sync_packet_fts_BEFORE_UPDATE BEFORE UPDATE ON `packet` BEGIN DELETE FROM `packet_fts` WHERE `rowid`=OLD.`rowid`; END", + "CREATE TRIGGER IF NOT EXISTS room_fts_content_sync_packet_fts_BEFORE_DELETE BEFORE DELETE ON `packet` BEGIN DELETE FROM `packet_fts` WHERE `rowid`=OLD.`rowid`; END", + "CREATE TRIGGER IF NOT EXISTS room_fts_content_sync_packet_fts_AFTER_UPDATE AFTER UPDATE ON `packet` BEGIN INSERT INTO `packet_fts`(`rowid`, `message_text`) VALUES (NEW.`rowid`, NEW.`message_text`); END", + "CREATE TRIGGER IF NOT EXISTS room_fts_content_sync_packet_fts_AFTER_INSERT AFTER INSERT ON `packet` BEGIN INSERT INTO `packet_fts`(`rowid`, `message_text`) VALUES (NEW.`rowid`, NEW.`message_text`); END" + ] + }, + { + "tableName": "contact_settings", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`contact_key` TEXT NOT NULL, `muteUntil` INTEGER NOT NULL, `last_read_message_uuid` INTEGER, `last_read_message_timestamp` INTEGER, `filtering_disabled` INTEGER NOT NULL DEFAULT 0, `draft` TEXT NOT NULL DEFAULT '', `pinned` INTEGER NOT NULL DEFAULT 0, `display_name` TEXT NOT NULL DEFAULT '', PRIMARY KEY(`contact_key`))", + "fields": [ + { + "fieldPath": "contact_key", + "columnName": "contact_key", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "muteUntil", + "columnName": "muteUntil", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastReadMessageUuid", + "columnName": "last_read_message_uuid", + "affinity": "INTEGER" + }, + { + "fieldPath": "lastReadMessageTimestamp", + "columnName": "last_read_message_timestamp", + "affinity": "INTEGER" + }, + { + "fieldPath": "filteringDisabled", + "columnName": "filtering_disabled", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "draft", + "columnName": "draft", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "pinned", + "columnName": "pinned", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "displayName", + "columnName": "display_name", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "contact_key" + ] + } + }, + { + "tableName": "log", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uuid` TEXT NOT NULL, `type` TEXT NOT NULL, `received_date` INTEGER NOT NULL, `message` TEXT NOT NULL, `from_num` INTEGER NOT NULL DEFAULT 0, `port_num` INTEGER NOT NULL DEFAULT 0, `from_radio` BLOB NOT NULL DEFAULT x'', PRIMARY KEY(`uuid`))", + "fields": [ + { + "fieldPath": "uuid", + "columnName": "uuid", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "message_type", + "columnName": "type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "received_date", + "columnName": "received_date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "raw_message", + "columnName": "message", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "fromNum", + "columnName": "from_num", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "portNum", + "columnName": "port_num", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "fromRadio", + "columnName": "from_radio", + "affinity": "BLOB", + "notNull": true, + "defaultValue": "x''" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "uuid" + ] + }, + "indices": [ + { + "name": "index_log_from_num", + "unique": false, + "columnNames": [ + "from_num" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_log_from_num` ON `${TABLE_NAME}` (`from_num`)" + }, + { + "name": "index_log_port_num", + "unique": false, + "columnNames": [ + "port_num" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_log_port_num` ON `${TABLE_NAME}` (`port_num`)" + } + ] + }, + { + "tableName": "quick_chat", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uuid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `message` TEXT NOT NULL, `mode` TEXT NOT NULL, `position` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "uuid", + "columnName": "uuid", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "message", + "columnName": "message", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "mode", + "columnName": "mode", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "position", + "columnName": "position", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "uuid" + ] + } + }, + { + "tableName": "reactions", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`myNodeNum` INTEGER NOT NULL DEFAULT 0, `reply_id` INTEGER NOT NULL, `user_id` TEXT NOT NULL, `emoji` TEXT NOT NULL, `timestamp` INTEGER NOT NULL, `snr` REAL, `rssi` INTEGER, `hopsAway` INTEGER NOT NULL DEFAULT -1, `packet_id` INTEGER NOT NULL DEFAULT 0, `status` INTEGER NOT NULL DEFAULT 0, `routing_error` INTEGER NOT NULL DEFAULT 0, `relays` INTEGER NOT NULL DEFAULT 0, `relay_node` INTEGER, `to` TEXT, `channel` INTEGER NOT NULL DEFAULT 0, `sfpp_hash` BLOB, PRIMARY KEY(`myNodeNum`, `reply_id`, `user_id`, `emoji`))", + "fields": [ + { + "fieldPath": "myNodeNum", + "columnName": "myNodeNum", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "replyId", + "columnName": "reply_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "userId", + "columnName": "user_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "emoji", + "columnName": "emoji", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "timestamp", + "columnName": "timestamp", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "snr", + "columnName": "snr", + "affinity": "REAL" + }, + { + "fieldPath": "rssi", + "columnName": "rssi", + "affinity": "INTEGER" + }, + { + "fieldPath": "hopsAway", + "columnName": "hopsAway", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "-1" + }, + { + "fieldPath": "packetId", + "columnName": "packet_id", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "status", + "columnName": "status", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "routingError", + "columnName": "routing_error", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "relays", + "columnName": "relays", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "relayNode", + "columnName": "relay_node", + "affinity": "INTEGER" + }, + { + "fieldPath": "to", + "columnName": "to", + "affinity": "TEXT" + }, + { + "fieldPath": "channel", + "columnName": "channel", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "sfpp_hash", + "columnName": "sfpp_hash", + "affinity": "BLOB" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "myNodeNum", + "reply_id", + "user_id", + "emoji" + ] + }, + "indices": [ + { + "name": "index_reactions_reply_id", + "unique": false, + "columnNames": [ + "reply_id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_reactions_reply_id` ON `${TABLE_NAME}` (`reply_id`)" + }, + { + "name": "index_reactions_packet_id", + "unique": false, + "columnNames": [ + "packet_id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_reactions_packet_id` ON `${TABLE_NAME}` (`packet_id`)" + } + ] + }, + { + "tableName": "metadata", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`num` INTEGER NOT NULL, `proto` BLOB NOT NULL, `timestamp` INTEGER NOT NULL, PRIMARY KEY(`num`))", + "fields": [ + { + "fieldPath": "num", + "columnName": "num", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "proto", + "columnName": "proto", + "affinity": "BLOB", + "notNull": true + }, + { + "fieldPath": "timestamp", + "columnName": "timestamp", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "num" + ] + }, + "indices": [ + { + "name": "index_metadata_num", + "unique": false, + "columnNames": [ + "num" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_metadata_num` ON `${TABLE_NAME}` (`num`)" + } + ] + }, + { + "tableName": "device_hardware", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`actively_supported` INTEGER NOT NULL, `architecture` TEXT NOT NULL, `display_name` TEXT NOT NULL, `has_ink_hud` INTEGER, `has_mui` INTEGER, `hwModel` INTEGER NOT NULL, `hw_model_slug` TEXT NOT NULL, `images` TEXT, `last_updated` INTEGER NOT NULL, `partition_scheme` TEXT, `platformio_target` TEXT NOT NULL, `requires_dfu` INTEGER, `support_level` INTEGER, `tags` TEXT, PRIMARY KEY(`platformio_target`))", + "fields": [ + { + "fieldPath": "activelySupported", + "columnName": "actively_supported", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "architecture", + "columnName": "architecture", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "displayName", + "columnName": "display_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "hasInkHud", + "columnName": "has_ink_hud", + "affinity": "INTEGER" + }, + { + "fieldPath": "hasMui", + "columnName": "has_mui", + "affinity": "INTEGER" + }, + { + "fieldPath": "hwModel", + "columnName": "hwModel", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "hwModelSlug", + "columnName": "hw_model_slug", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "images", + "columnName": "images", + "affinity": "TEXT" + }, + { + "fieldPath": "lastUpdated", + "columnName": "last_updated", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "partitionScheme", + "columnName": "partition_scheme", + "affinity": "TEXT" + }, + { + "fieldPath": "platformioTarget", + "columnName": "platformio_target", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "requiresDfu", + "columnName": "requires_dfu", + "affinity": "INTEGER" + }, + { + "fieldPath": "supportLevel", + "columnName": "support_level", + "affinity": "INTEGER" + }, + { + "fieldPath": "tags", + "columnName": "tags", + "affinity": "TEXT" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "platformio_target" + ] + } + }, + { + "tableName": "device_link", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`short_code` TEXT NOT NULL, `link_description` TEXT, `is_vendor` INTEGER NOT NULL, `regions` TEXT, `targets` TEXT, PRIMARY KEY(`short_code`))", + "fields": [ + { + "fieldPath": "shortCode", + "columnName": "short_code", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "linkDescription", + "columnName": "link_description", + "affinity": "TEXT" + }, + { + "fieldPath": "isVendor", + "columnName": "is_vendor", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "regions", + "columnName": "regions", + "affinity": "TEXT" + }, + { + "fieldPath": "targets", + "columnName": "targets", + "affinity": "TEXT" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "short_code" + ] + } + }, + { + "tableName": "firmware_release", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `page_url` TEXT NOT NULL, `release_notes` TEXT NOT NULL, `title` TEXT NOT NULL, `zip_url` TEXT NOT NULL, `last_updated` INTEGER NOT NULL, `release_type` TEXT NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "pageUrl", + "columnName": "page_url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "releaseNotes", + "columnName": "release_notes", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "zipUrl", + "columnName": "zip_url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastUpdated", + "columnName": "last_updated", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "releaseType", + "columnName": "release_type", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + } + }, + { + "tableName": "traceroute_node_position", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`log_uuid` TEXT NOT NULL, `request_id` INTEGER NOT NULL, `node_num` INTEGER NOT NULL, `position` BLOB NOT NULL, PRIMARY KEY(`log_uuid`, `node_num`), FOREIGN KEY(`log_uuid`) REFERENCES `log`(`uuid`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "logUuid", + "columnName": "log_uuid", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "requestId", + "columnName": "request_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "nodeNum", + "columnName": "node_num", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "position", + "columnName": "position", + "affinity": "BLOB", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "log_uuid", + "node_num" + ] + }, + "indices": [ + { + "name": "index_traceroute_node_position_log_uuid", + "unique": false, + "columnNames": [ + "log_uuid" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_traceroute_node_position_log_uuid` ON `${TABLE_NAME}` (`log_uuid`)" + }, + { + "name": "index_traceroute_node_position_request_id", + "unique": false, + "columnNames": [ + "request_id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_traceroute_node_position_request_id` ON `${TABLE_NAME}` (`request_id`)" + } + ], + "foreignKeys": [ + { + "table": "log", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "log_uuid" + ], + "referencedColumns": [ + "uuid" + ] + } + ] + }, + { + "tableName": "discovery_session", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `timestamp` INTEGER NOT NULL, `presets_scanned` TEXT NOT NULL, `home_preset` TEXT NOT NULL, `total_unique_nodes` INTEGER NOT NULL DEFAULT 0, `avg_channel_utilization` REAL NOT NULL DEFAULT 0.0, `total_messages` INTEGER NOT NULL DEFAULT 0, `total_sensor_packets` INTEGER NOT NULL DEFAULT 0, `furthest_node_distance` REAL NOT NULL DEFAULT 0.0, `completion_status` TEXT NOT NULL DEFAULT 'complete', `ai_summary` TEXT, `user_latitude` REAL NOT NULL DEFAULT 0.0, `user_longitude` REAL NOT NULL DEFAULT 0.0, `total_dwell_seconds` INTEGER NOT NULL DEFAULT 0, `device_address` TEXT, `home_lora_config` BLOB, `home_primary_channel` BLOB)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "timestamp", + "columnName": "timestamp", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "presetsScanned", + "columnName": "presets_scanned", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "homePreset", + "columnName": "home_preset", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "totalUniqueNodes", + "columnName": "total_unique_nodes", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "avgChannelUtilization", + "columnName": "avg_channel_utilization", + "affinity": "REAL", + "notNull": true, + "defaultValue": "0.0" + }, + { + "fieldPath": "totalMessages", + "columnName": "total_messages", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "totalSensorPackets", + "columnName": "total_sensor_packets", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "furthestNodeDistance", + "columnName": "furthest_node_distance", + "affinity": "REAL", + "notNull": true, + "defaultValue": "0.0" + }, + { + "fieldPath": "completionStatus", + "columnName": "completion_status", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "'complete'" + }, + { + "fieldPath": "aiSummary", + "columnName": "ai_summary", + "affinity": "TEXT" + }, + { + "fieldPath": "userLatitude", + "columnName": "user_latitude", + "affinity": "REAL", + "notNull": true, + "defaultValue": "0.0" + }, + { + "fieldPath": "userLongitude", + "columnName": "user_longitude", + "affinity": "REAL", + "notNull": true, + "defaultValue": "0.0" + }, + { + "fieldPath": "totalDwellSeconds", + "columnName": "total_dwell_seconds", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "deviceAddress", + "columnName": "device_address", + "affinity": "TEXT" + }, + { + "fieldPath": "homeLoraConfig", + "columnName": "home_lora_config", + "affinity": "BLOB" + }, + { + "fieldPath": "homePrimaryChannel", + "columnName": "home_primary_channel", + "affinity": "BLOB" + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + } + }, + { + "tableName": "discovery_preset_result", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `session_id` INTEGER NOT NULL, `preset_name` TEXT NOT NULL, `dwell_duration_seconds` INTEGER NOT NULL DEFAULT 0, `unique_nodes` INTEGER NOT NULL DEFAULT 0, `direct_neighbor_count` INTEGER NOT NULL DEFAULT 0, `mesh_neighbor_count` INTEGER NOT NULL DEFAULT 0, `infrastructure_node_count` INTEGER NOT NULL DEFAULT 0, `message_count` INTEGER NOT NULL DEFAULT 0, `sensor_packet_count` INTEGER NOT NULL DEFAULT 0, `avg_channel_utilization` REAL NOT NULL DEFAULT 0.0, `avg_airtime_rate` REAL NOT NULL DEFAULT 0.0, `packet_success_rate` REAL NOT NULL DEFAULT 0.0, `packet_failure_rate` REAL NOT NULL DEFAULT 0.0, `ai_summary` TEXT, `num_packets_tx` INTEGER NOT NULL DEFAULT 0, `num_packets_rx` INTEGER NOT NULL DEFAULT 0, `num_packets_rx_bad` INTEGER NOT NULL DEFAULT 0, `num_rx_dupe` INTEGER NOT NULL DEFAULT 0, `num_tx_relay` INTEGER NOT NULL DEFAULT 0, `num_tx_relay_canceled` INTEGER NOT NULL DEFAULT 0, `num_online_nodes` INTEGER NOT NULL DEFAULT 0, `num_total_nodes` INTEGER NOT NULL DEFAULT 0, `uptime_seconds` INTEGER NOT NULL DEFAULT 0, FOREIGN KEY(`session_id`) REFERENCES `discovery_session`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "sessionId", + "columnName": "session_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "presetName", + "columnName": "preset_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "dwellDurationSeconds", + "columnName": "dwell_duration_seconds", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "uniqueNodes", + "columnName": "unique_nodes", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "directNeighborCount", + "columnName": "direct_neighbor_count", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "meshNeighborCount", + "columnName": "mesh_neighbor_count", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "infrastructureNodeCount", + "columnName": "infrastructure_node_count", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "messageCount", + "columnName": "message_count", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "sensorPacketCount", + "columnName": "sensor_packet_count", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "avgChannelUtilization", + "columnName": "avg_channel_utilization", + "affinity": "REAL", + "notNull": true, + "defaultValue": "0.0" + }, + { + "fieldPath": "avgAirtimeRate", + "columnName": "avg_airtime_rate", + "affinity": "REAL", + "notNull": true, + "defaultValue": "0.0" + }, + { + "fieldPath": "packetSuccessRate", + "columnName": "packet_success_rate", + "affinity": "REAL", + "notNull": true, + "defaultValue": "0.0" + }, + { + "fieldPath": "packetFailureRate", + "columnName": "packet_failure_rate", + "affinity": "REAL", + "notNull": true, + "defaultValue": "0.0" + }, + { + "fieldPath": "aiSummary", + "columnName": "ai_summary", + "affinity": "TEXT" + }, + { + "fieldPath": "numPacketsTx", + "columnName": "num_packets_tx", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "numPacketsRx", + "columnName": "num_packets_rx", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "numPacketsRxBad", + "columnName": "num_packets_rx_bad", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "numRxDupe", + "columnName": "num_rx_dupe", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "numTxRelay", + "columnName": "num_tx_relay", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "numTxRelayCanceled", + "columnName": "num_tx_relay_canceled", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "numOnlineNodes", + "columnName": "num_online_nodes", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "numTotalNodes", + "columnName": "num_total_nodes", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "uptimeSeconds", + "columnName": "uptime_seconds", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_discovery_preset_result_session_id", + "unique": false, + "columnNames": [ + "session_id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_discovery_preset_result_session_id` ON `${TABLE_NAME}` (`session_id`)" + } + ], + "foreignKeys": [ + { + "table": "discovery_session", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "session_id" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "discovered_node", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `preset_result_id` INTEGER NOT NULL, `node_num` INTEGER NOT NULL, `short_name` TEXT, `long_name` TEXT, `neighbor_type` TEXT NOT NULL DEFAULT 'direct', `latitude` REAL, `longitude` REAL, `distance_from_user` REAL, `hop_count` INTEGER NOT NULL DEFAULT 0, `snr` REAL NOT NULL DEFAULT 0, `rssi` INTEGER, `message_count` INTEGER NOT NULL DEFAULT 0, `sensor_packet_count` INTEGER NOT NULL DEFAULT 0, `is_infrastructure` INTEGER NOT NULL DEFAULT 0, FOREIGN KEY(`preset_result_id`) REFERENCES `discovery_preset_result`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "presetResultId", + "columnName": "preset_result_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "nodeNum", + "columnName": "node_num", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "shortName", + "columnName": "short_name", + "affinity": "TEXT" + }, + { + "fieldPath": "longName", + "columnName": "long_name", + "affinity": "TEXT" + }, + { + "fieldPath": "neighborType", + "columnName": "neighbor_type", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "'direct'" + }, + { + "fieldPath": "latitude", + "columnName": "latitude", + "affinity": "REAL" + }, + { + "fieldPath": "longitude", + "columnName": "longitude", + "affinity": "REAL" + }, + { + "fieldPath": "distanceFromUser", + "columnName": "distance_from_user", + "affinity": "REAL" + }, + { + "fieldPath": "hopCount", + "columnName": "hop_count", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "snr", + "columnName": "snr", + "affinity": "REAL", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "rssi", + "columnName": "rssi", + "affinity": "INTEGER" + }, + { + "fieldPath": "messageCount", + "columnName": "message_count", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "sensorPacketCount", + "columnName": "sensor_packet_count", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "isInfrastructure", + "columnName": "is_infrastructure", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_discovered_node_preset_result_id", + "unique": false, + "columnNames": [ + "preset_result_id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_discovered_node_preset_result_id` ON `${TABLE_NAME}` (`preset_result_id`)" + }, + { + "name": "index_discovered_node_node_num", + "unique": false, + "columnNames": [ + "node_num" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_discovered_node_node_num` ON `${TABLE_NAME}` (`node_num`)" + } + ], + "foreignKeys": [ + { + "table": "discovery_preset_result", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "preset_result_id" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "event_firmware_edition", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`edition` TEXT NOT NULL, `display_name` TEXT NOT NULL, `welcome_message` TEXT NOT NULL, `event_start` TEXT, `event_end` TEXT, `time_zone` TEXT, `location` TEXT, `icon_url` TEXT, `accent_color` TEXT, `tag` TEXT, `domain` TEXT, `theme_json` TEXT, `firmware_json` TEXT, `links_json` TEXT NOT NULL, PRIMARY KEY(`edition`))", + "fields": [ + { + "fieldPath": "edition", + "columnName": "edition", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "displayName", + "columnName": "display_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "welcomeMessage", + "columnName": "welcome_message", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "eventStart", + "columnName": "event_start", + "affinity": "TEXT" + }, + { + "fieldPath": "eventEnd", + "columnName": "event_end", + "affinity": "TEXT" + }, + { + "fieldPath": "timeZone", + "columnName": "time_zone", + "affinity": "TEXT" + }, + { + "fieldPath": "location", + "columnName": "location", + "affinity": "TEXT" + }, + { + "fieldPath": "iconUrl", + "columnName": "icon_url", + "affinity": "TEXT" + }, + { + "fieldPath": "accentColor", + "columnName": "accent_color", + "affinity": "TEXT" + }, + { + "fieldPath": "tag", + "columnName": "tag", + "affinity": "TEXT" + }, + { + "fieldPath": "domain", + "columnName": "domain", + "affinity": "TEXT" + }, + { + "fieldPath": "themeJson", + "columnName": "theme_json", + "affinity": "TEXT" + }, + { + "fieldPath": "firmwareJson", + "columnName": "firmware_json", + "affinity": "TEXT" + }, + { + "fieldPath": "linksJson", + "columnName": "links_json", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "edition" + ] + } + }, + { + "tableName": "merge_marker", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`source_db_name` TEXT NOT NULL, `merged_at` INTEGER NOT NULL, PRIMARY KEY(`source_db_name`))", + "fields": [ + { + "fieldPath": "sourceDbName", + "columnName": "source_db_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "mergedAt", + "columnName": "merged_at", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "source_db_name" + ] + } + }, + { + "tableName": "channel_set", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `channel_set` BLOB NOT NULL, `last_reconciled` BLOB, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "channelSet", + "columnName": "channel_set", + "affinity": "BLOB", + "notNull": true + }, + { + "fieldPath": "lastReconciled", + "columnName": "last_reconciled", + "affinity": "BLOB" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + } + }, + { + "tableName": "bootloader_ota_quirks_cache", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `devices_json` TEXT NOT NULL, `soft_device_variants_json` TEXT NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "devicesJson", + "columnName": "devices_json", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "softDeviceVariantsJson", + "columnName": "soft_device_variants_json", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + } + }, + { + "tableName": "maintenance_uf2_cache", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `manifest_json` TEXT NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "manifestJson", + "columnName": "manifest_json", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + } + } + ], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'c0060fc2597e2f9c13762e40de774ea5')" + ] + } +} \ No newline at end of file diff --git a/core/database/src/androidHostTest/kotlin/org/meshtastic/core/database/dao/ChannelReconciliationTest.kt b/core/database/src/androidHostTest/kotlin/org/meshtastic/core/database/dao/ChannelReconciliationTest.kt new file mode 100644 index 0000000000..6bdecaf34a --- /dev/null +++ b/core/database/src/androidHostTest/kotlin/org/meshtastic/core/database/dao/ChannelReconciliationTest.kt @@ -0,0 +1,517 @@ +/* + * 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 . + */ +package org.meshtastic.core.database.dao + +import androidx.room3.Room +import androidx.sqlite.driver.bundled.BundledSQLiteDriver +import androidx.test.core.app.ApplicationProvider +import androidx.test.ext.junit.runners.AndroidJUnit4 +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.test.runTest +import okio.ByteString.Companion.toByteString +import org.junit.After +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.meshtastic.core.common.util.nowMillis +import org.meshtastic.core.database.MeshtasticDatabase +import org.meshtastic.core.database.MeshtasticDatabaseConstructor +import org.meshtastic.core.database.entity.MyNodeEntity +import org.meshtastic.core.database.entity.Packet +import org.meshtastic.core.database.entity.ReactionEntity +import org.meshtastic.core.model.ContactKey +import org.meshtastic.core.model.DataPacket +import org.meshtastic.core.model.MessageStatus +import org.meshtastic.core.model.NodeAddress +import org.meshtastic.core.model.util.channelIdentity +import org.meshtastic.core.model.util.planChannelReconciliation +import org.meshtastic.proto.ChannelSet +import org.meshtastic.proto.ChannelSettings +import org.meshtastic.proto.Config.LoRaConfig +import org.meshtastic.proto.Config.LoRaConfig.ModemPreset +import org.meshtastic.proto.PortNum +import org.robolectric.annotation.Config +import kotlin.test.assertEquals +import kotlin.test.assertNotNull +import kotlin.test.assertNull +import kotlin.test.assertTrue + +/** + * Conversations are keyed by channel index, so replacing a slot's occupant used to leave the old channel's messages + * under the new channel. These cover the re-keying that prevents it. + */ +@RunWith(AndroidJUnit4::class) +@Config(sdk = [34]) +class ChannelReconciliationTest { + private lateinit var database: MeshtasticDatabase + private lateinit var packetDao: PacketDao + + private val myNodeNum = 42424242 + + private val longFast = + LoRaConfig.Builder() + .also { wb -> + wb.use_preset = true + wb.modem_preset = ModemPreset.LONG_FAST + } + .build() + private val mediumFast = + LoRaConfig.Builder() + .also { wb -> + wb.use_preset = true + wb.modem_preset = ModemPreset.MEDIUM_FAST + } + .build() + private val defaultPsk = byteArrayOf(0x01).toByteString() + private val vcfmwPsk = byteArrayOf(0x11, 0x22, 0x33, 0x44).toByteString() + + @Before + fun createDb(): Unit = runTest { + val context = ApplicationProvider.getApplicationContext() + database = + Room.inMemoryDatabaseBuilder( + context = context, + factory = { MeshtasticDatabaseConstructor.initialize() }, + ) + .setDriver(BundledSQLiteDriver()) + .build() + database + .nodeInfoDao() + .setMyNodeInfo( + MyNodeEntity( + myNodeNum = myNodeNum, + model = null, + firmwareVersion = null, + couldUpdate = false, + shouldUpdate = false, + currentPacketId = 1L, + messageTimeoutMsec = 5 * 60 * 1000, + minAppVersion = 1, + maxChannels = 8, + hasWifi = false, + ), + ) + packetDao = database.packetDao() + } + + @After fun closeDb() = database.close() + + /** The reported bug: a scanned QR replaces the primary channel and the old mesh's messages stay behind. */ + @Test + fun `a replaced primary channel takes its history with it`() = runTest { + insertBroadcast(channel = 0, text = "hello from the old mesh") + val old = channelSet(longFast, ChannelSettings.Builder().also { wb -> wb.psk = defaultPsk }.build()) + val new = + channelSet( + longFast, + ChannelSettings.Builder() + .also { wb -> + wb.psk = vcfmwPsk + wb.name = "VCFMW" + } + .build(), + ) + + reconcile(old, new) + + assertEquals(0, messagesOn(ContactKey.broadcast(0).value).size, "primary must start empty") + val retired = ContactKey.retiredBroadcast(old.settings[0].channelIdentity(longFast).token).value + assertEquals(1, messagesOn(retired).size, "history must follow the channel it belongs to") + } + + /** + * A preset switch changes the effective channel name, so it is a different mesh even though neither the raw name + * nor the raw PSK moved. Matching on the raw fields missed this entirely. + */ + @Test + fun `switching modem preset retires the old channel`() = runTest { + insertBroadcast(channel = 0, text = "sent on LongFast") + val settings = ChannelSettings.Builder().also { wb -> wb.psk = defaultPsk }.build() + + reconcile(channelSet(longFast, settings), channelSet(mediumFast, settings)) + + assertEquals(0, messagesOn(ContactKey.broadcast(0).value).size) + val retired = ContactKey.retiredBroadcast(settings.channelIdentity(longFast).token).value + assertEquals(1, messagesOn(retired).size) + } + + @Test + fun `re-adding a channel restores its history to the live slot`() = runTest { + insertBroadcast(channel = 0, text = "sent on LongFast", packetId = 100) + insertReaction(replyId = 100, channel = 0, emoji = "\uD83D\uDC4D") + val settings = ChannelSettings.Builder().also { wb -> wb.psk = defaultPsk }.build() + + reconcile(channelSet(longFast, settings), channelSet(mediumFast, settings)) + reconcile(channelSet(mediumFast, settings), channelSet(longFast, settings)) + + val live = messagesOn(ContactKey.broadcast(0).value) + assertEquals(1, live.size, "the channel came back, so its messages must too") + assertEquals(0, live.single().data.channel, "and be addressable on the live slot again") + assertEquals("", packetDao.getContactSettings(ContactKey.broadcast(0).value)?.displayName.orEmpty()) + assertEquals(0, packetDao.getAllReactionsSnapshot().single().channel, "its reactions come back with it") + } + + /** + * A retired conversation keeps the channel index it was retired from, so its reactions sit on an index a live + * channel now owns. Reclaiming must lift only its own reactions, which is why they are scoped through the retired + * conversation's packet ids rather than by channel. + */ + @Test + fun `reclaiming lifts only its own reactions off a shared index`() = runTest { + insertBroadcast(channel = 0, text = "on the old mesh", packetId = 100) + insertReaction(replyId = 100, channel = 0, emoji = "\uD83D\uDC4D") + val old = + ChannelSettings.Builder() + .also { wb -> + wb.psk = defaultPsk + wb.name = "Old" + } + .build() + val new = + ChannelSettings.Builder() + .also { wb -> + wb.psk = vcfmwPsk + wb.name = "New" + } + .build() + + reconcile(channelSet(longFast, old), channelSet(longFast, new)) + + // The replacement channel now owns slot 0 and gathers its own message and reaction there. + insertBroadcast(channel = 0, text = "on the new mesh", packetId = 200) + insertReaction(replyId = 200, channel = 0, emoji = "\uD83C\uDF89") + + reconcile(channelSet(longFast, new), channelSet(longFast, new, old)) + + val reactions = packetDao.getAllReactionsSnapshot().associateBy { it.replyId } + assertEquals(1, reactions.getValue(100).channel, "the reclaimed conversation's reaction follows it") + assertEquals(0, reactions.getValue(200).channel, "the live channel's reaction stays put") + assertEquals("on the old mesh", messagesOn(ContactKey.broadcast(1).value).single().data.text) + assertEquals("on the new mesh", messagesOn(ContactKey.broadcast(0).value).single().data.text) + } + + /** + * Once a change has settled, recomputing against the new set has to produce nothing to do. That, rather than + * `applyChannelReconciliation` being idempotent, is what stops a reconnect from undoing the previous swap: the plan + * is derived from the stored baseline and committed with it in one transaction, so the same plan is never applied + * twice. + */ + @Test + fun `reconciling again once a swap has settled finds nothing to do`() = runTest { + insertBroadcast(channel = 0, text = "on A") + insertBroadcast(channel = 1, text = "on B") + val a = + ChannelSettings.Builder() + .also { wb -> + wb.psk = defaultPsk + wb.name = "A" + } + .build() + val b = + ChannelSettings.Builder() + .also { wb -> + wb.psk = vcfmwPsk + wb.name = "B" + } + .build() + val swapped = channelSet(longFast, b, a) + + reconcile(channelSet(longFast, a, b), swapped) + reconcile(swapped, swapped) + + assertEquals("on B", messagesOn(ContactKey.broadcast(0).value).single().data.text) + assertEquals("on A", messagesOn(ContactKey.broadcast(1).value).single().data.text) + } + + /** A swap must not leave a conversation's mute or pin on the channel it traded places with. */ + @Test + fun `swapping slots carries each conversation's settings`() = runTest { + insertBroadcast(channel = 0, text = "on A") + insertBroadcast(channel = 1, text = "on B") + packetDao.setPinned(listOf(ContactKey.broadcast(0).value), true) + val a = + ChannelSettings.Builder() + .also { wb -> + wb.psk = defaultPsk + wb.name = "A" + } + .build() + val b = + ChannelSettings.Builder() + .also { wb -> + wb.psk = vcfmwPsk + wb.name = "B" + } + .build() + + reconcile(channelSet(longFast, a, b), channelSet(longFast, b, a)) + + assertEquals(true, packetDao.getContactSettings(ContactKey.broadcast(1).value)?.pinned, "A was pinned") + assertEquals(false, packetDao.getContactSettings(ContactKey.broadcast(0).value)?.pinned ?: false) + } + + @Test + fun `swapping two channel slots moves both conversations`() = runTest { + insertBroadcast(channel = 0, text = "on A", packetId = 100) + insertBroadcast(channel = 1, text = "on B", packetId = 200) + insertReaction(replyId = 100, channel = 0, emoji = "👍") + insertReaction(replyId = 200, channel = 1, emoji = "🎉") + val a = + ChannelSettings.Builder() + .also { wb -> + wb.psk = defaultPsk + wb.name = "A" + } + .build() + val b = + ChannelSettings.Builder() + .also { wb -> + wb.psk = vcfmwPsk + wb.name = "B" + } + .build() + + reconcile(channelSet(longFast, a, b), channelSet(longFast, b, a)) + + assertEquals("on B", messagesOn(ContactKey.broadcast(0).value).single().data.text) + assertEquals("on A", messagesOn(ContactKey.broadcast(1).value).single().data.text) + val reactions = packetDao.getAllReactionsSnapshot().associateBy { it.replyId } + assertEquals(1, reactions.getValue(100).channel, "a reaction must follow its message") + assertEquals(0, reactions.getValue(200).channel) + } + + /** + * Deleting the first of two channels retires it and moves the survivor down into its slot. The retirement has to be + * applied before the move, or the survivor's packets land on the slot the retirement is about to sweep and go into + * the wrong archive. + */ + @Test + fun `deleting the first channel leaves the survivor intact on slot 0`() = runTest { + insertBroadcast(channel = 0, text = "on A") + insertBroadcast(channel = 1, text = "on B") + packetDao.setPinned(listOf(ContactKey.broadcast(1).value), true) + packetDao.setMuteUntil(listOf(ContactKey.broadcast(1).value), Long.MAX_VALUE) + val a = + ChannelSettings.Builder() + .also { wb -> + wb.psk = defaultPsk + wb.name = "A" + } + .build() + val b = + ChannelSettings.Builder() + .also { wb -> + wb.psk = vcfmwPsk + wb.name = "B" + } + .build() + + reconcile(channelSet(longFast, a, b), channelSet(longFast, b)) + + val live = messagesOn(ContactKey.broadcast(0).value) + assertEquals(1, live.size, "the surviving channel keeps its messages") + assertEquals("on B", live.single().data.text) + val settings = assertNotNull(packetDao.getContactSettings(ContactKey.broadcast(0).value)) + assertTrue(settings.pinned, "and its pin") + assertEquals(Long.MAX_VALUE, settings.muteUntil, "and its mute") + + val retired = ContactKey.retiredBroadcast(a.channelIdentity(longFast).token).value + assertEquals("on A", messagesOn(retired).single().data.text, "only A is archived") + } + + /** + * A message still queued when its channel is archived must never go out. Its stored channel index points at the + * slot it was composed for, and someone else is on that slot now. + */ + @Test + fun `a queued message in an archived conversation is not claimable for sending`() = runTest { + insertBroadcast(channel = 0, text = "never sent", packetId = 77, status = MessageStatus.QUEUED) + val old = + ChannelSettings.Builder() + .also { wb -> + wb.psk = defaultPsk + wb.name = "A" + } + .build() + + reconcile( + channelSet(longFast, old), + channelSet( + longFast, + ChannelSettings.Builder() + .also { wb -> + wb.psk = vcfmwPsk + wb.name = "B" + } + .build(), + ), + ) + + val row = packetDao.getAllPackets(PortNum.TEXT_MESSAGE_APP.value).first().single() + assertTrue(ContactKey(row.contact_key).isRetired, "precondition: the conversation was archived") + assertNull(packetDao.claimQueuedPacket(myNodeNum, row.uuid), "an archived conversation cannot be claimed") + assertEquals( + MessageStatus.QUEUED, + packetDao.getAllPackets(PortNum.TEXT_MESSAGE_APP.value).first().single().data.status, + "and the row is left alone rather than marked enroute", + ) + } + + /** A direct message is a conversation with a node, so it must survive its channel being replaced. */ + @Test + fun `direct messages are never retired`() = runTest { + val dmKey = "0!a1b2c3d4" + packetDao.insert( + Packet( + uuid = 0L, + myNodeNum = myNodeNum, + port_num = PortNum.TEXT_MESSAGE_APP.value, + contact_key = dmKey, + received_time = nowMillis, + read = false, + data = DataPacket(to = "!a1b2c3d4", channel = 0, text = "a private word"), + ), + ) + + reconcile( + channelSet(longFast, ChannelSettings.Builder().also { wb -> wb.psk = defaultPsk }.build()), + channelSet( + longFast, + ChannelSettings.Builder() + .also { wb -> + wb.psk = vcfmwPsk + wb.name = "VCFMW" + } + .build(), + ), + ) + + assertEquals(1, messagesOn(dmKey).size, "a DM thread belongs to the node, not the channel") + } + + /** A retired conversation is labelled from its own stored name; its old slot now names someone else. */ + @Test + fun `retiring stores the old channel name and carries its settings`() = runTest { + insertBroadcast(channel = 0, text = "hi") + packetDao.setMuteUntil(listOf(ContactKey.broadcast(0).value), Long.MAX_VALUE) + packetDao.setPinned(listOf(ContactKey.broadcast(0).value), true) + val old = + channelSet( + longFast, + ChannelSettings.Builder() + .also { wb -> + wb.psk = defaultPsk + wb.name = "VCFMW" + } + .build(), + ) + + reconcile( + old, + channelSet( + longFast, + ChannelSettings.Builder() + .also { wb -> + wb.psk = vcfmwPsk + wb.name = "Field Day" + } + .build(), + ), + ) + + val retiredKey = ContactKey.retiredBroadcast(old.settings[0].channelIdentity(longFast).token).value + val settings = assertNotNull(packetDao.getContactSettings(retiredKey)) + assertEquals("VCFMW", settings.displayName) + assertTrue(settings.pinned, "pinning follows the conversation") + assertEquals(Long.MAX_VALUE, settings.muteUntil) + assertNull(packetDao.getContactSettings(ContactKey.broadcast(0).value), "the new channel starts fresh") + } + + /** A blank channel name only resolves through the modem preset, so without it identity is a guess. */ + @Test + fun `an unknown lora config never retires anything`() = runTest { + insertBroadcast(channel = 0, text = "still mine") + val settings = ChannelSettings.Builder().also { wb -> wb.psk = defaultPsk }.build() + + reconcile( + channelSet(longFast, settings), + ChannelSet.Builder().also { wb -> wb.settings = listOf(settings) }.build(), + ) + + assertEquals(1, messagesOn(ContactKey.broadcast(0).value).size) + } + + /** Region is not part of channel identity, and the firmware does not hash it either. */ + @Test + fun `changing region alone leaves conversations alone`() = runTest { + insertBroadcast(channel = 0, text = "unaffected") + val settings = ChannelSettings.Builder().also { wb -> wb.psk = defaultPsk }.build() + + reconcile( + channelSet(longFast, settings), + channelSet(longFast.newBuilder().also { wb -> wb.region = LoRaConfig.RegionCode.EU_868 }.build(), settings), + ) + + assertEquals(1, messagesOn(ContactKey.broadcast(0).value).size) + } + + private suspend fun reconcile(old: ChannelSet, new: ChannelSet) { + packetDao.applyChannelReconciliation(planChannelReconciliation(old, new, packetDao.getRetiredContactTokens())) + } + + private fun channelSet(lora: LoRaConfig, vararg settings: ChannelSettings) = ChannelSet.Builder() + .also { wb -> + wb.settings = settings.toList() + wb.lora_config = lora + } + .build() + + private suspend fun insertBroadcast(channel: Int, text: String, packetId: Int = 0, status: MessageStatus? = null) { + packetDao.insert( + Packet( + uuid = 0L, + myNodeNum = myNodeNum, + port_num = PortNum.TEXT_MESSAGE_APP.value, + contact_key = ContactKey.broadcast(channel).value, + received_time = nowMillis, + read = false, + data = + DataPacket(to = NodeAddress.ID_BROADCAST, channel = channel, text = text).let { + if (status != null) it.copy(status = status) else it + }, + packetId = packetId, + ), + ) + } + + private suspend fun insertReaction(replyId: Int, channel: Int, emoji: String) { + packetDao.insert( + ReactionEntity( + myNodeNum = myNodeNum, + replyId = replyId, + userId = "!00000001", + emoji = emoji, + timestamp = nowMillis, + channel = channel, + to = NodeAddress.ID_BROADCAST, + ), + ) + } + + private suspend fun messagesOn(contactKey: String) = + packetDao.getAllPackets(PortNum.TEXT_MESSAGE_APP.value).first().filter { it.contact_key == contactKey } +} diff --git a/core/database/src/androidHostTest/kotlin/org/meshtastic/core/database/dao/MigrationTest.kt b/core/database/src/androidHostTest/kotlin/org/meshtastic/core/database/dao/MigrationTest.kt deleted file mode 100644 index b5ec1d2dbd..0000000000 --- a/core/database/src/androidHostTest/kotlin/org/meshtastic/core/database/dao/MigrationTest.kt +++ /dev/null @@ -1,180 +0,0 @@ -/* - * Copyright (c) 2026 Meshtastic LLC - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ -package org.meshtastic.core.database.dao - -import androidx.room3.Room -import androidx.sqlite.driver.bundled.BundledSQLiteDriver -import androidx.test.core.app.ApplicationProvider -import androidx.test.ext.junit.runners.AndroidJUnit4 -import kotlinx.coroutines.flow.first -import kotlinx.coroutines.test.runTest -import okio.ByteString.Companion.toByteString -import org.junit.After -import org.junit.Before -import org.junit.Test -import org.junit.runner.RunWith -import org.meshtastic.core.common.util.nowMillis -import org.meshtastic.core.database.MeshtasticDatabase -import org.meshtastic.core.database.MeshtasticDatabaseConstructor -import org.meshtastic.core.database.entity.MyNodeEntity -import org.meshtastic.core.database.entity.Packet -import org.meshtastic.core.model.DataPacket -import org.meshtastic.core.model.NodeAddress -import org.meshtastic.proto.ChannelSettings -import org.meshtastic.proto.PortNum -import org.robolectric.annotation.Config -import kotlin.test.assertEquals - -@RunWith(AndroidJUnit4::class) -@Config(sdk = [34]) -class MigrationTest { - private lateinit var database: MeshtasticDatabase - private lateinit var packetDao: PacketDao - private lateinit var nodeInfoDao: NodeInfoDao - - private val myNodeInfo: MyNodeEntity = - MyNodeEntity( - myNodeNum = 42424242, - model = null, - firmwareVersion = null, - couldUpdate = false, - shouldUpdate = false, - currentPacketId = 1L, - messageTimeoutMsec = 5 * 60 * 1000, - minAppVersion = 1, - maxChannels = 8, - hasWifi = false, - ) - - @Before - fun createDb(): Unit = runTest { - val context = ApplicationProvider.getApplicationContext() - database = - Room.inMemoryDatabaseBuilder( - context = context, - factory = { MeshtasticDatabaseConstructor.initialize() }, - ) - .setDriver(BundledSQLiteDriver()) - .build() - nodeInfoDao = database.nodeInfoDao().apply { setMyNodeInfo(myNodeInfo) } - packetDao = database.packetDao() - } - - @After - fun closeDb() { - database.close() - } - - @Test - fun testMigrateChannelsByPSK_duplicatePSK() = runTest { - // PSK \"AQ==\" is base64 for single byte 0x01 - val pskBytes = byteArrayOf(0x01).toByteString() - - // Create packets for Channel 0 - insertPacket(channel = 0, text = "Message Ch0") - - // Old settings: Channel 0 has PSK_A - val oldSettings = listOf(ChannelSettings(psk = pskBytes, name = "LongFast")) - - // New settings: Channel 0 has PSK_A, Channel 1 has PSK_A - val newSettings = - listOf( - ChannelSettings(psk = pskBytes, name = "LongFast"), - ChannelSettings(psk = pskBytes, name = "NewChan"), - ) - - // Perform migration - packetDao.migrateChannelsByPSK(oldSettings, newSettings) - - // Check packet channel - val p = getFirstPacket() - assertEquals(0, p.data.channel, "Packet should remain on channel 0") - } - - @Test - fun testMigrateChannelsByPSK_reorder() = runTest { - val pskA = byteArrayOf(0x01).toByteString() - val pskB = byteArrayOf(0x02).toByteString() - - insertPacket(channel = 0, text = "Msg A") - insertPacket(channel = 1, text = "Msg B") - - val oldSettings = listOf(ChannelSettings(psk = pskA, name = "A"), ChannelSettings(psk = pskB, name = "B")) - - val newSettings = listOf(ChannelSettings(psk = pskB, name = "B"), ChannelSettings(psk = pskA, name = "A")) - - packetDao.migrateChannelsByPSK(oldSettings, newSettings) - - val packets = getAllPackets() - assertEquals(1, packets.find { it.data.text == "Msg A" }?.data?.channel) - assertEquals(0, packets.find { it.data.text == "Msg B" }?.data?.channel) - } - - @Test - fun testMigrateChannelsByPSK_disambiguateByName() = runTest { - val pskA = byteArrayOf(0x01).toByteString() - - insertPacket(channel = 0, text = "Msg A1") - insertPacket(channel = 1, text = "Msg A2") - - val oldSettings = listOf(ChannelSettings(psk = pskA, name = "A1"), ChannelSettings(psk = pskA, name = "A2")) - - // Swap positions but keep names and PSKs - val newSettings = listOf(ChannelSettings(psk = pskA, name = "A2"), ChannelSettings(psk = pskA, name = "A1")) - - packetDao.migrateChannelsByPSK(oldSettings, newSettings) - - val packets = getAllPackets() - assertEquals(1, packets.find { it.data.text == "Msg A1" }?.data?.channel, "Msg A1 should move to index 1") - assertEquals(0, packets.find { it.data.text == "Msg A2" }?.data?.channel, "Msg A2 should move to index 0") - } - - @Test - fun testMigrateChannelsByPSK_preferSameIndexIfStillAmbiguous() = runTest { - val pskA = byteArrayOf(0x01).toByteString() - - insertPacket(channel = 0, text = "Msg A") - - val oldSettings = listOf(ChannelSettings(psk = pskA, name = "A")) - - // New settings has two identical channels (same PSK, same Name) - val newSettings = listOf(ChannelSettings(psk = pskA, name = "A"), ChannelSettings(psk = pskA, name = "A")) - - packetDao.migrateChannelsByPSK(oldSettings, newSettings) - - val p = getFirstPacket() - assertEquals(0, p.data.channel, "Should prefer keeping same index 0") - } - - private suspend fun insertPacket(channel: Int, text: String) { - val packet = - Packet( - uuid = 0L, - myNodeNum = 42424242, - port_num = PortNum.TEXT_MESSAGE_APP.value, - contact_key = "$channel!broadcast", - received_time = nowMillis, - read = false, - data = DataPacket(to = NodeAddress.ID_BROADCAST, channel = channel, text = text), - ) - packetDao.insert(packet) - } - - private suspend fun getAllPackets() = packetDao.getAllPackets(PortNum.TEXT_MESSAGE_APP.value).first() - - private suspend fun getFirstPacket() = getAllPackets().first() -} diff --git a/feature/map-maplibre/src/iosMain/kotlin/org/meshtastic/feature/map/maplibre/layers/SoftwareCanvasDecoding.ios.kt b/core/database/src/androidHostTest/kotlin/org/meshtastic/core/database/dao/NodeInfoDaoDirectFilterTest.kt similarity index 70% rename from feature/map-maplibre/src/iosMain/kotlin/org/meshtastic/feature/map/maplibre/layers/SoftwareCanvasDecoding.ios.kt rename to core/database/src/androidHostTest/kotlin/org/meshtastic/core/database/dao/NodeInfoDaoDirectFilterTest.kt index ff961863f7..7d1ae5b064 100644 --- a/feature/map-maplibre/src/iosMain/kotlin/org/meshtastic/feature/map/maplibre/layers/SoftwareCanvasDecoding.ios.kt +++ b/core/database/src/androidHostTest/kotlin/org/meshtastic/core/database/dao/NodeInfoDaoDirectFilterTest.kt @@ -14,9 +14,12 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -package org.meshtastic.feature.map.maplibre.layers +package org.meshtastic.core.database.dao -import coil3.request.ImageRequest +import androidx.test.ext.junit.runners.AndroidJUnit4 +import org.junit.runner.RunWith +import org.robolectric.annotation.Config -/** Nothing to do: Coil has no hardware-bitmap path on Apple platforms. */ -internal actual fun ImageRequest.Builder.decodeForSoftwareCanvas(): ImageRequest.Builder = this +@RunWith(AndroidJUnit4::class) +@Config(sdk = [34]) +class NodeInfoDaoDirectFilterTest : CommonNodeInfoDaoDirectFilterTest() diff --git a/core/database/src/commonMain/kotlin/org/meshtastic/core/database/Converters.kt b/core/database/src/commonMain/kotlin/org/meshtastic/core/database/Converters.kt index 0402310605..fa79c67c98 100644 --- a/core/database/src/commonMain/kotlin/org/meshtastic/core/database/Converters.kt +++ b/core/database/src/commonMain/kotlin/org/meshtastic/core/database/Converters.kt @@ -52,37 +52,43 @@ class Converters { fun dataToString(value: DataPacket): String = json.encodeToString(DataPacket.serializer(), value) @ColumnTypeConverter - fun bytesToFromRadio(bytes: ByteArray): FromRadio = FromRadio.ADAPTER.decodeOrNull(bytes, Logger) ?: FromRadio() + fun bytesToFromRadio(bytes: ByteArray): FromRadio = + FromRadio.ADAPTER.decodeOrNull(bytes, Logger) ?: FromRadio.Builder().build() @ColumnTypeConverter fun fromRadioToBytes(value: FromRadio): ByteArray = FromRadio.ADAPTER.encode(value) - @ColumnTypeConverter fun bytesToUser(bytes: ByteArray): User = User.ADAPTER.decodeOrNull(bytes, Logger) ?: User() + @ColumnTypeConverter + fun bytesToUser(bytes: ByteArray): User = User.ADAPTER.decodeOrNull(bytes, Logger) ?: User.Builder().build() @ColumnTypeConverter fun userToBytes(value: User): ByteArray = User.ADAPTER.encode(value) @ColumnTypeConverter - fun bytesToPosition(bytes: ByteArray): Position = Position.ADAPTER.decodeOrNull(bytes, Logger) ?: Position() + fun bytesToPosition(bytes: ByteArray): Position = + Position.ADAPTER.decodeOrNull(bytes, Logger) ?: Position.Builder().build() @ColumnTypeConverter fun positionToBytes(value: Position): ByteArray = Position.ADAPTER.encode(value) @ColumnTypeConverter - fun bytesToTelemetry(bytes: ByteArray): Telemetry = Telemetry.ADAPTER.decodeOrNull(bytes, Logger) ?: Telemetry() + fun bytesToTelemetry(bytes: ByteArray): Telemetry = + Telemetry.ADAPTER.decodeOrNull(bytes, Logger) ?: Telemetry.Builder().build() @ColumnTypeConverter fun telemetryToBytes(value: Telemetry): ByteArray = Telemetry.ADAPTER.encode(value) @ColumnTypeConverter - fun bytesToPaxcounter(bytes: ByteArray): Paxcount = Paxcount.ADAPTER.decodeOrNull(bytes, Logger) ?: Paxcount() + fun bytesToPaxcounter(bytes: ByteArray): Paxcount = + Paxcount.ADAPTER.decodeOrNull(bytes, Logger) ?: Paxcount.Builder().build() @ColumnTypeConverter fun paxCounterToBytes(value: Paxcount): ByteArray = Paxcount.ADAPTER.encode(value) @ColumnTypeConverter fun bytesToMetadata(bytes: ByteArray): DeviceMetadata = - DeviceMetadata.ADAPTER.decodeOrNull(bytes, Logger) ?: DeviceMetadata() + DeviceMetadata.ADAPTER.decodeOrNull(bytes, Logger) ?: DeviceMetadata.Builder().build() @ColumnTypeConverter fun metadataToBytes(value: DeviceMetadata): ByteArray = DeviceMetadata.ADAPTER.encode(value) @ColumnTypeConverter - fun bytesToChannelSet(bytes: ByteArray): ChannelSet = ChannelSet.ADAPTER.decodeOrNull(bytes, Logger) ?: ChannelSet() + fun bytesToChannelSet(bytes: ByteArray): ChannelSet = + ChannelSet.ADAPTER.decodeOrNull(bytes, Logger) ?: ChannelSet.Builder().build() @ColumnTypeConverter fun channelSetToBytes(value: ChannelSet): ByteArray = ChannelSet.ADAPTER.encode(value) diff --git a/core/database/src/commonMain/kotlin/org/meshtastic/core/database/MeshtasticDatabase.kt b/core/database/src/commonMain/kotlin/org/meshtastic/core/database/MeshtasticDatabase.kt index 5526e3b166..dd88d8abef 100644 --- a/core/database/src/commonMain/kotlin/org/meshtastic/core/database/MeshtasticDatabase.kt +++ b/core/database/src/commonMain/kotlin/org/meshtastic/core/database/MeshtasticDatabase.kt @@ -146,8 +146,11 @@ import org.meshtastic.core.database.entity.TracerouteNodePositionEntity AutoMigration(from = 55, to = 56), AutoMigration(from = 56, to = 57), AutoMigration(from = 57, to = 58), + AutoMigration(from = 58, to = 59), + AutoMigration(from = 59, to = 60), + AutoMigration(from = 60, to = 61), ], - version = 58, + version = 61, exportSchema = true, ) @androidx.room3.ConstructedBy(MeshtasticDatabaseConstructor::class) diff --git a/core/database/src/commonMain/kotlin/org/meshtastic/core/database/dao/ChannelSetDao.kt b/core/database/src/commonMain/kotlin/org/meshtastic/core/database/dao/ChannelSetDao.kt index dead03ae93..d01b77b10d 100644 --- a/core/database/src/commonMain/kotlin/org/meshtastic/core/database/dao/ChannelSetDao.kt +++ b/core/database/src/commonMain/kotlin/org/meshtastic/core/database/dao/ChannelSetDao.kt @@ -21,6 +21,7 @@ import androidx.room3.Insert import androidx.room3.OnConflictStrategy import androidx.room3.Query import kotlinx.coroutines.flow.Flow +import okio.ByteString import org.meshtastic.core.database.entity.ChannelSetEntity /** Per-device persistence of the connected device's [ChannelSetEntity]. */ @@ -45,4 +46,18 @@ interface ChannelSetDao { @Query("DELETE FROM channel_set") suspend fun clear() + + /** + * Drops the cached channel set but keeps [ChannelSetEntity.lastReconciled]. + * + * The handshake clears the cache before re-downloading the radio's channels. The stored conversations have not + * moved just because the cache was dropped, so the reconciliation baseline has to outlive it — otherwise every + * reconnect would look like "no baseline yet" and a channel replaced while the app was away would never be noticed. + */ + @Query("UPDATE channel_set SET channel_set = :emptyChannelSet WHERE id = 0") + suspend fun clearRetainingBaseline(emptyChannelSet: ByteArray) + + /** Records the channel set the stored conversations are now keyed against. */ + @Query("UPDATE channel_set SET last_reconciled = :baseline WHERE id = 0") + suspend fun setLastReconciled(baseline: ByteString) } diff --git a/core/database/src/commonMain/kotlin/org/meshtastic/core/database/dao/DiscoveryDao.kt b/core/database/src/commonMain/kotlin/org/meshtastic/core/database/dao/DiscoveryDao.kt index 38ecfc4633..0bb86ad47e 100644 --- a/core/database/src/commonMain/kotlin/org/meshtastic/core/database/dao/DiscoveryDao.kt +++ b/core/database/src/commonMain/kotlin/org/meshtastic/core/database/dao/DiscoveryDao.kt @@ -165,6 +165,37 @@ interface DiscoveryDao { ) suspend fun getMaxDistance(sessionId: Long): Double? + @Query("SELECT COUNT(*) FROM discovery_session WHERE id = :sessionId AND device_address = :deviceAddress") + suspend fun countSessionsForDevice(sessionId: Long, deviceAddress: String): Int + + /** + * Writes one dwell's preset result and its discovered nodes, but only if the parent session row exists in *this* + * database and belongs to [deviceAddress], returning null when it does not. + * + * The parent check and the child writes share one transaction on purpose. Every [SwitchingDiscoveryDao] call + * re-resolves the active database, so a caller that checks the session and then inserts issues two independent + * resolutions and a device/DB switch landing between them orphans the foreign key. Callers must not reimplement + * this as check-then-insert. + * + * The id alone is not an identity: every per-device database autogenerates from 1, so a dwell can match an + * unrelated session of the same id in another database. [deviceAddress] is what makes the match unambiguous. + * + * [nodes] are stamped with the new preset-result id, so callers pass them with any placeholder. + */ + @Transaction + suspend fun insertDwellIfSessionExists( + result: DiscoveryPresetResultEntity, + nodes: List, + deviceAddress: String, + ): Long? { + if (countSessionsForDevice(result.sessionId, deviceAddress) == 0) return null + val presetResultId = insertPresetResult(result) + if (nodes.isNotEmpty()) { + insertDiscoveredNodes(nodes.map { it.copy(presetResultId = presetResultId) }) + } + return presetResultId + } + @Transaction @Query("SELECT * FROM discovery_session WHERE id = :sessionId") suspend fun getSessionWithResults(sessionId: Long): DiscoverySessionEntity? diff --git a/core/database/src/commonMain/kotlin/org/meshtastic/core/database/dao/NodeInfoDao.kt b/core/database/src/commonMain/kotlin/org/meshtastic/core/database/dao/NodeInfoDao.kt index 1871027c9e..adaa018229 100644 --- a/core/database/src/commonMain/kotlin/org/meshtastic/core/database/dao/NodeInfoDao.kt +++ b/core/database/src/commonMain/kotlin/org/meshtastic/core/database/dao/NodeInfoDao.kt @@ -73,6 +73,9 @@ interface NodeInfoDao { return if (existingNodeEntity == null) { handleNewNodeUpsertValidation(incomingNode) } else { + // Never trusted here, whatever the number says: this path carries mesh-received NodeInfo, so matching the + // local node number only proves the sender claimed it. The radio's own key is trusted in installConfig, + // where selfNum comes from the device over the local link. handleExistingNodeUpsertValidation(existingNodeEntity, incomingNode) } } @@ -86,7 +89,7 @@ interface NodeInfoDao { private suspend fun handleNewNodeUpsertValidation(newNode: NodeEntity): NodeEntity { // Check if the new node's public key (if present and not empty) // is already claimed by another existing node. - if ((newNode.publicKey?.size ?: 0) > 0) { + if (newNode.publicKey.isUsableKey()) { val nodeWithSamePK = findNodeByPublicKey(newNode.publicKey) if (nodeWithSamePK != null && nodeWithSamePK.num != newNode.num) { // This is a potential impersonation attempt. @@ -139,42 +142,73 @@ interface NodeInfoDao { * This function implements safety checks to prevent public key conflicts (PKC) and ensure robust handling of key * updates. * + * First-wins: once a valid key is stored it is never replaced by a different inbound one. That would let any mesh + * or MQTT peer destroy a contact's trusted key by broadcasting a NodeInfo under their node number, breaking PKC + * direct messages until the node is deleted and re-added. Firmware refuses the same substitution + * (`NodeDB::updateUser` logs "Public Key mismatch, drop NodeInfo" and keeps its copy), so overwriting here threw + * away a key the radio itself still held. + * * @param existingNode The current state of the node in the database. * @param incomingNode The new node data being upserted. - * @return The resolved [ByteString] for the public key: - * - [NodeEntity.ERROR_BYTE_STRING]: If there is a mismatch between a valid existing key and a new incoming key. - * - `incomingNode.publicKey`: If the incoming key is new, matches the existing one, or if recovering from an error - * state. - * - `existingNode.publicKey`: If the incoming update has no key, or if the user is licensed but already has a valid - * key (prevents wiping). - * - [ByteString.EMPTY]: If the user is licensed and didn't previously have a key (or if key is explicitly cleared). + * @return the resolved key, and whether it matched: + * - the stored key with `keyMatch = false`: a *different* valid key arrived; the refusal is recorded, not applied. + * - `incomingNode.publicKey`: the incoming key is new or matches the stored one. + * - `existingNode.publicKey`: the incoming update has no key, or the user is licensed but already has a valid key + * (prevents wiping). + * - [ByteString.EMPTY]: the user is licensed and had no key before (or the key is explicitly cleared). */ - private fun resolvePublicKey(existingNode: NodeEntity, incomingNode: NodeEntity): ByteString? { + private fun resolvePublicKey(existingNode: NodeEntity, incomingNode: NodeEntity): ResolvedPublicKey { val existingKey = existingNode.publicKey ?: existingNode.user.public_key val incomingKey = incomingNode.publicKey - val incomingHasKey = (incomingKey?.size ?: 0) == KEY_SIZE - val existingHasKey = existingKey.size == KEY_SIZE && existingKey != NodeEntity.ERROR_BYTE_STRING + val incomingHasKey = incomingKey.isUsableKey() + val existingHasKey = existingKey.isUsableKey() return when { - incomingHasKey -> { - if (existingHasKey && incomingKey != existingKey) { - // Actual mismatch between two non-empty keys - NodeEntity.ERROR_BYTE_STRING - } else { - // New key, same key, or recovery from Error state - incomingKey + incomingHasKey -> + when { + existingHasKey && incomingKey != existingKey -> + // A different key for a node we already hold one for: keep ours, record the refusal. + ResolvedPublicKey(existingKey, keyMatch = false, newPublicKey = incomingKey) + + existingHasKey -> + // The key already on file. It settles nothing: a recorded refusal stands until the connected + // radio speaks for itself, or the next legitimate beacon would hide the substitute. + ResolvedPublicKey( + incomingKey, + keyMatch = existingNode.keyMatch && incomingNode.keyMatch, + newPublicKey = incomingNode.newPublicKey ?: existingNode.newPublicKey, + ) + + // A first key, or recovery from a legacy sentinel row. + else -> ResolvedPublicKey(incomingKey, keyMatch = true) } - } - existingHasKey -> existingKey + existingHasKey -> ResolvedPublicKey(existingKey, existingNode.keyMatch, existingNode.newPublicKey) - incomingNode.user.is_licensed -> ByteString.EMPTY + incomingNode.user.is_licensed -> ResolvedPublicKey(ByteString.EMPTY, keyMatch = true) - else -> existingKey + else -> ResolvedPublicKey(existingKey, existingNode.keyMatch, existingNode.newPublicKey) } } + /** + * A resolved public key, whether the inbound one matched it, and the refused key when it did not — see + * [resolvePublicKey]. + */ + private data class ResolvedPublicKey( + val key: ByteString?, + val keyMatch: Boolean, + val newPublicKey: ByteString? = null, + ) + + /** + * A key the DAO will act on: present, full length, and not the legacy mismatch sentinel. The sentinel is 32 bytes + * too, so a size check alone would record it as a refused key or, on the local link, write it over the real one. + */ + private fun ByteString?.isUsableKey(): Boolean = + this != null && size == KEY_SIZE && this != NodeEntity.ERROR_BYTE_STRING + /** * Handles the validation logic when upserting an existing node. * @@ -185,10 +219,11 @@ interface NodeInfoDao { * 2. **Update**: If it's a normal update, we validate the public key using [resolvePublicKey] to prevent conflicts * or accidental key wiping, and then update the node. * - * [trustIncomingKey] skips the mismatch check and accepts a valid incoming key as-is. Set only for the local node - * during a config install: the connected device is authoritative for its own key over the local link, and an - * erase-and-reflash legitimately re-keys it (a mismatch there would otherwise poison the local node with - * [NodeEntity.ERROR_BYTE_STRING] and break PKI traffic until app data is cleared). + * [trustIncomingKey] skips the mismatch check and accepts a valid incoming key as-is. Set only for the local node: + * the connected device is authoritative for its own key over the local link, and an erase-and-reflash, a factory + * reset or a 2.8 re-key legitimately changes it (a mismatch there would otherwise poison the local node with + * [NodeEntity.ERROR_BYTE_STRING] and break PKI traffic until app data is cleared). Every other node keeps + * first-wins. */ @Suppress("CyclomaticComplexMethod", "MagicNumber") private fun handleExistingNodeUpsertValidation( @@ -207,6 +242,8 @@ interface NodeInfoDao { return incomingNode.copy( user = existingNode.user, publicKey = existingNode.publicKey, + keyMatch = existingNode.keyMatch, + newPublicKey = existingNode.newPublicKey, longName = existingNode.longName, shortName = existingNode.shortName, manuallyVerified = existingNode.manuallyVerified, @@ -215,16 +252,20 @@ interface NodeInfoDao { ) } - val resolvedKey = - if (trustIncomingKey && (incomingNode.publicKey?.size ?: 0) == KEY_SIZE) { - incomingNode.publicKey + val resolved = + if (trustIncomingKey && incomingNode.publicKey.isUsableKey()) { + // The connected radio is authoritative for its own key, so this also clears any recorded mismatch. + ResolvedPublicKey(incomingNode.publicKey, keyMatch = true) } else { resolvePublicKey(existingNode, incomingNode) } return incomingNode.copy( - user = incomingNode.user.copy(public_key = resolvedKey ?: ByteString.EMPTY), - publicKey = resolvedKey, + user = + incomingNode.user.newBuilder().also { wb -> wb.public_key = resolved.key ?: ByteString.EMPTY }.build(), + publicKey = resolved.key, + keyMatch = resolved.keyMatch, + newPublicKey = resolved.newPublicKey, notes = resolvedNotes, powerChannelLabels = resolvedPowerChannelLabels, ) @@ -281,6 +322,9 @@ interface NodeInfoDao { // Text search (name/id) is applied in Kotlin (NodeRepositoryImpl), not here: SQLite's LIKE/UPPER/LOWER only // case-fold ASCII a-z/A-Z, so a WHERE-clause LIKE can't match e.g. "kolså" against "KOLSÅS" (#6750). + // + // Direct means hops_away = 0 AND NOT via_mqtt: an MQTT-bridged node carries the hop count the uplink gateway + // heard, not ours, so zero hops there is no claim about our radio. The row renderers read it the same way. @Query( """ WITH OurNode AS ( @@ -291,7 +335,7 @@ interface NodeInfoDao { SELECT * FROM nodes WHERE (:includeUnknown = 1 OR short_name IS NOT NULL) AND (:lastHeardMin = -1 OR last_heard >= :lastHeardMin) - AND (:hopsAwayMax = -1 OR (hops_away <= :hopsAwayMax AND hops_away >= 0) OR num = (SELECT myNodeNum FROM my_node LIMIT 1)) + AND (:onlyDirect = 0 OR (hops_away = 0 AND via_mqtt = 0) OR num = (SELECT myNodeNum FROM my_node LIMIT 1)) ORDER BY CASE WHEN num = (SELECT myNodeNum FROM my_node LIMIT 1) THEN 0 ELSE 1 @@ -326,7 +370,7 @@ interface NodeInfoDao { fun getNodes( sort: String, includeUnknown: Boolean, - hopsAwayMax: Int, + onlyDirect: Boolean, lastHeardMin: Int, ): Flow> @@ -487,7 +531,7 @@ interface NodeInfoDao { } // Batch validate new nodes' public keys (one query instead of N) - val publicKeysToCheck = newNodes.mapNotNull { node -> node.publicKey?.takeIf { it.size > 0 } }.distinct() + val publicKeysToCheck = newNodes.mapNotNull { node -> node.publicKey?.takeIf { it.isUsableKey() } }.distinct() val pkConflicts = if (publicKeysToCheck.isNotEmpty()) { publicKeysToCheck @@ -499,7 +543,7 @@ interface NodeInfoDao { } for (newNode in newNodes) { - if ((newNode.publicKey?.size ?: 0) > 0) { + if (newNode.publicKey.isUsableKey()) { val conflicting = pkConflicts[newNode.publicKey] if (conflicting != null && conflicting.num != newNode.num) { // Same key under a different num. Migrate when this is the connected device itself diff --git a/core/database/src/commonMain/kotlin/org/meshtastic/core/database/dao/PacketDao.kt b/core/database/src/commonMain/kotlin/org/meshtastic/core/database/dao/PacketDao.kt index b1db37ad92..041fc5428d 100644 --- a/core/database/src/commonMain/kotlin/org/meshtastic/core/database/dao/PacketDao.kt +++ b/core/database/src/commonMain/kotlin/org/meshtastic/core/database/dao/PacketDao.kt @@ -33,10 +33,12 @@ import org.meshtastic.core.database.entity.ContactSettings import org.meshtastic.core.database.entity.Packet import org.meshtastic.core.database.entity.PacketEntity import org.meshtastic.core.database.entity.ReactionEntity +import org.meshtastic.core.model.ContactKey import org.meshtastic.core.model.DataPacket import org.meshtastic.core.model.MessageStatus import org.meshtastic.core.model.NodeAddress -import org.meshtastic.proto.ChannelSettings +import org.meshtastic.core.model.util.ChannelKeyChange +import org.meshtastic.core.model.util.ConversationSlot import org.meshtastic.proto.MeshPacket @Suppress("TooManyFunctions", "LargeClass") @@ -283,10 +285,16 @@ interface PacketDao { /** * Atomically claims one stable packet row for sending. A returned QUEUED packet means this call performed the * QUEUED -> ENROUTE transition and owns the send; any other returned status means another path already handled it. + * + * A packet whose conversation was archived while it sat in the queue is never claimed. Its stored channel index + * still points at the slot it was composed for, and something else occupies that slot now, so sending it would put + * the message on a channel the user never chose. This runs in the same transaction as the status transition, which + * is what makes the check hold against a reconciliation landing mid-send. */ @Transaction suspend fun claimQueuedPacket(myNodeNum: Int, uuid: Long): Packet? { - val packet = getPacketByPersistedId(myNodeNum, uuid) ?: return null + val packet = + getPacketByPersistedId(myNodeNum, uuid)?.takeUnless { ContactKey(it.contact_key).isRetired } ?: return null if (packet.data.status == MessageStatus.QUEUED) { update(packet.copy(data = packet.data.copy(status = MessageStatus.ENROUTE))) } @@ -296,7 +304,9 @@ interface PacketDao { /** Legacy claim used only by pre-upgrade WorkManager jobs whose mesh packet ID still resolves to one row. */ @Transaction suspend fun claimQueuedPacketByPacketIdIfUnique(packetId: Int): Packet? { - val packet = findPacketsWithId(packetId).singleOrNull() ?: return null + val packet = + findPacketsWithId(packetId).singleOrNull()?.takeUnless { ContactKey(it.contact_key).isRetired } + ?: return null if (packet.data.status == MessageStatus.QUEUED) { update(packet.copy(data = packet.data.copy(status = MessageStatus.ENROUTE))) } @@ -735,55 +745,119 @@ interface PacketDao { // endregion /** - * One-time migration: Remap all message DataPacket.channel indices to new mapping using PSK after a channel - * reorder. For each Packet (with port_num = 1), finds the old PSK then sets the channel index to the matching - * newSettings index. Skips if PSKs do not match or are missing. + * Re-keys conversations after the radio's channel set changed, applying a plan from `planChannelReconciliation`. + * + * Conversations are keyed by channel *index*, so a slot that changes occupant would otherwise show the previous + * channel's history under the new channel. Each change moves one conversation: to another slot (the channel was + * reordered), to a retired key (the channel is gone), or back from a retired key (the channel came back). + * + * Direct messages move with their slot but are never retired — a DM is a conversation with a node, not with a + * channel, and losing that history because the channel set changed would be worse than the misfiling this repairs. + * + * All the rows that will move are read before any is written, so a slot swap cannot double-move a conversation. */ @Transaction - suspend fun migrateChannelsByPSK(oldSettings: List, newSettings: List) { - // Pre-calculate mapping from old index to new index - val indexMap = - oldSettings - .mapIndexed { oldIndex, oldChannel -> - val pskMatches = - newSettings.mapIndexedNotNull { index, channel -> - if (channel.psk == oldChannel.psk) index to channel else null - } + suspend fun applyChannelReconciliation(changes: List) { + if (changes.isEmpty()) return + val liveMoves = changes.filter { it.from is ConversationSlot.Live && it.to is ConversationSlot.Live } + val retirements = changes.filter { it.to is ConversationSlot.Retired } + val reclaims = changes.filter { it.from is ConversationSlot.Retired } - val newIndex = - when { - pskMatches.isEmpty() -> null + // Retirements first. A retirement frees its slot with a wholesale key rewrite, so any move that targets + // that slot has to land afterwards -- otherwise the moved conversation is sitting on the key the retirement + // is about to sweep, and both end up in the same archive. + for (change in retirements) { + val fromIndex = (change.from as ConversationSlot.Live).index + val token = (change.to as ConversationSlot.Retired).token + moveContact( + from = ContactKey.broadcast(fromIndex).value, + to = ContactKey.retiredBroadcast(token).value, + displayName = change.displayName, + ) + } - pskMatches.size == 1 -> pskMatches.first().first + applyLiveMoves(liveMoves) - else -> { - // Multiple matches with same PSK. Disambiguate by Name. - val nameMatches = pskMatches.filter { it.second.name == oldChannel.name } - if (nameMatches.size == 1) { - nameMatches.first().first - } else { - // Still ambiguous. Prefer keeping same index. - pskMatches.find { it.first == oldIndex }?.first ?: pskMatches.first().first - } - } - } - oldIndex to newIndex - } - .toMap() - - val allPackets = getAllUserPacketsForMigration() - for (packet in allPackets) { - val oldIndex = packet.data.channel - val newIndex = indexMap[oldIndex] - if (newIndex != null && oldIndex != newIndex) { - // Rebuild contact_key with the new index, keeping the rest unchanged - val oldKeySuffix = packet.contact_key.dropWhile { it.isDigit() } - val newContactKey = "$newIndex$oldKeySuffix" - update(packet.copy(contact_key = newContactKey, data = packet.data.copy(channel = newIndex))) - } + for (change in reclaims) { + val token = (change.from as ConversationSlot.Retired).token + val toIndex = (change.to as ConversationSlot.Live).index + // After the moves above, never before: a reclaimed reaction stamped with its new index would otherwise be + // swept up by a move out of that same index. Scoped by the retired conversation's packets, which still + // carry the retired key at this point. + retargetReactionsForContact(ContactKey.retiredBroadcast(token).value, toIndex) + moveContact( + from = ContactKey.retiredBroadcast(token).value, + to = ContactKey.broadcast(toIndex).value, + displayName = "", + toChannel = toIndex, + ) } } + /** + * Every retired conversation currently holding messages, by its channel-identity token. + * + * Messages, not settings: deleting an archived conversation removes its packets but leaves its settings row, and + * counting that row as parked would resurrect the deleted conversation's mute and pin onto the fresh channel if + * that channel were ever re-added. + */ + suspend fun getRetiredContactTokens(): Set = + getRetiredPacketContactKeys().mapNotNullTo(mutableSetOf()) { ContactKey(it).retiredToken } + + @Query("SELECT DISTINCT contact_key FROM packet WHERE contact_key LIKE '~%'") + suspend fun getRetiredPacketContactKeys(): List + + /** + * Moves one whole conversation to [to], carrying its settings (mute, pin, draft, read marker) with it. + * + * [displayName] labels a retired conversation, whose slot no longer names it; it is cleared on the way back. + * [toChannel], when given, restores the packets' channel index — a retired conversation keeps the index it was + * retired from so a later reclaim knows which reactions belong to it. + */ + @Transaction + suspend fun moveContact(from: String, to: String, displayName: String, toChannel: Int? = null) { + val existing = getContactSettings(from) + if (existing != null) { + deleteContactSettings(from) + deleteContactSettings(to) + insertContactSettingsIgnore(listOf(existing.copy(contact_key = to, displayName = displayName))) + } else if (displayName.isNotEmpty()) { + insertContactSettingsIgnore(listOf(ContactSettings(contact_key = to, displayName = displayName))) + } + if (toChannel != null) { + for (packet in getPacketsForContact(from)) { + update(packet.copy(contact_key = to, data = packet.data.copy(channel = toChannel))) + } + } else { + retargetContactKey(from = from, to = to) + } + } + + @Query("UPDATE packet SET contact_key = :to WHERE contact_key = :from") + suspend fun retargetContactKey(from: String, to: String) + + @Query("SELECT * FROM packet WHERE contact_key = :contact") + suspend fun getPacketsForContact(contact: String): List + + @Query("DELETE FROM contact_settings WHERE contact_key = :contact") + suspend fun deleteContactSettings(contact: String) + + @Query("UPDATE reactions SET channel = :to WHERE channel = :from") + suspend fun retargetReactionChannel(from: Int, to: Int) + + /** + * Re-stamps the reactions belonging to one conversation's messages. + * + * Scoped through the conversation's own packet ids rather than by channel, because a retired conversation keeps the + * index it was retired from and a live channel may since have taken that index over. A mesh packet id is only + * unique per sender, so the node scope keeps another device's rows out of it. + */ + @Suppress("MaxLineLength") + @Query( + "UPDATE reactions SET channel = :to WHERE (myNodeNum = 0 OR myNodeNum = (SELECT myNodeNum FROM my_node)) AND reply_id IN (SELECT packet_id FROM packet WHERE contact_key = :contact)", + ) + suspend fun retargetReactionsForContact(contact: String, to: Int) + @Query("SELECT * FROM packet WHERE port_num = 1") suspend fun getAllUserPacketsForMigration(): List @@ -1097,3 +1171,63 @@ fun shouldApplyOutgoingQueueStatus(current: MessageStatus?, status: MessageStatu else -> false } + +/** + * Parks a reaction's channel out of range while conversations swap slots. No live slot or PKI channel can hold a + * negative index, so a staged row cannot be picked up by the second phase of another move. + */ +private const val REACTION_CHANNEL_STAGING_OFFSET = 1000 + +/** + * Re-keys the conversations that only changed slot. + * + * Every affected row is read before any is written, so a 0<->1 swap cannot be applied twice, and reaction channels are + * staged through a value no live slot can hold for the same reason. Only text messages are re-keyed: every other port + * is queried by port_num rather than by contact key, so a stale key on a waypoint row is invisible and rewriting one + * costs a blob write for nothing. + * + * Callers must have applied retirements already - a move into a slot a retirement is about to sweep would be swept with + * it. + */ +private suspend fun PacketDao.applyLiveMoves(liveMoves: List) { + val sourceIndices = liveMoves.map { (it.from as ConversationSlot.Live).index }.toSet() + val movingPackets = + if (sourceIndices.isEmpty()) { + emptyList() + } else { + getAllUserPacketsForMigration().filter { ContactKey(it.contact_key).channelOrNull in sourceIndices } + } + val moveByIndex = + liveMoves.associate { change -> + (change.from as ConversationSlot.Live).index to (change.to as ConversationSlot.Live).index + } + + // Settings are re-keyed the same way: read every affected row, then rewrite, so a swap cannot land a + // conversation's mute or pin on the channel it traded places with. + val movingSettings = + getAllContactSettingsSnapshot().mapNotNull { settings -> + val key = ContactKey(settings.contact_key) + val newIndex = moveByIndex[key.channelOrNull] ?: return@mapNotNull null + settings to "$newIndex${key.addressString}" + } + + for (packet in movingPackets) { + val newIndex = moveByIndex[ContactKey(packet.contact_key).channelOrNull] ?: continue + val suffix = ContactKey(packet.contact_key).addressString + update(packet.copy(contact_key = "$newIndex$suffix", data = packet.data.copy(channel = newIndex))) + } + for ((settings, _) in movingSettings) { + deleteContactSettings(settings.contact_key) + } + for ((_, newKey) in movingSettings) { + deleteContactSettings(newKey) + } + insertContactSettingsIgnore(movingSettings.map { (settings, newKey) -> settings.copy(contact_key = newKey) }) + // Two-phase, via a value no live slot can hold: a direct 0->1 then 1->0 would collapse both onto one channel. + for ((from, _) in moveByIndex) { + retargetReactionChannel(from = from, to = from - REACTION_CHANNEL_STAGING_OFFSET) + } + for ((from, to) in moveByIndex) { + retargetReactionChannel(from = from - REACTION_CHANNEL_STAGING_OFFSET, to = to) + } +} diff --git a/core/database/src/commonMain/kotlin/org/meshtastic/core/database/dao/SwitchingDiscoveryDao.kt b/core/database/src/commonMain/kotlin/org/meshtastic/core/database/dao/SwitchingDiscoveryDao.kt index b60aab4f55..1b3bea9d38 100644 --- a/core/database/src/commonMain/kotlin/org/meshtastic/core/database/dao/SwitchingDiscoveryDao.kt +++ b/core/database/src/commonMain/kotlin/org/meshtastic/core/database/dao/SwitchingDiscoveryDao.kt @@ -56,6 +56,9 @@ class SwitchingDiscoveryDao(private val dbManager: DatabaseProvider) : Discovery override suspend fun getSession(sessionId: Long): DiscoverySessionEntity? = dbManager.withDb { it.discoveryDao().getSession(sessionId) } + override suspend fun countSessionsForDevice(sessionId: Long, deviceAddress: String): Int = + dbManager.withDb { it.discoveryDao().countSessionsForDevice(sessionId, deviceAddress) } ?: 0 + override suspend fun updateSessionCompletionStatus(sessionId: Long, status: String): Int = dbManager.withDb { it.discoveryDao().updateSessionCompletionStatus(sessionId, status) } ?: 0 @@ -106,6 +109,16 @@ class SwitchingDiscoveryDao(private val dbManager: DatabaseProvider) : Discovery override suspend fun insertPresetResult(result: DiscoveryPresetResultEntity): Long = checkNotNull(dbManager.withDb { it.discoveryDao().insertPresetResult(result) }) + /** + * One resolution, one transaction. Inheriting [DiscoveryDao]'s default body here would issue a separate [withDb] + * per statement and reintroduce the switch window the transaction exists to close. + */ + override suspend fun insertDwellIfSessionExists( + result: DiscoveryPresetResultEntity, + nodes: List, + deviceAddress: String, + ): Long? = dbManager.withDb { it.discoveryDao().insertDwellIfSessionExists(result, nodes, deviceAddress) } + override suspend fun updatePresetResult(result: DiscoveryPresetResultEntity) { dbManager.withDb { it.discoveryDao().updatePresetResult(result) } } diff --git a/core/database/src/commonMain/kotlin/org/meshtastic/core/database/entity/ChannelSetEntity.kt b/core/database/src/commonMain/kotlin/org/meshtastic/core/database/entity/ChannelSetEntity.kt index e5831d3028..75801d467e 100644 --- a/core/database/src/commonMain/kotlin/org/meshtastic/core/database/entity/ChannelSetEntity.kt +++ b/core/database/src/commonMain/kotlin/org/meshtastic/core/database/entity/ChannelSetEntity.kt @@ -19,6 +19,10 @@ package org.meshtastic.core.database.entity import androidx.room3.ColumnInfo import androidx.room3.Entity import androidx.room3.PrimaryKey +import co.touchlab.kermit.Logger +import okio.ByteString +import okio.ByteString.Companion.toByteString +import org.meshtastic.core.model.util.decodeOrNull import org.meshtastic.proto.ChannelSet /** @@ -34,9 +38,29 @@ import org.meshtastic.proto.ChannelSet data class ChannelSetEntity( @PrimaryKey @ColumnInfo(name = "id") val id: Int = SINGLETON_ID, @ColumnInfo(name = "channel_set") val channelSet: ChannelSet, + /** + * The channel set the stored conversations are currently keyed against, encoded as a [ChannelSet]. + * + * Conversations are keyed by channel index, so they have to be re-keyed whenever a slot changes occupant. This is + * the baseline that change is measured from. It is persisted rather than held in memory because the radio reboots + * after a preset change and the app is routinely killed across that gap; an in-memory baseline would be lost + * exactly when it is needed, leaving the messages permanently misfiled. Null before the first reconcile — then the + * current set is captured and nothing moves. + * + * Survives [ChannelSetDao.clearRetainingBaseline], which the handshake uses: the messages have not moved just + * because the cached channel set was dropped. + */ + @ColumnInfo(name = "last_reconciled") val lastReconciled: ByteString? = null, ) { + /** The baseline decoded, or null when no reconcile has run for this device yet. */ + val lastReconciledChannelSet: ChannelSet? + get() = lastReconciled?.let { ChannelSet.ADAPTER.decodeOrNull(it.toByteArray(), Logger) } + companion object { /** There is only ever one channel set per device, so every row uses this fixed primary key. */ const val SINGLETON_ID = 0 + + /** Encodes [channelSet] for storage in [lastReconciled]. */ + fun encodeBaseline(channelSet: ChannelSet): ByteString = ChannelSet.ADAPTER.encode(channelSet).toByteString() } } diff --git a/core/database/src/commonMain/kotlin/org/meshtastic/core/database/entity/MeshLog.kt b/core/database/src/commonMain/kotlin/org/meshtastic/core/database/entity/MeshLog.kt index 6038cc90a5..b0d9cbcabf 100644 --- a/core/database/src/commonMain/kotlin/org/meshtastic/core/database/entity/MeshLog.kt +++ b/core/database/src/commonMain/kotlin/org/meshtastic/core/database/entity/MeshLog.kt @@ -52,7 +52,7 @@ data class MeshLog( @ColumnInfo(name = "from_num", defaultValue = "0") val fromNum: Int = 0, @ColumnInfo(name = "port_num", defaultValue = "0") val portNum: Int = 0, @ColumnInfo(name = "from_radio", typeAffinity = ColumnInfo.BLOB, defaultValue = "x''") - val fromRadio: FromRadio = FromRadio(), + val fromRadio: FromRadio = FromRadio.Builder().build(), ) { val meshPacket: MeshPacket? diff --git a/core/database/src/commonMain/kotlin/org/meshtastic/core/database/entity/NodeEntity.kt b/core/database/src/commonMain/kotlin/org/meshtastic/core/database/entity/NodeEntity.kt index a6f63be758..a34d1f95f3 100644 --- a/core/database/src/commonMain/kotlin/org/meshtastic/core/database/entity/NodeEntity.kt +++ b/core/database/src/commonMain/kotlin/org/meshtastic/core/database/entity/NodeEntity.kt @@ -50,16 +50,17 @@ data class NodeWithRelations( snr = node.snr, rssi = node.rssi, lastHeard = node.lastHeard, - deviceMetrics = node.deviceMetrics ?: org.meshtastic.proto.DeviceMetrics(), + deviceMetrics = node.deviceMetrics ?: org.meshtastic.proto.DeviceMetrics.Builder().build(), channel = node.channel, viaMqtt = node.viaMqtt, hopsAway = node.hopsAway, isFavorite = node.isFavorite, isIgnored = node.isIgnored, isMuted = node.isMuted, - environmentMetrics = node.environmentMetrics ?: org.meshtastic.proto.EnvironmentMetrics(), - powerMetrics = node.powerMetrics ?: org.meshtastic.proto.PowerMetrics(), - airQualityMetrics = node.airQualityMetrics ?: org.meshtastic.proto.AirQualityMetrics(), + environmentMetrics = node.environmentMetrics ?: org.meshtastic.proto.EnvironmentMetrics.Builder().build(), + powerMetrics = node.powerMetrics ?: org.meshtastic.proto.PowerMetrics.Builder().build(), + airQualityMetrics = node.airQualityMetrics ?: org.meshtastic.proto.AirQualityMetrics.Builder().build(), + soilWaterMetrics = node.soilWaterMetrics ?: org.meshtastic.proto.SoilWaterMetrics.Builder().build(), paxcounter = node.paxcounter, publicKey = node.publicKey ?: node.user.public_key, notes = node.notes, @@ -70,6 +71,8 @@ data class NodeWithRelations( manuallyVerified = node.manuallyVerified, signsPackets = node.signsPackets, heardOnCurrentLora = node.heardOnCurrentLora, + keyMatch = node.keyMatch, + newPublicKey = node.newPublicKey, ) fun toEntity() = with(node) { @@ -90,6 +93,7 @@ data class NodeWithRelations( environmentTelemetry = environmentTelemetry, powerTelemetry = powerTelemetry, airQualityTelemetry = airQualityTelemetry, + soilWaterTelemetry = soilWaterTelemetry, paxcounter = paxcounter, publicKey = publicKey ?: user.public_key, notes = notes, @@ -99,6 +103,8 @@ data class NodeWithRelations( lastTransport = lastTransport, signsPackets = signsPackets, heardOnCurrentLora = heardOnCurrentLora, + keyMatch = keyMatch, + newPublicKey = newPublicKey, ) } } @@ -126,16 +132,17 @@ data class MetadataEntity( ) data class NodeEntity( @PrimaryKey(autoGenerate = false) val num: Int, // This is immutable, and used as a key - @ColumnInfo(typeAffinity = ColumnInfo.BLOB) var user: User = User(), + @ColumnInfo(typeAffinity = ColumnInfo.BLOB) var user: User = User.Builder().build(), @ColumnInfo(name = "long_name") var longName: String? = null, @ColumnInfo(name = "short_name") var shortName: String? = null, // used in includeUnknown filter - @ColumnInfo(typeAffinity = ColumnInfo.BLOB) var position: WirePosition = WirePosition(), + @ColumnInfo(typeAffinity = ColumnInfo.BLOB) var position: WirePosition = WirePosition.Builder().build(), var latitude: Double = 0.0, var longitude: Double = 0.0, var snr: Float = Float.MAX_VALUE, var rssi: Int = Int.MAX_VALUE, @ColumnInfo(name = "last_heard") var lastHeard: Int = 0, // the last time we've seen this node in secs since 1970 - @ColumnInfo(name = "device_metrics", typeAffinity = ColumnInfo.BLOB) var deviceTelemetry: Telemetry = Telemetry(), + @ColumnInfo(name = "device_metrics", typeAffinity = ColumnInfo.BLOB) + var deviceTelemetry: Telemetry = Telemetry.Builder().build(), var channel: Int = 0, @ColumnInfo(name = "via_mqtt") var viaMqtt: Boolean = false, @ColumnInfo(name = "hops_away") var hopsAway: Int = -1, @@ -143,11 +150,14 @@ data class NodeEntity( @ColumnInfo(name = "is_ignored", defaultValue = "0") var isIgnored: Boolean = false, @ColumnInfo(name = "is_muted", defaultValue = "0") var isMuted: Boolean = false, @ColumnInfo(name = "environment_metrics", typeAffinity = ColumnInfo.BLOB) - var environmentTelemetry: Telemetry = Telemetry(), - @ColumnInfo(name = "power_metrics", typeAffinity = ColumnInfo.BLOB) var powerTelemetry: Telemetry = Telemetry(), + var environmentTelemetry: Telemetry = Telemetry.Builder().build(), + @ColumnInfo(name = "power_metrics", typeAffinity = ColumnInfo.BLOB) + var powerTelemetry: Telemetry = Telemetry.Builder().build(), @ColumnInfo(name = "air_quality_metrics", typeAffinity = ColumnInfo.BLOB, defaultValue = "x''") - var airQualityTelemetry: Telemetry = Telemetry(), - @ColumnInfo(typeAffinity = ColumnInfo.BLOB) var paxcounter: Paxcount = Paxcount(), + var airQualityTelemetry: Telemetry = Telemetry.Builder().build(), + @ColumnInfo(name = "soil_water_metrics", typeAffinity = ColumnInfo.BLOB, defaultValue = "x''") + var soilWaterTelemetry: Telemetry = Telemetry.Builder().build(), + @ColumnInfo(typeAffinity = ColumnInfo.BLOB) var paxcounter: Paxcount = Paxcount.Builder().build(), @ColumnInfo(name = "public_key") var publicKey: ByteString? = null, @ColumnInfo(name = "notes", defaultValue = "") var notes: String = "", @ColumnInfo(name = "power_channel_labels", defaultValue = "[]") var powerChannelLabels: List = emptyList(), @@ -159,11 +169,31 @@ data class NodeEntity( /** True when this node signs its broadcasts via XEdDSA (NodeInfo.has_xeddsa_signed). */ @ColumnInfo(name = "has_xeddsa_signed", defaultValue = "0") var signsPackets: Boolean = false, /** - * True when the radio has heard this node over RF since its current LoRa config took effect - * (NodeInfo.heard_on_current_lora). Defaults true so nodes stored before this column existed, and nodes from - * firmware that does not report it, are never shown as unheard. + * True when the radio has heard this node over RF on the LoRa configuration it is using now + * (NodeInfo.heard_on_current_lora). The radio derives this from the slot each node was heard on rather than + * clearing it on a config change, so returning to a configuration restores the previous answers. Defaults true so + * nodes stored before this column existed, and nodes from firmware that does not report it, are never shown as + * unheard. */ @ColumnInfo(name = "heard_on_current_lora", defaultValue = "1") var heardOnCurrentLora: Boolean = true, + /** + * False once a *different* public key has arrived for a node one is already stored for. + * + * The stored key stands (first-wins) and this records the refusal, matching firmware — which drops the whole + * NodeInfo on a key mismatch rather than overwriting — and Meshtastic-Apple. Overwriting the trusted key instead + * would let any mesh or MQTT peer destroy it by broadcasting a NodeInfo under that node's number. + * + * Defaults true so rows written before this column existed are not read as mismatched; those rows record a mismatch + * the old way, as [ERROR_BYTE_STRING] in [publicKey]. + */ + @ColumnInfo(name = "key_match", defaultValue = "1") var keyMatch: Boolean = true, + /** + * The key that was refused, kept so the mismatch can be shown as more than a warning. + * + * Null whenever [keyMatch] is true. Rows that recorded a mismatch the old way, as [ERROR_BYTE_STRING] in + * [publicKey], have no rejected key to report and stay null. + */ + @ColumnInfo(name = "new_public_key") var newPublicKey: ByteString? = null, ) { val deviceMetrics: org.meshtastic.proto.DeviceMetrics? get() = deviceTelemetry.device_metrics @@ -177,6 +207,9 @@ data class NodeEntity( val airQualityMetrics: org.meshtastic.proto.AirQualityMetrics? get() = airQualityTelemetry.air_quality_metrics + val soilWaterMetrics: org.meshtastic.proto.SoilWaterMetrics? + get() = soilWaterTelemetry.soil_water_metrics + val isUnknownUser get() = user.hw_model == HardwareModel.UNSET @@ -184,7 +217,7 @@ data class NodeEntity( get() = (publicKey ?: user.public_key).size > 0 fun setPosition(p: WirePosition, defaultTime: Int = currentTime()) { - position = p.copy(time = if (p.time != 0) p.time else defaultTime) + position = p.newBuilder().also { wb -> wb.time = if (p.time != 0) p.time else defaultTime }.build() latitude = degD(p.latitude_i ?: 0) longitude = degD(p.longitude_i ?: 0) } @@ -213,16 +246,17 @@ data class NodeEntity( snr = snr, rssi = rssi, lastHeard = lastHeard, - deviceMetrics = deviceMetrics ?: org.meshtastic.proto.DeviceMetrics(), + deviceMetrics = deviceMetrics ?: org.meshtastic.proto.DeviceMetrics.Builder().build(), channel = channel, viaMqtt = viaMqtt, hopsAway = hopsAway, isFavorite = isFavorite, isIgnored = isIgnored, isMuted = isMuted, - environmentMetrics = environmentMetrics ?: org.meshtastic.proto.EnvironmentMetrics(), - powerMetrics = powerMetrics ?: org.meshtastic.proto.PowerMetrics(), - airQualityMetrics = airQualityMetrics ?: org.meshtastic.proto.AirQualityMetrics(), + environmentMetrics = environmentMetrics ?: org.meshtastic.proto.EnvironmentMetrics.Builder().build(), + powerMetrics = powerMetrics ?: org.meshtastic.proto.PowerMetrics.Builder().build(), + airQualityMetrics = airQualityMetrics ?: org.meshtastic.proto.AirQualityMetrics.Builder().build(), + soilWaterMetrics = soilWaterMetrics ?: org.meshtastic.proto.SoilWaterMetrics.Builder().build(), paxcounter = paxcounter, publicKey = publicKey ?: user.public_key, notes = notes, @@ -231,5 +265,7 @@ data class NodeEntity( lastTransport = lastTransport, signsPackets = signsPackets, heardOnCurrentLora = heardOnCurrentLora, + keyMatch = keyMatch, + newPublicKey = newPublicKey, ) } diff --git a/core/database/src/commonMain/kotlin/org/meshtastic/core/database/entity/Packet.kt b/core/database/src/commonMain/kotlin/org/meshtastic/core/database/entity/Packet.kt index b27559e176..49f9592a8d 100644 --- a/core/database/src/commonMain/kotlin/org/meshtastic/core/database/entity/Packet.kt +++ b/core/database/src/commonMain/kotlin/org/meshtastic/core/database/entity/Packet.kt @@ -96,7 +96,9 @@ private fun ReactionEntity.belongsTo(packet: Packet): Boolean { channel == 0 && status == MessageStatus.RECEIVED && recipient !is NodeAddress.Broadcast - if (packetContact.channel != channel && !isLegacyInboundPkiChannel) return false + // A retired conversation has no live slot, so there is no index to compare against; it is frozen and receives no + // new packets, which leaves the addressing check below as sufficient scoping. + if (!packetContact.isRetired && packetContact.channel != channel && !isLegacyInboundPkiChannel) return false val senderIsLocal = status != MessageStatus.RECEIVED || @@ -179,6 +181,12 @@ data class ContactSettings( @ColumnInfo(name = "draft", defaultValue = "''") val draft: String = "", /** Sorts this conversation above its unpinned siblings within its own list section. */ @ColumnInfo(name = "pinned", defaultValue = "0") val pinned: Boolean = false, + /** + * The conversation's own label, set only for a retired channel. A live channel is labelled from the radio's channel + * set by slot index, which stops working the moment the slot belongs to someone else — so the effective name is + * captured here when the conversation is retired. Empty for every live conversation. + */ + @ColumnInfo(name = "display_name", defaultValue = "''") val displayName: String = "", ) { val isMuted get() = nowMillis <= muteUntil @@ -212,7 +220,7 @@ data class ReactionEntity( ) suspend fun ReactionEntity.toReaction(getNode: suspend (userId: String?) -> Node?): Reaction { - val user = getNode(userId)?.user ?: org.meshtastic.proto.User(id = userId) + val user = getNode(userId)?.user ?: org.meshtastic.proto.User.Builder().also { wb -> wb.id = userId }.build() return Reaction( replyId = replyId, user = user, diff --git a/core/database/src/commonTest/kotlin/org/meshtastic/core/database/ConvertersTest.kt b/core/database/src/commonTest/kotlin/org/meshtastic/core/database/ConvertersTest.kt index 2ddd87fc7f..f06825d4f0 100644 --- a/core/database/src/commonTest/kotlin/org/meshtastic/core/database/ConvertersTest.kt +++ b/core/database/src/commonTest/kotlin/org/meshtastic/core/database/ConvertersTest.kt @@ -63,7 +63,19 @@ class ConvertersTest { @Test fun `from radio converter round trips`() { assertProtoRoundTrip( - expected = FromRadio(queueStatus = QueueStatus(res = 1, free = 2, mesh_packet_id = 3)), + expected = + FromRadio.Builder() + .also { wb -> + wb.queueStatus = + QueueStatus.Builder() + .also { wb -> + wb.res = 1 + wb.free = 2 + wb.mesh_packet_id = 3 + } + .build() + } + .build(), toBytes = converters::fromRadioToBytes, fromBytes = converters::bytesToFromRadio, ) @@ -73,7 +85,14 @@ class ConvertersTest { fun `user converter round trips`() { assertProtoRoundTrip( expected = - User(id = "!abcdef01", long_name = "Test User", short_name = "TU", hw_model = HardwareModel.TBEAM), + User.Builder() + .also { wb -> + wb.id = "!abcdef01" + wb.long_name = "Test User" + wb.short_name = "TU" + wb.hw_model = HardwareModel.TBEAM + } + .build(), toBytes = converters::userToBytes, fromBytes = converters::bytesToUser, ) @@ -83,7 +102,15 @@ class ConvertersTest { fun `position converter round trips`() { assertProtoRoundTrip( expected = - Position(latitude_i = 450000000, longitude_i = 900000000, altitude = 123, time = 456, sats_in_view = 7), + Position.Builder() + .also { wb -> + wb.latitude_i = 450000000 + wb.longitude_i = 900000000 + wb.altitude = 123 + wb.time = 456 + wb.sats_in_view = 7 + } + .build(), toBytes = converters::positionToBytes, fromBytes = converters::bytesToPosition, ) @@ -93,17 +120,21 @@ class ConvertersTest { fun `telemetry converter round trips`() { assertProtoRoundTrip( expected = - Telemetry( - time = 1000, - device_metrics = - DeviceMetrics( - battery_level = 85, - voltage = 4.1f, - channel_utilization = 0.12f, - air_util_tx = 0.05f, - uptime_seconds = 123456, - ), - ), + Telemetry.Builder() + .also { wb -> + wb.time = 1000 + wb.device_metrics = + DeviceMetrics.Builder() + .also { wb -> + wb.battery_level = 85 + wb.voltage = 4.1f + wb.channel_utilization = 0.12f + wb.air_util_tx = 0.05f + wb.uptime_seconds = 123456 + } + .build() + } + .build(), toBytes = converters::telemetryToBytes, fromBytes = converters::bytesToTelemetry, ) @@ -112,7 +143,14 @@ class ConvertersTest { @Test fun `paxcount converter round trips`() { assertProtoRoundTrip( - expected = Paxcount(wifi = 10, ble = 5, uptime = 1000), + expected = + Paxcount.Builder() + .also { wb -> + wb.wifi = 10 + wb.ble = 5 + wb.uptime = 1000 + } + .build(), toBytes = converters::paxCounterToBytes, fromBytes = converters::bytesToPaxcounter, ) @@ -121,7 +159,14 @@ class ConvertersTest { @Test fun `device metadata converter round trips`() { assertProtoRoundTrip( - expected = DeviceMetadata(firmware_version = "2.5.0", hw_model = HardwareModel.HELTEC_V3, hasWifi = false), + expected = + DeviceMetadata.Builder() + .also { wb -> + wb.firmware_version = "2.5.0" + wb.hw_model = HardwareModel.HELTEC_V3 + wb.hasWifi = false + } + .build(), toBytes = converters::metadataToBytes, fromBytes = converters::bytesToMetadata, ) @@ -129,24 +174,39 @@ class ConvertersTest { @Test fun `empty proto messages round trip to empty defaults`() { - assertEquals(FromRadio(), converters.bytesToFromRadio(converters.fromRadioToBytes(FromRadio()))) - assertEquals(User(), converters.bytesToUser(converters.userToBytes(User()))) - assertEquals(Position(), converters.bytesToPosition(converters.positionToBytes(Position()))) - assertEquals(Telemetry(), converters.bytesToTelemetry(converters.telemetryToBytes(Telemetry()))) - assertEquals(Paxcount(), converters.bytesToPaxcounter(converters.paxCounterToBytes(Paxcount()))) - assertEquals(DeviceMetadata(), converters.bytesToMetadata(converters.metadataToBytes(DeviceMetadata()))) + assertEquals( + FromRadio.Builder().build(), + converters.bytesToFromRadio(converters.fromRadioToBytes(FromRadio.Builder().build())), + ) + assertEquals(User.Builder().build(), converters.bytesToUser(converters.userToBytes(User.Builder().build()))) + assertEquals( + Position.Builder().build(), + converters.bytesToPosition(converters.positionToBytes(Position.Builder().build())), + ) + assertEquals( + Telemetry.Builder().build(), + converters.bytesToTelemetry(converters.telemetryToBytes(Telemetry.Builder().build())), + ) + assertEquals( + Paxcount.Builder().build(), + converters.bytesToPaxcounter(converters.paxCounterToBytes(Paxcount.Builder().build())), + ) + assertEquals( + DeviceMetadata.Builder().build(), + converters.bytesToMetadata(converters.metadataToBytes(DeviceMetadata.Builder().build())), + ) } @Test fun `empty byte arrays decode to empty proto messages`() { val emptyBytes = byteArrayOf() - assertEquals(FromRadio(), converters.bytesToFromRadio(emptyBytes)) - assertEquals(User(), converters.bytesToUser(emptyBytes)) - assertEquals(Position(), converters.bytesToPosition(emptyBytes)) - assertEquals(Telemetry(), converters.bytesToTelemetry(emptyBytes)) - assertEquals(Paxcount(), converters.bytesToPaxcounter(emptyBytes)) - assertEquals(DeviceMetadata(), converters.bytesToMetadata(emptyBytes)) + assertEquals(FromRadio.Builder().build(), converters.bytesToFromRadio(emptyBytes)) + assertEquals(User.Builder().build(), converters.bytesToUser(emptyBytes)) + assertEquals(Position.Builder().build(), converters.bytesToPosition(emptyBytes)) + assertEquals(Telemetry.Builder().build(), converters.bytesToTelemetry(emptyBytes)) + assertEquals(Paxcount.Builder().build(), converters.bytesToPaxcounter(emptyBytes)) + assertEquals(DeviceMetadata.Builder().build(), converters.bytesToMetadata(emptyBytes)) } @Test diff --git a/core/database/src/commonTest/kotlin/org/meshtastic/core/database/DatabaseMergerTest.kt b/core/database/src/commonTest/kotlin/org/meshtastic/core/database/DatabaseMergerTest.kt index 0785e68993..a27b80dc16 100644 --- a/core/database/src/commonTest/kotlin/org/meshtastic/core/database/DatabaseMergerTest.kt +++ b/core/database/src/commonTest/kotlin/org/meshtastic/core/database/DatabaseMergerTest.kt @@ -96,7 +96,14 @@ class DatabaseMergerTest { private fun node(num: Int, notes: String = "") = NodeEntity( num = num, - user = User(id = "!$num", long_name = "Node $num", hw_model = HardwareModel.TBEAM), + user = + User.Builder() + .also { wb -> + wb.id = "!$num" + wb.long_name = "Node $num" + wb.hw_model = HardwareModel.TBEAM + } + .build(), notes = notes, ) @@ -123,7 +130,9 @@ class DatabaseMergerTest { dest.meshLogDao().insert(logEntry("destlog", PortNum.TELEMETRY_APP.value, time = 50)) dest .nodeInfoDao() - .upsert(MetadataEntity(num = 10, proto = DeviceMetadata(), timestamp = 1000)) // newer than src + .upsert( + MetadataEntity(num = 10, proto = DeviceMetadata.Builder().build(), timestamp = 1000), + ) // newer than src // Secondary (source): two messages, notes for #20 (dest blank → fill) and a source-only node (#30 → insert), // the same reaction (dedupe) plus a new one, and a conflicting contact setting (dest's must win). @@ -150,17 +159,21 @@ class DatabaseMergerTest { logUuid = "srcposition", requestId = 7, nodeNum = 30, - position = Position(), + position = Position.Builder().build(), ), ), ) source.nodeInfoDao().upsert(node(40)) // source-only node, no notes → still brought over source .nodeInfoDao() - .upsert(MetadataEntity(num = 10, proto = DeviceMetadata(), timestamp = 500)) // older → dest wins + .upsert( + MetadataEntity(num = 10, proto = DeviceMetadata.Builder().build(), timestamp = 500), + ) // older → dest wins source .nodeInfoDao() - .upsert(MetadataEntity(num = 30, proto = DeviceMetadata(), timestamp = 500)) // dest lacks → added + .upsert( + MetadataEntity(num = 30, proto = DeviceMetadata.Builder().build(), timestamp = 500), + ) // dest lacks → added val srcSession = source .discoveryDao() diff --git a/core/database/src/commonTest/kotlin/org/meshtastic/core/database/dao/CommonNodeIdentityMigrationDaoTest.kt b/core/database/src/commonTest/kotlin/org/meshtastic/core/database/dao/CommonNodeIdentityMigrationDaoTest.kt index 17758b41c3..77390f1883 100644 --- a/core/database/src/commonTest/kotlin/org/meshtastic/core/database/dao/CommonNodeIdentityMigrationDaoTest.kt +++ b/core/database/src/commonTest/kotlin/org/meshtastic/core/database/dao/CommonNodeIdentityMigrationDaoTest.kt @@ -86,13 +86,15 @@ abstract class CommonNodeIdentityMigrationDaoTest { ) = NodeEntity( num = num, user = - User( - id = NodeAddress.numToDefaultId(num), - long_name = longName, - short_name = longName.takeLast(4), - hw_model = HardwareModel.TBEAM, - public_key = key, - ), + User.Builder() + .also { wb -> + wb.id = NodeAddress.numToDefaultId(num) + wb.long_name = longName + wb.short_name = longName.takeLast(4) + wb.hw_model = HardwareModel.TBEAM + wb.public_key = key + } + .build(), notes = notes, isFavorite = isFavorite, lastHeard = 1000, diff --git a/core/database/src/commonTest/kotlin/org/meshtastic/core/database/dao/CommonNodeInfoDaoAtomicDeleteTest.kt b/core/database/src/commonTest/kotlin/org/meshtastic/core/database/dao/CommonNodeInfoDaoAtomicDeleteTest.kt index 1c1367ce13..d1e17c542d 100644 --- a/core/database/src/commonTest/kotlin/org/meshtastic/core/database/dao/CommonNodeInfoDaoAtomicDeleteTest.kt +++ b/core/database/src/commonTest/kotlin/org/meshtastic/core/database/dao/CommonNodeInfoDaoAtomicDeleteTest.kt @@ -102,8 +102,13 @@ abstract class CommonNodeInfoDaoAtomicDeleteTest { } private suspend fun seedNodeAndMetadata(nodeNum: Int, label: String) { - dao.upsert(NodeEntity(num = nodeNum, user = User(id = "!node-$label"))) - dao.upsert(MetadataEntity(num = nodeNum, proto = DeviceMetadata(firmware_version = label))) + dao.upsert(NodeEntity(num = nodeNum, user = User.Builder().also { wb -> wb.id = "!node-$label" }.build())) + dao.upsert( + MetadataEntity( + num = nodeNum, + proto = DeviceMetadata.Builder().also { wb -> wb.firmware_version = label }.build(), + ), + ) } private companion object { diff --git a/core/database/src/commonTest/kotlin/org/meshtastic/core/database/dao/CommonNodeInfoDaoDirectFilterTest.kt b/core/database/src/commonTest/kotlin/org/meshtastic/core/database/dao/CommonNodeInfoDaoDirectFilterTest.kt new file mode 100644 index 0000000000..d8470cf9f0 --- /dev/null +++ b/core/database/src/commonTest/kotlin/org/meshtastic/core/database/dao/CommonNodeInfoDaoDirectFilterTest.kt @@ -0,0 +1,122 @@ +/* + * 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 . + */ +package org.meshtastic.core.database.dao + +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.test.runTest +import org.meshtastic.core.database.MeshtasticDatabase +import org.meshtastic.core.database.entity.MyNodeEntity +import org.meshtastic.core.database.entity.NodeEntity +import org.meshtastic.core.database.getInMemoryDatabaseBuilder +import org.meshtastic.core.testing.setupTestContext +import org.meshtastic.proto.User +import kotlin.test.AfterTest +import kotlin.test.Test +import kotlin.test.assertEquals + +/** + * What the node list's "Only show direct nodes" filter resolves to. + * + * Direct is hops_away = 0 AND not via_mqtt. An MQTT-bridged node carries the hop count its uplink gateway heard, so + * zero hops there says nothing about our own radio, and the row renderers withhold the direct-node signal treatment + * from it. The query has to agree, or the filter hands back a node it presents as not direct. + */ +abstract class CommonNodeInfoDaoDirectFilterTest { + private lateinit var database: MeshtasticDatabase + private lateinit var dao: NodeInfoDao + + private val myNodeNum = 42424242 + + private suspend fun createDb() { + setupTestContext() + database = getInMemoryDatabaseBuilder().build() + dao = database.nodeInfoDao() + dao.setMyNodeInfo( + MyNodeEntity( + myNodeNum = myNodeNum, + model = "TBEAM", + firmwareVersion = "2.8.0", + couldUpdate = false, + shouldUpdate = false, + currentPacketId = 1L, + messageTimeoutMsec = 300000, + minAppVersion = 1, + maxChannels = 8, + hasWifi = false, + ), + ) + } + + @AfterTest + fun closeDb() { + database.close() + } + + private fun node(num: Int, hopsAway: Int, viaMqtt: Boolean = false) = NodeEntity( + num = num, + user = User.Builder().also { wb -> wb.id = "!$num" }.build(), + shortName = "N$num", + hopsAway = hopsAway, + viaMqtt = viaMqtt, + ) + + private suspend fun visible(onlyDirect: Boolean) = + dao.getNodes(sort = "last_heard", includeUnknown = true, onlyDirect = onlyDirect, lastHeardMin = -1) + .first() + .map { it.node.num } + .sorted() + + @Test + fun `the direct filter keeps a node our radio heard at zero hops`() = runTest { + createDb() + dao.putAll(listOf(node(1, hopsAway = 0))) + + assertEquals(listOf(1), visible(onlyDirect = true)) + } + + @Test + fun `the direct filter drops an mqtt node reporting zero hops`() = runTest { + createDb() + dao.putAll(listOf(node(1, hopsAway = 0), node(2, hopsAway = 0, viaMqtt = true))) + + assertEquals(listOf(1), visible(onlyDirect = true)) + } + + @Test + fun `the direct filter drops relayed nodes and nodes never measured`() = runTest { + createDb() + dao.putAll(listOf(node(1, hopsAway = 0), node(2, hopsAway = 1), node(3, hopsAway = -1))) + + assertEquals(listOf(1), visible(onlyDirect = true)) + } + + @Test + fun `the connected radio survives the direct filter`() = runTest { + createDb() + dao.putAll(listOf(node(myNodeNum, hopsAway = -1, viaMqtt = true))) + + assertEquals(listOf(myNodeNum), visible(onlyDirect = true)) + } + + @Test + fun `the filter off keeps every node`() = runTest { + createDb() + dao.putAll(listOf(node(1, hopsAway = 0), node(2, hopsAway = 0, viaMqtt = true), node(3, hopsAway = 2))) + + assertEquals(listOf(1, 2, 3), visible(onlyDirect = false)) + } +} diff --git a/core/database/src/commonTest/kotlin/org/meshtastic/core/database/dao/CommonNodeInfoDaoTest.kt b/core/database/src/commonTest/kotlin/org/meshtastic/core/database/dao/CommonNodeInfoDaoTest.kt index 7c73212adc..5f4204c681 100644 --- a/core/database/src/commonTest/kotlin/org/meshtastic/core/database/dao/CommonNodeInfoDaoTest.kt +++ b/core/database/src/commonTest/kotlin/org/meshtastic/core/database/dao/CommonNodeInfoDaoTest.kt @@ -18,6 +18,7 @@ package org.meshtastic.core.database.dao import kotlinx.coroutines.flow.first import kotlinx.coroutines.test.runTest +import okio.ByteString.Companion.toByteString import org.meshtastic.core.common.util.nowMillis import org.meshtastic.core.database.MeshtasticDatabase import org.meshtastic.core.database.entity.MyNodeEntity @@ -28,6 +29,7 @@ import org.meshtastic.proto.User import kotlin.test.AfterTest import kotlin.test.Test import kotlin.test.assertEquals +import kotlin.test.assertFalse import kotlin.test.assertNotNull import kotlin.test.assertTrue @@ -75,7 +77,14 @@ abstract class CommonNodeInfoDaoTest { val node = NodeEntity( num = 1234, - user = User(long_name = "Test Node", id = "!test", hw_model = org.meshtastic.proto.HardwareModel.TBEAM), + user = + User.Builder() + .also { wb -> + wb.long_name = "Test Node" + wb.id = "!test" + wb.hw_model = org.meshtastic.proto.HardwareModel.TBEAM + } + .build(), lastHeard = (nowMillis / 1000).toInt(), ) dao.upsert(node) @@ -87,8 +96,8 @@ abstract class CommonNodeInfoDaoTest { @Test fun testNodeDBbyNum() = runTest { createDb() - val node1 = NodeEntity(num = 1, user = User(id = "!1")) - val node2 = NodeEntity(num = 2, user = User(id = "!2")) + val node1 = NodeEntity(num = 1, user = User.Builder().also { wb -> wb.id = "!1" }.build()) + val node2 = NodeEntity(num = 2, user = User.Builder().also { wb -> wb.id = "!2" }.build()) dao.putAll(listOf(node1, node2)) val nodes = dao.nodeDBbyNum().first() @@ -100,18 +109,352 @@ abstract class CommonNodeInfoDaoTest { @Test fun testDeleteNode() = runTest { createDb() - val node = NodeEntity(num = 1, user = User(id = "!1")) + val node = NodeEntity(num = 1, user = User.Builder().also { wb -> wb.id = "!1" }.build()) dao.upsert(node) dao.deleteNode(1) val result = dao.getNodeByNum(1) assertEquals(null, result) } + @Test + fun `a remote node changing its key keeps the stored key and records the refusal`() = runTest { + createDb() + val trusted = ByteArray(32) { 1 }.toByteString() + val substitute = ByteArray(32) { 2 }.toByteString() + dao.upsert( + NodeEntity( + num = 1, + user = + User.Builder() + .also { wb -> + wb.id = "!1" + wb.public_key = trusted + } + .build(), + ), + ) + dao.upsert( + NodeEntity( + num = 1, + user = + User.Builder() + .also { wb -> + wb.id = "!1" + wb.public_key = substitute + } + .build(), + ), + ) + + // First-wins: anyone can broadcast a NodeInfo under another node's number, so the substitute is refused + // rather than applied. Overwriting would break PKC direct messages to that contact. + val stored = dao.getNodeByNum(1)?.node + assertEquals(trusted, stored?.publicKey) + assertEquals(trusted, stored?.user?.public_key) + assertFalse(stored?.keyMatch ?: true) + assertEquals(substitute, stored?.newPublicKey) + } + + @Test + fun `the refused key is kept so the mismatch can name it`() = runTest { + createDb() + val trusted = ByteArray(32) { 1 }.toByteString() + val substitute = ByteArray(32) { 2 }.toByteString() + dao.upsert( + NodeEntity( + num = 1, + user = + User.Builder() + .also { wb -> + wb.id = "!1" + wb.public_key = trusted + } + .build(), + ), + ) + + // Nothing is refused yet, so there is no key to report. + assertEquals(null, dao.getNodeByNum(1)?.node?.newPublicKey) + + dao.upsert( + NodeEntity( + num = 1, + user = + User.Builder() + .also { wb -> + wb.id = "!1" + wb.public_key = substitute + } + .build(), + ), + ) + assertEquals(substitute, dao.getNodeByNum(1)?.node?.newPublicKey) + + // The key already on file arriving again settles nothing: the refusal stands until the connected radio + // speaks for itself, or the next legitimate beacon would hide the substitute. + dao.upsert( + NodeEntity( + num = 1, + user = + User.Builder() + .also { wb -> + wb.id = "!1" + wb.public_key = trusted + } + .build(), + ), + ) + val stillFlagged = dao.getNodeByNum(1)?.node + assertEquals(trusted, stillFlagged?.publicKey) + assertFalse(stillFlagged?.keyMatch ?: true) + assertEquals(substitute, stillFlagged?.newPublicKey) + } + + @Test + fun `the connected radio re-keying clears the refused key along with the mismatch`() = runTest { + createDb() + val own = myNodeInfo.myNodeNum + val before = ByteArray(32) { 1 }.toByteString() + dao.upsert( + NodeEntity( + num = own, + user = + User.Builder() + .also { wb -> + wb.id = "!own" + wb.public_key = before + } + .build(), + ), + ) + dao.upsert( + NodeEntity( + num = own, + user = + User.Builder() + .also { wb -> + wb.id = "!own" + wb.public_key = ByteArray(32) { 9 }.toByteString() + } + .build(), + ), + ) + assertFalse(dao.getNodeByNum(own)?.node?.keyMatch ?: true) + + // The local link is authoritative, so accepting the radio's own key also drops what was refused. + val after = ByteArray(32) { 2 }.toByteString() + dao.installConfig( + myNodeInfo, + listOf( + NodeEntity( + num = own, + user = + User.Builder() + .also { wb -> + wb.id = "!own" + wb.public_key = after + } + .build(), + ), + ), + ) + + val stored = dao.getNodeByNum(own)?.node + assertEquals(after, stored?.publicKey) + assertTrue(stored?.keyMatch ?: false) + assertEquals(null, stored?.newPublicKey) + } + + @Test + fun `the legacy mismatch sentinel arriving as a key is neither refused nor stored`() = runTest { + createDb() + val trusted = ByteArray(32) { 1 }.toByteString() + dao.upsert( + NodeEntity( + num = 1, + user = + User.Builder() + .also { wb -> + wb.id = "!1" + wb.public_key = trusted + } + .build(), + ), + ) + + // A row that recorded a mismatch the old way carries the sentinel as its key. Re-upserting it through the + // repository must not read as a fresh substitution, and the sentinel is not a key anyone refused. + dao.upsert( + NodeEntity( + num = 1, + user = + User.Builder() + .also { wb -> + wb.id = "!1" + wb.public_key = NodeEntity.ERROR_BYTE_STRING + } + .build(), + ), + ) + val remote = dao.getNodeByNum(1)?.node + assertEquals(trusted, remote?.publicKey) + assertTrue(remote?.keyMatch ?: false) + assertEquals(null, remote?.newPublicKey) + + // Nor may the local link write it over the connected radio's real key. A key of its own, or the new-node + // guard would read this upsert as node 1 claiming a second number and never insert it. + val own = myNodeInfo.myNodeNum + val ownKey = ByteArray(32) { 3 }.toByteString() + dao.upsert( + NodeEntity( + num = own, + user = + User.Builder() + .also { wb -> + wb.id = "!own" + wb.public_key = ownKey + } + .build(), + ), + ) + dao.installConfig( + myNodeInfo, + listOf( + NodeEntity( + num = own, + user = + User.Builder() + .also { wb -> + wb.id = "!own" + wb.public_key = NodeEntity.ERROR_BYTE_STRING + } + .build(), + ), + ), + ) + assertEquals(ownKey, dao.getNodeByNum(own)?.node?.publicKey) + } + + @Test + fun `the stored key surviving a substitution still reads as a mismatch to the UI`() = runTest { + createDb() + val trusted = ByteArray(32) { 1 }.toByteString() + dao.upsert( + NodeEntity( + num = 1, + user = + User.Builder() + .also { wb -> + wb.id = "!1" + wb.public_key = trusted + } + .build(), + ), + ) + dao.upsert( + NodeEntity( + num = 1, + user = + User.Builder() + .also { wb -> + wb.id = "!1" + wb.public_key = ByteArray(32) { 2 }.toByteString() + } + .build(), + ), + ) + + assertTrue(dao.getNodeByNum(1)!!.toModel().mismatchKey) + } + + @Test + fun `the connected radio re-keying replaces its stored key rather than flagging a mismatch`() = runTest { + createDb() + val own = myNodeInfo.myNodeNum + val before = ByteArray(32) { 1 }.toByteString() + // What a 2.8 upgrade or a factory reset does: the same radio comes back under a new key. + val after = ByteArray(32) { 2 }.toByteString() + dao.upsert( + NodeEntity( + num = own, + user = + User.Builder() + .also { wb -> + wb.id = "!own" + wb.public_key = before + } + .build(), + ), + ) + + // Through installConfig, which is the local link. selfNum comes from the device's own MyNodeInfo there. + dao.installConfig( + myNodeInfo, + listOf( + NodeEntity( + num = own, + user = + User.Builder() + .also { wb -> + wb.id = "!own" + wb.public_key = after + } + .build(), + ), + ), + ) + + val stored = dao.getNodeByNum(own)?.node + assertEquals(after, stored?.publicKey) + assertEquals(after, stored?.user?.public_key) + assertTrue(stored?.keyMatch ?: false) + } + + @Test + fun `a mesh packet claiming the local node number cannot replace the stored key`() = runTest { + createDb() + val own = myNodeInfo.myNodeNum + val real = ByteArray(32) { 1 }.toByteString() + dao.upsert( + NodeEntity( + num = own, + user = + User.Builder() + .also { wb -> + wb.id = "!own" + wb.public_key = real + } + .build(), + ), + ) + + // The single-upsert path carries mesh-received NodeInfo, and `from` is attacker controlled, so matching the + // local node number proves only that the sender claimed it. + dao.upsert( + NodeEntity( + num = own, + user = + User.Builder() + .also { wb -> + wb.id = "!own" + wb.public_key = ByteArray(32) { 9 }.toByteString() + } + .build(), + ), + ) + + // First-wins keeps the stored key; the refusal is recorded and still reads as a mismatch to the UI. + val stored = dao.getNodeByNum(own) + assertEquals(real, stored?.node?.publicKey) + assertFalse(stored?.node?.keyMatch ?: true) + assertTrue(stored!!.toModel().mismatchKey) + } + @Test fun testClearNodeInfo() = runTest { createDb() - val node1 = NodeEntity(num = 1, user = User(id = "!1"), isFavorite = true) - val node2 = NodeEntity(num = 2, user = User(id = "!2"), isFavorite = false) + val node1 = NodeEntity(num = 1, user = User.Builder().also { wb -> wb.id = "!1" }.build(), isFavorite = true) + val node2 = NodeEntity(num = 2, user = User.Builder().also { wb -> wb.id = "!2" }.build(), isFavorite = false) dao.putAll(listOf(node1, node2)) dao.clearNodeInfo(preserveFavorites = true) diff --git a/core/database/src/commonTest/kotlin/org/meshtastic/core/database/dao/CommonPacketDaoTest.kt b/core/database/src/commonTest/kotlin/org/meshtastic/core/database/dao/CommonPacketDaoTest.kt index 870ceedcaa..6b4daf23e4 100644 --- a/core/database/src/commonTest/kotlin/org/meshtastic/core/database/dao/CommonPacketDaoTest.kt +++ b/core/database/src/commonTest/kotlin/org/meshtastic/core/database/dao/CommonPacketDaoTest.kt @@ -287,12 +287,14 @@ abstract class CommonPacketDaoTest { createDb() insertSentPacket(packetId = 8009, status = MessageStatus.DELIVERED) val outgoing = - MeshPacket( - from = myNodeNum, - to = NodeAddress.NODENUM_BROADCAST, - id = 8009, - decoded = Data(portnum = PortNum.TEXT_MESSAGE_APP), - ) + MeshPacket.Builder() + .also { wb -> + wb.from = myNodeNum + wb.to = NodeAddress.NODENUM_BROADCAST + wb.id = 8009 + wb.decoded = Data.Builder().also { wb -> wb.portnum = PortNum.TEXT_MESSAGE_APP }.build() + } + .build() val prior = assertNotNull(packetDao.applyOutgoingQueueStatus(outgoing, MessageStatus.ENROUTE)) diff --git a/core/database/src/commonTest/kotlin/org/meshtastic/core/database/dao/SwitchingDiscoveryDaoTest.kt b/core/database/src/commonTest/kotlin/org/meshtastic/core/database/dao/SwitchingDiscoveryDaoTest.kt index adf49506b8..b9795b4eb9 100644 --- a/core/database/src/commonTest/kotlin/org/meshtastic/core/database/dao/SwitchingDiscoveryDaoTest.kt +++ b/core/database/src/commonTest/kotlin/org/meshtastic/core/database/dao/SwitchingDiscoveryDaoTest.kt @@ -26,12 +26,16 @@ import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.test.runTest import org.meshtastic.core.database.DatabaseProvider import org.meshtastic.core.database.MeshtasticDatabase +import org.meshtastic.core.database.entity.DiscoveredNodeEntity +import org.meshtastic.core.database.entity.DiscoveryPresetResultEntity import org.meshtastic.core.database.entity.DiscoverySessionEntity import org.meshtastic.core.database.entity.DiscoverySessionStatus import org.meshtastic.core.database.getInMemoryDatabaseBuilder import kotlin.test.AfterTest import kotlin.test.Test import kotlin.test.assertEquals +import kotlin.test.assertNotNull +import kotlin.test.assertNull import kotlin.test.assertTrue /** @@ -52,8 +56,12 @@ class SwitchingDiscoveryDaoTest { dbB.close() } - private fun session(timestamp: Long) = - DiscoverySessionEntity(timestamp = timestamp, presetsScanned = "LongFast", homePreset = "LongFast") + private fun session(timestamp: Long, deviceAddress: String = DEVICE_A) = DiscoverySessionEntity( + timestamp = timestamp, + presetsScanned = "LongFast", + homePreset = "LongFast", + deviceAddress = deviceAddress, + ) @Test fun suspendCallsResolveTheCurrentDbPerCall() = runTest { @@ -96,6 +104,75 @@ class SwitchingDiscoveryDaoTest { ) } + /** + * Regression for the FK-787 crash (Crashlytics `d79ee407`). A caller cannot check the parent session and then + * insert its child: the two calls resolve the active database independently, so a switch landing between them + * writes a `discovery_preset_result` whose `session_id` has no row. `insertDwellIfSessionExists` must do both in + * one resolution and report the missing parent instead of raising a constraint failure. + */ + @Test + fun dwellWriteAfterASwitchReportsTheMissingParentInsteadOfViolatingTheForeignKey() = runTest { + val sessionId = dao.insertSession(session(timestamp = 1)) + assertEquals(1, dbA.discoveryDao().getAllSessionsSnapshot().size, "parent session lives in DB A") + + provider.switchTo(dbB) + + val presetResultId = + dao.insertDwellIfSessionExists( + result = DiscoveryPresetResultEntity(sessionId = sessionId, presetName = "LongFast"), + nodes = emptyList(), + deviceAddress = DEVICE_A, + ) + + assertNull(presetResultId, "a dwell whose session is not in the active DB must report, not insert") + assertEquals(0, dbB.discoveryDao().getPresetResults(sessionId).size, "no orphan row in the new DB") + assertEquals(0, dbA.discoveryDao().getPresetResults(sessionId).size, "and none written back to the old DB") + } + + /** The same call must still write normally when the parent session is present. */ + @Test + fun dwellWriteSucceedsWhenTheSessionIsInTheActiveDb() = runTest { + val sessionId = dao.insertSession(session(timestamp = 1)) + + val presetResultId = + dao.insertDwellIfSessionExists( + result = DiscoveryPresetResultEntity(sessionId = sessionId, presetName = "LongFast"), + nodes = listOf(DiscoveredNodeEntity(presetResultId = 0L, nodeNum = 42L)), + deviceAddress = DEVICE_A, + ) + + assertNotNull(presetResultId, "a dwell with a live parent session must be written") + assertEquals(1, dbA.discoveryDao().getPresetResults(sessionId).size) + val nodes = dbA.discoveryDao().getDiscoveredNodes(presetResultId) + assertEquals(1, nodes.size, "child nodes are stamped with the new preset-result id inside the transaction") + assertEquals(42L, nodes.first().nodeNum) + } + + /** + * Every per-device database autogenerates session ids from 1, so an id alone is not an identity: DB A's session 1 + * and DB B's session 1 are unrelated scans. A parent check on the id alone passes after a switch and silently + * attaches the dwell to the other radio's session, so the check matches the device address too. + */ + @Test + fun dwellWriteAfterASwitchRejectsAnUnrelatedSessionSharingTheId() = runTest { + val sessionId = dao.insertSession(session(timestamp = 1)) + val otherId = dbB.discoveryDao().insertSession(session(timestamp = 2, deviceAddress = DEVICE_B)) + assertEquals(sessionId, otherId, "both databases autogenerate the same first id - that is the trap") + + provider.switchTo(dbB) + + val presetResultId = + dao.insertDwellIfSessionExists( + result = DiscoveryPresetResultEntity(sessionId = sessionId, presetName = "LongFast"), + nodes = listOf(DiscoveredNodeEntity(presetResultId = 0L, nodeNum = 42L)), + deviceAddress = DEVICE_A, + ) + + assertNull(presetResultId, "a session id matching another radio's session must not be treated as the parent") + assertEquals(0, dbB.discoveryDao().getPresetResults(otherId).size, "the other radio's session gains no dwell") + assertEquals(0, dbA.discoveryDao().getPresetResults(sessionId).size, "and none is written back to the old DB") + } + @Test fun flowsRelatchOntoTheCurrentDb() = runTest { dao.insertSession(session(timestamp = 1)) @@ -118,6 +195,11 @@ class SwitchingDiscoveryDaoTest { assertEquals(0, relatched.size, "the same collector re-latched onto the new (empty) DB") } + private companion object { + const val DEVICE_A = "x:AA:BB:CC:DD:EE:01" + const val DEVICE_B = "x:AA:BB:CC:DD:EE:02" + } + /** Minimal [DatabaseProvider] whose active DB the test can swap, mirroring a device/DB switch. */ @OptIn(ExperimentalCoroutinesApi::class) private class TestProvider(db: MeshtasticDatabase) : DatabaseProvider { diff --git a/core/database/src/commonTest/kotlin/org/meshtastic/core/database/dao/TracerouteNodePositionDaoTest.kt b/core/database/src/commonTest/kotlin/org/meshtastic/core/database/dao/TracerouteNodePositionDaoTest.kt index 199f56cf23..cdb6cdc9ad 100644 --- a/core/database/src/commonTest/kotlin/org/meshtastic/core/database/dao/TracerouteNodePositionDaoTest.kt +++ b/core/database/src/commonTest/kotlin/org/meshtastic/core/database/dao/TracerouteNodePositionDaoTest.kt @@ -65,8 +65,12 @@ class TracerouteNodePositionDaoTest { ) } - private fun position(nodeNum: Int) = - TracerouteNodePositionEntity(logUuid = logUuid, requestId = 1, nodeNum = nodeNum, position = Position()) + private fun position(nodeNum: Int) = TracerouteNodePositionEntity( + logUuid = logUuid, + requestId = 1, + nodeNum = nodeNum, + position = Position.Builder().build(), + ) @Test fun testReplaceByLogUuidIsAtomic() = runTest { @@ -134,7 +138,7 @@ class TracerouteNodePositionDaoTest { logUuid = "nonexistent-log", requestId = 1, nodeNum = 99, - position = Position(), + position = Position.Builder().build(), ) assertFailsWith { tracerouteDao.replaceByLogUuid(logUuid, listOf(position(30), badEntity)) diff --git a/core/database/src/commonTest/kotlin/org/meshtastic/core/database/entity/ReactionKeyTest.kt b/core/database/src/commonTest/kotlin/org/meshtastic/core/database/entity/ReactionKeyTest.kt index 155a45c365..57571483f9 100644 --- a/core/database/src/commonTest/kotlin/org/meshtastic/core/database/entity/ReactionKeyTest.kt +++ b/core/database/src/commonTest/kotlin/org/meshtastic/core/database/entity/ReactionKeyTest.kt @@ -207,7 +207,9 @@ class ReactionKeyTest { channel = channel, ) - private val getNode: suspend (String?) -> Node = { userId -> Node(num = 1, user = User(id = userId.orEmpty())) } + private val getNode: suspend (String?) -> Node = { userId -> + Node(num = 1, user = User.Builder().also { wb -> wb.id = userId.orEmpty() }.build()) + } private companion object { const val MY_NODE_NUM = 42 diff --git a/core/database/src/jvmTest/kotlin/org/meshtastic/core/database/MeshtasticDatabaseMigrationTest.kt b/core/database/src/jvmTest/kotlin/org/meshtastic/core/database/MeshtasticDatabaseMigrationTest.kt index 269f3ba440..6096fcfd66 100644 --- a/core/database/src/jvmTest/kotlin/org/meshtastic/core/database/MeshtasticDatabaseMigrationTest.kt +++ b/core/database/src/jvmTest/kotlin/org/meshtastic/core/database/MeshtasticDatabaseMigrationTest.kt @@ -380,6 +380,53 @@ class MeshtasticDatabaseMigrationTest { } } + /** + * 58→59 adds `nodes.key_match` and `nodes.new_public_key`, the record of a refused key substitution. `key_match` + * defaults to 1 so rows written before the column existed are not read as mismatched on first launch; those rows + * recorded a mismatch the old way, as the zero sentinel in `public_key`, and have no refused key to report, so + * `new_public_key` stays null. This proves both defaults and that the stored key survives the addition byte for + * byte, which is the whole point of first-wins. + */ + @Test + fun keyMatchColumnsDefaultToMatchedAndPreserveNodes() = runTest { + val storedKeyHex = "01".repeat(PUBLIC_KEY_BYTES) + helper.createDatabase(KEY_MATCH_FROM_VERSION).use { connection -> + // Every NOT NULL column without a default in schema 58; the BLOBs are empty protos. + val columns = + "num, user, position, latitude, longitude, snr, rssi, last_heard, device_metrics, channel, " + + "via_mqtt, hops_away, is_favorite, environment_metrics, power_metrics, paxcounter" + connection.execSQL( + "INSERT INTO nodes ($columns, long_name, public_key) VALUES " + + "(42, x'', x'', 0.0, 0.0, 0.0, 0, 1000, x'', 0, 0, 1, 1, x'', x'', x'', " + + "'Minnie Mouse', x'$storedKeyHex')", + ) + connection.execSQL( + "INSERT INTO nodes ($columns, long_name) VALUES " + + "(43, x'', x'', 0.0, 0.0, 0.0, 0, 2000, x'', 0, 0, 2, 0, x'', x'', x'', 'Mickey')", + ) + } + + helper.runMigrationsAndValidate( + KEY_MATCH_TO_VERSION, + listOf(MeshtasticDatabase.MIGRATION_52_53), + ).use { connection -> + // Both rows survive; neither reads as a mismatch, and neither has a refused key to report. + assertEquals(listOf("42", "43"), queryColumn(connection, "SELECT num FROM nodes ORDER BY num")) + assertEquals(listOf("1", "1"), queryColumn(connection, "SELECT key_match FROM nodes ORDER BY num")) + assertEquals( + listOf(null, null), + queryColumn(connection, "SELECT new_public_key FROM nodes ORDER BY num"), + ) + // The stored key is exactly what was written; the column addition touched nothing. + assertEquals( + listOf(storedKeyHex.uppercase()), + queryColumn(connection, "SELECT hex(public_key) FROM nodes WHERE num = 42"), + ) + assertEquals(listOf("Minnie Mouse"), queryColumn(connection, "SELECT long_name FROM nodes WHERE num = 42")) + assertEquals(listOf("1000"), queryColumn(connection, "SELECT last_heard FROM nodes WHERE num = 42")) + } + } + private fun queryColumn(connection: SQLiteConnection, sql: String): List = connection.prepare(sql).use { statement -> buildList { @@ -408,6 +455,9 @@ class MeshtasticDatabaseMigrationTest { const val PINNED_COLUMN_TO_VERSION = 57 const val HEARD_ON_LORA_FROM_VERSION = 57 const val HEARD_ON_LORA_TO_VERSION = 58 + const val KEY_MATCH_FROM_VERSION = 58 + const val KEY_MATCH_TO_VERSION = 59 + const val PUBLIC_KEY_BYTES = 32 /** Room's runtime FTS content-sync triggers, verbatim from the generated MeshtasticDatabase_Impl. */ val FTS_SYNC_TRIGGERS = diff --git a/feature/map-maplibre/src/jvmMain/kotlin/org/meshtastic/feature/map/maplibre/layers/SoftwareCanvasDecoding.jvm.kt b/core/database/src/jvmTest/kotlin/org/meshtastic/core/database/dao/NodeInfoDaoDirectFilterTest.kt similarity index 73% rename from feature/map-maplibre/src/jvmMain/kotlin/org/meshtastic/feature/map/maplibre/layers/SoftwareCanvasDecoding.jvm.kt rename to core/database/src/jvmTest/kotlin/org/meshtastic/core/database/dao/NodeInfoDaoDirectFilterTest.kt index 11b80ca291..70cf539aa4 100644 --- a/feature/map-maplibre/src/jvmMain/kotlin/org/meshtastic/feature/map/maplibre/layers/SoftwareCanvasDecoding.jvm.kt +++ b/core/database/src/jvmTest/kotlin/org/meshtastic/core/database/dao/NodeInfoDaoDirectFilterTest.kt @@ -14,9 +14,6 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -package org.meshtastic.feature.map.maplibre.layers +package org.meshtastic.core.database.dao -import coil3.request.ImageRequest - -/** Nothing to do: the desktop decoder has no hardware-bitmap path. */ -internal actual fun ImageRequest.Builder.decodeForSoftwareCanvas(): ImageRequest.Builder = this +class NodeInfoDaoDirectFilterTest : CommonNodeInfoDaoDirectFilterTest() diff --git a/core/database/src/jvmTest/kotlin/org/meshtastic/core/database/dao/PowerChannelLabelsDaoTest.kt b/core/database/src/jvmTest/kotlin/org/meshtastic/core/database/dao/PowerChannelLabelsDaoTest.kt index 92d69b643c..9d0b72c686 100644 --- a/core/database/src/jvmTest/kotlin/org/meshtastic/core/database/dao/PowerChannelLabelsDaoTest.kt +++ b/core/database/src/jvmTest/kotlin/org/meshtastic/core/database/dao/PowerChannelLabelsDaoTest.kt @@ -43,7 +43,14 @@ class PowerChannelLabelsDaoTest { private fun node(num: Int, labels: List = emptyList()) = NodeEntity( num = num, - user = User(id = "!$num", long_name = "Node $num", hw_model = HardwareModel.TBEAM), + user = + User.Builder() + .also { wb -> + wb.id = "!$num" + wb.long_name = "Node $num" + wb.hw_model = HardwareModel.TBEAM + } + .build(), powerChannelLabels = labels, ) diff --git a/core/datastore/src/androidMain/kotlin/org/meshtastic/core/datastore/di/CoreDatastoreAndroidModule.kt b/core/datastore/src/androidMain/kotlin/org/meshtastic/core/datastore/di/CoreDatastoreAndroidModule.kt index d915c5e206..55bba0abaf 100644 --- a/core/datastore/src/androidMain/kotlin/org/meshtastic/core/datastore/di/CoreDatastoreAndroidModule.kt +++ b/core/datastore/src/androidMain/kotlin/org/meshtastic/core/datastore/di/CoreDatastoreAndroidModule.kt @@ -65,7 +65,7 @@ class LocalConfigDataStoreModule { fun provideLocalConfigDataStore(context: Context, scope: DataStoreScope): CoreLocalConfigDataStore = protoStore( serializer = LocalConfigSerializer, producePath = { context.dataStoreFile("local_config.pb").toOkioPath() }, - produceNewData = { LocalConfig() }, + produceNewData = { LocalConfig.Builder().build() }, scope = scope, ) .asCoreLocalConfigDataStore() @@ -77,7 +77,7 @@ class ModuleConfigDataStoreModule { fun provideModuleConfigDataStore(context: Context, scope: DataStoreScope): CoreModuleConfigDataStore = protoStore( serializer = ModuleConfigSerializer, producePath = { context.dataStoreFile("module_config.pb").toOkioPath() }, - produceNewData = { LocalModuleConfig() }, + produceNewData = { LocalModuleConfig.Builder().build() }, scope = scope, ) .asCoreModuleConfigDataStore() @@ -89,7 +89,7 @@ class ChannelSetDataStoreModule { fun provideChannelSetDataStore(context: Context, scope: DataStoreScope): CoreChannelSetDataStore = protoStore( serializer = ChannelSetSerializer, producePath = { context.dataStoreFile("channel_set.pb").toOkioPath() }, - produceNewData = { ChannelSet() }, + produceNewData = { ChannelSet.Builder().build() }, scope = scope, ) .asCoreChannelSetDataStore() @@ -101,7 +101,7 @@ class LocalStatsDataStoreModule { fun provideLocalStatsDataStore(context: Context, scope: DataStoreScope): CoreLocalStatsDataStore = protoStore( serializer = LocalStatsSerializer, producePath = { context.dataStoreFile("local_stats.pb").toOkioPath() }, - produceNewData = { LocalStats() }, + produceNewData = { LocalStats.Builder().build() }, scope = scope, ) .asCoreLocalStatsDataStore() diff --git a/core/datastore/src/commonMain/kotlin/org/meshtastic/core/datastore/ChannelSetDataSource.kt b/core/datastore/src/commonMain/kotlin/org/meshtastic/core/datastore/ChannelSetDataSource.kt index 12d15f1a34..b911670c1e 100644 --- a/core/datastore/src/commonMain/kotlin/org/meshtastic/core/datastore/ChannelSetDataSource.kt +++ b/core/datastore/src/commonMain/kotlin/org/meshtastic/core/datastore/ChannelSetDataSource.kt @@ -35,19 +35,19 @@ class ChannelSetDataSource(private val channelSetStore: CoreChannelSetDataStore) // dataStore.data throws an IOException when an error is encountered when reading data if (exception is IOException) { Logger.e { "Error reading DeviceConfig settings: ${exception.message}" } - emit(ChannelSet()) + emit(ChannelSet.Builder().build()) } else { throw exception } } suspend fun clearChannelSet() { - channelSetStore.updateData { ChannelSet() } + channelSetStore.updateData { ChannelSet.Builder().build() } } /** Replaces all [ChannelSettings] in a single atomic operation. */ suspend fun replaceAllSettings(settingsList: List) { - channelSetStore.updateData { it.copy(settings = settingsList) } + channelSetStore.updateData { it.newBuilder().also { wb -> wb.settings = settingsList }.build() } } /** Updates the [ChannelSettings] list with the provided channel. */ @@ -57,15 +57,15 @@ class ChannelSetDataSource(private val channelSetStore: CoreChannelSetDataStore) val settings = preference.settings.toMutableList() // Resize to fit channel while (settings.size <= channel.index) { - settings.add(ChannelSettings()) + settings.add(ChannelSettings.Builder().build()) } // use setSettings() to ensure settingsList and channel indexes match - settings[channel.index] = channel.settings ?: ChannelSettings() - preference.copy(settings = settings) + settings[channel.index] = channel.settings ?: ChannelSettings.Builder().build() + preference.newBuilder().also { wb -> wb.settings = settings }.build() } } suspend fun setLoraConfig(config: Config.LoRaConfig) { - channelSetStore.updateData { it.copy(lora_config = config) } + channelSetStore.updateData { it.newBuilder().also { wb -> wb.lora_config = config }.build() } } } diff --git a/core/datastore/src/commonMain/kotlin/org/meshtastic/core/datastore/LocalConfigDataSource.kt b/core/datastore/src/commonMain/kotlin/org/meshtastic/core/datastore/LocalConfigDataSource.kt index d40bc290ff..b58f24f44f 100644 --- a/core/datastore/src/commonMain/kotlin/org/meshtastic/core/datastore/LocalConfigDataSource.kt +++ b/core/datastore/src/commonMain/kotlin/org/meshtastic/core/datastore/LocalConfigDataSource.kt @@ -33,27 +33,27 @@ class LocalConfigDataSource(private val localConfigStore: CoreLocalConfigDataSto // dataStore.data throws an IOException when an error is encountered when reading data if (exception is IOException) { Logger.e { "Error reading LocalConfig settings: ${exception.message}" } - emit(LocalConfig()) + emit(LocalConfig.Builder().build()) } else { throw exception } } suspend fun clearLocalConfig() { - localConfigStore.updateData { LocalConfig() } + localConfigStore.updateData { LocalConfig.Builder().build() } } /** Updates [LocalConfig] from each [Config] oneOf. */ suspend fun setLocalConfig(config: Config) = localConfigStore.updateData { current -> when { - config.device != null -> current.copy(device = config.device) - config.position != null -> current.copy(position = config.position) - config.power != null -> current.copy(power = config.power) - config.network != null -> current.copy(network = config.network) - config.display != null -> current.copy(display = config.display) - config.lora != null -> current.copy(lora = config.lora) - config.bluetooth != null -> current.copy(bluetooth = config.bluetooth) - config.security != null -> current.copy(security = config.security) + config.device != null -> current.newBuilder().also { wb -> wb.device = config.device }.build() + config.position != null -> current.newBuilder().also { wb -> wb.position = config.position }.build() + config.power != null -> current.newBuilder().also { wb -> wb.power = config.power }.build() + config.network != null -> current.newBuilder().also { wb -> wb.network = config.network }.build() + config.display != null -> current.newBuilder().also { wb -> wb.display = config.display }.build() + config.lora != null -> current.newBuilder().also { wb -> wb.lora = config.lora }.build() + config.bluetooth != null -> current.newBuilder().also { wb -> wb.bluetooth = config.bluetooth }.build() + config.security != null -> current.newBuilder().also { wb -> wb.security = config.security }.build() else -> current } } diff --git a/core/datastore/src/commonMain/kotlin/org/meshtastic/core/datastore/LocalStatsDataSource.kt b/core/datastore/src/commonMain/kotlin/org/meshtastic/core/datastore/LocalStatsDataSource.kt index ad893efef9..4807c5f689 100644 --- a/core/datastore/src/commonMain/kotlin/org/meshtastic/core/datastore/LocalStatsDataSource.kt +++ b/core/datastore/src/commonMain/kotlin/org/meshtastic/core/datastore/LocalStatsDataSource.kt @@ -40,7 +40,7 @@ open class LocalStatsDataSourceImpl(private val localStatsStore: CoreLocalStatsD localStatsStore.data.catch { exception -> if (exception is IOException) { Logger.e { "Error reading LocalStats: ${exception.message}" } - emit(LocalStats()) + emit(LocalStats.Builder().build()) } else { throw exception } @@ -51,6 +51,6 @@ open class LocalStatsDataSourceImpl(private val localStatsStore: CoreLocalStatsD } override suspend fun clearLocalStats() { - localStatsStore.updateData { LocalStats() } + localStatsStore.updateData { LocalStats.Builder().build() } } } diff --git a/core/datastore/src/commonMain/kotlin/org/meshtastic/core/datastore/ModuleConfigDataSource.kt b/core/datastore/src/commonMain/kotlin/org/meshtastic/core/datastore/ModuleConfigDataSource.kt index 16843e664a..8352730667 100644 --- a/core/datastore/src/commonMain/kotlin/org/meshtastic/core/datastore/ModuleConfigDataSource.kt +++ b/core/datastore/src/commonMain/kotlin/org/meshtastic/core/datastore/ModuleConfigDataSource.kt @@ -33,52 +33,62 @@ class ModuleConfigDataSource(private val moduleConfigStore: CoreModuleConfigData // dataStore.data throws an IOException when an error is encountered when reading data if (exception is IOException) { Logger.e { "Error reading LocalModuleConfig settings: ${exception.message}" } - emit(LocalModuleConfig()) + emit(LocalModuleConfig.Builder().build()) } else { throw exception } } suspend fun clearLocalModuleConfig() { - moduleConfigStore.updateData { LocalModuleConfig() } + moduleConfigStore.updateData { LocalModuleConfig.Builder().build() } } /** Updates [LocalModuleConfig] from each [ModuleConfig] oneOf. */ @Suppress("CyclomaticComplexMethod") suspend fun setLocalModuleConfig(config: ModuleConfig) = moduleConfigStore.updateData { current -> when { - config.mqtt != null -> current.copy(mqtt = config.mqtt) + config.mqtt != null -> current.newBuilder().also { wb -> wb.mqtt = config.mqtt }.build() - config.serial != null -> current.copy(serial = config.serial) + config.serial != null -> current.newBuilder().also { wb -> wb.serial = config.serial }.build() config.external_notification != null -> - current.copy(external_notification = config.external_notification) + current.newBuilder().also { wb -> wb.external_notification = config.external_notification }.build() - config.store_forward != null -> current.copy(store_forward = config.store_forward) + config.store_forward != null -> + current.newBuilder().also { wb -> wb.store_forward = config.store_forward }.build() - config.range_test != null -> current.copy(range_test = config.range_test) + config.range_test != null -> + current.newBuilder().also { wb -> wb.range_test = config.range_test }.build() - config.telemetry != null -> current.copy(telemetry = config.telemetry) + config.telemetry != null -> current.newBuilder().also { wb -> wb.telemetry = config.telemetry }.build() - config.canned_message != null -> current.copy(canned_message = config.canned_message) + config.canned_message != null -> + current.newBuilder().also { wb -> wb.canned_message = config.canned_message }.build() - config.audio != null -> current.copy(audio = config.audio) + config.audio != null -> current.newBuilder().also { wb -> wb.audio = config.audio }.build() - config.remote_hardware != null -> current.copy(remote_hardware = config.remote_hardware) + config.remote_hardware != null -> + current.newBuilder().also { wb -> wb.remote_hardware = config.remote_hardware }.build() - config.neighbor_info != null -> current.copy(neighbor_info = config.neighbor_info) + config.neighbor_info != null -> + current.newBuilder().also { wb -> wb.neighbor_info = config.neighbor_info }.build() - config.ambient_lighting != null -> current.copy(ambient_lighting = config.ambient_lighting) + config.ambient_lighting != null -> + current.newBuilder().also { wb -> wb.ambient_lighting = config.ambient_lighting }.build() - config.detection_sensor != null -> current.copy(detection_sensor = config.detection_sensor) + config.detection_sensor != null -> + current.newBuilder().also { wb -> wb.detection_sensor = config.detection_sensor }.build() - config.paxcounter != null -> current.copy(paxcounter = config.paxcounter) + config.paxcounter != null -> + current.newBuilder().also { wb -> wb.paxcounter = config.paxcounter }.build() - config.statusmessage != null -> current.copy(statusmessage = config.statusmessage) + config.statusmessage != null -> + current.newBuilder().also { wb -> wb.statusmessage = config.statusmessage }.build() - config.tak != null -> current.copy(tak = config.tak) + config.tak != null -> current.newBuilder().also { wb -> wb.tak = config.tak }.build() - config.mesh_beacon != null -> current.copy(mesh_beacon = config.mesh_beacon) + config.mesh_beacon != null -> + current.newBuilder().also { wb -> wb.mesh_beacon = config.mesh_beacon }.build() else -> current } diff --git a/core/datastore/src/commonMain/kotlin/org/meshtastic/core/datastore/serializer/ChannelSetSerializer.kt b/core/datastore/src/commonMain/kotlin/org/meshtastic/core/datastore/serializer/ChannelSetSerializer.kt index 7e6eed7094..5493c75651 100644 --- a/core/datastore/src/commonMain/kotlin/org/meshtastic/core/datastore/serializer/ChannelSetSerializer.kt +++ b/core/datastore/src/commonMain/kotlin/org/meshtastic/core/datastore/serializer/ChannelSetSerializer.kt @@ -25,7 +25,7 @@ import org.meshtastic.proto.ChannelSet /** Serializer for the [ChannelSet] object defined in apponly.proto. */ object ChannelSetSerializer : OkioSerializer { - override val defaultValue: ChannelSet = ChannelSet() + override val defaultValue: ChannelSet = ChannelSet.Builder().build() override suspend fun readFrom(source: BufferedSource): ChannelSet { try { diff --git a/core/datastore/src/commonMain/kotlin/org/meshtastic/core/datastore/serializer/LocalConfigSerializer.kt b/core/datastore/src/commonMain/kotlin/org/meshtastic/core/datastore/serializer/LocalConfigSerializer.kt index 3eeffc95e7..70c44deb22 100644 --- a/core/datastore/src/commonMain/kotlin/org/meshtastic/core/datastore/serializer/LocalConfigSerializer.kt +++ b/core/datastore/src/commonMain/kotlin/org/meshtastic/core/datastore/serializer/LocalConfigSerializer.kt @@ -25,7 +25,7 @@ import org.meshtastic.proto.LocalConfig /** Serializer for the [LocalConfig] object defined in localonly.proto. */ object LocalConfigSerializer : OkioSerializer { - override val defaultValue: LocalConfig = LocalConfig() + override val defaultValue: LocalConfig = LocalConfig.Builder().build() override suspend fun readFrom(source: BufferedSource): LocalConfig { try { diff --git a/core/datastore/src/commonMain/kotlin/org/meshtastic/core/datastore/serializer/LocalStatsSerializer.kt b/core/datastore/src/commonMain/kotlin/org/meshtastic/core/datastore/serializer/LocalStatsSerializer.kt index 1f7ad6b7e1..74a952cbef 100644 --- a/core/datastore/src/commonMain/kotlin/org/meshtastic/core/datastore/serializer/LocalStatsSerializer.kt +++ b/core/datastore/src/commonMain/kotlin/org/meshtastic/core/datastore/serializer/LocalStatsSerializer.kt @@ -25,7 +25,7 @@ import org.meshtastic.proto.LocalStats /** Serializer for the [LocalStats] object defined in telemetry.proto. */ object LocalStatsSerializer : OkioSerializer { - override val defaultValue: LocalStats = LocalStats() + override val defaultValue: LocalStats = LocalStats.Builder().build() override suspend fun readFrom(source: BufferedSource): LocalStats { try { diff --git a/core/datastore/src/commonMain/kotlin/org/meshtastic/core/datastore/serializer/ModuleConfigSerializer.kt b/core/datastore/src/commonMain/kotlin/org/meshtastic/core/datastore/serializer/ModuleConfigSerializer.kt index e1d98edcc8..cc1c1ec7d4 100644 --- a/core/datastore/src/commonMain/kotlin/org/meshtastic/core/datastore/serializer/ModuleConfigSerializer.kt +++ b/core/datastore/src/commonMain/kotlin/org/meshtastic/core/datastore/serializer/ModuleConfigSerializer.kt @@ -25,7 +25,7 @@ import org.meshtastic.proto.LocalModuleConfig /** Serializer for the [LocalModuleConfig] object defined in localonly.proto. */ object ModuleConfigSerializer : OkioSerializer { - override val defaultValue: LocalModuleConfig = LocalModuleConfig() + override val defaultValue: LocalModuleConfig = LocalModuleConfig.Builder().build() override suspend fun readFrom(source: BufferedSource): LocalModuleConfig { try { diff --git a/core/domain/src/commonMain/kotlin/org/meshtastic/core/domain/usecase/settings/ExportNodeDatabaseUseCase.kt b/core/domain/src/commonMain/kotlin/org/meshtastic/core/domain/usecase/settings/ExportNodeDatabaseUseCase.kt index 8a4a8a9e45..17865a5764 100644 --- a/core/domain/src/commonMain/kotlin/org/meshtastic/core/domain/usecase/settings/ExportNodeDatabaseUseCase.kt +++ b/core/domain/src/commonMain/kotlin/org/meshtastic/core/domain/usecase/settings/ExportNodeDatabaseUseCase.kt @@ -33,6 +33,7 @@ import org.meshtastic.proto.HardwareModel import org.meshtastic.proto.MeshPacket import org.meshtastic.proto.Paxcount import org.meshtastic.proto.PowerMetrics +import org.meshtastic.proto.SoilWaterMetrics import kotlin.time.Instant import org.meshtastic.proto.Position as WirePosition @@ -104,6 +105,7 @@ private fun Node.toExport(): NodeExport = NodeExport( powerMetrics = powerMetrics.toExport(), powerChannelLabels = powerChannelLabels.takeIf { it.isNotEmpty() }, airQualityMetrics = airQualityMetrics.toExport(), + soilWaterMetrics = soilWaterMetrics.toExport(), paxcounter = paxcounter.toExport(), metadata = metadata?.toExport(), ) @@ -172,6 +174,8 @@ private fun EnvironmentMetrics.toExport(): EnvironmentMetricsExport? = Environme adcVoltageCh5 = adc_voltage_ch5, adcVoltageCh6 = adc_voltage_ch6, adcVoltageCh7 = adc_voltage_ch7, + lightningStrikeCount1h = lightning_strike_count_1h, + lightningDistanceKm = lightning_distance_km, ) .takeUnless { it == EnvironmentMetricsExport() } @@ -225,9 +229,29 @@ private fun AirQualityMetrics.toExport(): AirQualityMetricsExport? = AirQualityM pmVocIdx = pm_voc_idx, pmNoxIdx = pm_nox_idx, particlesTps = particles_tps, + pmStatusFlags = pm_status_flags, ) .takeUnless { it == AirQualityMetricsExport() } +private fun SoilWaterMetrics.toExport(): SoilWaterMetricsExport? = SoilWaterMetricsExport( + soilPh = soil_ph, + ph = ph, + electricalConductivity = electrical_conductivity, + salinity = salinity, + nitrogen = nitrogen, + phosphorus = phosphorus, + potassium = potassium, + dissolvedOxygen = dissolved_oxygen, + orp = orp, + chemicalOxygenDemand = chemical_oxygen_demand, + turbidity = turbidity, + nitrate = nitrate, + ammonium = ammonium, + biochemicalOxygenDemand = biochemical_oxygen_demand, + solarIrradiance = solar_irradiance, +) + .takeUnless { it == SoilWaterMetricsExport() } + private fun Paxcount.toExport(): PaxcountExport? = PaxcountExport(wifi = wifi, ble = ble, uptime = uptime).takeUnless { it == PaxcountExport() } diff --git a/core/domain/src/commonMain/kotlin/org/meshtastic/core/domain/usecase/settings/ImportSecurityConfigUseCase.kt b/core/domain/src/commonMain/kotlin/org/meshtastic/core/domain/usecase/settings/ImportSecurityConfigUseCase.kt index 2c1171f11b..516a001210 100644 --- a/core/domain/src/commonMain/kotlin/org/meshtastic/core/domain/usecase/settings/ImportSecurityConfigUseCase.kt +++ b/core/domain/src/commonMain/kotlin/org/meshtastic/core/domain/usecase/settings/ImportSecurityConfigUseCase.kt @@ -36,6 +36,11 @@ open class ImportSecurityConfigUseCase { val privateKey = stored.privateKeyBase64.decodeBase64() requireNotNull(publicKey) { "public_key is not valid base64" } requireNotNull(privateKey) { "private_key is not valid base64" } - Config.SecurityConfig(public_key = publicKey, private_key = privateKey) + Config.SecurityConfig.Builder() + .also { wb -> + wb.public_key = publicKey + wb.private_key = privateKey + } + .build() } } diff --git a/core/domain/src/commonMain/kotlin/org/meshtastic/core/domain/usecase/settings/InstallProfileUseCase.kt b/core/domain/src/commonMain/kotlin/org/meshtastic/core/domain/usecase/settings/InstallProfileUseCase.kt index 536cc3a6a7..9bc2415591 100644 --- a/core/domain/src/commonMain/kotlin/org/meshtastic/core/domain/usecase/settings/InstallProfileUseCase.kt +++ b/core/domain/src/commonMain/kotlin/org/meshtastic/core/domain/usecase/settings/InstallProfileUseCase.kt @@ -112,11 +112,13 @@ constructor( if (profile.long_name != null || profile.short_name != null || profile.is_unmessagable != null) { currentUser?.let { setOwner( - it.copy( - long_name = profile.long_name ?: it.long_name, - short_name = profile.short_name ?: it.short_name, - is_unmessagable = profile.is_unmessagable ?: it.is_unmessagable, - ), + it.newBuilder() + .also { wb -> + wb.long_name = profile.long_name ?: it.long_name + wb.short_name = profile.short_name ?: it.short_name + wb.is_unmessagable = profile.is_unmessagable ?: it.is_unmessagable + } + .build(), ) } } @@ -124,13 +126,13 @@ constructor( private suspend fun AdminEditScope.installConfig(config: LocalConfig?) { config?.let { lc -> - lc.device?.let { setConfig(Config(device = it)) } - lc.position?.let { setConfig(Config(position = it)) } - lc.power?.let { setConfig(Config(power = it)) } - lc.network?.let { setConfig(Config(network = it)) } - lc.display?.let { setConfig(Config(display = it)) } - lc.bluetooth?.let { setConfig(Config(bluetooth = it)) } - lc.security?.let { setConfig(Config(security = it)) } + lc.device?.let { setConfig(Config.Builder().also { wb -> wb.device = it }.build()) } + lc.position?.let { setConfig(Config.Builder().also { wb -> wb.position = it }.build()) } + lc.power?.let { setConfig(Config.Builder().also { wb -> wb.power = it }.build()) } + lc.network?.let { setConfig(Config.Builder().also { wb -> wb.network = it }.build()) } + lc.display?.let { setConfig(Config.Builder().also { wb -> wb.display = it }.build()) } + lc.bluetooth?.let { setConfig(Config.Builder().also { wb -> wb.bluetooth = it }.build()) } + lc.security?.let { setConfig(Config.Builder().also { wb -> wb.security = it }.build()) } } } @@ -148,24 +150,34 @@ constructor( } private suspend fun AdminEditScope.installModuleConfigPart1(lmc: LocalModuleConfig) { - lmc.mqtt?.let { setModuleConfig(ModuleConfig(mqtt = it)) } - lmc.serial?.let { setModuleConfig(ModuleConfig(serial = it)) } - lmc.external_notification?.let { setModuleConfig(ModuleConfig(external_notification = it)) } - lmc.store_forward?.let { setModuleConfig(ModuleConfig(store_forward = it)) } - lmc.range_test?.let { setModuleConfig(ModuleConfig(range_test = it)) } - lmc.telemetry?.let { setModuleConfig(ModuleConfig(telemetry = it)) } - lmc.canned_message?.let { setModuleConfig(ModuleConfig(canned_message = it)) } - lmc.audio?.let { setModuleConfig(ModuleConfig(audio = it)) } + lmc.mqtt?.let { setModuleConfig(ModuleConfig.Builder().also { wb -> wb.mqtt = it }.build()) } + lmc.serial?.let { setModuleConfig(ModuleConfig.Builder().also { wb -> wb.serial = it }.build()) } + lmc.external_notification?.let { + setModuleConfig(ModuleConfig.Builder().also { wb -> wb.external_notification = it }.build()) + } + lmc.store_forward?.let { setModuleConfig(ModuleConfig.Builder().also { wb -> wb.store_forward = it }.build()) } + lmc.range_test?.let { setModuleConfig(ModuleConfig.Builder().also { wb -> wb.range_test = it }.build()) } + lmc.telemetry?.let { setModuleConfig(ModuleConfig.Builder().also { wb -> wb.telemetry = it }.build()) } + lmc.canned_message?.let { + setModuleConfig(ModuleConfig.Builder().also { wb -> wb.canned_message = it }.build()) + } + lmc.audio?.let { setModuleConfig(ModuleConfig.Builder().also { wb -> wb.audio = it }.build()) } } private suspend fun AdminEditScope.installModuleConfigPart2(lmc: LocalModuleConfig) { - lmc.remote_hardware?.let { setModuleConfig(ModuleConfig(remote_hardware = it)) } - lmc.neighbor_info?.let { setModuleConfig(ModuleConfig(neighbor_info = it)) } - lmc.ambient_lighting?.let { setModuleConfig(ModuleConfig(ambient_lighting = it)) } - lmc.detection_sensor?.let { setModuleConfig(ModuleConfig(detection_sensor = it)) } - lmc.paxcounter?.let { setModuleConfig(ModuleConfig(paxcounter = it)) } - lmc.statusmessage?.let { setModuleConfig(ModuleConfig(statusmessage = it)) } - lmc.tak?.let { setModuleConfig(ModuleConfig(tak = it)) } + lmc.remote_hardware?.let { + setModuleConfig(ModuleConfig.Builder().also { wb -> wb.remote_hardware = it }.build()) + } + lmc.neighbor_info?.let { setModuleConfig(ModuleConfig.Builder().also { wb -> wb.neighbor_info = it }.build()) } + lmc.ambient_lighting?.let { + setModuleConfig(ModuleConfig.Builder().also { wb -> wb.ambient_lighting = it }.build()) + } + lmc.detection_sensor?.let { + setModuleConfig(ModuleConfig.Builder().also { wb -> wb.detection_sensor = it }.build()) + } + lmc.paxcounter?.let { setModuleConfig(ModuleConfig.Builder().also { wb -> wb.paxcounter = it }.build()) } + lmc.statusmessage?.let { setModuleConfig(ModuleConfig.Builder().also { wb -> wb.statusmessage = it }.build()) } + lmc.tak?.let { setModuleConfig(ModuleConfig.Builder().also { wb -> wb.tak = it }.build()) } } private suspend fun AdminEditScope.installChannelsAndLora( @@ -173,6 +185,6 @@ constructor( loraConfig: Config.LoRaConfig?, ) { replacementPlan?.channelWrites?.forEach { setChannel(it) } - loraConfig?.let { setConfig(Config(lora = it)) } + loraConfig?.let { setConfig(Config.Builder().also { wb -> wb.lora = it }.build()) } } } diff --git a/core/domain/src/commonMain/kotlin/org/meshtastic/core/domain/usecase/settings/NodeDatabaseExport.kt b/core/domain/src/commonMain/kotlin/org/meshtastic/core/domain/usecase/settings/NodeDatabaseExport.kt index e6c98da160..8191a4d12c 100644 --- a/core/domain/src/commonMain/kotlin/org/meshtastic/core/domain/usecase/settings/NodeDatabaseExport.kt +++ b/core/domain/src/commonMain/kotlin/org/meshtastic/core/domain/usecase/settings/NodeDatabaseExport.kt @@ -67,6 +67,7 @@ data class NodeExport( val powerMetrics: PowerMetricsExport? = null, val powerChannelLabels: List? = null, val airQualityMetrics: AirQualityMetricsExport? = null, + val soilWaterMetrics: SoilWaterMetricsExport? = null, val paxcounter: PaxcountExport? = null, val metadata: DeviceMetadataExport? = null, ) @@ -139,6 +140,10 @@ data class EnvironmentMetricsExport( val adcVoltageCh5: Float? = null, val adcVoltageCh6: Float? = null, val adcVoltageCh7: Float? = null, + /** AS3935 lightning detector: strikes counted in the last hour. */ + val lightningStrikeCount1h: Int? = null, + /** AS3935 lightning detector: estimated distance to the storm's leading edge, in km. */ + val lightningDistanceKm: Float? = null, ) @Serializable @@ -188,6 +193,28 @@ data class AirQualityMetricsExport( val pmVocIdx: Float? = null, val pmNoxIdx: Float? = null, val particlesTps: Float? = null, + /** Raw PM sensor status register bitmask as the sensor's datasheet defines it (SEN6X family); 0 means no faults. */ + val pmStatusFlags: Int? = null, +) + +/** Soil probe and water-quality sonde chemistry; units follow the proto (mg/kg, mg/l, ppm, mV, NTU, mS/cm, W/m²). */ +@Serializable +data class SoilWaterMetricsExport( + val soilPh: Float? = null, + val ph: Float? = null, + val electricalConductivity: Float? = null, + val salinity: Float? = null, + val nitrogen: Float? = null, + val phosphorus: Float? = null, + val potassium: Float? = null, + val dissolvedOxygen: Float? = null, + val orp: Float? = null, + val chemicalOxygenDemand: Float? = null, + val turbidity: Float? = null, + val nitrate: Float? = null, + val ammonium: Float? = null, + val biochemicalOxygenDemand: Float? = null, + val solarIrradiance: Float? = null, ) @Serializable data class PaxcountExport(val wifi: Int = 0, val ble: Int = 0, val uptime: Int = 0) diff --git a/core/domain/src/commonTest/kotlin/org/meshtastic/core/domain/usecase/settings/ExportDataUseCaseTest.kt b/core/domain/src/commonTest/kotlin/org/meshtastic/core/domain/usecase/settings/ExportDataUseCaseTest.kt index 96bc13c7de..5f571067c8 100644 --- a/core/domain/src/commonTest/kotlin/org/meshtastic/core/domain/usecase/settings/ExportDataUseCaseTest.kt +++ b/core/domain/src/commonTest/kotlin/org/meshtastic/core/domain/usecase/settings/ExportDataUseCaseTest.kt @@ -63,14 +63,24 @@ class ExportDataUseCaseTest { received_date = 1000000000L, raw_message = "", fromRadio = - FromRadio( - packet = - MeshPacket( - from = 1234, - rx_snr = 5.0f, - decoded = Data(portnum = PortNum.TEXT_MESSAGE_APP, payload = "Hello".encodeUtf8()), - ), - ), + FromRadio.Builder() + .also { wb -> + wb.packet = + MeshPacket.Builder() + .also { wb -> + wb.from = 1234 + wb.rx_snr = 5.0f + wb.decoded = + Data.Builder() + .also { wb -> + wb.portnum = PortNum.TEXT_MESSAGE_APP + wb.payload = "Hello".encodeUtf8() + } + .build() + } + .build() + } + .build(), ) meshLogRepository.setLogs(listOf(log)) @@ -91,17 +101,28 @@ class ExportDataUseCaseTest { received_date = 1000000000L, raw_message = "", fromRadio = - FromRadio( - packet = - MeshPacket( - from = 1234, - rx_snr = 5.0f, - hop_limit = 2, - hop_start = 3, - relay_node = 77, // 0x4d -> matches the tail of a node id such as !........4d - decoded = Data(portnum = PortNum.TEXT_MESSAGE_APP, payload = "Hello".encodeUtf8()), - ), - ), + FromRadio.Builder() + .also { wb -> + wb.packet = + MeshPacket.Builder() + .also { wb -> + wb.from = 1234 + wb.rx_snr = 5.0f + wb.hop_limit = 2 + wb.hop_start = 3 + wb.relay_node = 77 + // 0x4d -> matches the tail of a node id such as !........4d + wb.decoded = + Data.Builder() + .also { wb -> + wb.portnum = PortNum.TEXT_MESSAGE_APP + wb.payload = "Hello".encodeUtf8() + } + .build() + } + .build() + } + .build(), ) meshLogRepository.setLogs(listOf(log)) @@ -122,14 +143,24 @@ class ExportDataUseCaseTest { received_date = 1000000000L, raw_message = "", fromRadio = - FromRadio( - packet = - MeshPacket( - from = 1234, - rx_snr = 5.0f, - decoded = Data(portnum = PortNum.TEXT_MESSAGE_APP, payload = "Hello".encodeUtf8()), - ), - ), + FromRadio.Builder() + .also { wb -> + wb.packet = + MeshPacket.Builder() + .also { wb -> + wb.from = 1234 + wb.rx_snr = 5.0f + wb.decoded = + Data.Builder() + .also { wb -> + wb.portnum = PortNum.TEXT_MESSAGE_APP + wb.payload = "Hello".encodeUtf8() + } + .build() + } + .build() + } + .build(), ) meshLogRepository.setLogs(listOf(log)) diff --git a/core/domain/src/commonTest/kotlin/org/meshtastic/core/domain/usecase/settings/ExportNodeDatabaseUseCaseTest.kt b/core/domain/src/commonTest/kotlin/org/meshtastic/core/domain/usecase/settings/ExportNodeDatabaseUseCaseTest.kt index 81196708db..f579f3f77d 100644 --- a/core/domain/src/commonTest/kotlin/org/meshtastic/core/domain/usecase/settings/ExportNodeDatabaseUseCaseTest.kt +++ b/core/domain/src/commonTest/kotlin/org/meshtastic/core/domain/usecase/settings/ExportNodeDatabaseUseCaseTest.kt @@ -32,9 +32,12 @@ import okio.ByteString.Companion.encodeUtf8 import org.meshtastic.core.model.Node import org.meshtastic.core.testing.FakeNodeRepository import org.meshtastic.core.testing.TestDataFactory +import org.meshtastic.proto.AirQualityMetrics import org.meshtastic.proto.DeviceMetrics +import org.meshtastic.proto.EnvironmentMetrics import org.meshtastic.proto.HardwareModel import org.meshtastic.proto.Position +import org.meshtastic.proto.SoilWaterMetrics import org.meshtastic.proto.User import kotlin.test.BeforeTest import kotlin.test.Test @@ -84,9 +87,69 @@ class ExportNodeDatabaseUseCaseTest { assertFalse("position" in node) assertFalse("deviceMetrics" in node) assertFalse("environmentMetrics" in node) + assertFalse("airQualityMetrics" in node) + assertFalse("soilWaterMetrics" in node) assertFalse("publicKey" in node) } + @Test + fun `lightning readings export from environment metrics`() = runTest { + val env = + EnvironmentMetrics.Builder() + .also { wb -> + wb.lightning_strike_count_1h = 0 + wb.lightning_distance_km = 12f + } + .build() + nodeRepository.setNodes(listOf(Node(num = 7, environmentMetrics = env))) + + val metrics = exportJson()["nodes"]!!.jsonArray.single().jsonObject["environmentMetrics"]!!.jsonObject + + // A quiet hour is 0 strikes, a reading in its own right - it must not be dropped as absent. + assertEquals(0, metrics["lightningStrikeCount1h"]?.jsonPrimitive?.int) + assertEquals(12f, metrics["lightningDistanceKm"]?.jsonPrimitive?.float) + assertFalse("temperature" in metrics) + } + + @Test + fun `pm status flags export raw and survive at zero`() = runTest { + val aq = + AirQualityMetrics.Builder() + .also { wb -> + wb.pm25_standard = 9 + wb.pm_status_flags = 0 + } + .build() + nodeRepository.setNodes(listOf(Node(num = 7, airQualityMetrics = aq))) + + val metrics = exportJson()["nodes"]!!.jsonArray.single().jsonObject["airQualityMetrics"]!!.jsonObject + + // A status register at 0 means "no faults", which is a reading, not absence. + assertEquals(0, metrics["pmStatusFlags"]?.jsonPrimitive?.int) + assertEquals(9, metrics["pm25Standard"]?.jsonPrimitive?.int) + } + + @Test + fun `soil water metrics export with absent fields omitted`() = runTest { + val soil = + SoilWaterMetrics.Builder() + .also { wb -> + wb.soil_ph = 6.5f + wb.electrical_conductivity = 1.2f + wb.nitrogen = 0f + } + .build() + nodeRepository.setNodes(listOf(Node(num = 7, soilWaterMetrics = soil))) + + val metrics = exportJson()["nodes"]!!.jsonArray.single().jsonObject["soilWaterMetrics"]!!.jsonObject + + assertEquals(6.5f, metrics["soilPh"]?.jsonPrimitive?.float) + assertEquals(1.2f, metrics["electricalConductivity"]?.jsonPrimitive?.float) + assertEquals(0f, metrics["nitrogen"]?.jsonPrimitive?.float) + assertFalse("phosphorus" in metrics) + assertFalse("ph" in metrics) + } + @Test fun `zero readings are real and survive the export`() = runTest { // 0 dB SNR and 0 dBm RSSI are genuine measurements, not absence. @@ -106,14 +169,28 @@ class ExportNodeDatabaseUseCaseTest { Node( num = 7, user = - User( - id = "!00000007", - long_name = "Base Camp", - short_name = "BASE", - hw_model = HardwareModel.TBEAM, - ), - position = Position(latitude_i = 450000000, longitude_i = -930000000), - deviceMetrics = DeviceMetrics(battery_level = 85, voltage = 3.9f), + User.Builder() + .also { wb -> + wb.id = "!00000007" + wb.long_name = "Base Camp" + wb.short_name = "BASE" + wb.hw_model = HardwareModel.TBEAM + } + .build(), + position = + Position.Builder() + .also { wb -> + wb.latitude_i = 450000000 + wb.longitude_i = -930000000 + } + .build(), + deviceMetrics = + DeviceMetrics.Builder() + .also { wb -> + wb.battery_level = 85 + wb.voltage = 3.9f + } + .build(), publicKey = key, isFavorite = true, viaMqtt = true, diff --git a/core/domain/src/commonTest/kotlin/org/meshtastic/core/domain/usecase/settings/ExportProfileUseCaseTest.kt b/core/domain/src/commonTest/kotlin/org/meshtastic/core/domain/usecase/settings/ExportProfileUseCaseTest.kt index 15e818519c..1e3ebdd631 100644 --- a/core/domain/src/commonTest/kotlin/org/meshtastic/core/domain/usecase/settings/ExportProfileUseCaseTest.kt +++ b/core/domain/src/commonTest/kotlin/org/meshtastic/core/domain/usecase/settings/ExportProfileUseCaseTest.kt @@ -35,7 +35,7 @@ class ExportProfileUseCaseTest { @Test fun `invoke writes encoded profile to output stream`() { // Arrange - val profile = DeviceProfile(long_name = "Export Node") + val profile = DeviceProfile.Builder().also { wb -> wb.long_name = "Export Node" }.build() val buffer = Buffer() // Act diff --git a/core/domain/src/commonTest/kotlin/org/meshtastic/core/domain/usecase/settings/ImportProfileUseCaseTest.kt b/core/domain/src/commonTest/kotlin/org/meshtastic/core/domain/usecase/settings/ImportProfileUseCaseTest.kt index 7c42b2ac35..5da4953e60 100644 --- a/core/domain/src/commonTest/kotlin/org/meshtastic/core/domain/usecase/settings/ImportProfileUseCaseTest.kt +++ b/core/domain/src/commonTest/kotlin/org/meshtastic/core/domain/usecase/settings/ImportProfileUseCaseTest.kt @@ -35,7 +35,7 @@ class ImportProfileUseCaseTest { @Test fun `invoke with valid data returns profile`() { // Arrange - val profile = DeviceProfile(long_name = "Test Node") + val profile = DeviceProfile.Builder().also { wb -> wb.long_name = "Test Node" }.build() val buffer = Buffer().write(profile.encode()) // Act diff --git a/core/domain/src/commonTest/kotlin/org/meshtastic/core/domain/usecase/settings/InstallProfileUseCaseTest.kt b/core/domain/src/commonTest/kotlin/org/meshtastic/core/domain/usecase/settings/InstallProfileUseCaseTest.kt index 0b8e19d36f..bfefe1b32a 100644 --- a/core/domain/src/commonTest/kotlin/org/meshtastic/core/domain/usecase/settings/InstallProfileUseCaseTest.kt +++ b/core/domain/src/commonTest/kotlin/org/meshtastic/core/domain/usecase/settings/InstallProfileUseCaseTest.kt @@ -75,7 +75,13 @@ class InstallProfileUseCaseTest { @Test fun `invoke calls begin and commit edit settings`() = runTest { - useCase(1234, DeviceProfile(), User(), currentLoraConfig = null, isLocal = false) + useCase( + 1234, + DeviceProfile.Builder().build(), + User.Builder().build(), + currentLoraConfig = null, + isLocal = false, + ) assertTrue(radioController.editSettingsCalled) } @@ -83,42 +89,54 @@ class InstallProfileUseCaseTest { @Test fun `invoke installs all sections of a full profile`() = runTest { val profile = - DeviceProfile( - long_name = "Full Node", - short_name = "FULL", - config = - org.meshtastic.proto.LocalConfig( - device = DeviceConfig(), - position = PositionConfig(), - power = PowerConfig(), - network = NetworkConfig(), - display = DisplayConfig(), - lora = LoRaConfig(), - bluetooth = BluetoothConfig(), - security = SecurityConfig(), - ), - module_config = - org.meshtastic.proto.LocalModuleConfig( - mqtt = MQTTConfig(), - serial = SerialConfig(), - external_notification = ExternalNotificationConfig(), - store_forward = StoreForwardConfig(), - range_test = RangeTestConfig(), - telemetry = TelemetryConfig(), - canned_message = CannedMessageConfig(), - audio = AudioConfig(), - remote_hardware = RemoteHardwareConfig(), - neighbor_info = NeighborInfoConfig(), - ambient_lighting = AmbientLightingConfig(), - detection_sensor = DetectionSensorConfig(), - paxcounter = PaxcounterConfig(), - statusmessage = StatusMessageConfig(), - tak = TAKConfig(), - ), - fixed_position = org.meshtastic.proto.Position(), - ) + DeviceProfile.Builder() + .also { wb -> + wb.long_name = "Full Node" + wb.short_name = "FULL" + wb.config = + org.meshtastic.proto.LocalConfig.Builder() + .also { wb -> + wb.device = DeviceConfig.Builder().build() + wb.position = PositionConfig.Builder().build() + wb.power = PowerConfig.Builder().build() + wb.network = NetworkConfig.Builder().build() + wb.display = DisplayConfig.Builder().build() + wb.lora = LoRaConfig.Builder().build() + wb.bluetooth = BluetoothConfig.Builder().build() + wb.security = SecurityConfig.Builder().build() + } + .build() + wb.module_config = + org.meshtastic.proto.LocalModuleConfig.Builder() + .also { wb -> + wb.mqtt = MQTTConfig.Builder().build() + wb.serial = SerialConfig.Builder().build() + wb.external_notification = ExternalNotificationConfig.Builder().build() + wb.store_forward = StoreForwardConfig.Builder().build() + wb.range_test = RangeTestConfig.Builder().build() + wb.telemetry = TelemetryConfig.Builder().build() + wb.canned_message = CannedMessageConfig.Builder().build() + wb.audio = AudioConfig.Builder().build() + wb.remote_hardware = RemoteHardwareConfig.Builder().build() + wb.neighbor_info = NeighborInfoConfig.Builder().build() + wb.ambient_lighting = AmbientLightingConfig.Builder().build() + wb.detection_sensor = DetectionSensorConfig.Builder().build() + wb.paxcounter = PaxcounterConfig.Builder().build() + wb.statusmessage = StatusMessageConfig.Builder().build() + wb.tak = TAKConfig.Builder().build() + } + .build() + wb.fixed_position = org.meshtastic.proto.Position.Builder().build() + } + .build() - useCase(1234, profile, org.meshtastic.proto.User(long_name = "Old"), currentLoraConfig = null, isLocal = false) + useCase( + 1234, + profile, + org.meshtastic.proto.User.Builder().also { wb -> wb.long_name = "Old" }.build(), + currentLoraConfig = null, + isLocal = false, + ) assertTrue(radioController.editSettingsCalled) } @@ -127,14 +145,25 @@ class InstallProfileUseCaseTest { fun `fixed position queue rejection aborts profile installation after closing the edit transaction`() = runTest { val rejection = PacketQueueRejectedException("Fixed position") radioController.onSetFixedPosition = { _, _ -> throw rejection } - val profile = DeviceProfile(fixed_position = org.meshtastic.proto.Position(latitude_i = 1, longitude_i = 1)) + val profile = + DeviceProfile.Builder() + .also { wb -> + wb.fixed_position = + org.meshtastic.proto.Position.Builder() + .also { wb -> + wb.latitude_i = 1 + wb.longitude_i = 1 + } + .build() + } + .build() val failure = assertFailsWith { useCase( destNum = 1234, profile = profile, - currentUser = User(), + currentUser = User.Builder().build(), currentLoraConfig = null, isLocal = false, ) @@ -147,9 +176,21 @@ class InstallProfileUseCaseTest { @Test fun `invoke installs is_unmessagable but never auto-installs is_licensed`() = runTest { - val profile = DeviceProfile(is_unmessagable = true, is_licensed = true) + val profile = + DeviceProfile.Builder() + .also { wb -> + wb.is_unmessagable = true + wb.is_licensed = true + } + .build() - useCase(1234, profile, User(long_name = "Old"), currentLoraConfig = null, isLocal = false) + useCase( + 1234, + profile, + User.Builder().also { wb -> wb.long_name = "Old" }.build(), + currentLoraConfig = null, + isLocal = false, + ) assertEquals(true, radioController.lastSetOwnerUser?.is_unmessagable) assertEquals(false, radioController.lastSetOwnerUser?.is_licensed) @@ -157,40 +198,56 @@ class InstallProfileUseCaseTest { @Test fun `invoke normalizes channels refreshes local cache and writes URL LoRa once`() = runTest { - val primary = ChannelSettings(name = "Node A Primary") - val secondary = ChannelSettings(name = "Node A Secondary") + val primary = ChannelSettings.Builder().also { wb -> wb.name = "Node A Primary" }.build() + val secondary = ChannelSettings.Builder().also { wb -> wb.name = "Node A Secondary" }.build() val urlLoraConfig = - LoRaConfig( - use_preset = true, - modem_preset = LoRaConfig.ModemPreset.MEDIUM_FAST, - region = LoRaConfig.RegionCode.US, - ) + LoRaConfig.Builder() + .also { wb -> + wb.use_preset = true + wb.modem_preset = LoRaConfig.ModemPreset.MEDIUM_FAST + wb.region = LoRaConfig.RegionCode.US + } + .build() val profileLoraConfig = - LoRaConfig( - use_preset = true, - modem_preset = LoRaConfig.ModemPreset.LONG_SLOW, - region = LoRaConfig.RegionCode.EU_868, - ) - val currentLoraConfig = LoRaConfig(region = LoRaConfig.RegionCode.ANZ) - val oldSettings = listOf(ChannelSettings(name = "Node B Primary")) - val cachedLoraConfig = LoRaConfig(region = LoRaConfig.RegionCode.EU_433) - radioConfigRepository.setChannelSet(ChannelSet(settings = oldSettings, lora_config = cachedLoraConfig)) + LoRaConfig.Builder() + .also { wb -> + wb.use_preset = true + wb.modem_preset = LoRaConfig.ModemPreset.LONG_SLOW + wb.region = LoRaConfig.RegionCode.EU_868 + } + .build() + val currentLoraConfig = LoRaConfig.Builder().also { wb -> wb.region = LoRaConfig.RegionCode.ANZ }.build() + val oldSettings = listOf(ChannelSettings.Builder().also { wb -> wb.name = "Node B Primary" }.build()) + val cachedLoraConfig = LoRaConfig.Builder().also { wb -> wb.region = LoRaConfig.RegionCode.EU_433 }.build() + radioConfigRepository.setChannelSet( + ChannelSet.Builder() + .also { wb -> + wb.settings = oldSettings + wb.lora_config = cachedLoraConfig + } + .build(), + ) val exportedProfile = - DeviceProfile( - config = org.meshtastic.proto.LocalConfig(lora = profileLoraConfig), - channel_url = - ChannelSet( - settings = listOf(primary, ChannelSettings(), primary, secondary), - lora_config = urlLoraConfig, - ) - .getChannelUrl() - .toString(), - ) + DeviceProfile.Builder() + .also { wb -> + wb.config = + org.meshtastic.proto.LocalConfig.Builder().also { wb -> wb.lora = profileLoraConfig }.build() + wb.channel_url = + ChannelSet.Builder() + .also { wb -> + wb.settings = listOf(primary, ChannelSettings.Builder().build(), primary, secondary) + wb.lora_config = urlLoraConfig + } + .build() + .getChannelUrl() + .toString() + } + .build() useCase( 4321, exportedProfile, - User(long_name = "Node B"), + User.Builder().also { wb -> wb.long_name = "Node B" }.build(), currentLoraConfig = currentLoraConfig, isLocal = true, ) @@ -216,32 +273,61 @@ class InstallProfileUseCaseTest { assertEquals(listOf(primary, secondary), radioConfigRepository.currentChannelSet.settings) assertEquals(urlLoraConfig, radioConfigRepository.currentChannelSet.lora_config) assertEquals( - listOf(FakeRadioController.ConfigWrite(destination = 4321, config = Config(lora = urlLoraConfig))), + listOf( + FakeRadioController.ConfigWrite( + destination = 4321, + config = Config.Builder().also { wb -> wb.lora = urlLoraConfig }.build(), + ), + ), radioController.configWrites, ) assertEquals( channelWrites.map { SettingsOperation.SetChannel(it.channel) } + - SettingsOperation.SetConfig(Config(lora = urlLoraConfig)), + SettingsOperation.SetConfig(Config.Builder().also { wb -> wb.lora = urlLoraConfig }.build()), radioController.settingsOperations, ) } @Test fun `invoke treats blank channel URL as absent and installs profile LoRa once`() = runTest { - val oldSettings = listOf(ChannelSettings(name = "Keep Me")) - val currentLoraConfig = LoRaConfig(region = LoRaConfig.RegionCode.EU_868) - val profileLoraConfig = LoRaConfig(region = LoRaConfig.RegionCode.US) - val cachedLoraConfig = LoRaConfig(region = LoRaConfig.RegionCode.ANZ) - radioConfigRepository.setChannelSet(ChannelSet(settings = oldSettings, lora_config = cachedLoraConfig)) + val oldSettings = listOf(ChannelSettings.Builder().also { wb -> wb.name = "Keep Me" }.build()) + val currentLoraConfig = LoRaConfig.Builder().also { wb -> wb.region = LoRaConfig.RegionCode.EU_868 }.build() + val profileLoraConfig = LoRaConfig.Builder().also { wb -> wb.region = LoRaConfig.RegionCode.US }.build() + val cachedLoraConfig = LoRaConfig.Builder().also { wb -> wb.region = LoRaConfig.RegionCode.ANZ }.build() + radioConfigRepository.setChannelSet( + ChannelSet.Builder() + .also { wb -> + wb.settings = oldSettings + wb.lora_config = cachedLoraConfig + } + .build(), + ) val profile = - DeviceProfile(channel_url = " \t\n", config = org.meshtastic.proto.LocalConfig(lora = profileLoraConfig)) + DeviceProfile.Builder() + .also { wb -> + wb.channel_url = " \t\n" + wb.config = + org.meshtastic.proto.LocalConfig.Builder().also { wb -> wb.lora = profileLoraConfig }.build() + } + .build() - useCase(4321, profile, User(long_name = "Node B"), currentLoraConfig = currentLoraConfig, isLocal = true) + useCase( + 4321, + profile, + User.Builder().also { wb -> wb.long_name = "Node B" }.build(), + currentLoraConfig = currentLoraConfig, + isLocal = true, + ) assertTrue(radioController.editSettingsCalled) assertTrue(radioController.localChannels.isEmpty()) assertEquals( - listOf(FakeRadioController.ConfigWrite(destination = 4321, config = Config(lora = profileLoraConfig))), + listOf( + FakeRadioController.ConfigWrite( + destination = 4321, + config = Config.Builder().also { wb -> wb.lora = profileLoraConfig }.build(), + ), + ), radioController.configWrites, ) assertEquals( @@ -254,15 +340,35 @@ class InstallProfileUseCaseTest { @Test fun `invoke replaces local channels and preserves cached LoRa when no LoRa write is needed`() = runTest { - val cachedLoraConfig = LoRaConfig(region = LoRaConfig.RegionCode.ANZ) - val importedPrimary = ChannelSettings(name = "Imported Primary") + val cachedLoraConfig = LoRaConfig.Builder().also { wb -> wb.region = LoRaConfig.RegionCode.ANZ }.build() + val importedPrimary = ChannelSettings.Builder().also { wb -> wb.name = "Imported Primary" }.build() radioConfigRepository.setChannelSet( - ChannelSet(settings = listOf(ChannelSettings(name = "Old Primary")), lora_config = cachedLoraConfig), + ChannelSet.Builder() + .also { wb -> + wb.settings = listOf(ChannelSettings.Builder().also { wb -> wb.name = "Old Primary" }.build()) + wb.lora_config = cachedLoraConfig + } + .build(), ) val profile = - DeviceProfile(channel_url = ChannelSet(settings = listOf(importedPrimary)).getChannelUrl().toString()) + DeviceProfile.Builder() + .also { wb -> + wb.channel_url = + ChannelSet.Builder() + .also { wb -> wb.settings = listOf(importedPrimary) } + .build() + .getChannelUrl() + .toString() + } + .build() - useCase(4321, profile, User(long_name = "Node B"), currentLoraConfig = cachedLoraConfig, isLocal = true) + useCase( + 4321, + profile, + User.Builder().also { wb -> wb.long_name = "Node B" }.build(), + currentLoraConfig = cachedLoraConfig, + isLocal = true, + ) assertTrue(radioController.localConfigs.isEmpty()) assertEquals(listOf(importedPrimary), radioConfigRepository.currentChannelSet.settings) @@ -271,38 +377,72 @@ class InstallProfileUseCaseTest { @Test fun `invoke skips redundant LoRa write when desired config is already active`() = runTest { - val currentLoraConfig = LoRaConfig(region = LoRaConfig.RegionCode.US) - val profile = DeviceProfile(config = org.meshtastic.proto.LocalConfig(lora = currentLoraConfig)) + val currentLoraConfig = LoRaConfig.Builder().also { wb -> wb.region = LoRaConfig.RegionCode.US }.build() + val profile = + DeviceProfile.Builder() + .also { wb -> + wb.config = + org.meshtastic.proto.LocalConfig.Builder().also { wb -> wb.lora = currentLoraConfig }.build() + } + .build() - useCase(4321, profile, User(long_name = "Node B"), currentLoraConfig = currentLoraConfig, isLocal = false) + useCase( + 4321, + profile, + User.Builder().also { wb -> wb.long_name = "Node B" }.build(), + currentLoraConfig = currentLoraConfig, + isLocal = false, + ) assertTrue(radioController.localConfigs.isEmpty()) } @Test fun `invoke does not replace local cache for a remote profile install`() = runTest { - val oldSettings = listOf(ChannelSettings(name = "Local Primary")) - val remotePrimary = ChannelSettings(name = "Remote Primary") + val oldSettings = listOf(ChannelSettings.Builder().also { wb -> wb.name = "Local Primary" }.build()) + val remotePrimary = ChannelSettings.Builder().also { wb -> wb.name = "Remote Primary" }.build() val cachedChannelSet = - ChannelSet(settings = oldSettings, lora_config = LoRaConfig(region = LoRaConfig.RegionCode.ANZ)) + ChannelSet.Builder() + .also { wb -> + wb.settings = oldSettings + wb.lora_config = LoRaConfig.Builder().also { wb -> wb.region = LoRaConfig.RegionCode.ANZ }.build() + } + .build() radioConfigRepository.setChannelSet(cachedChannelSet) val profile = - DeviceProfile( - channel_url = - ChannelSet( - settings = listOf(remotePrimary), - lora_config = LoRaConfig(region = LoRaConfig.RegionCode.US), - ) - .getChannelUrl() - .toString(), - ) + DeviceProfile.Builder() + .also { wb -> + wb.channel_url = + ChannelSet.Builder() + .also { wb -> + wb.settings = listOf(remotePrimary) + wb.lora_config = + LoRaConfig.Builder().also { wb -> wb.region = LoRaConfig.RegionCode.US }.build() + } + .build() + .getChannelUrl() + .toString() + } + .build() - useCase(4321, profile, User(long_name = "Remote"), currentLoraConfig = null, isLocal = false) + useCase( + 4321, + profile, + User.Builder().also { wb -> wb.long_name = "Remote" }.build(), + currentLoraConfig = null, + isLocal = false, + ) assertEquals( FakeRadioController.ChannelWrite( 4321, - Channel(index = 0, role = Channel.Role.PRIMARY, settings = remotePrimary), + Channel.Builder() + .also { wb -> + wb.index = 0 + wb.role = Channel.Role.PRIMARY + wb.settings = remotePrimary + } + .build(), ), radioController.channelWrites.first(), ) @@ -312,24 +452,43 @@ class InstallProfileUseCaseTest { @Test fun `invoke leaves local cache unchanged when a channel write fails`() = runTest { - val oldSettings = listOf(ChannelSettings(name = "Local Primary")) + val oldSettings = listOf(ChannelSettings.Builder().also { wb -> wb.name = "Local Primary" }.build()) val cachedChannelSet = - ChannelSet(settings = oldSettings, lora_config = LoRaConfig(region = LoRaConfig.RegionCode.ANZ)) + ChannelSet.Builder() + .also { wb -> + wb.settings = oldSettings + wb.lora_config = LoRaConfig.Builder().also { wb -> wb.region = LoRaConfig.RegionCode.ANZ }.build() + } + .build() radioConfigRepository.setChannelSet(cachedChannelSet) radioController.failChannelWriteAfter = 2 val profile = - DeviceProfile( - channel_url = - ChannelSet( - settings = listOf(ChannelSettings(name = "Imported Primary")), - lora_config = LoRaConfig(region = LoRaConfig.RegionCode.US), - ) - .getChannelUrl() - .toString(), - ) + DeviceProfile.Builder() + .also { wb -> + wb.channel_url = + ChannelSet.Builder() + .also { wb -> + wb.settings = + listOf( + ChannelSettings.Builder().also { wb -> wb.name = "Imported Primary" }.build(), + ) + wb.lora_config = + LoRaConfig.Builder().also { wb -> wb.region = LoRaConfig.RegionCode.US }.build() + } + .build() + .getChannelUrl() + .toString() + } + .build() assertFailsWith { - useCase(4321, profile, User(long_name = "Local"), currentLoraConfig = null, isLocal = true) + useCase( + 4321, + profile, + User.Builder().also { wb -> wb.long_name = "Local" }.build(), + currentLoraConfig = null, + isLocal = true, + ) } assertEquals(cachedChannelSet, radioConfigRepository.currentChannelSet) @@ -338,24 +497,46 @@ class InstallProfileUseCaseTest { @Test fun `invoke rejects an empty channel set before opening the transaction`() = runTest { val destinationPrimary = - Channel(role = Channel.Role.PRIMARY, index = 0, settings = ChannelSettings(name = "Node B Primary")) + Channel.Builder() + .also { wb -> + wb.role = Channel.Role.PRIMARY + wb.index = 0 + wb.settings = ChannelSettings.Builder().also { wb -> wb.name = "Node B Primary" }.build() + } + .build() radioController.channelWrites.add( FakeRadioController.ChannelWrite(destination = null, channel = destinationPrimary), ) val profile = - DeviceProfile( - long_name = "Must Not Apply", - channel_url = - ChannelSet( - settings = emptyList(), - lora_config = LoRaConfig(use_preset = true, region = LoRaConfig.RegionCode.US), - ) - .getChannelUrl() - .toString(), - ) + DeviceProfile.Builder() + .also { wb -> + wb.long_name = "Must Not Apply" + wb.channel_url = + ChannelSet.Builder() + .also { wb -> + wb.settings = emptyList() + wb.lora_config = + LoRaConfig.Builder() + .also { wb -> + wb.use_preset = true + wb.region = LoRaConfig.RegionCode.US + } + .build() + } + .build() + .getChannelUrl() + .toString() + } + .build() assertFailsWith { - useCase(4321, profile, User(long_name = "Node B"), currentLoraConfig = null, isLocal = true) + useCase( + 4321, + profile, + User.Builder().also { wb -> wb.long_name = "Node B" }.build(), + currentLoraConfig = null, + isLocal = true, + ) } assertFalse(radioController.editSettingsCalled) @@ -366,11 +547,23 @@ class InstallProfileUseCaseTest { @Test fun `invoke rejects a malformed channel URL before opening the transaction`() = runTest { - val profile = DeviceProfile(long_name = "Must Not Apply", channel_url = "https://example.com/not-a-channel") + val profile = + DeviceProfile.Builder() + .also { wb -> + wb.long_name = "Must Not Apply" + wb.channel_url = "https://example.com/not-a-channel" + } + .build() val error = assertFailsWith { - useCase(4321, profile, User(long_name = "Node B"), currentLoraConfig = null, isLocal = true) + useCase( + 4321, + profile, + User.Builder().also { wb -> wb.long_name = "Node B" }.build(), + currentLoraConfig = null, + isLocal = true, + ) } assertFalse(radioController.editSettingsCalled) diff --git a/core/domain/src/commonTest/kotlin/org/meshtastic/core/domain/usecase/settings/IsOtaCapableUseCaseTest.kt b/core/domain/src/commonTest/kotlin/org/meshtastic/core/domain/usecase/settings/IsOtaCapableUseCaseTest.kt index 213a0f98f5..927dd7ed1c 100644 --- a/core/domain/src/commonTest/kotlin/org/meshtastic/core/domain/usecase/settings/IsOtaCapableUseCaseTest.kt +++ b/core/domain/src/commonTest/kotlin/org/meshtastic/core/domain/usecase/settings/IsOtaCapableUseCaseTest.kt @@ -64,7 +64,7 @@ class IsOtaCapableUseCaseTest { @Test fun `invoke returns true when ota capable`() = runTest { // Arrange - val node = Node(num = 123, user = User(hw_model = HardwareModel.TBEAM)) + val node = Node(num = 123, user = User.Builder().also { wb -> wb.hw_model = HardwareModel.TBEAM }.build()) dev.mokkery.every { nodeRepository.ourNodeInfo } returns MutableStateFlow(node) dev.mokkery.every { radioController.connectionState } returns MutableStateFlow(org.meshtastic.core.model.ConnectionState.Connected) @@ -88,7 +88,7 @@ class IsOtaCapableUseCaseTest { @Test fun `invoke returns false when ota not capable`() = runTest { // Arrange - val node = Node(num = 123, user = User(hw_model = HardwareModel.TBEAM)) + val node = Node(num = 123, user = User.Builder().also { wb -> wb.hw_model = HardwareModel.TBEAM }.build()) dev.mokkery.every { nodeRepository.ourNodeInfo } returns MutableStateFlow(node) dev.mokkery.every { radioController.connectionState } returns MutableStateFlow(org.meshtastic.core.model.ConnectionState.Connected) @@ -106,7 +106,7 @@ class IsOtaCapableUseCaseTest { @Test fun `invoke returns true when requires Dfu and actively supported`() = runTest { // Arrange - val node = Node(num = 123, user = User(hw_model = HardwareModel.TBEAM)) + val node = Node(num = 123, user = User.Builder().also { wb -> wb.hw_model = HardwareModel.TBEAM }.build()) dev.mokkery.every { nodeRepository.ourNodeInfo } returns MutableStateFlow(node) dev.mokkery.every { radioController.connectionState } returns MutableStateFlow(org.meshtastic.core.model.ConnectionState.Connected) @@ -130,7 +130,7 @@ class IsOtaCapableUseCaseTest { @Test fun `invoke returns false when hardware model is UNSET`() = runTest { // Arrange - val node = Node(num = 123, user = User(hw_model = HardwareModel.UNSET)) + val node = Node(num = 123, user = User.Builder().also { wb -> wb.hw_model = HardwareModel.UNSET }.build()) dev.mokkery.every { nodeRepository.ourNodeInfo } returns MutableStateFlow(node) dev.mokkery.every { radioController.connectionState } returns MutableStateFlow(org.meshtastic.core.model.ConnectionState.Connected) @@ -170,7 +170,7 @@ class IsOtaCapableUseCaseTest { @Test fun `invoke returns false when address is not ota capable`() = runTest { - val node = Node(num = 123, user = User(hw_model = HardwareModel.TBEAM)) + val node = Node(num = 123, user = User.Builder().also { wb -> wb.hw_model = HardwareModel.TBEAM }.build()) dev.mokkery.every { nodeRepository.ourNodeInfo } returns MutableStateFlow(node) dev.mokkery.every { radioController.connectionState } returns MutableStateFlow(org.meshtastic.core.model.ConnectionState.Connected) diff --git a/core/domain/src/commonTest/kotlin/org/meshtastic/core/domain/usecase/settings/ProcessRadioResponseUseCaseTest.kt b/core/domain/src/commonTest/kotlin/org/meshtastic/core/domain/usecase/settings/ProcessRadioResponseUseCaseTest.kt index f843e717de..bfbb2e0243 100644 --- a/core/domain/src/commonTest/kotlin/org/meshtastic/core/domain/usecase/settings/ProcessRadioResponseUseCaseTest.kt +++ b/core/domain/src/commonTest/kotlin/org/meshtastic/core/domain/usecase/settings/ProcessRadioResponseUseCaseTest.kt @@ -41,15 +41,24 @@ class ProcessRadioResponseUseCaseTest { fun `invoke with routing error returns error result`() { // Arrange val packet = - MeshPacket( - from = 123, - decoded = - Data( - portnum = PortNum.ROUTING_APP, - request_id = 42, - payload = Routing(error_reason = Routing.Error.NO_ROUTE).encode().toByteString(), - ), - ) + MeshPacket.Builder() + .also { wb -> + wb.from = 123 + wb.decoded = + Data.Builder() + .also { wb -> + wb.portnum = PortNum.ROUTING_APP + wb.request_id = 42 + wb.payload = + Routing.Builder() + .also { wb -> wb.error_reason = Routing.Error.NO_ROUTE } + .build() + .encode() + .toByteString() + } + .build() + } + .build() // Act val result = useCase(packet, 123, setOf(42)) @@ -62,11 +71,19 @@ class ProcessRadioResponseUseCaseTest { @Test fun `routing response without error reason is not treated as an error`() { val packet = - MeshPacket( - from = 123, - decoded = - Data(portnum = PortNum.ROUTING_APP, request_id = 42, payload = Routing().encode().toByteString()), - ) + MeshPacket.Builder() + .also { wb -> + wb.from = 123 + wb.decoded = + Data.Builder() + .also { wb -> + wb.portnum = PortNum.ROUTING_APP + wb.request_id = 42 + wb.payload = Routing.Builder().build().encode().toByteString() + } + .build() + } + .build() val result = useCase(packet, 123, setOf(42)) @@ -76,15 +93,24 @@ class ProcessRadioResponseUseCaseTest { @Test fun `routing ack from a node other than the addressed one is reported instead of dropped`() { val packet = - MeshPacket( - from = 456, - decoded = - Data( - portnum = PortNum.ROUTING_APP, - request_id = 42, - payload = Routing(error_reason = Routing.Error.NONE).encode().toByteString(), - ), - ) + MeshPacket.Builder() + .also { wb -> + wb.from = 456 + wb.decoded = + Data.Builder() + .also { wb -> + wb.portnum = PortNum.ROUTING_APP + wb.request_id = 42 + wb.payload = + Routing.Builder() + .also { wb -> wb.error_reason = Routing.Error.NONE } + .build() + .encode() + .toByteString() + } + .build() + } + .build() val result = useCase(packet, 123, setOf(42)) @@ -94,17 +120,22 @@ class ProcessRadioResponseUseCaseTest { @Test fun `invoke with metadata response returns metadata result`() { // Arrange - val metadata = DeviceMetadata(firmware_version = "2.5.0") - val adminMsg = AdminMessage(get_device_metadata_response = metadata) + val metadata = DeviceMetadata.Builder().also { wb -> wb.firmware_version = "2.5.0" }.build() + val adminMsg = AdminMessage.Builder().also { wb -> wb.get_device_metadata_response = metadata }.build() val packet = - MeshPacket( - from = 123, - decoded = Data( - portnum = PortNum.ADMIN_APP, - request_id = 42, - payload = adminMsg.encode().toByteString(), - ), - ) + MeshPacket.Builder() + .also { wb -> + wb.from = 123 + wb.decoded = + Data.Builder() + .also { wb -> + wb.portnum = PortNum.ADMIN_APP + wb.request_id = 42 + wb.payload = adminMsg.encode().toByteString() + } + .build() + } + .build() // Act val result = useCase(packet, 123, setOf(42)) @@ -117,16 +148,22 @@ class ProcessRadioResponseUseCaseTest { @Test fun `invoke with canned messages response returns canned messages result`() { // Arrange - val adminMsg = AdminMessage(get_canned_message_module_messages_response = "Hello World") + val adminMsg = + AdminMessage.Builder().also { wb -> wb.get_canned_message_module_messages_response = "Hello World" }.build() val packet = - MeshPacket( - from = 123, - decoded = Data( - portnum = PortNum.ADMIN_APP, - request_id = 42, - payload = adminMsg.encode().toByteString(), - ), - ) + MeshPacket.Builder() + .also { wb -> + wb.from = 123 + wb.decoded = + Data.Builder() + .also { wb -> + wb.portnum = PortNum.ADMIN_APP + wb.request_id = 42 + wb.payload = adminMsg.encode().toByteString() + } + .build() + } + .build() // Act val result = useCase(packet, 123, setOf(42)) @@ -138,33 +175,43 @@ class ProcessRadioResponseUseCaseTest { @Test fun `invoke with unexpected sender returns error`() { - val adminMsg = AdminMessage() + val adminMsg = AdminMessage.Builder().build() val packet = - MeshPacket( - from = 456, - decoded = Data( - portnum = PortNum.ADMIN_APP, - request_id = 42, - payload = adminMsg.encode().toByteString(), - ), - ) + MeshPacket.Builder() + .also { wb -> + wb.from = 456 + wb.decoded = + Data.Builder() + .also { wb -> + wb.portnum = PortNum.ADMIN_APP + wb.request_id = 42 + wb.payload = adminMsg.encode().toByteString() + } + .build() + } + .build() val result = useCase(packet, 123, setOf(42)) assertTrue(result is RadioResponseResult.Error) } @Test fun `invoke with owner response returns owner result`() { - val owner = org.meshtastic.proto.User(long_name = "Owner") - val adminMsg = AdminMessage(get_owner_response = owner) + val owner = org.meshtastic.proto.User.Builder().also { wb -> wb.long_name = "Owner" }.build() + val adminMsg = AdminMessage.Builder().also { wb -> wb.get_owner_response = owner }.build() val packet = - MeshPacket( - from = 123, - decoded = Data( - portnum = PortNum.ADMIN_APP, - request_id = 42, - payload = adminMsg.encode().toByteString(), - ), - ) + MeshPacket.Builder() + .also { wb -> + wb.from = 123 + wb.decoded = + Data.Builder() + .also { wb -> + wb.portnum = PortNum.ADMIN_APP + wb.request_id = 42 + wb.payload = adminMsg.encode().toByteString() + } + .build() + } + .build() val result = useCase(packet, 123, setOf(42)) assertTrue(result is RadioResponseResult.Owner) assertEquals("Owner", result.user.long_name) @@ -172,17 +219,28 @@ class ProcessRadioResponseUseCaseTest { @Test fun `invoke with config response returns config result`() { - val config = org.meshtastic.proto.Config(lora = org.meshtastic.proto.Config.LoRaConfig(use_preset = true)) - val adminMsg = AdminMessage(get_config_response = config) + val config = + org.meshtastic.proto.Config.Builder() + .also { wb -> + wb.lora = + org.meshtastic.proto.Config.LoRaConfig.Builder().also { wb -> wb.use_preset = true }.build() + } + .build() + val adminMsg = AdminMessage.Builder().also { wb -> wb.get_config_response = config }.build() val packet = - MeshPacket( - from = 123, - decoded = Data( - portnum = PortNum.ADMIN_APP, - request_id = 42, - payload = adminMsg.encode().toByteString(), - ), - ) + MeshPacket.Builder() + .also { wb -> + wb.from = 123 + wb.decoded = + Data.Builder() + .also { wb -> + wb.portnum = PortNum.ADMIN_APP + wb.request_id = 42 + wb.payload = adminMsg.encode().toByteString() + } + .build() + } + .build() val result = useCase(packet, 123, setOf(42)) assertTrue(result is RadioResponseResult.ConfigResponse) } @@ -190,34 +248,54 @@ class ProcessRadioResponseUseCaseTest { @Test fun `invoke with module config response returns module config result`() { val config = - org.meshtastic.proto.ModuleConfig(mqtt = org.meshtastic.proto.ModuleConfig.MQTTConfig(enabled = true)) - val adminMsg = AdminMessage(get_module_config_response = config) + org.meshtastic.proto.ModuleConfig.Builder() + .also { wb -> + wb.mqtt = + org.meshtastic.proto.ModuleConfig.MQTTConfig.Builder().also { wb -> wb.enabled = true }.build() + } + .build() + val adminMsg = AdminMessage.Builder().also { wb -> wb.get_module_config_response = config }.build() val packet = - MeshPacket( - from = 123, - decoded = Data( - portnum = PortNum.ADMIN_APP, - request_id = 42, - payload = adminMsg.encode().toByteString(), - ), - ) + MeshPacket.Builder() + .also { wb -> + wb.from = 123 + wb.decoded = + Data.Builder() + .also { wb -> + wb.portnum = PortNum.ADMIN_APP + wb.request_id = 42 + wb.payload = adminMsg.encode().toByteString() + } + .build() + } + .build() val result = useCase(packet, 123, setOf(42)) assertTrue(result is RadioResponseResult.ModuleConfigResponse) } @Test fun `invoke with channel response returns channel result`() { - val channel = org.meshtastic.proto.Channel(settings = org.meshtastic.proto.ChannelSettings(name = "Main")) - val adminMsg = AdminMessage(get_channel_response = channel) + val channel = + org.meshtastic.proto.Channel.Builder() + .also { wb -> + wb.settings = org.meshtastic.proto.ChannelSettings.Builder().also { wb -> wb.name = "Main" }.build() + } + .build() + val adminMsg = AdminMessage.Builder().also { wb -> wb.get_channel_response = channel }.build() val packet = - MeshPacket( - from = 123, - decoded = Data( - portnum = PortNum.ADMIN_APP, - request_id = 42, - payload = adminMsg.encode().toByteString(), - ), - ) + MeshPacket.Builder() + .also { wb -> + wb.from = 123 + wb.decoded = + Data.Builder() + .also { wb -> + wb.portnum = PortNum.ADMIN_APP + wb.request_id = 42 + wb.payload = adminMsg.encode().toByteString() + } + .build() + } + .build() val result = useCase(packet, 123, setOf(42)) assertTrue(result is RadioResponseResult.ChannelResponse) assertEquals("Main", result.channel.settings?.name) diff --git a/core/domain/src/commonTest/kotlin/org/meshtastic/core/domain/usecase/settings/RadioConfigUseCaseTest.kt b/core/domain/src/commonTest/kotlin/org/meshtastic/core/domain/usecase/settings/RadioConfigUseCaseTest.kt index 826034317f..ba9529d933 100644 --- a/core/domain/src/commonTest/kotlin/org/meshtastic/core/domain/usecase/settings/RadioConfigUseCaseTest.kt +++ b/core/domain/src/commonTest/kotlin/org/meshtastic/core/domain/usecase/settings/RadioConfigUseCaseTest.kt @@ -60,7 +60,7 @@ class RadioConfigUseCaseTest { @Test fun `setOwner calls radioController`() = runTest { - val user = User(long_name = "New Name") + val user = User.Builder().also { wb -> wb.long_name = "New Name" }.build() useCase.setOwner(1234, user) // Verify call implicitly or by adding tracking to FakeRadioController if needed. // FakeRadioController already has getPacketId returning 1. @@ -68,7 +68,16 @@ class RadioConfigUseCaseTest { @Test fun `setHamMode calls radioController and returns packetId`() = runTest { - val packetId = useCase.setHamMode(1234, HamParameters(call_sign = "KK7ABC", short_name = "KK7A")) + val packetId = + useCase.setHamMode( + 1234, + HamParameters.Builder() + .also { wb -> + wb.call_sign = "KK7ABC" + wb.short_name = "KK7A" + } + .build(), + ) assertEquals(1, packetId) } @@ -80,13 +89,19 @@ class RadioConfigUseCaseTest { @Test fun `setConfig calls radioController`() = runTest { - val config = Config(lora = Config.LoRaConfig(use_preset = true)) + val config = + Config.Builder() + .also { wb -> wb.lora = Config.LoRaConfig.Builder().also { wb -> wb.use_preset = true }.build() } + .build() useCase.setConfig(1234, config) } @Test fun `setModuleConfig calls radioController`() = runTest { - val config = ModuleConfig(mqtt = ModuleConfig.MQTTConfig(enabled = true)) + val config = + ModuleConfig.Builder() + .also { wb -> wb.mqtt = ModuleConfig.MQTTConfig.Builder().also { wb -> wb.enabled = true }.build() } + .build() useCase.setModuleConfig(1234, config) } @@ -111,7 +126,7 @@ class RadioConfigUseCaseTest { @Test fun `setRemoteChannel calls radioController`() = runTest { - useCase.setRemoteChannel(1234, org.meshtastic.proto.Channel()) + useCase.setRemoteChannel(1234, org.meshtastic.proto.Channel.Builder().build()) } @Test fun `getRingtone calls radioController`() = runTest { useCase.getRingtone(1234) } diff --git a/core/model/src/androidDeviceTest/kotlin/org/meshtastic/core/model/ChannelTest.kt b/core/model/src/androidDeviceTest/kotlin/org/meshtastic/core/model/ChannelTest.kt index 656afc56aa..2d9cdf9061 100644 --- a/core/model/src/androidDeviceTest/kotlin/org/meshtastic/core/model/ChannelTest.kt +++ b/core/model/src/androidDeviceTest/kotlin/org/meshtastic/core/model/ChannelTest.kt @@ -30,7 +30,13 @@ import org.meshtastic.proto.Config class ChannelTest { @Test fun channelUrlGood() { - val ch = ChannelSet(settings = listOf(Channel.default.settings), lora_config = Channel.default.loraConfig) + val ch = + ChannelSet.Builder() + .also { wb -> + wb.settings = listOf(Channel.default.settings) + wb.lora_config = Channel.default.loraConfig + } + .build() val channelUrl = ch.getChannelUrl() Assert.assertTrue(channelUrl.toString().startsWith(CHANNEL_URL_PREFIX)) @@ -71,7 +77,14 @@ class ChannelTest { // Skip UNRECOGNIZED if it exists (Wire generates it sometimes) or generic UNSET values if applicable if (preset.name == "UNSET" || preset.name == "UNRECOGNIZED") return@forEach - val loraConfig = Channel.default.loraConfig.copy(use_preset = true, modem_preset = preset) + val loraConfig = + Channel.default.loraConfig + .newBuilder() + .also { wb -> + wb.use_preset = true + wb.modem_preset = preset + } + .build() val channel = Channel(loraConfig = loraConfig) // We want to ensure it is NOT "Invalid" diff --git a/core/model/src/androidDeviceTest/kotlin/org/meshtastic/core/model/util/SharedContactTest.kt b/core/model/src/androidDeviceTest/kotlin/org/meshtastic/core/model/util/SharedContactTest.kt index f4bdcbc07d..c8f9ff8d29 100644 --- a/core/model/src/androidDeviceTest/kotlin/org/meshtastic/core/model/util/SharedContactTest.kt +++ b/core/model/src/androidDeviceTest/kotlin/org/meshtastic/core/model/util/SharedContactTest.kt @@ -29,7 +29,19 @@ class SharedContactTest { @Test fun testSharedContactUrlRoundTrip() { - val original = SharedContact(user = User(long_name = "Suzume", short_name = "SZ"), node_num = 12345) + val original = + SharedContact.Builder() + .also { wb -> + wb.user = + User.Builder() + .also { wb -> + wb.long_name = "Suzume" + wb.short_name = "SZ" + } + .build() + wb.node_num = 12345 + } + .build() val url = original.getSharedContactUrl() val parsed = url.toSharedContact() @@ -40,7 +52,13 @@ class SharedContactTest { @Test fun testWwwHostIsAccepted() { - val original = SharedContact(user = User(long_name = "Suzume"), node_num = 12345) + val original = + SharedContact.Builder() + .also { wb -> + wb.user = User.Builder().also { wb -> wb.long_name = "Suzume" }.build() + wb.node_num = 12345 + } + .build() val urlStr = original.getSharedContactUrl().toString().replace("meshtastic.org", "www.meshtastic.org") val url = Uri.parse(urlStr) val contact = url.toSharedContact() @@ -49,7 +67,13 @@ class SharedContactTest { @Test fun testLongPathIsAccepted() { - val original = SharedContact(user = User(long_name = "Suzume"), node_num = 12345) + val original = + SharedContact.Builder() + .also { wb -> + wb.user = User.Builder().also { wb -> wb.long_name = "Suzume" }.build() + wb.node_num = 12345 + } + .build() val urlStr = original.getSharedContactUrl().toString().replace("/v/", "/contact/v/") val url = Uri.parse(urlStr) val contact = url.toSharedContact() @@ -58,7 +82,13 @@ class SharedContactTest { @Test(expected = MalformedMeshtasticUrlException::class) fun testInvalidHostThrows() { - val original = SharedContact(user = User(long_name = "Suzume"), node_num = 12345) + val original = + SharedContact.Builder() + .also { wb -> + wb.user = User.Builder().also { wb -> wb.long_name = "Suzume" }.build() + wb.node_num = 12345 + } + .build() val urlStr = original.getSharedContactUrl().toString().replace("meshtastic.org", "example.com") val url = Uri.parse(urlStr) url.toSharedContact() @@ -66,7 +96,13 @@ class SharedContactTest { @Test(expected = MalformedMeshtasticUrlException::class) fun testInvalidPathThrows() { - val original = SharedContact(user = User(long_name = "Suzume"), node_num = 12345) + val original = + SharedContact.Builder() + .also { wb -> + wb.user = User.Builder().also { wb -> wb.long_name = "Suzume" }.build() + wb.node_num = 12345 + } + .build() val urlStr = original.getSharedContactUrl().toString().replace("/v/", "/wrong/") val url = Uri.parse(urlStr) url.toSharedContact() diff --git a/core/model/src/commonMain/kotlin/org/meshtastic/core/model/Capabilities.kt b/core/model/src/commonMain/kotlin/org/meshtastic/core/model/Capabilities.kt index c3e03fb507..5e129b863d 100644 --- a/core/model/src/commonMain/kotlin/org/meshtastic/core/model/Capabilities.kt +++ b/core/model/src/commonMain/kotlin/org/meshtastic/core/model/Capabilities.kt @@ -98,9 +98,9 @@ data class Capabilities(val firmwareVersion: String?, internal val forceEnableAl val supportsMeshBeacon = atLeast(V2_8_0) /** - * Whether the node reports [NodeInfo.heard_on_current_lora] - whether it has heard each node since its current LoRa - * config took effect. Gated to [UNRELEASED] until the firmware side ships. Older firmware never sends the field, - * and a proto3 bool defaults to false, so an ungated read marks every node as unheard. + * Whether the node reports [NodeInfo.heard_on_current_lora] - whether it has heard each node over RF on the LoRa + * configuration it is using now. Gated to [UNRELEASED] until the firmware side ships. Older firmware never sends + * the field, and a proto3 bool defaults to false, so an ungated read marks every node as unheard. * * Deliberately outside [forceEnableAll]: every other capability being wrong in a debug build shows a UI the * firmware ignores, but this one being wrong persists false into the node DB and offers real nodes for removal. diff --git a/core/model/src/commonMain/kotlin/org/meshtastic/core/model/Channel.kt b/core/model/src/commonMain/kotlin/org/meshtastic/core/model/Channel.kt index b68025b0bb..6a8aa77fa8 100644 --- a/core/model/src/commonMain/kotlin/org/meshtastic/core/model/Channel.kt +++ b/core/model/src/commonMain/kotlin/org/meshtastic/core/model/Channel.kt @@ -51,12 +51,22 @@ data class Channel(val settings: ChannelSettings = default.settings, val loraCon private val cleartextPSK = ByteString.EMPTY private val defaultPSK = byteArrayOfInts(1) // a shortstring code to indicate we need our default PSK + // Folded into the hash of an AEAD channel so it routes apart from a non-AEAD channel with the same name + PSK. + private const val AEAD_HASH_MARKER = 0xAE + // The default channel that devices ship with val default = Channel( - ChannelSettings(psk = defaultPSK.toByteString()), + ChannelSettings.Builder().also { wb -> wb.psk = defaultPSK.toByteString() }.build(), // references: NodeDB::installDefaultConfig / Channels::initDefaultChannel - LoRaConfig(use_preset = true, modem_preset = ModemPreset.LONG_FAST, hop_limit = 3, tx_enabled = true), + LoRaConfig.Builder() + .also { wb -> + wb.use_preset = true + wb.modem_preset = ModemPreset.LONG_FAST + wb.hop_limit = 3 + wb.tx_enabled = true + } + .build(), ) fun getRandomKey(size: Int = 32): ByteString = platformRandomBytes(size).toByteString() @@ -118,9 +128,15 @@ data class Channel(val settings: ChannelSettings = default.settings, val loraCon } } - /** Given a channel name and psk, return the (0 to 255) hash for that channel */ + /** + * The (0 to 255) on-air hash, byte-identical to firmware Channels::generateHash: xor of name and PSK, then 0xAE + * folded in when use_aead is set. Identity (equals) stays name + PSK; the AEAD bit only moves the hash. + */ val hash: Int - get() = xorHash(name.encodeToByteArray()) xor xorHash(psk.toByteArray()) + get() { + val base = xorHash(name.encodeToByteArray()) xor xorHash(psk.toByteArray()) + return if (settings.use_aead) base xor AEAD_HASH_MARKER else base + } val channelNum: Int get() = loraConfig.channelNum(name) diff --git a/core/model/src/commonMain/kotlin/org/meshtastic/core/model/ChannelOption.kt b/core/model/src/commonMain/kotlin/org/meshtastic/core/model/ChannelOption.kt index a81eddf337..a99cdbbb14 100644 --- a/core/model/src/commonMain/kotlin/org/meshtastic/core/model/ChannelOption.kt +++ b/core/model/src/commonMain/kotlin/org/meshtastic/core/model/ChannelOption.kt @@ -115,6 +115,9 @@ internal fun LoRaConfig.radioFreq(channelNum: Int): Float { */ private val FIRMWARE_2_8 = DeviceVersion("2.8.0") +/** The firmware release that introduced the LONG_TURBO preset (firmware#8985). */ +private val FIRMWARE_2_7_17 = DeviceVersion("2.7.17") + /** * Regulatory regions for radio usage * @@ -460,39 +463,52 @@ enum class RegionInfo( enum class ChannelOption( val modemPreset: ModemPreset, val bandwidth: Float, - val snrLimit: Float, + val spreadingFactor: Int, val minFirmware: DeviceVersion? = null, ) { // Grouped by range and speed for better readability. - // snrLimit = demodulation floor for the preset's spreading factor (see [ModemPreset.snrLimit]). + // Preset parameters mirror firmware's modemPresetToParams (src/mesh/MeshRadio.h). // minFirmware = first firmware release whose preset table has the entry ([Capabilities.supportsPreset]); // older firmware silently falls back to LONG_FAST when sent an unknown preset. - VERY_LONG_SLOW(ModemPreset.VERY_LONG_SLOW, 0.0625f, snrLimit = -20f), // SF12 - LONG_TURBO(ModemPreset.LONG_TURBO, 0.500f, snrLimit = -12.5f), // SF9 - LONG_FAST(ModemPreset.LONG_FAST, 0.250f, snrLimit = -17.5f), // SF11 - LONG_MODERATE(ModemPreset.LONG_MODERATE, 0.125f, snrLimit = -17.5f), // SF11 - // SF12: physically -20 dB. NB: Meshtastic-Apple's snrLimit() returns -7.5 here, which is the SF7 value - // and an apparent bug — see meshtastic/Meshtastic-Android#5446. - LONG_SLOW(ModemPreset.LONG_SLOW, 0.125f, snrLimit = -20f), // SF12 - MEDIUM_FAST(ModemPreset.MEDIUM_FAST, 0.250f, snrLimit = -12.5f), // SF9 - MEDIUM_SLOW(ModemPreset.MEDIUM_SLOW, 0.250f, snrLimit = -15f), // SF10 - MEDIUM_TURBO(ModemPreset.MEDIUM_TURBO, 0.500f, snrLimit = -12.5f, minFirmware = FIRMWARE_2_8), // SF9 - SHORT_FAST(ModemPreset.SHORT_FAST, 0.250f, snrLimit = -7.5f), // SF7 - SHORT_SLOW(ModemPreset.SHORT_SLOW, 0.250f, snrLimit = -10f), // SF8 - SHORT_TURBO(ModemPreset.SHORT_TURBO, 0.500f, snrLimit = -7.5f), // SF7 - LITE_FAST(ModemPreset.LITE_FAST, 0.125f, snrLimit = -12.5f, minFirmware = FIRMWARE_2_8), - LITE_SLOW(ModemPreset.LITE_SLOW, 0.125f, snrLimit = -15f, minFirmware = FIRMWARE_2_8), - NARROW_FAST(ModemPreset.NARROW_FAST, 0.0625f, snrLimit = -10f, minFirmware = FIRMWARE_2_8), - NARROW_SLOW(ModemPreset.NARROW_SLOW, 0.0625f, snrLimit = -12.5f, minFirmware = FIRMWARE_2_8), + // Historical parameters for firmware predating the removal of VERY_LONG_SLOW. + VERY_LONG_SLOW(ModemPreset.VERY_LONG_SLOW, 0.0625f, spreadingFactor = 12), + LONG_TURBO(ModemPreset.LONG_TURBO, 0.500f, spreadingFactor = 11, minFirmware = FIRMWARE_2_7_17), + LONG_FAST(ModemPreset.LONG_FAST, 0.250f, spreadingFactor = 11), + LONG_MODERATE(ModemPreset.LONG_MODERATE, 0.125f, spreadingFactor = 11), + LONG_SLOW(ModemPreset.LONG_SLOW, 0.125f, spreadingFactor = 12), + MEDIUM_FAST(ModemPreset.MEDIUM_FAST, 0.250f, spreadingFactor = 9), + MEDIUM_SLOW(ModemPreset.MEDIUM_SLOW, 0.250f, spreadingFactor = 10), + MEDIUM_TURBO(ModemPreset.MEDIUM_TURBO, 0.500f, spreadingFactor = 9, minFirmware = FIRMWARE_2_8), + SHORT_FAST(ModemPreset.SHORT_FAST, 0.250f, spreadingFactor = 7), + SHORT_SLOW(ModemPreset.SHORT_SLOW, 0.250f, spreadingFactor = 8), + SHORT_TURBO(ModemPreset.SHORT_TURBO, 0.500f, spreadingFactor = 7), + LITE_FAST(ModemPreset.LITE_FAST, 0.125f, spreadingFactor = 9, minFirmware = FIRMWARE_2_8), + LITE_SLOW(ModemPreset.LITE_SLOW, 0.125f, spreadingFactor = 10, minFirmware = FIRMWARE_2_8), + NARROW_FAST(ModemPreset.NARROW_FAST, 0.0625f, spreadingFactor = 7, minFirmware = FIRMWARE_2_8), + NARROW_SLOW(ModemPreset.NARROW_SLOW, 0.0625f, spreadingFactor = 8, minFirmware = FIRMWARE_2_8), // 15.625 kHz LoRa bandwidth (firmware modemPresetToParams; the proto's "20kHz" is the // padded channel spacing, not the modem bandwidth used for numChannels/radioFreq math). - TINY_FAST(ModemPreset.TINY_FAST, 0.015625f, snrLimit = -7.5f, minFirmware = FIRMWARE_2_8), // SF7 - TINY_SLOW(ModemPreset.TINY_SLOW, 0.015625f, snrLimit = -10f, minFirmware = FIRMWARE_2_8), // SF8 + TINY_FAST(ModemPreset.TINY_FAST, 0.015625f, spreadingFactor = 7, minFirmware = FIRMWARE_2_8), + TINY_SLOW(ModemPreset.TINY_SLOW, 0.015625f, spreadingFactor = 8, minFirmware = FIRMWARE_2_8), ; + // Semtech demodulation floor: -7.5 dB at SF7, improving 2.5 dB per SF step. + // Bandwidth changes sensitivity in dBm, not this SNR threshold. + val snrLimit: Float + get() = SNR_FLOOR_SF7_DB - SNR_FLOOR_PER_SF_DB * (spreadingFactor - MIN_SPREADING_FACTOR) + companion object { + /** SF7's demodulation floor, the anchor for [snrLimit]. */ + private const val SNR_FLOOR_SF7_DB = -7.5f + + /** Each spreading-factor step doubles the symbol length, buying 2.5 dB of demodulation floor. */ + private const val SNR_FLOOR_PER_SF_DB = 2.5f + + /** The lowest spreading factor any Meshtastic preset uses, and the anchor [SNR_FLOOR_SF7_DB] describes. */ + private const val MIN_SPREADING_FACTOR = 7 + /** The default channel option for new configurations. */ val DEFAULT = LONG_FAST diff --git a/core/model/src/commonMain/kotlin/org/meshtastic/core/model/Contact.kt b/core/model/src/commonMain/kotlin/org/meshtastic/core/model/Contact.kt index acb63e166f..4abbe674cf 100644 --- a/core/model/src/commonMain/kotlin/org/meshtastic/core/model/Contact.kt +++ b/core/model/src/commonMain/kotlin/org/meshtastic/core/model/Contact.kt @@ -31,6 +31,8 @@ data class Contact( val draft: String = "", /** Sorts above unpinned conversations within the same list section. */ val isPinned: Boolean = false, + /** True when this channel is no longer configured on the radio: history is kept, but nothing can be sent. */ + val isRetired: Boolean = false, ) data class ContactSettings( @@ -42,4 +44,6 @@ data class ContactSettings( val isMuted: Boolean = false, val draft: String = "", val pinned: Boolean = false, + /** The conversation's own label, set only for a retired channel whose slot no longer names it. */ + val displayName: String = "", ) diff --git a/core/model/src/commonMain/kotlin/org/meshtastic/core/model/MeshLog.kt b/core/model/src/commonMain/kotlin/org/meshtastic/core/model/MeshLog.kt index c3a04d4c38..b23ab73953 100644 --- a/core/model/src/commonMain/kotlin/org/meshtastic/core/model/MeshLog.kt +++ b/core/model/src/commonMain/kotlin/org/meshtastic/core/model/MeshLog.kt @@ -36,7 +36,7 @@ data class MeshLog( val raw_message: String, val fromNum: Int = 0, val portNum: Int = 0, - val fromRadio: FromRadio = FromRadio(), + val fromRadio: FromRadio = FromRadio.Builder().build(), ) { val meshPacket = fromRadio.packet diff --git a/core/model/src/commonMain/kotlin/org/meshtastic/core/model/Node.kt b/core/model/src/commonMain/kotlin/org/meshtastic/core/model/Node.kt index e312cbc3d1..a16a0bb693 100644 --- a/core/model/src/commonMain/kotlin/org/meshtastic/core/model/Node.kt +++ b/core/model/src/commonMain/kotlin/org/meshtastic/core/model/Node.kt @@ -35,6 +35,7 @@ import org.meshtastic.proto.MeshPacket import org.meshtastic.proto.Paxcount import org.meshtastic.proto.Position import org.meshtastic.proto.PowerMetrics +import org.meshtastic.proto.SoilWaterMetrics import org.meshtastic.proto.User /** @@ -46,22 +47,23 @@ import org.meshtastic.proto.User data class Node( val num: Int, val metadata: DeviceMetadata? = null, - val user: User = User(), - val position: Position = Position(), + val user: User = User.Builder().build(), + val position: Position = Position.Builder().build(), val snr: Float = Float.MAX_VALUE, val rssi: Int = Int.MAX_VALUE, val lastHeard: Int = 0, // the last time we've seen this node in secs since 1970 - val deviceMetrics: DeviceMetrics = DeviceMetrics(), + val deviceMetrics: DeviceMetrics = DeviceMetrics.Builder().build(), val channel: Int = 0, val viaMqtt: Boolean = false, val hopsAway: Int = -1, val isFavorite: Boolean = false, val isIgnored: Boolean = false, val isMuted: Boolean = false, - val environmentMetrics: EnvironmentMetrics = EnvironmentMetrics(), - val powerMetrics: PowerMetrics = PowerMetrics(), - val airQualityMetrics: AirQualityMetrics = AirQualityMetrics(), - val paxcounter: Paxcount = Paxcount(), + val environmentMetrics: EnvironmentMetrics = EnvironmentMetrics.Builder().build(), + val powerMetrics: PowerMetrics = PowerMetrics.Builder().build(), + val airQualityMetrics: AirQualityMetrics = AirQualityMetrics.Builder().build(), + val soilWaterMetrics: SoilWaterMetrics = SoilWaterMetrics.Builder().build(), + val paxcounter: Paxcount = Paxcount.Builder().build(), val publicKey: ByteString? = null, val notes: String = "", /** User-editable labels per power-metrics channel (e.g. "Solar", "Battery"), indexed by channel - 1. */ @@ -69,14 +71,43 @@ data class Node( val manuallyVerified: Boolean = false, /** True when this node signs its broadcasts via XEdDSA (NodeInfo.has_xeddsa_signed). Automatic trust. */ val signsPackets: Boolean = false, - /** False when the radio has not heard this node since its current LoRa config took effect. */ + /** + * False when the radio has not heard this node over RF on the LoRa configuration it is using now. The radio derives + * this rather than storing it, so returning to a configuration restores the previous answers. Ask + * [isUnheardOnCurrentLora] rather than this flag: false alone does not mean the node became unreachable. + */ val heardOnCurrentLora: Boolean = true, val nodeStatus: String? = null, /** The transport mechanism this node was last heard over (see [MeshPacket.TransportMechanism]). */ val lastTransport: Int = 0, + /** + * False once a different public key arrived for a node one is already stored for. The stored key stands; this + * records the refusal. See [mismatchKey], which is what the UI asks. + */ + val keyMatch: Boolean = true, + /** The key a mismatch refused, kept so the warning can name it. Null whenever [keyMatch] is true. */ + val newPublicKey: ByteString? = null, ) { val capabilities: Capabilities by lazy { Capabilities(metadata?.firmware_version) } + /** + * True when the radio does not report having heard this node over RF on the LoRa configuration in force now, and it + * is not an [viaMqtt] node. + * + * This is not a claim that the node was ever heard over RF. `heard_on_current_lora` is a single bool, and the radio + * does not expose its "heard over RF at least once" bit separately, so a node heard under different settings and a + * node never heard at all (one added as a shared contact, say) are indistinguishable here. Both read true. Callers + * that must not act on the second case need another signal - the removal offer relies on favourites, which is what + * firmware marks a contact. + * + * [viaMqtt] nodes are excluded because they arrive over the internet rather than over our own radio, so the radio + * never marks them heard over RF and [heardOnCurrentLora] is false for them permanently - not because a setting + * changed. Presenting those as unreachable would badge every node on an MQTT-uplinked mesh and offer it for + * removal, and removing one is pointless because it returns on the next uplinked packet. + */ + val isUnheardOnCurrentLora: Boolean + get() = !heardOnCurrentLora && !viaMqtt + val isOnline: Boolean get() = isOnline(onlineTimeThreshold()) @@ -92,8 +123,16 @@ data class Node( val hasPKC get() = (publicKey ?: user.public_key).size > 0 + /** + * True when a different public key has arrived for this node than the one on file. + * + * Two shapes, because the app used to record a mismatch by overwriting the stored key with [ERROR_BYTE_STRING]. It + * now keeps the key and clears [keyMatch] instead — firmware drops the NodeInfo outright rather than overwrite, so + * destroying the trusted key handed any mesh or MQTT peer a way to break PKC direct messages to a contact. Rows + * written before that change still carry the sentinel, so both still read as a mismatch. + */ val mismatchKey - get() = (publicKey ?: user.public_key) == ERROR_BYTE_STRING + get() = !keyMatch || (publicKey ?: user.public_key) == ERROR_BYTE_STRING /** * Last measured SNR in dB, or null when this node has no reading yet ([snr] still holds [SNR_UNSET]). @@ -110,13 +149,16 @@ data class Node( get() = rssi.takeIf { it != RSSI_UNSET } val hasEnvironmentMetrics: Boolean - get() = environmentMetrics != EnvironmentMetrics() + get() = environmentMetrics != EnvironmentMetrics.Builder().build() val hasPowerMetrics: Boolean - get() = powerMetrics != PowerMetrics() + get() = powerMetrics != PowerMetrics.Builder().build() val hasAirQualityMetrics: Boolean - get() = airQualityMetrics != AirQualityMetrics() + get() = airQualityMetrics != AirQualityMetrics.Builder().build() + + val hasSoilWaterMetrics: Boolean + get() = soilWaterMetrics != SoilWaterMetrics.Builder().build() val batteryLevel get() = deviceMetrics.battery_level @@ -230,7 +272,14 @@ data class Node( val safeUserId = userId.padStart(DEFAULT_ID_SUFFIX_LENGTH, '0').takeLast(DEFAULT_ID_SUFFIX_LENGTH) val longName = "$fallbackNamePrefix $safeUserId" val defaultUser = - User(id = userId, long_name = longName, short_name = safeUserId, hw_model = HardwareModel.UNSET) + User.Builder() + .also { wb -> + wb.id = userId + wb.long_name = longName + wb.short_name = safeUserId + wb.hw_model = HardwareModel.UNSET + } + .build() return Node(num = nodeNum, user = defaultUser) } } diff --git a/core/model/src/commonMain/kotlin/org/meshtastic/core/model/NodeAddress.kt b/core/model/src/commonMain/kotlin/org/meshtastic/core/model/NodeAddress.kt index f6faa5d113..628e46dd28 100644 --- a/core/model/src/commonMain/kotlin/org/meshtastic/core/model/NodeAddress.kt +++ b/core/model/src/commonMain/kotlin/org/meshtastic/core/model/NodeAddress.kt @@ -92,35 +92,68 @@ sealed class NodeAddress { * Type-safe wrapper for contact key strings (channel index + node address). * * Contact keys are persisted as strings in the format `""` (e.g. `"0^all"`, `"1!a1b2c3d4"`). + * + * A broadcast conversation whose channel is no longer configured is *retired* rather than deleted, and takes the form + * `"~^all"` — see [retiredBroadcast]. A retired key has no [channelOrNull], which is what makes every "which + * live slot is this?" site treat it as having none without needing to know the form exists. */ @JvmInline value class ContactKey(val value: String) { /** * The channel index if the key carries a leading channel digit, or `null` for a legacy unprefixed direct-message - * key. Callers that must distinguish "channel 0" from "no channel prefix" (e.g. PKI vs legacy DM routing) need this - * rather than [channel]. + * key or a retired conversation. Callers that must distinguish "channel 0" from "no channel prefix" (e.g. PKI vs + * legacy DM routing) need this rather than [channel]. */ val channelOrNull: Int? - get() = value.firstOrNull()?.takeIf { it.isDigit() }?.digitToInt() + get() = if (isRetired) null else value.firstOrNull()?.takeIf { it.isDigit() }?.digitToInt() /** The channel index (first character). Returns 0 if the key is empty or has no channel digit. */ val channel: Int get() = channelOrNull ?: 0 + /** True when this conversation's channel is no longer configured on the radio and its history is read-only. */ + val isRetired: Boolean + get() = value.startsWith(RETIRED_PREFIX) + + /** The channel-identity token a retired conversation is parked under, or null when this key is live. */ + val retiredToken: String? + get() = if (isRetired) value.drop(1).takeWhile { it != ADDRESS_SEPARATOR } else null + /** - * The node address portion: everything after the channel digit, or the whole key when there is no channel prefix. - * Empty if the key is empty. + * The node address portion: everything after the channel digit (or the retired token), or the whole key when there + * is no prefix at all. Empty if the key is empty. */ val addressString: String - get() = if (channelOrNull != null) value.substring(1) else value + get() = + when { + isRetired -> value.dropWhile { it != ADDRESS_SEPARATOR } + channelOrNull != null -> value.substring(1) + else -> value + } /** Parsed [NodeAddress] for the contact. */ val address: NodeAddress get() = NodeAddress.fromString(addressString) companion object { + /** + * Marks a retired conversation. `~` is RFC 3986 *unreserved*, so a retired key survives the deep links, + * launcher shortcut ids and notification extras that carry contact keys verbatim. + */ + const val RETIRED_PREFIX = "~" + + private const val ADDRESS_SEPARATOR = '^' + /** Create a broadcast contact key for the given channel. */ fun broadcast(channel: Int = 0): ContactKey = NodeAddress.Broadcast.toContactKey(channel) + + /** + * Create the key a broadcast conversation is parked under once its channel leaves the radio's channel set. + * + * [token] is a `ChannelIdentity.token`, so re-adding the same channel later resolves back to this exact key and + * the history returns to the live slot. + */ + fun retiredBroadcast(token: String): ContactKey = ContactKey("$RETIRED_PREFIX$token${NodeAddress.ID_BROADCAST}") } } diff --git a/core/model/src/commonMain/kotlin/org/meshtastic/core/model/NodeSecurityIndicator.kt b/core/model/src/commonMain/kotlin/org/meshtastic/core/model/NodeSecurityIndicator.kt new file mode 100644 index 0000000000..c9c46384ab --- /dev/null +++ b/core/model/src/commonMain/kotlin/org/meshtastic/core/model/NodeSecurityIndicator.kt @@ -0,0 +1,88 @@ +/* + * 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 . + */ +package org.meshtastic.core.model + +/** + * Which security indicator a surface shows beside a node's name (design#149). + * + * 2.8 nodes show verification state instead of the PKI lock, older and unversioned ones keep the locks, and a mismatch + * shows at any version. Every surface goes through [of] so they cannot drift apart. + */ +enum class NodeSecurityIndicator { + /** The node's latest key does not match the stored one. Outranks every other state, at any version. */ + KEY_MISMATCH, + + /** The user verified this node's key in person, or it is the connected radio itself. */ + VERIFIED_CONTACT, + + /** + * The radio verified this node's XEdDSA-signed broadcast. Every 2.8 node signs, so it is that firmware's baseline. + */ + SIGNED_NODE, + + /** Pre-2.8 or unknown firmware: a public key is on file and matches. */ + PUBLIC_KEY, + + /** Pre-2.8 or unknown firmware: no public key on file. Not a channel-key fallback, the send is refused (#145). */ + NO_PUBLIC_KEY, + + ; + + companion object { + /** The indicator for [node], where [isOwnNode] marks the row for the connected radio. */ + fun of(node: Node, isOwnNode: Boolean = false): NodeSecurityIndicator = resolve( + firmwareVersion = node.metadata?.firmware_version, + hasPublicKey = node.hasPKC, + mismatchKey = node.mismatchKey, + signed = node.signsPackets, + verified = node.manuallyVerified, + isOwnNode = isOwnNode, + ) + + /** Decides the indicator from snapshot fields alone, so the precedence is testable without a [Node]. */ + fun resolve( + firmwareVersion: String?, + hasPublicKey: Boolean, + mismatchKey: Boolean, + signed: Boolean = false, + verified: Boolean = false, + isOwnNode: Boolean = false, + ): NodeSecurityIndicator = when { + mismatchKey -> KEY_MISMATCH + + // Neither is gated on 2.8: you hold your own radio's key, and meeting someone in person stays true. + verified || isOwnNode -> VERIFIED_CONTACT + + // A heard signature is the fact itself, the version gate is only its proxy. + signed || signsBroadcasts(firmwareVersion) -> SIGNED_NODE + + hasPublicKey -> PUBLIC_KEY + + else -> NO_PUBLIC_KEY + } + + /** + * True when [firmwareVersion] is known and is 2.8 or newer. + * + * Strict on unknown, unlike the permissive [Capabilities] gates. [DeviceVersion] pads a two-component version, + * so a radio reporting "2.8" still reads as 2.8. + */ + fun signsBroadcasts(firmwareVersion: String?): Boolean = + firmwareVersion?.let { DeviceVersion(it) }?.takeIf { it.isValid }?.let { it >= DeviceVersion("2.8.0") } + ?: false + } +} diff --git a/core/model/src/commonMain/kotlin/org/meshtastic/core/model/RegionPresetConstraint.kt b/core/model/src/commonMain/kotlin/org/meshtastic/core/model/RegionPresetConstraint.kt index b5b00023cc..a3194ec6b9 100644 --- a/core/model/src/commonMain/kotlin/org/meshtastic/core/model/RegionPresetConstraint.kt +++ b/core/model/src/commonMain/kotlin/org/meshtastic/core/model/RegionPresetConstraint.kt @@ -79,9 +79,10 @@ fun LoRaRegionPresetMap?.repairPresetFor(region: RegionCode, current: ModemPrese * [current] selected. * * Keeps [current] (legality-repaired via [repairPresetFor]) except at fresh setup: when [previousRegion] is UNSET and - * [current] is the [ModemPreset.LONG_FAST] placeholder a factory-flashed node reports (proto default), the region's - * advertised default is adopted instead (the firmware map's when present, else the app's built-in default). Any other - * preset at UNSET was set deliberately (e.g. a vendor build pinning USERPREFS_LORACONFIG_MODEM_PRESET) and is kept. + * [current] is the [ModemPreset.LONG_FAST] placeholder a factory-flashed node reports (proto default), a region default + * is adopted instead: the app's [firstSetupDefaultFor] where the firmware map confirms it, else the map's own default. + * Any other preset at UNSET was set deliberately (e.g. a vendor build pinning USERPREFS_LORACONFIG_MODEM_PRESET) and is + * kept. * * A build that pins a preset can also advertise it as an UNSET map entry (firmware #11507), stating outright that the * preset is deliberate; that keeps even a pinned LONG_FAST, which the placeholder heuristic alone cannot distinguish. @@ -95,8 +96,8 @@ fun LoRaRegionPresetMap?.presetForRegionChange( val pinned = constraintFor(RegionCode.UNSET) != null val freshSetup = previousRegion == RegionCode.UNSET && !pinned && current == ModemPreset.LONG_FAST return if (freshSetup) { - // Re-repair the adopted default: a malformed map's advertised default may not be in its own legal set. - val preferred = constraintFor(newRegion)?.defaultPreset ?: defaultPresetFor(newRegion) ?: repaired + // Re-repaired: a malformed map's advertised default may not be in its own legal set. + val preferred = firstSetupDefaultFor(newRegion) ?: constraintFor(newRegion)?.defaultPreset ?: repaired repairPresetFor(newRegion, preferred) } else { repaired @@ -104,9 +105,9 @@ fun LoRaRegionPresetMap?.presetForRegionChange( } /** - * The app's built-in default modem presets for regions whose default differs from the global [ChannelOption.DEFAULT]. - * Mirrors the per-region defaults newer firmware advertises in [LoRaRegionPresetMap], so the default channel and a - * fresh setup over old firmware (which sends no map) land on the same preset a new node would. + * The app's first-setup default modem presets, for regions whose default differs from the global + * [ChannelOption.DEFAULT]. Firmware's [LoRaRegionPresetMap] advertises only its region-table default, which for US is + * not what its own on-device chooser installs on a first-ever setup, so these override the advertised default. */ private val REGION_DEFAULT_PRESETS = mapOf(RegionCode.US to ModemPreset.LONG_TURBO) @@ -115,3 +116,15 @@ private val REGION_DEFAULT_PRESETS = mapOf(RegionCode.US to ModemPreset.LONG_TUR * should fall back to [ChannelOption.DEFAULT] / the current preset. */ fun defaultPresetFor(region: RegionCode): ModemPreset? = REGION_DEFAULT_PRESETS[region] + +/** + * The preset a first-ever setup in [region] should install, or `null` to leave the choice alone. + * + * Gated on the firmware map: an unconstrained node is running firmware whose own region chooser installs the + * region-table default, and overriding it there would make the same node set up from the phone and from its screen + * disagree. Only a preset the map lists as legal is adopted. + */ +fun LoRaRegionPresetMap?.firstSetupDefaultFor(region: RegionCode): ModemPreset? { + val constraint = constraintFor(region) ?: return null + return defaultPresetFor(region)?.takeIf { it in constraint.presets } +} diff --git a/core/model/src/commonMain/kotlin/org/meshtastic/core/model/RouteDiscovery.kt b/core/model/src/commonMain/kotlin/org/meshtastic/core/model/RouteDiscovery.kt index 71fdd87fe9..b112838b73 100644 --- a/core/model/src/commonMain/kotlin/org/meshtastic/core/model/RouteDiscovery.kt +++ b/core/model/src/commonMain/kotlin/org/meshtastic/core/model/RouteDiscovery.kt @@ -41,15 +41,18 @@ val MeshPacket.fullRouteDiscovery: RouteDiscovery? val hopStartVal = hop_start val hasBitfield = (d.bitfield ?: 0) != 0 - return originalRd.copy( - route = fullRoute, - route_back = - if ((hopStartVal > 0 || hasBitfield) && originalRd.snr_back.isNotEmpty()) { - fullRouteBack - } else { - originalRd.route_back - }, - ) + return originalRd + .newBuilder() + .also { wb -> + wb.route = fullRoute + wb.route_back = + if ((hopStartVal > 0 || hasBitfield) && originalRd.snr_back.isNotEmpty()) { + fullRouteBack + } else { + originalRd.route_back + } + } + .build() } return null } diff --git a/core/model/src/commonMain/kotlin/org/meshtastic/core/model/util/ChannelReconciliation.kt b/core/model/src/commonMain/kotlin/org/meshtastic/core/model/util/ChannelReconciliation.kt new file mode 100644 index 0000000000..0f4d53dd34 --- /dev/null +++ b/core/model/src/commonMain/kotlin/org/meshtastic/core/model/util/ChannelReconciliation.kt @@ -0,0 +1,119 @@ +/* + * 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 . + */ +package org.meshtastic.core.model.util + +import org.meshtastic.proto.ChannelSet + +/** Where a broadcast conversation's messages live: a configured channel slot, or parked under a retired identity. */ +sealed interface ConversationSlot { + /** Slot [index] of the radio's channel set. */ + data class Live(val index: Int) : ConversationSlot + + /** No longer configured; parked under a `ChannelIdentity.token`. */ + data class Retired(val token: String) : ConversationSlot +} + +/** + * One conversation re-key. [displayName] is the effective channel name at the time of the change, kept so a retired + * conversation can still be labelled once its slot belongs to someone else. + */ +data class ChannelKeyChange(val from: ConversationSlot, val to: ConversationSlot, val displayName: String) + +/** + * Works out how conversations must move when the radio's channel set changes from [old] to [new]. + * + * Conversations are keyed by channel *index*, so a slot that changes occupant would otherwise show the previous + * channel's messages under the new channel. Matching is on resolved [ChannelIdentity] — the effective name (a blank + * name resolves to the modem preset's display name) and the expanded PSK (a one-byte marker expands to the full default + * key) — which is the same notion [ChannelSettings.isAlreadyJoined] and [normalizeReplacementSettings] use, and the + * same pair the firmware hashes into the on-air channel. So changing the modem preset *is* a channel change even though + * neither raw field moved, and changing region alone is not. + * + * Every change has a distinct [ChannelKeyChange.from], so applying the list needs a single pass over the rows and a + * slot swap needs no ordering care. + * + * @param retiredTokens tokens that currently hold parked conversations, so a re-added channel reclaims its history. + * @return the moves, retirements and reclaims to apply; slots that keep their occupant produce nothing. + */ +fun planChannelReconciliation( + old: ChannelSet, + new: ChannelSet, + retiredTokens: Set = emptySet(), +): List { + val oldIdentities = old.identitiesBySlot() + val newIdentities = new.identitiesBySlot() + val newSlotsByToken = newIdentities.entries.sortedBy { it.key }.groupBy({ it.value.token }, { it.key }) + + // Two slots can resolve to the same channel — a blank-named secondary carrying the one-byte default PSK has the + // primary's identity — so pairing has to be one-to-one. A slot that still holds its own identity claims itself + // before anything else moves, and every move then takes the lowest slot nobody has claimed yet. Without this a + // duplicate would be moved on top of the conversation that rightfully owns the slot. + val staysPut = oldIdentities.filterKeys { index -> newIdentities[index]?.token == oldIdentities[index]?.token } + val claimedSlots = staysPut.keys.toMutableSet() + + // Two ways the new set can be unknowable rather than empty, and retiring against either would archive live + // conversations: a null lora_config cannot resolve a blank channel name (it would read as "Custom"), and an empty + // channel list means the radio's channels have not arrived yet — a real radio always has a primary. Moves still + // apply; only the decision that a channel is *gone* waits for a set that can support it. + val canRetire = new.lora_config != null && new.settings.isNotEmpty() + + val changes = mutableListOf() + for ((oldIndex, identity) in oldIdentities) { + if (oldIndex in staysPut) continue + val newIndex = newSlotsByToken[identity.token]?.firstOrNull { slot -> slot !in claimedSlots } + val destination = + when { + newIndex != null -> ConversationSlot.Live(newIndex).also { claimedSlots += newIndex } + canRetire -> ConversationSlot.Retired(identity.token) + else -> null + } + if (destination != null) { + changes += ChannelKeyChange(ConversationSlot.Live(oldIndex), destination, identity.name) + } + } + + val reclaimed = mutableSetOf() + for ((newIndex, identity) in newIdentities.entries.sortedBy { it.key }) { + val token = identity.token + // Only reclaim what is actually parked, once, and never into a slot another conversation already holds or is + // moving into. Claiming the token last leaves it available to a later slot when this one is spoken for. + if (token in retiredTokens && newIndex !in claimedSlots && reclaimed.add(token)) { + claimedSlots += newIndex + changes += + ChannelKeyChange( + from = ConversationSlot.Retired(token), + to = ConversationSlot.Live(newIndex), + displayName = identity.name, + ) + } + } + return changes +} + +/** + * Resolved identity per occupied slot, or nothing at all when the LoRa config is unknown (a blank channel name is only + * resolvable through the modem preset). Blank padding secondaries are skipped: a gap left by a removed channel is + * padded with a bare `ChannelSettings`, and treating that as a channel would retire whatever sat there. Slot 0 is never + * padding; it always represents a real channel. + */ +private fun ChannelSet.identitiesBySlot(): Map { + val lora = lora_config ?: return emptyMap() + return settings + .withIndex() + .filter { (index, channel) -> index == 0 || !channel.isChannelPlaceholder() } + .associate { (index, channel) -> index to channel.channelIdentity(lora) } +} diff --git a/core/model/src/commonMain/kotlin/org/meshtastic/core/model/util/ChannelSet.kt b/core/model/src/commonMain/kotlin/org/meshtastic/core/model/util/ChannelSet.kt index c7a1054278..252e59f9f7 100644 --- a/core/model/src/commonMain/kotlin/org/meshtastic/core/model/util/ChannelSet.kt +++ b/core/model/src/commonMain/kotlin/org/meshtastic/core/model/util/ChannelSet.kt @@ -65,7 +65,7 @@ fun CommonUri.toChannelSet(): ChannelSet { } val shouldAdd = fragment?.substringAfter('?', "")?.addParameter() ?: getBooleanQueryParameter("add", false) - return if (shouldAdd) url.copy(lora_config = null) else url + return if (shouldAdd) url.newBuilder().also { wb -> wb.lora_config = null }.build() else url } private fun String.addParameter(): Boolean? = split('&') @@ -76,13 +76,13 @@ private fun String.addParameter(): Boolean? = split('&') /** @return A list of globally unique channel IDs usable with MQTT subscribe() */ val ChannelSet.subscribeList: List get() { - val loraConfig = this.lora_config ?: LoRaConfig() + val loraConfig = this.lora_config ?: LoRaConfig.Builder().build() return settings.filter { it.downlink_enabled }.map { Channel(it, loraConfig).name } } fun ChannelSet.getChannel(index: Int): Channel? = if (settings.size > index) { val s = settings[index] - Channel(s, lora_config ?: LoRaConfig()) + Channel(s, lora_config ?: LoRaConfig.Builder().build()) } else { null } @@ -112,7 +112,14 @@ fun ChannelSettings.isAlreadyJoined( ): Boolean { val lora = currentLora ?: return false if (currentChannels.isEmpty()) return false - val offerLora = lora.copy(use_preset = true, modem_preset = offerPreset ?: lora.modem_preset) + val offerLora = + lora + .newBuilder() + .also { wb -> + wb.use_preset = true + wb.modem_preset = offerPreset ?: lora.modem_preset + } + .build() val offerIdentity = channelIdentity(offerLora) return currentChannels.withIndex().any { (index, settings) -> (index == 0 || !settings.isChannelPlaceholder()) && settings.channelIdentity(lora) == offerIdentity @@ -162,7 +169,8 @@ fun MeshBeacon.beaconJoinOption(currentLora: LoRaConfig?, currentChannels: List< if (lora.channel_num != 0 || lora.numChannels <= 0) return BeaconJoinOption.SWITCH // Hash the *effective* names: an empty channel name resolves to its preset display name ("LongFast", …), which is // what firmware hashes for the slot — comparing raw "" on both sides would misclassify an unnamed primary. - val currentSlot = lora.channelNum(Channel(currentChannels.firstOrNull() ?: ChannelSettings(), lora).name) + val currentSlot = + lora.channelNum(Channel(currentChannels.firstOrNull() ?: ChannelSettings.Builder().build(), lora).name) val offeredSlot = lora.channelNum(Channel(offer, lora).name) return if (offeredSlot == currentSlot) BeaconJoinOption.ADD else BeaconJoinOption.SWITCH } @@ -188,7 +196,7 @@ fun MeshBeacon.beaconJoinOption(currentLora: LoRaConfig?, currentChannels: List< fun MeshBeacon.toJoinChannelSet(option: BeaconJoinOption, currentLora: LoRaConfig?): ChannelSet? { val offerChannel = (offer_channel ?: return null).withoutPositionSharing() return when (option) { - BeaconJoinOption.ADD -> ChannelSet(settings = listOf(offerChannel)) + BeaconJoinOption.ADD -> ChannelSet.Builder().also { wb -> wb.settings = listOf(offerChannel) }.build() BeaconJoinOption.SWITCH -> { // Start from the shared device default ([Channel.default] — use_preset=true, hop_limit/tx_enabled set, @@ -198,13 +206,21 @@ fun MeshBeacon.toJoinChannelSet(option: BeaconJoinOption, currentLora: LoRaConfi // firmware re-derive the frequency from the offered channel name (Apple FR-006). This is sent as a full // LoRaConfig replacement, so carry region (a zero region disables transmit); the beacon proto advertises // no other RF fields. - val base = currentLora ?: LoRaConfig() + val base = currentLora ?: LoRaConfig.Builder().build() val loraConfig = - Channel.default.loraConfig.copy( - modem_preset = offer_preset ?: base.modem_preset, - region = if (offer_region != RegionCode.UNSET) offer_region else base.region, - ) - ChannelSet(settings = listOf(offerChannel), lora_config = loraConfig) + Channel.default.loraConfig + .newBuilder() + .also { wb -> + wb.modem_preset = offer_preset ?: base.modem_preset + wb.region = if (offer_region != RegionCode.UNSET) offer_region else base.region + } + .build() + ChannelSet.Builder() + .also { wb -> + wb.settings = listOf(offerChannel) + wb.lora_config = loraConfig + } + .build() } BeaconJoinOption.NONE -> null @@ -215,8 +231,15 @@ fun MeshBeacon.toJoinChannelSet(option: BeaconJoinOption, currentLora: LoRaConfi * Zeroes `position_precision` on the offered channel so joining a beaconed mesh never broadcasts our location to a mesh * of strangers (privacy-first; Apple sets `positionPrecision = 0` on both add and switch). */ -private fun ChannelSettings.withoutPositionSharing(): ChannelSettings = - copy(module_settings = (module_settings ?: ModuleSettings()).copy(position_precision = 0)) +private fun ChannelSettings.withoutPositionSharing(): ChannelSettings = this.newBuilder() + .also { wb -> + wb.module_settings = + (module_settings ?: ModuleSettings.Builder().build()) + .newBuilder() + .also { wb -> wb.position_precision = 0 } + .build() + } + .build() /** * Return a URL that represents the [ChannelSet] @@ -224,7 +247,10 @@ private fun ChannelSettings.withoutPositionSharing(): ChannelSettings = * @param upperCasePrefix portions of the URL can be upper case to make for more efficient QR codes */ fun ChannelSet.getChannelUrl(upperCasePrefix: Boolean = false, shouldAdd: Boolean = false): CommonUri { - val channelBytes = ChannelSet.ADAPTER.encode(if (shouldAdd) copy(lora_config = null) else this) + val channelBytes = + ChannelSet.ADAPTER.encode( + if (shouldAdd) this.newBuilder().also { wb -> wb.lora_config = null }.build() else this, + ) val enc = channelBytes.toByteString().base64Url().replace("=", "") val p = if (upperCasePrefix) CHANNEL_URL_PREFIX.uppercase() else CHANNEL_URL_PREFIX val query = if (shouldAdd) "?add=true" else "" diff --git a/core/model/src/commonMain/kotlin/org/meshtastic/core/model/util/ChannelSetReplacement.kt b/core/model/src/commonMain/kotlin/org/meshtastic/core/model/util/ChannelSetReplacement.kt index c3442a8413..5315acff68 100644 --- a/core/model/src/commonMain/kotlin/org/meshtastic/core/model/util/ChannelSetReplacement.kt +++ b/core/model/src/commonMain/kotlin/org/meshtastic/core/model/util/ChannelSetReplacement.kt @@ -16,6 +16,7 @@ */ package org.meshtastic.core.model.util +import okio.Buffer import okio.ByteString import org.meshtastic.proto.Channel import org.meshtastic.proto.ChannelSet @@ -85,16 +86,18 @@ fun getChannelReplacementList( if (endIndex < 0) return@buildList for (index in 0..endIndex) { add( - Channel( - role = - when (index) { - 0 -> if (new.isEmpty()) Channel.Role.DISABLED else Channel.Role.PRIMARY - in 1..new.lastIndex -> Channel.Role.SECONDARY - else -> Channel.Role.DISABLED - }, - index = index, - settings = new.getOrNull(index) ?: ChannelSettings(), - ), + Channel.Builder() + .also { wb -> + wb.role = + when (index) { + 0 -> if (new.isEmpty()) Channel.Role.DISABLED else Channel.Role.PRIMARY + in 1..new.lastIndex -> Channel.Role.SECONDARY + else -> Channel.Role.DISABLED + } + wb.index = index + wb.settings = new.getOrNull(index) ?: ChannelSettings.Builder().build() + } + .build(), ) } } @@ -110,7 +113,7 @@ fun normalizeReplacementSettings( loraConfig: Config.LoRaConfig?, ): List { if (settings.size <= 1) return settings - val effectiveLora = loraConfig ?: Config.LoRaConfig() + val effectiveLora = loraConfig ?: Config.LoRaConfig.Builder().build() val primary = settings.first() val seen = mutableSetOf() if (!primary.isChannelPlaceholder()) { @@ -132,8 +135,35 @@ fun ChannelSettings.isChannelPlaceholder(): Boolean = name.isNullOrBlank() && ps data class ChannelIdentity(val name: String, val psk: ByteString) { // Never expose an effective PSK through diagnostics or an auto-generated data-class toString. override fun toString(): String = "ChannelIdentity(name=$name, psk=)" + + /** + * Stable opaque token for this identity, used to key a retired conversation (see `ContactKey`). + * + * Deliberately one-way and truncated: a contact key reaches logs and deep links, so it must never carry a PSK or be + * reversible into one. The name length is prefixed so `("ab", "c")` and `("a", "bc")` cannot collide. + */ + val token: String + get() { + val nameBytes = name.encodeToByteArray() + return Buffer() + .writeInt(nameBytes.size) + .write(nameBytes) + .write(psk) + .readByteString() + .sha256() + .hex() + .substring(0, CHANNEL_IDENTITY_TOKEN_LENGTH) + } } +/** + * Hex characters of the identity digest kept in a retired contact key. + * + * 64 bits rather than the 32 that eight slots would need: a collision would silently merge two archived conversations, + * and the key format is not released yet, so widening it now costs nothing and never has to be migrated later. + */ +const val CHANNEL_IDENTITY_TOKEN_LENGTH = 16 + /** Resolves this setting's semantic identity under [loraConfig]. */ fun ChannelSettings.channelIdentity(loraConfig: Config.LoRaConfig): ChannelIdentity { val channel = ModelChannel(settings = this, loraConfig = loraConfig) diff --git a/core/model/src/commonMain/kotlin/org/meshtastic/core/model/util/DistanceExtensions.kt b/core/model/src/commonMain/kotlin/org/meshtastic/core/model/util/DistanceExtensions.kt index d9fc9f797c..5e7229494f 100644 --- a/core/model/src/commonMain/kotlin/org/meshtastic/core/model/util/DistanceExtensions.kt +++ b/core/model/src/commonMain/kotlin/org/meshtastic/core/model/util/DistanceExtensions.kt @@ -95,6 +95,17 @@ fun Int.toDistanceString(system: MeasurementSystem): String { fun Int.toElevationString(system: MeasurementSystem): String = formatElevationLocalized(this.toDouble(), system) ?: this.metersIn(system).toString(system) +/** + * Formats a distance already in kilometres (a storm front, not a node) in [system]'s large unit only: whole km, or + * miles to one decimal. The AS3935 reports whole kilometres, so a 1 km reading must not fall through to feet. + */ +@Suppress("MagicNumber") +fun Float.toStormDistanceString(system: MeasurementSystem): String = if (system == MeasurementSystem.METRIC) { + formatMeasure(this.toDouble(), MeasureUnitKind.KILOMETER, 0) +} else { + formatMeasure(this * 0.621371, MeasureUnitKind.MILE, 1) +} + @Suppress("MagicNumber") fun Float.toSpeedString(system: MeasurementSystem): String = formatSpeedLocalized(this.toDouble(), system) ?: if (system == MeasurementSystem.METRIC) { diff --git a/core/model/src/commonMain/kotlin/org/meshtastic/core/model/util/Extensions.kt b/core/model/src/commonMain/kotlin/org/meshtastic/core/model/util/Extensions.kt index 57b7e84a51..9a6a146845 100644 --- a/core/model/src/commonMain/kotlin/org/meshtastic/core/model/util/Extensions.kt +++ b/core/model/src/commonMain/kotlin/org/meshtastic/core/model/util/Extensions.kt @@ -113,10 +113,15 @@ fun MeshPacket.snrOrNull(): Float? = rx_snr fun MeshPacket.isDirectSignal(): Boolean = rxTimeOrNull() != null && hop_start == hop_limit && via_mqtt != true && isLora() -/** Returns true if this telemetry packet contains valid, plot-able environment metrics. */ +/** + * Returns true if this telemetry packet carries environment metrics worth listing: a temperature and humidity pair, or + * a lightning reading. An AS3935 on its own reports only lightning, so it is admitted without the climate pair. + */ fun Telemetry.hasValidEnvironmentMetrics(): Boolean { val metrics = this.environment_metrics ?: return false - return metrics.relative_humidity != null && metrics.temperature != null && !metrics.temperature!!.isNaN() + val hasClimate = metrics.relative_humidity != null && metrics.temperature != null && !metrics.temperature!!.isNaN() + val hasLightning = metrics.lightning_strike_count_1h != null || metrics.lightning_distance_km?.isNaN() == false + return hasClimate || hasLightning } /** diff --git a/core/model/src/commonMain/kotlin/org/meshtastic/core/model/util/SharedContact.kt b/core/model/src/commonMain/kotlin/org/meshtastic/core/model/util/SharedContact.kt index 6a58930385..e9e304573b 100644 --- a/core/model/src/commonMain/kotlin/org/meshtastic/core/model/util/SharedContact.kt +++ b/core/model/src/commonMain/kotlin/org/meshtastic/core/model/util/SharedContact.kt @@ -22,9 +22,25 @@ import okio.ByteString import okio.ByteString.Companion.decodeBase64 import okio.ByteString.Companion.toByteString import org.meshtastic.core.common.util.CommonUri +import org.meshtastic.core.model.Node import org.meshtastic.proto.SharedContact import org.meshtastic.proto.User +/** + * The [SharedContact] to encode for [node]. + * + * [isOwnContact] marks it manually verified (design#149 point 2): you hold your own radio's key, and a QR shown in + * person is the in-person exchange. Relaying someone else's contact only passes on what was already recorded, it never + * asserts verification on their behalf. + */ +fun Node.toSharedContact(isOwnContact: Boolean = false): SharedContact = SharedContact.Builder() + .also { wb -> + wb.node_num = num + wb.user = user + wb.manually_verified = isOwnContact || manuallyVerified + } + .build() + /** * Return a [SharedContact] that represents the contact encoded by the URL. * diff --git a/core/model/src/commonMain/kotlin/org/meshtastic/core/model/util/TelemetryChannels.kt b/core/model/src/commonMain/kotlin/org/meshtastic/core/model/util/TelemetryChannels.kt index dd8c311f6b..4a4b88c10c 100644 --- a/core/model/src/commonMain/kotlin/org/meshtastic/core/model/util/TelemetryChannels.kt +++ b/core/model/src/commonMain/kotlin/org/meshtastic/core/model/util/TelemetryChannels.kt @@ -62,27 +62,27 @@ fun EnvironmentMetrics.adcVoltage(channel: Int): Float? = when (channel) { /** Returns a copy with 1-Wire [channel] set to [value]; an out-of-range [channel] is a no-op. */ fun EnvironmentMetrics.withOneWireTemperature(channel: Int, value: Float?): EnvironmentMetrics = when (channel) { - 0 -> copy(one_wire_temperature_ch0 = value) - 1 -> copy(one_wire_temperature_ch1 = value) - 2 -> copy(one_wire_temperature_ch2 = value) - 3 -> copy(one_wire_temperature_ch3 = value) - 4 -> copy(one_wire_temperature_ch4 = value) - 5 -> copy(one_wire_temperature_ch5 = value) - 6 -> copy(one_wire_temperature_ch6 = value) - 7 -> copy(one_wire_temperature_ch7 = value) + 0 -> this.newBuilder().also { wb -> wb.one_wire_temperature_ch0 = value }.build() + 1 -> this.newBuilder().also { wb -> wb.one_wire_temperature_ch1 = value }.build() + 2 -> this.newBuilder().also { wb -> wb.one_wire_temperature_ch2 = value }.build() + 3 -> this.newBuilder().also { wb -> wb.one_wire_temperature_ch3 = value }.build() + 4 -> this.newBuilder().also { wb -> wb.one_wire_temperature_ch4 = value }.build() + 5 -> this.newBuilder().also { wb -> wb.one_wire_temperature_ch5 = value }.build() + 6 -> this.newBuilder().also { wb -> wb.one_wire_temperature_ch6 = value }.build() + 7 -> this.newBuilder().also { wb -> wb.one_wire_temperature_ch7 = value }.build() else -> this } /** Returns a copy with ADC [channel] set to [value]; an out-of-range [channel] is a no-op. */ fun EnvironmentMetrics.withAdcVoltage(channel: Int, value: Float?): EnvironmentMetrics = when (channel) { - 0 -> copy(adc_voltage_ch0 = value) - 1 -> copy(adc_voltage_ch1 = value) - 2 -> copy(adc_voltage_ch2 = value) - 3 -> copy(adc_voltage_ch3 = value) - 4 -> copy(adc_voltage_ch4 = value) - 5 -> copy(adc_voltage_ch5 = value) - 6 -> copy(adc_voltage_ch6 = value) - 7 -> copy(adc_voltage_ch7 = value) + 0 -> this.newBuilder().also { wb -> wb.adc_voltage_ch0 = value }.build() + 1 -> this.newBuilder().also { wb -> wb.adc_voltage_ch1 = value }.build() + 2 -> this.newBuilder().also { wb -> wb.adc_voltage_ch2 = value }.build() + 3 -> this.newBuilder().also { wb -> wb.adc_voltage_ch3 = value }.build() + 4 -> this.newBuilder().also { wb -> wb.adc_voltage_ch4 = value }.build() + 5 -> this.newBuilder().also { wb -> wb.adc_voltage_ch5 = value }.build() + 6 -> this.newBuilder().also { wb -> wb.adc_voltage_ch6 = value }.build() + 7 -> this.newBuilder().also { wb -> wb.adc_voltage_ch7 = value }.build() else -> this } diff --git a/core/model/src/commonTest/kotlin/org/meshtastic/core/model/CapabilitiesTest.kt b/core/model/src/commonTest/kotlin/org/meshtastic/core/model/CapabilitiesTest.kt index 6d4848316f..b57dd8edeb 100644 --- a/core/model/src/commonTest/kotlin/org/meshtastic/core/model/CapabilitiesTest.kt +++ b/core/model/src/commonTest/kotlin/org/meshtastic/core/model/CapabilitiesTest.kt @@ -138,7 +138,8 @@ class CapabilitiesTest { ChannelOption.NARROW_FAST, ChannelOption.NARROW_SLOW, ) - assertEquals(gated, ChannelOption.entries.filter { it.minFirmware != null }.toSet()) + // LongTurbo's radio support lands earlier, in v2.7.17 (firmware#8985), so it gates on its own version. + assertEquals(gated + ChannelOption.LONG_TURBO, ChannelOption.entries.filter { it.minFirmware != null }.toSet()) val old = caps("2.7.26") val new = caps("2.8.0") @@ -146,6 +147,8 @@ class CapabilitiesTest { assertFalse(old.supportsPreset(preset), "${preset.name} should be hidden on 2.7 firmware") assertTrue(new.supportsPreset(preset), "${preset.name} should be shown on 2.8 firmware") } + assertFalse(caps("2.7.16").supportsPreset(ChannelOption.LONG_TURBO), "LongTurbo predates 2.7.17") + assertTrue(old.supportsPreset(ChannelOption.LONG_TURBO), "LongTurbo ships from 2.7.17") // Established presets are never gated, even with unknown firmware. assertTrue(caps(null).supportsPreset(ChannelOption.LONG_FAST)) // Unknown firmware hides gated presets; debug forceEnableAll shows them. diff --git a/core/model/src/commonTest/kotlin/org/meshtastic/core/model/ChannelHashTest.kt b/core/model/src/commonTest/kotlin/org/meshtastic/core/model/ChannelHashTest.kt new file mode 100644 index 0000000000..9c446657d5 --- /dev/null +++ b/core/model/src/commonTest/kotlin/org/meshtastic/core/model/ChannelHashTest.kt @@ -0,0 +1,59 @@ +/* + * 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 . + */ +package org.meshtastic.core.model + +import okio.ByteString.Companion.toByteString +import org.meshtastic.core.model.util.byteArrayOfInts +import org.meshtastic.proto.ChannelSettings +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertNotEquals + +class ChannelHashTest { + private val psk = + byteArrayOfInts(0xD4, 0xF1, 0xBB, 0x3A, 0x20, 0x29, 0x07, 0x59, 0xF0, 0xBC, 0xFF, 0xAB, 0xCF, 0x4E, 0x69, 0x01) + .toByteString() + + private val plainSettings = + ChannelSettings.Builder() + .also { wb -> + wb.name = "Secret" + wb.psk = psk + } + .build() + + private val aeadSettings = plainSettings.newBuilder().also { wb -> wb.use_aead = true }.build() + + @Test + fun defaultChannelHashMatchesFirmware() { + assertEquals(8, Channel.default.hash) + } + + @Test + fun aeadFoldsMarkerIntoHash() { + val plain = Channel(plainSettings) + val aead = Channel(aeadSettings) + + assertEquals(plain.hash xor 0xAE, aead.hash) + assertNotEquals(plain.hash, aead.hash) + } + + @Test + fun aeadDoesNotChangeIdentity() { + assertEquals(Channel(plainSettings), Channel(aeadSettings)) + } +} diff --git a/core/model/src/commonTest/kotlin/org/meshtastic/core/model/LoRaRegionPresetsTest.kt b/core/model/src/commonTest/kotlin/org/meshtastic/core/model/LoRaRegionPresetsTest.kt index f73d7d8d76..d7f1f8987c 100644 --- a/core/model/src/commonTest/kotlin/org/meshtastic/core/model/LoRaRegionPresetsTest.kt +++ b/core/model/src/commonTest/kotlin/org/meshtastic/core/model/LoRaRegionPresetsTest.kt @@ -31,33 +31,57 @@ class LoRaRegionPresetsTest { // group 0: standard US presets (unlicensed). group 1: a licensed-only region. private val map = - LoRaRegionPresetMap( - groups = - listOf( - LoRaPresetGroup( - presets = listOf(ModemPreset.LONG_FAST, ModemPreset.LONG_SLOW, ModemPreset.SHORT_FAST), - default_preset = ModemPreset.LONG_FAST, - licensed_only = false, - ), - LoRaPresetGroup( - presets = listOf(ModemPreset.TINY_FAST, ModemPreset.TINY_SLOW), - default_preset = ModemPreset.TINY_FAST, - licensed_only = true, - ), - // group 2: models EU_N_868's superset advertising - LONG_FAST is legal but not the default. - LoRaPresetGroup( - presets = listOf(ModemPreset.LONG_FAST, ModemPreset.MEDIUM_FAST), - default_preset = ModemPreset.MEDIUM_FAST, - licensed_only = false, - ), - ), - region_groups = - listOf( - LoRaRegionPresets(region = RegionCode.US, group_index = 0), - LoRaRegionPresets(region = RegionCode.UA_433, group_index = 1), - LoRaRegionPresets(region = RegionCode.EU_N_868, group_index = 2), - ), - ) + LoRaRegionPresetMap.Builder() + .also { wb -> + wb.groups = + listOf( + LoRaPresetGroup.Builder() + .also { wb -> + wb.presets = + listOf(ModemPreset.LONG_FAST, ModemPreset.LONG_SLOW, ModemPreset.SHORT_FAST) + wb.default_preset = ModemPreset.LONG_FAST + wb.licensed_only = false + } + .build(), + LoRaPresetGroup.Builder() + .also { wb -> + wb.presets = listOf(ModemPreset.TINY_FAST, ModemPreset.TINY_SLOW) + wb.default_preset = ModemPreset.TINY_FAST + wb.licensed_only = true + } + .build(), + // group 2: models EU_N_868's superset advertising - LONG_FAST is legal but not the default. + LoRaPresetGroup.Builder() + .also { wb -> + wb.presets = listOf(ModemPreset.LONG_FAST, ModemPreset.MEDIUM_FAST) + wb.default_preset = ModemPreset.MEDIUM_FAST + wb.licensed_only = false + } + .build(), + ) + wb.region_groups = + listOf( + LoRaRegionPresets.Builder() + .also { wb -> + wb.region = RegionCode.US + wb.group_index = 0 + } + .build(), + LoRaRegionPresets.Builder() + .also { wb -> + wb.region = RegionCode.UA_433 + wb.group_index = 1 + } + .build(), + LoRaRegionPresets.Builder() + .also { wb -> + wb.region = RegionCode.EU_N_868 + wb.group_index = 2 + } + .build(), + ) + } + .build() @Test fun `null map imposes no constraint`() { @@ -76,10 +100,20 @@ class LoRaRegionPresetsTest { @Test fun `out of range group_index is treated as unconstrained`() { val broken = - LoRaRegionPresetMap( - groups = emptyList(), - region_groups = listOf(LoRaRegionPresets(region = RegionCode.US, group_index = 5)), - ) + LoRaRegionPresetMap.Builder() + .also { wb -> + wb.groups = emptyList() + wb.region_groups = + listOf( + LoRaRegionPresets.Builder() + .also { wb -> + wb.region = RegionCode.US + wb.group_index = 5 + } + .build(), + ) + } + .build() assertNull(broken.constraintFor(RegionCode.US)) } @@ -87,10 +121,28 @@ class LoRaRegionPresetsTest { fun `a group with no presets is treated as unconstrained`() { // A degenerate/malformed group must degrade to the full preset list, not an empty picker. val empty = - LoRaRegionPresetMap( - groups = listOf(LoRaPresetGroup(presets = emptyList(), licensed_only = false)), - region_groups = listOf(LoRaRegionPresets(region = RegionCode.US, group_index = 0)), - ) + LoRaRegionPresetMap.Builder() + .also { wb -> + wb.groups = + listOf( + LoRaPresetGroup.Builder() + .also { wb -> + wb.presets = emptyList() + wb.licensed_only = false + } + .build(), + ) + wb.region_groups = + listOf( + LoRaRegionPresets.Builder() + .also { wb -> + wb.region = RegionCode.US + wb.group_index = 0 + } + .build(), + ) + } + .build() assertNull(empty.constraintFor(RegionCode.US)) assertEquals(ModemPreset.LONG_FAST, empty.repairPresetFor(RegionCode.US, ModemPreset.LONG_FAST)) } @@ -131,17 +183,30 @@ class LoRaRegionPresetsTest { // A malformed map whose advertised default is not in its own legal set. private val oddMap = - LoRaRegionPresetMap( - groups = - listOf( - LoRaPresetGroup( - presets = listOf(ModemPreset.MEDIUM_FAST, ModemPreset.MEDIUM_SLOW), - default_preset = ModemPreset.LONG_FAST, // not in presets - licensed_only = false, - ), - ), - region_groups = listOf(LoRaRegionPresets(region = RegionCode.US, group_index = 0)), - ) + LoRaRegionPresetMap.Builder() + .also { wb -> + wb.groups = + listOf( + LoRaPresetGroup.Builder() + .also { wb -> + wb.presets = listOf(ModemPreset.MEDIUM_FAST, ModemPreset.MEDIUM_SLOW) + wb.default_preset = ModemPreset.LONG_FAST + // not in presets + wb.licensed_only = false + } + .build(), + ) + wb.region_groups = + listOf( + LoRaRegionPresets.Builder() + .also { wb -> + wb.region = RegionCode.US + wb.group_index = 0 + } + .build(), + ) + } + .build() @Test fun `repair falls back to the first legal preset when the default is not in the group`() { @@ -190,14 +255,26 @@ class LoRaRegionPresetsTest { } @Test - fun `fresh setup placeholder adopts the built-in default when unconstrained`() { + fun `fresh setup keeps LongFast when firmware advertises no map`() { + // Pre-2.8 firmware installs its own region-table default on its screen; the app must not diverge from it. val nullMap: LoRaRegionPresetMap? = null assertEquals( - ModemPreset.LONG_TURBO, + ModemPreset.LONG_FAST, nullMap.presetForRegionChange(RegionCode.UNSET, RegionCode.US, ModemPreset.LONG_FAST), ) } + @Test + fun `no first-setup default is offered without a map`() { + val nullMap: LoRaRegionPresetMap? = null + assertNull(nullMap.firstSetupDefaultFor(RegionCode.US)) + } + + @Test + fun `no first-setup default when the map omits it from the region`() { + assertNull(map.firstSetupDefaultFor(RegionCode.US)) + } + @Test fun `fresh setup pin that is illegal in the region is still repaired`() { assertEquals( @@ -217,16 +294,27 @@ class LoRaRegionPresetsTest { // A pinned build advertises its preset as UNSET's map entry (firmware #11507): the same fixture plus a // single-preset UNSET group, here stating a deliberate LONG_FAST pin. private val pinnedMap = - map.copy( - groups = - map.groups + - LoRaPresetGroup( - presets = listOf(ModemPreset.LONG_FAST), - default_preset = ModemPreset.LONG_FAST, - licensed_only = false, - ), - region_groups = map.region_groups + LoRaRegionPresets(region = RegionCode.UNSET, group_index = 3), - ) + map.newBuilder() + .also { wb -> + wb.groups = + map.groups + + LoRaPresetGroup.Builder() + .also { wb -> + wb.presets = listOf(ModemPreset.LONG_FAST) + wb.default_preset = ModemPreset.LONG_FAST + wb.licensed_only = false + } + .build() + wb.region_groups = + map.region_groups + + LoRaRegionPresets.Builder() + .also { wb -> + wb.region = RegionCode.UNSET + wb.group_index = 3 + } + .build() + } + .build() @Test fun `an UNSET map entry marks even a LongFast pin as deliberate at fresh setup`() { @@ -244,4 +332,74 @@ class LoRaRegionPresetsTest { pinnedMap.presetForRegionChange(RegionCode.UNSET, RegionCode.UA_433, ModemPreset.LONG_FAST), ) } + + // Mirrors what firmware 2.8 advertises for US: its whole legal preset list, defaulting to the region-table + // LongFast rather than the LongTurbo its own first-setup chooser installs. + private val firmware28Map = + LoRaRegionPresetMap.Builder() + .also { wb -> + wb.groups = + listOf( + LoRaPresetGroup.Builder() + .also { g -> + g.presets = + listOf( + ModemPreset.LONG_FAST, + ModemPreset.LONG_SLOW, + ModemPreset.MEDIUM_SLOW, + ModemPreset.MEDIUM_FAST, + ModemPreset.SHORT_SLOW, + ModemPreset.SHORT_FAST, + ModemPreset.LONG_MODERATE, + ModemPreset.SHORT_TURBO, + ModemPreset.LONG_TURBO, + ModemPreset.MEDIUM_TURBO, + ) + g.default_preset = ModemPreset.LONG_FAST + g.licensed_only = false + } + .build(), + ) + wb.region_groups = + listOf( + LoRaRegionPresets.Builder() + .also { r -> + r.region = RegionCode.US + r.group_index = 0 + } + .build(), + ) + } + .build() + + @Test + fun `first-setup default is offered when the map lists it as legal`() { + assertEquals(ModemPreset.LONG_TURBO, firmware28Map.firstSetupDefaultFor(RegionCode.US)) + } + + @Test + fun `fresh US setup adopts LongTurbo over the advertised LongFast default`() { + assertEquals( + ModemPreset.LONG_TURBO, + firmware28Map.presetForRegionChange(RegionCode.UNSET, RegionCode.US, ModemPreset.LONG_FAST), + ) + } + + @Test + fun `a later US change on the advertised map keeps the running preset`() { + // Default adoption stays a fresh-setup rule: an already-configured node is never moved off its preset. + assertEquals( + ModemPreset.LONG_FAST, + firmware28Map.presetForRegionChange(RegionCode.EU_868, RegionCode.US, ModemPreset.LONG_FAST), + ) + } + + @Test + fun `fresh setup falls back to the advertised default when the built-in is illegal there`() { + // The US group here omits LONG_TURBO, so the built-in cannot be adopted. + assertEquals( + ModemPreset.LONG_FAST, + map.presetForRegionChange(RegionCode.UNSET, RegionCode.US, ModemPreset.LONG_FAST), + ) + } } diff --git a/core/model/src/commonTest/kotlin/org/meshtastic/core/model/LocalStatsTest.kt b/core/model/src/commonTest/kotlin/org/meshtastic/core/model/LocalStatsTest.kt index 70028926db..793f192c78 100644 --- a/core/model/src/commonTest/kotlin/org/meshtastic/core/model/LocalStatsTest.kt +++ b/core/model/src/commonTest/kotlin/org/meshtastic/core/model/LocalStatsTest.kt @@ -25,13 +25,13 @@ class LocalStatsTest { @Test fun `the never-reported sentinel maps to null`() { - assertNull(LocalStats(noise_floor = 0).noiseFloorOrNull) - assertNull(LocalStats().noiseFloorOrNull) + assertNull(LocalStats.Builder().also { wb -> wb.noise_floor = 0 }.build().noiseFloorOrNull) + assertNull(LocalStats.Builder().build().noiseFloorOrNull) } @Test fun `a real reading passes through unchanged`() { - assertEquals(-70, LocalStats(noise_floor = -70).noiseFloorOrNull) - assertEquals(1, LocalStats(noise_floor = 1).noiseFloorOrNull) + assertEquals(-70, LocalStats.Builder().also { wb -> wb.noise_floor = -70 }.build().noiseFloorOrNull) + assertEquals(1, LocalStats.Builder().also { wb -> wb.noise_floor = 1 }.build().noiseFloorOrNull) } } diff --git a/core/model/src/commonTest/kotlin/org/meshtastic/core/model/NodeSecurityIndicatorTest.kt b/core/model/src/commonTest/kotlin/org/meshtastic/core/model/NodeSecurityIndicatorTest.kt new file mode 100644 index 0000000000..85035c28f6 --- /dev/null +++ b/core/model/src/commonTest/kotlin/org/meshtastic/core/model/NodeSecurityIndicatorTest.kt @@ -0,0 +1,183 @@ +/* + * 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 . + */ +package org.meshtastic.core.model + +import okio.ByteString.Companion.toByteString +import org.meshtastic.core.model.NodeSecurityIndicator.Companion.resolve +import org.meshtastic.core.model.NodeSecurityIndicator.Companion.signsBroadcasts +import org.meshtastic.proto.DeviceMetadata +import org.meshtastic.proto.User +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertFalse +import kotlin.test.assertTrue + +class NodeSecurityIndicatorTest { + + @Test + fun `2_8 and newer report signing support - older and unknown do not`() { + // DeviceMetadata truncates to two components, so a stored "2.8" has to read as 2.8. + assertTrue(signsBroadcasts("2.8")) + assertTrue(signsBroadcasts("2.8.0")) + assertTrue(signsBroadcasts("2.8.1")) + assertTrue(signsBroadcasts("2.10.0")) + assertFalse(signsBroadcasts("2.7.26")) + assertFalse(signsBroadcasts("2.5.14")) + assertFalse(signsBroadcasts(null)) + assertFalse(signsBroadcasts("")) + assertFalse(signsBroadcasts("unknown")) + } + + @Test + fun `2_8 nodes show verification state instead of the locks`() { + assertEquals( + NodeSecurityIndicator.SIGNED_NODE, + resolve(firmwareVersion = "2.8.0", hasPublicKey = true, mismatchKey = false), + ) + assertEquals( + NodeSecurityIndicator.SIGNED_NODE, + resolve(firmwareVersion = "2.8.0", hasPublicKey = false, mismatchKey = false), + ) + } + + @Test + fun `a heard signature shows the signed glyph even when the version is unknown`() { + assertEquals( + NodeSecurityIndicator.SIGNED_NODE, + resolve(firmwareVersion = null, hasPublicKey = true, mismatchKey = false, signed = true), + ) + assertEquals( + NodeSecurityIndicator.SIGNED_NODE, + resolve(firmwareVersion = "2.7.26", hasPublicKey = true, mismatchKey = false, signed = true), + ) + // Never heard signing and no 2.8 report: the locks still describe it honestly. + assertEquals( + NodeSecurityIndicator.PUBLIC_KEY, + resolve(firmwareVersion = "2.7.26", hasPublicKey = true, mismatchKey = false, signed = false), + ) + } + + @Test + fun `older and unversioned nodes keep the locks`() { + assertEquals( + NodeSecurityIndicator.PUBLIC_KEY, + resolve(firmwareVersion = "2.7.26", hasPublicKey = true, mismatchKey = false), + ) + assertEquals( + NodeSecurityIndicator.NO_PUBLIC_KEY, + resolve(firmwareVersion = "2.7.26", hasPublicKey = false, mismatchKey = false), + ) + assertEquals( + NodeSecurityIndicator.PUBLIC_KEY, + resolve(firmwareVersion = null, hasPublicKey = true, mismatchKey = false), + ) + assertEquals( + NodeSecurityIndicator.NO_PUBLIC_KEY, + resolve(firmwareVersion = null, hasPublicKey = false, mismatchKey = false), + ) + } + + @Test + fun `a key mismatch is a warning at any version - even for a verified contact`() { + assertEquals( + NodeSecurityIndicator.KEY_MISMATCH, + resolve(firmwareVersion = "2.8.0", hasPublicKey = true, mismatchKey = true), + ) + assertEquals( + NodeSecurityIndicator.KEY_MISMATCH, + resolve(firmwareVersion = "2.7.26", hasPublicKey = true, mismatchKey = true), + ) + assertEquals( + NodeSecurityIndicator.KEY_MISMATCH, + resolve(firmwareVersion = "2.8.0", hasPublicKey = true, mismatchKey = true, verified = true), + ) + assertEquals( + NodeSecurityIndicator.KEY_MISMATCH, + resolve(firmwareVersion = "2.8.0", hasPublicKey = true, mismatchKey = true, signed = true), + ) + } + + @Test + fun `the connected radio reads as verified at any version`() { + assertEquals( + NodeSecurityIndicator.VERIFIED_CONTACT, + resolve(firmwareVersion = "2.8.0", hasPublicKey = false, mismatchKey = false, isOwnNode = true), + ) + // The user holds their own radio's key whatever it runs, so this does not depend on version. + assertEquals( + NodeSecurityIndicator.VERIFIED_CONTACT, + resolve(firmwareVersion = "2.7.26", hasPublicKey = true, mismatchKey = false, isOwnNode = true), + ) + } + + @Test + fun `an in-person verified contact outranks signing at any version`() { + assertEquals( + NodeSecurityIndicator.VERIFIED_CONTACT, + resolve(firmwareVersion = "2.8.0", hasPublicKey = true, mismatchKey = false, verified = true), + ) + assertEquals( + NodeSecurityIndicator.VERIFIED_CONTACT, + resolve( + firmwareVersion = "2.8.0", + hasPublicKey = true, + mismatchKey = false, + verified = true, + signed = true, + ), + ) + // Meeting someone in person is the same evidence whatever their radio runs. + assertEquals( + NodeSecurityIndicator.VERIFIED_CONTACT, + resolve(firmwareVersion = "2.7.26", hasPublicKey = true, mismatchKey = false, verified = true), + ) + } + + @Test + fun `of reads the same decision off a Node`() { + val key = ByteArray(32) { 1 }.toByteString() + val signedOn28 = + Node( + num = 1, + metadata = DeviceMetadata.Builder().also { wb -> wb.firmware_version = "2.8.0" }.build(), + user = User.Builder().also { wb -> wb.public_key = key }.build(), + ) + assertEquals(NodeSecurityIndicator.SIGNED_NODE, NodeSecurityIndicator.of(signedOn28)) + assertEquals(NodeSecurityIndicator.VERIFIED_CONTACT, NodeSecurityIndicator.of(signedOn28, isOwnNode = true)) + assertEquals( + NodeSecurityIndicator.VERIFIED_CONTACT, + NodeSecurityIndicator.of(signedOn28.copy(manuallyVerified = true)), + ) + + val legacy = + signedOn28.copy(metadata = DeviceMetadata.Builder().also { wb -> wb.firmware_version = "2.7.26" }.build()) + assertEquals(NodeSecurityIndicator.PUBLIC_KEY, NodeSecurityIndicator.of(legacy)) + assertEquals(NodeSecurityIndicator.SIGNED_NODE, NodeSecurityIndicator.of(legacy.copy(signsPackets = true))) + assertEquals( + NodeSecurityIndicator.NO_PUBLIC_KEY, + NodeSecurityIndicator.of(legacy.copy(user = User.Builder().build())), + ) + + val mismatched = + legacy.copy(user = User.Builder().also { wb -> wb.public_key = Node.ERROR_BYTE_STRING }.build()) + assertEquals(NodeSecurityIndicator.KEY_MISMATCH, NodeSecurityIndicator.of(mismatched)) + assertEquals( + NodeSecurityIndicator.KEY_MISMATCH, + NodeSecurityIndicator.of(mismatched.copy(manuallyVerified = true)), + ) + } +} diff --git a/core/model/src/commonTest/kotlin/org/meshtastic/core/model/NodeTelemetryStringsTest.kt b/core/model/src/commonTest/kotlin/org/meshtastic/core/model/NodeTelemetryStringsTest.kt index 10c50c9f66..21646de772 100644 --- a/core/model/src/commonTest/kotlin/org/meshtastic/core/model/NodeTelemetryStringsTest.kt +++ b/core/model/src/commonTest/kotlin/org/meshtastic/core/model/NodeTelemetryStringsTest.kt @@ -33,40 +33,65 @@ class NodeTelemetryStringsTest { @Test fun absent_environment_metrics_render_nothing() { - assertEquals(emptyList(), telemetry(EnvironmentMetrics())) + assertEquals(emptyList(), telemetry(EnvironmentMetrics.Builder().build())) } @Test fun zero_temperature_is_reported() { - assertEquals(listOf("0.0°C"), telemetry(EnvironmentMetrics(temperature = 0f))) + assertEquals( + listOf("0.0°C"), + telemetry(EnvironmentMetrics.Builder().also { wb -> wb.temperature = 0f }.build()), + ) } @Test fun zero_voltage_and_current_are_reported() { - val strings = telemetry(EnvironmentMetrics(voltage = 0f, current = 0f)) + val strings = + telemetry( + EnvironmentMetrics.Builder() + .also { wb -> + wb.voltage = 0f + wb.current = 0f + } + .build(), + ) assertEquals(listOf("0.00 V", "0.0 mA"), strings) } @Test fun zero_soil_readings_are_reported() { - val strings = telemetry(EnvironmentMetrics(soil_temperature = 0f, soil_moisture = 0)) + val strings = + telemetry( + EnvironmentMetrics.Builder() + .also { wb -> + wb.soil_temperature = 0f + wb.soil_moisture = 0 + } + .build(), + ) assertEquals(listOf("0.0°C", "0%"), strings) } @Test fun soil_moisture_no_longer_requires_a_soil_temperature() { - assertEquals(listOf("42%"), telemetry(EnvironmentMetrics(soil_moisture = 42))) + assertEquals( + listOf("42%"), + telemetry(EnvironmentMetrics.Builder().also { wb -> wb.soil_moisture = 42 }.build()), + ) } @Test fun out_of_range_soil_moisture_is_still_rejected() { - assertEquals(emptyList(), telemetry(EnvironmentMetrics(soil_moisture = 101))) + assertEquals(emptyList(), telemetry(EnvironmentMetrics.Builder().also { wb -> wb.soil_moisture = 101 }.build())) } @Test fun zero_humidity_stays_filtered() { // 0 %RH is not physically reachable, so unlike the other metrics its zero-guard is intentional. - assertTrue(telemetry(EnvironmentMetrics(relative_humidity = 0f)).isEmpty()) - assertEquals(listOf("41%"), telemetry(EnvironmentMetrics(relative_humidity = 41f))) + assertTrue(telemetry(EnvironmentMetrics.Builder().also { wb -> wb.relative_humidity = 0f }.build()).isEmpty()) + assertEquals( + listOf("41%"), + telemetry(EnvironmentMetrics.Builder().also { wb -> wb.relative_humidity = 41f }.build()), + ) } } diff --git a/core/model/src/commonTest/kotlin/org/meshtastic/core/model/NodeTest.kt b/core/model/src/commonTest/kotlin/org/meshtastic/core/model/NodeTest.kt index 9a65b79b9e..efa412f0cb 100644 --- a/core/model/src/commonTest/kotlin/org/meshtastic/core/model/NodeTest.kt +++ b/core/model/src/commonTest/kotlin/org/meshtastic/core/model/NodeTest.kt @@ -103,17 +103,43 @@ class NodeTest { @Test fun isUnknownUser_falseWhenHardwareModelIsKnown() { - val node = Node(num = 1, user = User(hw_model = HardwareModel.TLORA_V2)) + val node = Node(num = 1, user = User.Builder().also { wb -> wb.hw_model = HardwareModel.TLORA_V2 }.build()) assertFalse(node.isUnknownUser) } @Test fun validPosition_returnsPositionOnlyForValidCoordinates() { - val validPosition = Position(latitude_i = 377749000, longitude_i = -1224194000) + val validPosition = + Position.Builder() + .also { wb -> + wb.latitude_i = 377749000 + wb.longitude_i = -1224194000 + } + .build() val validNode = Node(num = 1, position = validPosition) - val zeroNode = Node(num = 2, position = Position(latitude_i = 0, longitude_i = 0)) - val outOfRangeNode = Node(num = 3, position = Position(latitude_i = 910000000, longitude_i = -1224194000)) + val zeroNode = + Node( + num = 2, + position = + Position.Builder() + .also { wb -> + wb.latitude_i = 0 + wb.longitude_i = 0 + } + .build(), + ) + val outOfRangeNode = + Node( + num = 3, + position = + Position.Builder() + .also { wb -> + wb.latitude_i = 910000000 + wb.longitude_i = -1224194000 + } + .build(), + ) assertEquals(validPosition, validNode.validPosition) assertEquals(null, zeroNode.validPosition) @@ -123,7 +149,7 @@ class NodeTest { @Test fun hasPKC_usesUserPublicKeyWhenNodeKeyIsMissing() { val key = ByteArray(32) { (it + 1).toByte() }.toByteString() - val node = Node(num = 1, user = User(public_key = key)) + val node = Node(num = 1, user = User.Builder().also { wb -> wb.public_key = key }.build()) assertTrue(node.hasPKC) assertFalse(node.mismatchKey) @@ -139,7 +165,17 @@ class NodeTest { @Test fun matchesSearch_isCaseInsensitiveForNonAsciiLetters() { - val node = Node(num = 1, user = User(long_name = "KOLSÅS", short_name = "KOLS")) + val node = + Node( + num = 1, + user = + User.Builder() + .also { wb -> + wb.long_name = "KOLSÅS" + wb.short_name = "KOLS" + } + .build(), + ) // #6750: "kols" matched via ASCII-only SQL LIKE folding, but "kolså" did not. assertTrue(node.matchesSearch("kols")) @@ -150,7 +186,7 @@ class NodeTest { @Test fun matchesSearch_matchesHexAndDecimalNodeId() { - val node = Node(num = -1, user = User(id = "!ffffffff")) + val node = Node(num = -1, user = User.Builder().also { wb -> wb.id = "!ffffffff" }.build()) assertTrue(node.matchesSearch("ffffffff")) assertTrue(node.matchesSearch("4294967295")) @@ -162,6 +198,29 @@ class NodeTest { assertTrue(Node(num = 1).matchesSearch("")) } - private fun nodeWithPosition(num: Int, latitudeI: Int, longitudeI: Int): Node = - Node(num = num, position = Position(latitude_i = latitudeI, longitude_i = longitudeI)) + private fun nodeWithPosition(num: Int, latitudeI: Int, longitudeI: Int): Node = Node( + num = num, + position = + Position.Builder() + .also { wb -> + wb.latitude_i = latitudeI + wb.longitude_i = longitudeI + } + .build(), + ) + + @Test + fun `isUnheardOnCurrentLora is true for a node not heard on the config in force now`() { + assertTrue(Node(num = 1, heardOnCurrentLora = false).isUnheardOnCurrentLora) + assertFalse(Node(num = 1, heardOnCurrentLora = true).isUnheardOnCurrentLora) + } + + @Test + fun `isUnheardOnCurrentLora excludes MQTT nodes the radio never heard over RF`() { + // The radio sets the mark only on an RF hear, so a node that only ever arrives over MQTT reports false for + // the lifetime of the entry. Presenting that as "unreachable since you changed settings" would badge every + // node on an MQTT-uplinked mesh and offer it for removal, and removing one is pointless. + assertFalse(Node(num = 1, heardOnCurrentLora = false, viaMqtt = true).isUnheardOnCurrentLora) + assertFalse(Node(num = 1, heardOnCurrentLora = true, viaMqtt = true).isUnheardOnCurrentLora) + } } diff --git a/core/model/src/commonTest/kotlin/org/meshtastic/core/model/RegionInfoTest.kt b/core/model/src/commonTest/kotlin/org/meshtastic/core/model/RegionInfoTest.kt index f8b07bd425..4006b4b27b 100644 --- a/core/model/src/commonTest/kotlin/org/meshtastic/core/model/RegionInfoTest.kt +++ b/core/model/src/commonTest/kotlin/org/meshtastic/core/model/RegionInfoTest.kt @@ -30,8 +30,14 @@ import kotlin.test.assertNotNull */ class RegionInfoTest { - private fun lora(region: RegionCode, preset: ModemPreset, channelNum: Int = 0) = - LoRaConfig(use_preset = true, modem_preset = preset, region = region, channel_num = channelNum) + private fun lora(region: RegionCode, preset: ModemPreset, channelNum: Int = 0) = LoRaConfig.Builder() + .also { wb -> + wb.use_preset = true + wb.modem_preset = preset + wb.region = region + wb.channel_num = channelNum + } + .build() /** * Every region the firmware's region table defines must be selectable in the app. The proto enum is a superset diff --git a/core/model/src/commonTest/kotlin/org/meshtastic/core/model/RetiredContactKeyTest.kt b/core/model/src/commonTest/kotlin/org/meshtastic/core/model/RetiredContactKeyTest.kt new file mode 100644 index 0000000000..cb8e4c718f --- /dev/null +++ b/core/model/src/commonTest/kotlin/org/meshtastic/core/model/RetiredContactKeyTest.kt @@ -0,0 +1,52 @@ +/* + * 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 . + */ +package org.meshtastic.core.model + +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertFalse +import kotlin.test.assertNull +import kotlin.test.assertTrue + +class RetiredContactKeyTest { + private val retired = ContactKey.retiredBroadcast("a1b2c3d4") + + @Test + fun `a retired key parses as a broadcast with no live slot`() { + assertTrue(retired.isRetired) + assertEquals("a1b2c3d4", retired.retiredToken) + assertNull(retired.channelOrNull, "a retired conversation has no slot to send on") + assertEquals(NodeAddress.ID_BROADCAST, retired.addressString) + assertEquals(NodeAddress.Broadcast, retired.address) + } + + @Test + fun `it still reads as a channel conversation rather than a direct message`() { + assertTrue(retired.value.endsWith(NodeAddress.ID_BROADCAST)) + } + + @Test + fun `live keys are unaffected`() { + listOf(ContactKey.broadcast(0), ContactKey.broadcast(3), ContactKey("8!a1b2c3d4"), ContactKey("!a1b2c3d4")) + .forEach { key -> + assertFalse(key.isRetired, "${key.value} must not read as retired") + assertNull(key.retiredToken) + } + assertEquals(3, ContactKey.broadcast(3).channelOrNull) + assertEquals("!a1b2c3d4", ContactKey("8!a1b2c3d4").addressString) + } +} diff --git a/core/model/src/commonTest/kotlin/org/meshtastic/core/model/WaypointLockTest.kt b/core/model/src/commonTest/kotlin/org/meshtastic/core/model/WaypointLockTest.kt index 4d1da75c51..1effdf5e85 100644 --- a/core/model/src/commonTest/kotlin/org/meshtastic/core/model/WaypointLockTest.kt +++ b/core/model/src/commonTest/kotlin/org/meshtastic/core/model/WaypointLockTest.kt @@ -28,17 +28,39 @@ class WaypointLockTest { @Test fun unlockedWaypointIsNotLocked() { - assertFalse(Waypoint(id = 1, locked_to = 0).isLocked) + assertFalse( + Waypoint.Builder() + .also { wb -> + wb.id = 1 + wb.locked_to = 0 + } + .build() + .isLocked, + ) } @Test fun lockedWaypointIsLocked() { - assertTrue(Waypoint(id = 1, locked_to = myNodeNum).isLocked) + assertTrue( + Waypoint.Builder() + .also { wb -> + wb.id = 1 + wb.locked_to = myNodeNum + } + .build() + .isLocked, + ) } @Test fun unlockedWaypointIsModifiableByAnyone() { - val wp = Waypoint(id = 1, locked_to = 0) + val wp = + Waypoint.Builder() + .also { wb -> + wb.id = 1 + wb.locked_to = 0 + } + .build() assertTrue(wp.isModifiableBy(myNodeNum)) assertTrue(wp.isModifiableBy(otherNodeNum)) // Editable by anyone includes the "identity unknown" case. @@ -47,7 +69,13 @@ class WaypointLockTest { @Test fun lockedWaypointIsModifiableOnlyByOwner() { - val wp = Waypoint(id = 1, locked_to = myNodeNum) + val wp = + Waypoint.Builder() + .also { wb -> + wb.id = 1 + wb.locked_to = myNodeNum + } + .build() assertTrue(wp.isModifiableBy(myNodeNum)) assertFalse(wp.isModifiableBy(otherNodeNum)) assertFalse(wp.isModifiableBy(null)) @@ -60,7 +88,23 @@ class WaypointLockTest { */ @Test fun placeholderLockShutsOutTheCreatorButRealOwnerLockDoesNot() { - assertFalse(Waypoint(id = 1, locked_to = 1).isModifiableBy(myNodeNum)) - assertTrue(Waypoint(id = 1, locked_to = myNodeNum).isModifiableBy(myNodeNum)) + assertFalse( + Waypoint.Builder() + .also { wb -> + wb.id = 1 + wb.locked_to = 1 + } + .build() + .isModifiableBy(myNodeNum), + ) + assertTrue( + Waypoint.Builder() + .also { wb -> + wb.id = 1 + wb.locked_to = myNodeNum + } + .build() + .isModifiableBy(myNodeNum), + ) } } diff --git a/core/model/src/commonTest/kotlin/org/meshtastic/core/model/geofence/ActiveWaypointsTest.kt b/core/model/src/commonTest/kotlin/org/meshtastic/core/model/geofence/ActiveWaypointsTest.kt index c0aa572188..c08e54da7b 100644 --- a/core/model/src/commonTest/kotlin/org/meshtastic/core/model/geofence/ActiveWaypointsTest.kt +++ b/core/model/src/commonTest/kotlin/org/meshtastic/core/model/geofence/ActiveWaypointsTest.kt @@ -32,8 +32,26 @@ class ActiveWaypointsTest { /** The P1 regression: re-broadcasts of the same waypoint are separate rows; latest transmission must win. */ @Test fun latestTransmissionWinsPerWaypointId() { - val old = packet(Waypoint(id = 42, geofence_radius = 100, expire = 0)) - val new = packet(Waypoint(id = 42, geofence_radius = 5000, expire = 0)) + val old = + packet( + Waypoint.Builder() + .also { wb -> + wb.id = 42 + wb.geofence_radius = 100 + wb.expire = 0 + } + .build(), + ) + val new = + packet( + Waypoint.Builder() + .also { wb -> + wb.id = 42 + wb.geofence_radius = 5000 + wb.expire = 0 + } + .build(), + ) val active = listOf(old, new).activeWaypointPackets(now) @@ -43,9 +61,36 @@ class ActiveWaypointsTest { @Test fun expiredWaypointsAreDropped() { - val expired = packet(Waypoint(id = 1, geofence_radius = 100, expire = 500)) // before now=1000 - val active = packet(Waypoint(id = 2, geofence_radius = 100, expire = 2000)) // after now - val never = packet(Waypoint(id = 3, geofence_radius = 100, expire = 0)) // never expires + val expired = + packet( + Waypoint.Builder() + .also { wb -> + wb.id = 1 + wb.geofence_radius = 100 + wb.expire = 500 + } + .build(), + ) // before now=1000 + val active = + packet( + Waypoint.Builder() + .also { wb -> + wb.id = 2 + wb.geofence_radius = 100 + wb.expire = 2000 + } + .build(), + ) // after now + val never = + packet( + Waypoint.Builder() + .also { wb -> + wb.id = 3 + wb.geofence_radius = 100 + wb.expire = 0 + } + .build(), + ) // never expires val result = listOf(expired, active, never).activeWaypointPackets(now) @@ -56,7 +101,13 @@ class ActiveWaypointsTest { private val myNodeNum = 7 - private fun geofence(id: Int) = Waypoint(id = id, geofence_radius = 100, notify_on_enter = true) + private fun geofence(id: Int) = Waypoint.Builder() + .also { wb -> + wb.id = id + wb.geofence_radius = 100 + wb.notify_on_enter = true + } + .build() private fun localPacket(wp: Waypoint) = DataPacket(to = "!abcdabcd", channel = 0, waypoint = wp) // from = ^local @@ -83,7 +134,15 @@ class ActiveWaypointsTest { @Test fun geofencesToMonitorDropsWaypointsWithNoCrossingNotifications() { - val silent = localPacket(Waypoint(id = 1, geofence_radius = 100)) // notify_on_enter/exit both false + val silent = + localPacket( + Waypoint.Builder() + .also { wb -> + wb.id = 1 + wb.geofence_radius = 100 + } + .build(), + ) // notify_on_enter/exit both false assertTrue(listOf(silent).geofencesToMonitor(myNodeNum, optedInIds = emptySet()).isEmpty()) } diff --git a/core/model/src/commonTest/kotlin/org/meshtastic/core/model/geofence/WaypointToGeofenceTest.kt b/core/model/src/commonTest/kotlin/org/meshtastic/core/model/geofence/WaypointToGeofenceTest.kt index ae01a67257..1a8c2667bd 100644 --- a/core/model/src/commonTest/kotlin/org/meshtastic/core/model/geofence/WaypointToGeofenceTest.kt +++ b/core/model/src/commonTest/kotlin/org/meshtastic/core/model/geofence/WaypointToGeofenceTest.kt @@ -27,16 +27,26 @@ import kotlin.test.assertTrue class WaypointToGeofenceTest { private val box = - BoundingBox( - longitude_west_i = 200_000_000, - latitude_south_i = 100_000_000, - longitude_east_i = 210_000_000, - latitude_north_i = 110_000_000, - ) + BoundingBox.Builder() + .also { wb -> + wb.longitude_west_i = 200_000_000 + wb.latitude_south_i = 100_000_000 + wb.longitude_east_i = 210_000_000 + wb.latitude_north_i = 110_000_000 + } + .build() @Test fun radiusOnlyDecodesCircleNoBox() { - val wp = Waypoint(id = 1, latitude_i = 100_000_000, longitude_i = 200_000_000, geofence_radius = 500) + val wp = + Waypoint.Builder() + .also { wb -> + wb.id = 1 + wb.latitude_i = 100_000_000 + wb.longitude_i = 200_000_000 + wb.geofence_radius = 500 + } + .build() val geofence = wp.toGeofence() assertNotNullCircleAt(geofence?.circle, 10.0, 20.0, 500) assertNull(geofence?.box) @@ -44,7 +54,13 @@ class WaypointToGeofenceTest { @Test fun boxOnlyDecodesBoxNoCircle() { - val wp = Waypoint(id = 1, bounding_box = box) + val wp = + Waypoint.Builder() + .also { wb -> + wb.id = 1 + wb.bounding_box = box + } + .build() val geofence = wp.toGeofence() assertNull(geofence?.circle) assertEquals(GeofenceBox(south = 10.0, west = 20.0, north = 11.0, east = 21.0), geofence?.box) @@ -52,20 +68,30 @@ class WaypointToGeofenceTest { @Test fun neitherShapeDecodesToNull() { - assertNull(Waypoint(id = 1).toGeofence()) - assertNull(Waypoint(id = 1, geofence_radius = 0).toGeofence()) + assertNull(Waypoint.Builder().also { wb -> wb.id = 1 }.build().toGeofence()) + assertNull( + Waypoint.Builder() + .also { wb -> + wb.id = 1 + wb.geofence_radius = 0 + } + .build() + .toGeofence(), + ) } @Test fun bothShapesDecode() { val wp = - Waypoint( - id = 1, - latitude_i = 100_000_000, - longitude_i = 200_000_000, - geofence_radius = 500, - bounding_box = box, - ) + Waypoint.Builder() + .also { wb -> + wb.id = 1 + wb.latitude_i = 100_000_000 + wb.longitude_i = 200_000_000 + wb.geofence_radius = 500 + wb.bounding_box = box + } + .build() val geofence = wp.toGeofence() assertNotNullCircleAt(geofence?.circle, 10.0, 20.0, 500) assertEquals(GeofenceBox(south = 10.0, west = 20.0, north = 11.0, east = 21.0), geofence?.box) @@ -76,39 +102,66 @@ class WaypointToGeofenceTest { // A box whose corners arrive transposed (south>north, west>east) should still describe the intended // rectangle after decode. val inverted = - BoundingBox( - longitude_west_i = 210_000_000, // 21 (east-most) given as west - latitude_south_i = 110_000_000, // 11 (north-most) given as south - longitude_east_i = 200_000_000, // 20 - latitude_north_i = 100_000_000, // 10 - ) - val geofence = Waypoint(id = 1, bounding_box = inverted).toGeofence() + BoundingBox.Builder() + .also { wb -> + wb.longitude_west_i = 210_000_000 // 21 (east-most) given as west + wb.latitude_south_i = 110_000_000 // 11 (north-most) given as south + wb.longitude_east_i = 200_000_000 // 20 + wb.latitude_north_i = 100_000_000 // 10 + } + .build() + val geofence = + Waypoint.Builder() + .also { wb -> + wb.id = 1 + wb.bounding_box = inverted + } + .build() + .toGeofence() assertEquals(GeofenceBox(south = 10.0, west = 20.0, north = 11.0, east = 21.0), geofence?.box) } @Test fun notifiesOnCrossingTruthTable() { - assertFalse(Waypoint(id = 1).notifiesOnCrossing) - assertTrue(Waypoint(id = 1, notify_on_enter = true).notifiesOnCrossing) - assertTrue(Waypoint(id = 1, notify_on_exit = true).notifiesOnCrossing) + assertFalse(Waypoint.Builder().also { wb -> wb.id = 1 }.build().notifiesOnCrossing) + assertTrue( + Waypoint.Builder() + .also { wb -> + wb.id = 1 + wb.notify_on_enter = true + } + .build() + .notifiesOnCrossing, + ) + assertTrue( + Waypoint.Builder() + .also { wb -> + wb.id = 1 + wb.notify_on_exit = true + } + .build() + .notifiesOnCrossing, + ) } /** R2: geofence fields survive an unrelated edit and a proto encode/decode round-trip. */ @Test fun geofenceFieldsSurviveEditAndRoundTrip() { val original = - Waypoint( - id = 7, - latitude_i = 100_000_000, - longitude_i = 200_000_000, - name = "old", - geofence_radius = 500, - bounding_box = box, - notify_on_enter = true, - notify_on_exit = true, - notify_favorites_only = true, - ) - val edited = original.copy(name = "new") + Waypoint.Builder() + .also { wb -> + wb.id = 7 + wb.latitude_i = 100_000_000 + wb.longitude_i = 200_000_000 + wb.name = "old" + wb.geofence_radius = 500 + wb.bounding_box = box + wb.notify_on_enter = true + wb.notify_on_exit = true + wb.notify_favorites_only = true + } + .build() + val edited = original.newBuilder().also { wb -> wb.name = "new" }.build() assertEquals(500, edited.geofence_radius) assertEquals(box, edited.bounding_box) assertTrue(edited.notify_on_enter) diff --git a/core/model/src/commonTest/kotlin/org/meshtastic/core/model/util/ChannelReconciliationTest.kt b/core/model/src/commonTest/kotlin/org/meshtastic/core/model/util/ChannelReconciliationTest.kt new file mode 100644 index 0000000000..f26068dd80 --- /dev/null +++ b/core/model/src/commonTest/kotlin/org/meshtastic/core/model/util/ChannelReconciliationTest.kt @@ -0,0 +1,391 @@ +/* + * 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 . + */ +package org.meshtastic.core.model.util + +import okio.ByteString.Companion.toByteString +import org.meshtastic.proto.ChannelSet +import org.meshtastic.proto.ChannelSettings +import org.meshtastic.proto.Config.LoRaConfig +import org.meshtastic.proto.Config.LoRaConfig.ModemPreset +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertNotEquals +import kotlin.test.assertTrue +import org.meshtastic.core.model.Channel as ModelChannel + +class ChannelReconciliationTest { + private val longFast = + LoRaConfig.Builder() + .also { wb -> + wb.use_preset = true + wb.modem_preset = ModemPreset.LONG_FAST + } + .build() + private val mediumFast = + LoRaConfig.Builder() + .also { wb -> + wb.use_preset = true + wb.modem_preset = ModemPreset.MEDIUM_FAST + } + .build() + private val defaultPsk = byteArrayOf(0x01).toByteString() + private val otherPsk = byteArrayOf(0x09, 0x08).toByteString() + + private fun set(lora: LoRaConfig?, vararg settings: ChannelSettings) = ChannelSet.Builder() + .also { wb -> + wb.settings = settings.toList() + wb.lora_config = lora + } + .build() + + @Test + fun `an unchanged set produces no changes`() { + val settings = + ChannelSettings.Builder() + .also { wb -> + wb.psk = defaultPsk + wb.name = "A" + } + .build() + assertEquals(emptyList(), planChannelReconciliation(set(longFast, settings), set(longFast, settings))) + } + + @Test + fun `a replaced slot retires the previous occupant`() { + val old = + ChannelSettings.Builder() + .also { wb -> + wb.psk = defaultPsk + wb.name = "A" + } + .build() + val changes = + planChannelReconciliation( + set(longFast, old), + set(longFast, ChannelSettings.Builder().also { wb -> wb.psk = otherPsk }.build()), + ) + + val change = changes.single() + assertEquals(ConversationSlot.Live(0), change.from) + assertEquals(ConversationSlot.Retired(old.channelIdentity(longFast).token), change.to) + assertEquals("A", change.displayName) + } + + @Test + fun `a reordered channel moves rather than retires`() { + val a = + ChannelSettings.Builder() + .also { wb -> + wb.psk = defaultPsk + wb.name = "A" + } + .build() + val b = + ChannelSettings.Builder() + .also { wb -> + wb.psk = otherPsk + wb.name = "B" + } + .build() + val changes = planChannelReconciliation(set(longFast, a, b), set(longFast, b, a)) + + assertEquals( + setOf( + ChannelKeyChange(ConversationSlot.Live(0), ConversationSlot.Live(1), "A"), + ChannelKeyChange(ConversationSlot.Live(1), ConversationSlot.Live(0), "B"), + ), + changes.toSet(), + ) + } + + /** Effective name comes from the preset, so a preset switch is a channel change with neither raw field moved. */ + @Test + fun `a preset switch is a channel change`() { + val settings = ChannelSettings.Builder().also { wb -> wb.psk = defaultPsk }.build() + val changes = planChannelReconciliation(set(longFast, settings), set(mediumFast, settings)) + + assertEquals("LongFast", changes.single().displayName) + assertTrue(changes.single().to is ConversationSlot.Retired) + } + + /** + * The radio can report the same channel in two slots (the handshake writes each slot as it arrives, with no + * de-duplication). A conversation that still holds its own slot must stay there rather than being dragged to the + * duplicate — the behaviour the old PSK migration had, and the reason it preferred the same index. + */ + @Test + fun `a duplicated channel does not drag a conversation off its own slot`() { + val settings = + ChannelSettings.Builder() + .also { wb -> + wb.psk = defaultPsk + wb.name = "A" + } + .build() + assertEquals(emptyList(), planChannelReconciliation(set(longFast, settings), set(longFast, settings, settings))) + } + + /** With the identity gone from its own slot, the lowest duplicate is a deterministic destination. */ + @Test + fun `a duplicated channel resolves to its lowest slot`() { + val a = + ChannelSettings.Builder() + .also { wb -> + wb.psk = defaultPsk + wb.name = "A" + } + .build() + val b = + ChannelSettings.Builder() + .also { wb -> + wb.psk = otherPsk + wb.name = "B" + } + .build() + val changes = planChannelReconciliation(set(longFast, b, a), set(longFast, b, b, a, a)) + + assertEquals(ConversationSlot.Live(2), changes.single { it.from == ConversationSlot.Live(1) }.to) + } + + /** A parked channel must not be reclaimed twice when the radio reports it in two slots. */ + @Test + fun `a duplicated channel is reclaimed only once`() { + val settings = + ChannelSettings.Builder() + .also { wb -> + wb.psk = defaultPsk + wb.name = "A" + } + .build() + val token = settings.channelIdentity(longFast).token + val changes = + planChannelReconciliation( + old = set(longFast, ChannelSettings.Builder().also { wb -> wb.psk = otherPsk }.build()), + new = set(longFast, settings, settings), + retiredTokens = setOf(token), + ) + + assertEquals(1, changes.count { it.from == ConversationSlot.Retired(token) }) + } + + /** + * Two slots can genuinely resolve to the same channel: a blank-named secondary carrying the one-byte default PSK is + * not a placeholder (`psk.size == 0` is false), so it has the primary's identity. The slot that still holds that + * identity must keep its own conversation — merging the other one into it is the very failure this repairs. + */ + @Test + fun `a duplicate old slot is retired rather than merged into the slot that kept the identity`() { + val settings = ChannelSettings.Builder().also { wb -> wb.psk = defaultPsk }.build() + val changes = planChannelReconciliation(set(longFast, settings, settings), set(longFast, settings)) + + val change = changes.single() + assertEquals(ConversationSlot.Live(1), change.from) + assertTrue(change.to is ConversationSlot.Retired, "slot 1 must not be merged onto slot 0") + } + + /** With duplicates on both sides the pairing must be one-to-one, never two conversations onto one slot. */ + @Test + fun `duplicate slots pair off one to one`() { + val a = + ChannelSettings.Builder() + .also { wb -> + wb.psk = defaultPsk + wb.name = "A" + } + .build() + val b = + ChannelSettings.Builder() + .also { wb -> + wb.psk = otherPsk + wb.name = "B" + } + .build() + // A holds slot 1 in both sets, so it stays; B and the other A trade places around it. + val changes = planChannelReconciliation(set(longFast, b, a, a), set(longFast, a, a, b)) + + assertEquals( + setOf( + ChannelKeyChange(ConversationSlot.Live(0), ConversationSlot.Live(2), "B"), + ChannelKeyChange(ConversationSlot.Live(2), ConversationSlot.Live(0), "A"), + ), + changes.toSet(), + ) + assertEquals( + changes.size, + changes.map { it.to }.distinct().size, + "no two conversations may be sent to the same slot", + ) + } + + @Test + fun `a parked channel is reclaimed when it comes back`() { + val settings = + ChannelSettings.Builder() + .also { wb -> + wb.psk = defaultPsk + wb.name = "A" + } + .build() + val token = settings.channelIdentity(longFast).token + val changes = + planChannelReconciliation( + old = set(longFast, ChannelSettings.Builder().also { wb -> wb.psk = otherPsk }.build()), + new = set(longFast, settings), + retiredTokens = setOf(token), + ) + + assertTrue(changes.any { it.from == ConversationSlot.Retired(token) && it.to == ConversationSlot.Live(0) }) + } + + /** A blank name only resolves through the preset, so an unknown LoRa config cannot decide identity. */ + @Test + fun `a missing lora config on either side retires nothing`() { + val settings = ChannelSettings.Builder().also { wb -> wb.psk = defaultPsk }.build() + assertEquals(emptyList(), planChannelReconciliation(set(longFast, settings), set(null, settings))) + assertEquals(emptyList(), planChannelReconciliation(set(null, settings), set(longFast, settings))) + } + + /** + * An empty channel list means "not downloaded yet", never "every channel was deleted" — a real radio always has a + * primary. Retiring against it would archive the whole device's history mid-handshake. + */ + @Test + fun `an empty new channel list retires nothing`() { + val settings = + ChannelSettings.Builder() + .also { wb -> + wb.psk = defaultPsk + wb.name = "A" + } + .build() + assertEquals( + emptyList(), + planChannelReconciliation( + set(longFast, settings), + ChannelSet.Builder().also { wb -> wb.lora_config = longFast }.build(), + ), + ) + } + + /** A gap left by a removed secondary is padded with a bare ChannelSettings; that is not a channel. */ + @Test + fun `blank padding secondaries are not treated as channels`() { + val primary = + ChannelSettings.Builder() + .also { wb -> + wb.psk = defaultPsk + wb.name = "A" + } + .build() + val changes = + planChannelReconciliation( + old = set(longFast, primary, ChannelSettings.Builder().build()), + new = set(longFast, primary), + ) + assertEquals(emptyList(), changes) + } + + @Test + fun `region is not part of channel identity`() { + val settings = ChannelSettings.Builder().also { wb -> wb.psk = defaultPsk }.build() + val changes = + planChannelReconciliation( + set(longFast, settings), + set(longFast.newBuilder().also { wb -> wb.region = LoRaConfig.RegionCode.EU_868 }.build(), settings), + ) + assertEquals(emptyList(), changes) + } + + /** + * The default PSK marker `AQ==` expands to the same key on every preset, so the effective name carries the whole + * distinction between one stock channel and another. This is the case the reported bug ran into. + */ + @Test + fun `the default psk is disambiguated across presets`() { + val stock = ChannelSettings.Builder().also { wb -> wb.psk = defaultPsk }.build() + val tokens = + listOf( + longFast, + mediumFast, + LoRaConfig.Builder() + .also { wb -> + wb.use_preset = true + wb.modem_preset = ModemPreset.SHORT_FAST + } + .build(), + ) + .map { lora -> stock.channelIdentity(lora).token } + + assertEquals(tokens.size, tokens.distinct().size, "each preset must be a distinct channel") + } + + /** + * Known limit, tracked in meshtastic/design#157: a custom LoRa config has no preset name, so every blank-named + * custom channel resolves to "Custom" and two different custom meshes share an identity. This matches the firmware, + * which also hashes only name and PSK into the channel — custom meshes are separated by frequency, not by channel. + * Pinned here so that changing it is a deliberate act. + */ + @Test + fun `two different custom lora configs are not yet distinguished`() { + val stock = ChannelSettings.Builder().also { wb -> wb.psk = defaultPsk }.build() + val slow = + LoRaConfig.Builder() + .also { wb -> + wb.use_preset = false + wb.bandwidth = 125 + wb.spread_factor = 11 + wb.coding_rate = 8 + } + .build() + val fast = + LoRaConfig.Builder() + .also { wb -> + wb.use_preset = false + wb.bandwidth = 250 + wb.spread_factor = 7 + wb.coding_rate = 5 + } + .build() + + assertEquals("Custom", ModelChannel(settings = stock, loraConfig = slow).name) + assertEquals(stock.channelIdentity(slow).token, stock.channelIdentity(fast).token) + } + + @Test + fun `a token distinguishes name from psk and never leaks either`() { + val a = + ChannelSettings.Builder() + .also { wb -> + wb.psk = defaultPsk + wb.name = "ab" + } + .build() + .channelIdentity(longFast) + val b = + ChannelSettings.Builder() + .also { wb -> + wb.psk = defaultPsk + wb.name = "ba" + } + .build() + .channelIdentity(longFast) + assertNotEquals(a.token, b.token) + assertEquals(CHANNEL_IDENTITY_TOKEN_LENGTH, a.token.length) + assertTrue(a.token.all { it.isDigit() || it in 'a'..'f' }) + assertEquals("ChannelIdentity(name=ab, psk=)", a.toString()) + } +} diff --git a/core/model/src/commonTest/kotlin/org/meshtastic/core/model/util/ChannelSetReplacementTest.kt b/core/model/src/commonTest/kotlin/org/meshtastic/core/model/util/ChannelSetReplacementTest.kt index c270f9f740..e563dad832 100644 --- a/core/model/src/commonTest/kotlin/org/meshtastic/core/model/util/ChannelSetReplacementTest.kt +++ b/core/model/src/commonTest/kotlin/org/meshtastic/core/model/util/ChannelSetReplacementTest.kt @@ -27,12 +27,15 @@ class ChannelSetReplacementTest { @Test fun `replacement plan normalizes padding and duplicates before slot validation`() { - val primary = ChannelSettings(name = "Primary") - val uniqueSecondaries = (1..7).map { ChannelSettings(name = "Secondary $it") } - val rawSettings = listOf(primary, ChannelSettings(), primary) + uniqueSecondaries + val primary = ChannelSettings.Builder().also { wb -> wb.name = "Primary" }.build() + val uniqueSecondaries = + (1..7).map { ChannelSettings.Builder().also { wb -> wb.name = "Secondary $it" }.build() } + val rawSettings = listOf(primary, ChannelSettings.Builder().build(), primary) + uniqueSecondaries val plan = - ChannelSet(settings = rawSettings) + ChannelSet.Builder() + .also { wb -> wb.settings = rawSettings } + .build() .toChannelReplacementPlan( currentSettings = emptyList(), fallbackLoraConfig = null, @@ -48,9 +51,11 @@ class ChannelSetReplacementTest { @Test fun `replacement plan rejects empty profile channel set before producing writes`() { assertFailsWith { - ChannelSet(settings = emptyList()) + ChannelSet.Builder() + .also { wb -> wb.settings = emptyList() } + .build() .toChannelReplacementPlan( - currentSettings = listOf(ChannelSettings(name = "Existing")), + currentSettings = listOf(ChannelSettings.Builder().also { wb -> wb.name = "Existing" }.build()), fallbackLoraConfig = null, requirePrimary = true, ) diff --git a/core/model/src/commonTest/kotlin/org/meshtastic/core/model/util/ChannelSetUrlTest.kt b/core/model/src/commonTest/kotlin/org/meshtastic/core/model/util/ChannelSetUrlTest.kt index e047d4c2f1..30bc7566df 100644 --- a/core/model/src/commonTest/kotlin/org/meshtastic/core/model/util/ChannelSetUrlTest.kt +++ b/core/model/src/commonTest/kotlin/org/meshtastic/core/model/util/ChannelSetUrlTest.kt @@ -72,25 +72,32 @@ class ChannelSetUrlTest { return ChannelSet.ADAPTER.decode(bytes) } - private fun channelSet(channelCount: Int): ChannelSet = ChannelSet( - settings = - (0 until channelCount).map { index -> - ChannelSettings( - name = "Conference-$index", - psk = byteArrayOf(index.toByte(), (index + 16).toByte()).toByteString(), - module_settings = ModuleSettings(position_precision = index), - ) - }, - lora_config = - LoRaConfig( - use_preset = true, - modem_preset = ModemPreset.LONG_FAST, - region = RegionCode.US, - hop_limit = 5, - channel_num = 13, - tx_enabled = true, - ), - ) + private fun channelSet(channelCount: Int): ChannelSet = ChannelSet.Builder() + .also { wb -> + wb.settings = + (0 until channelCount).map { index -> + ChannelSettings.Builder() + .also { wb -> + wb.name = "Conference-$index" + wb.psk = byteArrayOf(index.toByte(), (index + 16).toByte()).toByteString() + wb.module_settings = + ModuleSettings.Builder().also { wb -> wb.position_precision = index }.build() + } + .build() + } + wb.lora_config = + LoRaConfig.Builder() + .also { wb -> + wb.use_preset = true + wb.modem_preset = ModemPreset.LONG_FAST + wb.region = RegionCode.US + wb.hop_limit = 5 + wb.channel_num = 13 + wb.tx_enabled = true + } + .build() + } + .build() private companion object { const val REPLACE_CHANNEL_URL = "https://meshtastic.org/e/#CgMSAQESBggBQANIAQ" diff --git a/core/model/src/commonTest/kotlin/org/meshtastic/core/model/util/DistanceExtensionsTest.kt b/core/model/src/commonTest/kotlin/org/meshtastic/core/model/util/DistanceExtensionsTest.kt index 925525e4d1..f2b3e11495 100644 --- a/core/model/src/commonTest/kotlin/org/meshtastic/core/model/util/DistanceExtensionsTest.kt +++ b/core/model/src/commonTest/kotlin/org/meshtastic/core/model/util/DistanceExtensionsTest.kt @@ -47,6 +47,15 @@ class DistanceExtensionsTest { assertEquals("1.0 mi", 1609.toDistanceString(MeasurementSystem.IMPERIAL)) } + /** Storm distance never drops to feet: 1 km is the detector's "overhead" reading and must read as km or mi. */ + @Test + fun `storm distance stays in the large unit`() { + assertEquals("1 km", 1f.toStormDistanceString(MeasurementSystem.METRIC)) + assertEquals("0.6 mi", 1f.toStormDistanceString(MeasurementSystem.IMPERIAL)) + assertEquals("40 km", 40f.toStormDistanceString(MeasurementSystem.METRIC)) + assertEquals("24.9 mi", 40f.toStormDistanceString(MeasurementSystem.IMPERIAL)) + } + @Test fun `kmhIn handles zero`() { assertEquals(0, 0.kmhIn(MeasurementSystem.METRIC)) diff --git a/core/model/src/commonTest/kotlin/org/meshtastic/core/model/util/MeshBeaconOfferTest.kt b/core/model/src/commonTest/kotlin/org/meshtastic/core/model/util/MeshBeaconOfferTest.kt index df995cc78d..d7968dc53a 100644 --- a/core/model/src/commonTest/kotlin/org/meshtastic/core/model/util/MeshBeaconOfferTest.kt +++ b/core/model/src/commonTest/kotlin/org/meshtastic/core/model/util/MeshBeaconOfferTest.kt @@ -33,12 +33,19 @@ import kotlin.test.assertNull class MeshBeaconOfferTest { private val radioLora = - LoRaConfig(use_preset = true, modem_preset = ModemPreset.LONG_FAST, region = RegionCode.US, channel_num = 0) - private val radioChannels = listOf(ChannelSettings(name = "HomeMesh")) + LoRaConfig.Builder() + .also { wb -> + wb.use_preset = true + wb.modem_preset = ModemPreset.LONG_FAST + wb.region = RegionCode.US + wb.channel_num = 0 + } + .build() + private val radioChannels = listOf(ChannelSettings.Builder().also { wb -> wb.name = "HomeMesh" }.build()) @Test fun `no offer channel yields NONE`() { - val beacon = MeshBeacon(message = "hi") + val beacon = MeshBeacon.Builder().also { wb -> wb.message = "hi" }.build() assertEquals(BeaconJoinOption.NONE, beacon.beaconJoinOption(radioLora, radioChannels)) } @@ -46,11 +53,13 @@ class MeshBeaconOfferTest { fun `matching preset region and slot yields ADD`() { // Offering the radio's own primary channel name forces an identical name-hash slot → addable with no reboot. val beacon = - MeshBeacon( - offer_channel = ChannelSettings(name = "HomeMesh"), - offer_preset = ModemPreset.LONG_FAST, - offer_region = RegionCode.US, - ) + MeshBeacon.Builder() + .also { wb -> + wb.offer_channel = ChannelSettings.Builder().also { wb -> wb.name = "HomeMesh" }.build() + wb.offer_preset = ModemPreset.LONG_FAST + wb.offer_region = RegionCode.US + } + .build() assertEquals(BeaconJoinOption.ADD, beacon.beaconJoinOption(radioLora, radioChannels)) } @@ -59,41 +68,50 @@ class MeshBeaconOfferTest { // The radio's primary has an empty name (resolves to the preset display name "LongFast" for the slot hash); a // beacon offering a channel literally named "LongFast" on the same preset+region is the same slot -> ADD. // Without effective-name resolution the empty primary would hash "" and misclassify as SWITCH. - val emptyPrimary = listOf(ChannelSettings(name = "")) + val emptyPrimary = listOf(ChannelSettings.Builder().also { wb -> wb.name = "" }.build()) val beacon = - MeshBeacon( - offer_channel = ChannelSettings(name = "LongFast"), - offer_preset = ModemPreset.LONG_FAST, - offer_region = RegionCode.US, - ) + MeshBeacon.Builder() + .also { wb -> + wb.offer_channel = ChannelSettings.Builder().also { wb -> wb.name = "LongFast" }.build() + wb.offer_preset = ModemPreset.LONG_FAST + wb.offer_region = RegionCode.US + } + .build() assertEquals(BeaconJoinOption.ADD, beacon.beaconJoinOption(radioLora, emptyPrimary)) } @Test fun `different preset forces SWITCH`() { val beacon = - MeshBeacon( - offer_channel = ChannelSettings(name = "HomeMesh"), - offer_preset = ModemPreset.SHORT_FAST, - offer_region = RegionCode.US, - ) + MeshBeacon.Builder() + .also { wb -> + wb.offer_channel = ChannelSettings.Builder().also { wb -> wb.name = "HomeMesh" }.build() + wb.offer_preset = ModemPreset.SHORT_FAST + wb.offer_region = RegionCode.US + } + .build() assertEquals(BeaconJoinOption.SWITCH, beacon.beaconJoinOption(radioLora, radioChannels)) } @Test fun `different region forces SWITCH`() { val beacon = - MeshBeacon( - offer_channel = ChannelSettings(name = "HomeMesh"), - offer_preset = ModemPreset.LONG_FAST, - offer_region = RegionCode.EU_868, - ) + MeshBeacon.Builder() + .also { wb -> + wb.offer_channel = ChannelSettings.Builder().also { wb -> wb.name = "HomeMesh" }.build() + wb.offer_preset = ModemPreset.LONG_FAST + wb.offer_region = RegionCode.EU_868 + } + .build() assertEquals(BeaconJoinOption.SWITCH, beacon.beaconJoinOption(radioLora, radioChannels)) } @Test fun `null lora config forces SWITCH`() { - val beacon = MeshBeacon(offer_channel = ChannelSettings(name = "HomeMesh")) + val beacon = + MeshBeacon.Builder() + .also { wb -> wb.offer_channel = ChannelSettings.Builder().also { wb -> wb.name = "HomeMesh" }.build() } + .build() assertEquals( BeaconJoinOption.SWITCH, beacon.beaconJoinOption(currentLora = null, currentChannels = emptyList()), @@ -103,11 +121,13 @@ class MeshBeaconOfferTest { @Test fun `toJoinChannelSet omits lora for ADD and includes it for SWITCH`() { val beacon = - MeshBeacon( - offer_channel = ChannelSettings(name = "PartyNet"), - offer_preset = ModemPreset.LONG_FAST, - offer_region = RegionCode.US, - ) + MeshBeacon.Builder() + .also { wb -> + wb.offer_channel = ChannelSettings.Builder().also { wb -> wb.name = "PartyNet" }.build() + wb.offer_preset = ModemPreset.LONG_FAST + wb.offer_region = RegionCode.US + } + .build() assertNull(beacon.toJoinChannelSet(BeaconJoinOption.ADD, radioLora)?.lora_config, "ADD must not retune") assertNotNull(beacon.toJoinChannelSet(BeaconJoinOption.SWITCH, radioLora)?.lora_config, "SWITCH carries lora") assertNull(beacon.toJoinChannelSet(BeaconJoinOption.NONE, radioLora)) @@ -118,21 +138,26 @@ class MeshBeaconOfferTest { // The join must NOT copy the current config: a stale channel_num / override / manual preset would strand the // radio on the old slot. use_preset is set, the offered preset+region applied, and every RF field left blank. val current = - radioLora.copy( - modem_preset = ModemPreset.MEDIUM_FAST, - region = RegionCode.EU_868, - hop_limit = 7, - tx_power = 27, - tx_enabled = true, - channel_num = 5, - override_frequency = 915.5f, - ) + radioLora + .newBuilder() + .also { wb -> + wb.modem_preset = ModemPreset.MEDIUM_FAST + wb.region = RegionCode.EU_868 + wb.hop_limit = 7 + wb.tx_power = 27 + wb.tx_enabled = true + wb.channel_num = 5 + wb.override_frequency = 915.5f + } + .build() val beacon = - MeshBeacon( - offer_channel = ChannelSettings(name = "PartyNet"), - offer_preset = ModemPreset.LONG_FAST, - offer_region = RegionCode.US, - ) + MeshBeacon.Builder() + .also { wb -> + wb.offer_channel = ChannelSettings.Builder().also { wb -> wb.name = "PartyNet" }.build() + wb.offer_preset = ModemPreset.LONG_FAST + wb.offer_region = RegionCode.US + } + .build() val lora = beacon.toJoinChannelSet(BeaconJoinOption.SWITCH, current)?.lora_config assertNotNull(lora) assertEquals(true, lora.use_preset, "use_preset must be set") @@ -148,8 +173,18 @@ class MeshBeaconOfferTest { fun `SWITCH without an offered preset keeps the current preset and region and resets channel_num`() { // A channel-only beacon (no preset) must still reset channel_num=0 so firmware re-derives the frequency from // the new primary name, keep the current preset, and carry the current region (a zero region disables TX). - val current = radioLora.copy(modem_preset = ModemPreset.MEDIUM_FAST, channel_num = 5) - val beacon = MeshBeacon(offer_channel = ChannelSettings(name = "PartyNet")) + val current = + radioLora + .newBuilder() + .also { wb -> + wb.modem_preset = ModemPreset.MEDIUM_FAST + wb.channel_num = 5 + } + .build() + val beacon = + MeshBeacon.Builder() + .also { wb -> wb.offer_channel = ChannelSettings.Builder().also { wb -> wb.name = "PartyNet" }.build() } + .build() val lora = beacon.toJoinChannelSet(BeaconJoinOption.SWITCH, current)?.lora_config assertNotNull(lora, "SWITCH must always carry lora so channel_num resets") assertEquals(0, lora.channel_num, "channel_num reset to 0 even without an offered preset") @@ -161,15 +196,22 @@ class MeshBeaconOfferTest { fun `join strips position sharing from the offered channel`() { // Privacy: joining a stranger's mesh must never broadcast our location (Apple sets positionPrecision=0). val beacon = - MeshBeacon( - offer_channel = - ChannelSettings( - name = "PartyNet", - module_settings = org.meshtastic.proto.ModuleSettings(position_precision = 32), - ), - offer_preset = ModemPreset.LONG_FAST, - offer_region = RegionCode.US, - ) + MeshBeacon.Builder() + .also { wb -> + wb.offer_channel = + ChannelSettings.Builder() + .also { wb -> + wb.name = "PartyNet" + wb.module_settings = + org.meshtastic.proto.ModuleSettings.Builder() + .also { wb -> wb.position_precision = 32 } + .build() + } + .build() + wb.offer_preset = ModemPreset.LONG_FAST + wb.offer_region = RegionCode.US + } + .build() val added = beacon.toJoinChannelSet(BeaconJoinOption.ADD, radioLora)?.settings?.first() val switched = beacon.toJoinChannelSet(BeaconJoinOption.SWITCH, radioLora)?.settings?.first() assertEquals(0, added?.module_settings?.position_precision, "ADD zeroes position precision") @@ -182,12 +224,20 @@ class MeshBeaconOfferTest { MeshBeaconOffer( fromNodeNum = 42, beacon = - MeshBeacon( - message = "Join us", - offer_channel = ChannelSettings(name = "PartyNet", psk = "secret".encodeUtf8()), - offer_preset = ModemPreset.LONG_FAST, - offer_region = RegionCode.US, - ), + MeshBeacon.Builder() + .also { wb -> + wb.message = "Join us" + wb.offer_channel = + ChannelSettings.Builder() + .also { wb -> + wb.name = "PartyNet" + wb.psk = "secret".encodeUtf8() + } + .build() + wb.offer_preset = ModemPreset.LONG_FAST + wb.offer_region = RegionCode.US + } + .build(), snr = 6.5f, rssi = -70, ) @@ -200,7 +250,13 @@ class MeshBeaconOfferTest { val offer = MeshBeaconOffer( fromNodeNum = 42, - beacon = MeshBeacon(message = "Join us", offer_channel = ChannelSettings(name = "PartyNet")), + beacon = + MeshBeacon.Builder() + .also { wb -> + wb.message = "Join us" + wb.offer_channel = ChannelSettings.Builder().also { wb -> wb.name = "PartyNet" }.build() + } + .build(), snr = 6.5f, rssi = null, ) @@ -216,41 +272,83 @@ class MeshBeaconOfferTest { @Test fun `offer matching a configured channel by name and psk is already joined`() { - val configured = listOf(ChannelSettings(name = "PartyNet", psk = "secret".encodeUtf8())) - val beacon = - MeshBeacon( - offer_channel = ChannelSettings(name = "PartyNet", psk = "secret".encodeUtf8()), - offer_preset = ModemPreset.LONG_FAST, + val configured = + listOf( + ChannelSettings.Builder() + .also { wb -> + wb.name = "PartyNet" + wb.psk = "secret".encodeUtf8() + } + .build(), ) + val beacon = + MeshBeacon.Builder() + .also { wb -> + wb.offer_channel = + ChannelSettings.Builder() + .also { wb -> + wb.name = "PartyNet" + wb.psk = "secret".encodeUtf8() + } + .build() + wb.offer_preset = ModemPreset.LONG_FAST + } + .build() assertEquals(true, beacon.isAlreadyJoined(radioLora, configured)) } @Test fun `offer for a channel not configured on the radio is not already joined`() { - val configured = listOf(ChannelSettings(name = "HomeMesh")) + val configured = listOf(ChannelSettings.Builder().also { wb -> wb.name = "HomeMesh" }.build()) val beacon = - MeshBeacon(offer_channel = ChannelSettings(name = "PartyNet"), offer_preset = ModemPreset.LONG_FAST) + MeshBeacon.Builder() + .also { wb -> + wb.offer_channel = ChannelSettings.Builder().also { wb -> wb.name = "PartyNet" }.build() + wb.offer_preset = ModemPreset.LONG_FAST + } + .build() assertEquals(false, beacon.isAlreadyJoined(radioLora, configured)) } @Test fun `same name but different psk is a different mesh and is not already joined`() { // Same name under a different key is deliberately NOT a match. - val configured = listOf(ChannelSettings(name = "PartyNet", psk = "secret".encodeUtf8())) - val beacon = - MeshBeacon( - offer_channel = ChannelSettings(name = "PartyNet", psk = "different".encodeUtf8()), - offer_preset = ModemPreset.LONG_FAST, + val configured = + listOf( + ChannelSettings.Builder() + .also { wb -> + wb.name = "PartyNet" + wb.psk = "secret".encodeUtf8() + } + .build(), ) + val beacon = + MeshBeacon.Builder() + .also { wb -> + wb.offer_channel = + ChannelSettings.Builder() + .also { wb -> + wb.name = "PartyNet" + wb.psk = "different".encodeUtf8() + } + .build() + wb.offer_preset = ModemPreset.LONG_FAST + } + .build() assertEquals(false, beacon.isAlreadyJoined(radioLora, configured)) } @Test fun `empty-name primary channel matches an offer naming the preset display name`() { // The radio's primary has a blank name, which resolves to the preset display name for identity purposes. - val configured = listOf(ChannelSettings(name = "")) + val configured = listOf(ChannelSettings.Builder().also { wb -> wb.name = "" }.build()) val beacon = - MeshBeacon(offer_channel = ChannelSettings(name = "LongFast"), offer_preset = ModemPreset.LONG_FAST) + MeshBeacon.Builder() + .also { wb -> + wb.offer_channel = ChannelSettings.Builder().also { wb -> wb.name = "LongFast" }.build() + wb.offer_preset = ModemPreset.LONG_FAST + } + .build() assertEquals(true, beacon.isAlreadyJoined(radioLora, configured)) } @@ -258,33 +356,51 @@ class MeshBeaconOfferTest { fun `default 1-byte psk shorthand matches its expanded full-length equivalent`() { // Both sides resolve to the same expanded default key bytes even though one is the 1-byte shorthand. val expandedDefaultKey = - ChannelSettings(name = "HomeMesh", psk = byteArrayOf(1).toByteString()).let { - it.copy(psk = Channel(it, radioLora).psk) - } + ChannelSettings.Builder() + .also { wb -> + wb.name = "HomeMesh" + wb.psk = byteArrayOf(1).toByteString() + } + .build() + .let { it.newBuilder().also { wb -> wb.psk = Channel(it, radioLora).psk }.build() } val configured = listOf(expandedDefaultKey) val beacon = - MeshBeacon( - offer_channel = ChannelSettings(name = "HomeMesh", psk = byteArrayOf(1).toByteString()), - offer_preset = ModemPreset.LONG_FAST, - ) + MeshBeacon.Builder() + .also { wb -> + wb.offer_channel = + ChannelSettings.Builder() + .also { wb -> + wb.name = "HomeMesh" + wb.psk = byteArrayOf(1).toByteString() + } + .build() + wb.offer_preset = ModemPreset.LONG_FAST + } + .build() assertEquals(true, beacon.isAlreadyJoined(radioLora, configured)) } @Test fun `isAlreadyJoined returns false when lora config is unknown so a real invitation is never hidden`() { - val beacon = MeshBeacon(offer_channel = ChannelSettings(name = "HomeMesh")) + val beacon = + MeshBeacon.Builder() + .also { wb -> wb.offer_channel = ChannelSettings.Builder().also { wb -> wb.name = "HomeMesh" }.build() } + .build() assertEquals(false, beacon.isAlreadyJoined(currentLora = null, currentChannels = radioChannels)) } @Test fun `isAlreadyJoined returns false when no channels are configured`() { - val beacon = MeshBeacon(offer_channel = ChannelSettings(name = "HomeMesh")) + val beacon = + MeshBeacon.Builder() + .also { wb -> wb.offer_channel = ChannelSettings.Builder().also { wb -> wb.name = "HomeMesh" }.build() } + .build() assertEquals(false, beacon.isAlreadyJoined(currentLora = radioLora, currentChannels = emptyList())) } @Test fun `isAlreadyJoined returns false for a beacon with no offer channel`() { - val beacon = MeshBeacon(message = "hi") + val beacon = MeshBeacon.Builder().also { wb -> wb.message = "hi" }.build() assertEquals(false, beacon.isAlreadyJoined(radioLora, radioChannels)) } @@ -292,8 +408,18 @@ class MeshBeaconOfferTest { fun `blank-name no-psk secondary placeholder slot is not mistaken for an already-joined channel`() { // SwitchingChannelSetDataSource pads a gap left by a removed channel with a bare ChannelSettings(); a beacon // offering a genuinely blank cleartext channel on the same preset must not match that padding. - val configured = listOf(ChannelSettings(name = "HomeMesh"), ChannelSettings()) - val beacon = MeshBeacon(offer_channel = ChannelSettings(), offer_preset = ModemPreset.LONG_FAST) + val configured = + listOf( + ChannelSettings.Builder().also { wb -> wb.name = "HomeMesh" }.build(), + ChannelSettings.Builder().build(), + ) + val beacon = + MeshBeacon.Builder() + .also { wb -> + wb.offer_channel = ChannelSettings.Builder().build() + wb.offer_preset = ModemPreset.LONG_FAST + } + .build() assertEquals(false, beacon.isAlreadyJoined(radioLora, configured)) } } diff --git a/core/model/src/commonTest/kotlin/org/meshtastic/core/model/util/MeshDataMapperTest.kt b/core/model/src/commonTest/kotlin/org/meshtastic/core/model/util/MeshDataMapperTest.kt index b051242f15..114cb634ae 100644 --- a/core/model/src/commonTest/kotlin/org/meshtastic/core/model/util/MeshDataMapperTest.kt +++ b/core/model/src/commonTest/kotlin/org/meshtastic/core/model/util/MeshDataMapperTest.kt @@ -44,29 +44,39 @@ class MeshDataMapperTest { @Test fun toDataPacket_returnsNullWhenPacketHasNoDecodedData() { - assertNull(mapper.toDataPacket(MeshPacket(from = 0x12345678))) + assertNull(mapper.toDataPacket(MeshPacket.Builder().also { wb -> wb.from = 0x12345678 }.build())) } @Test fun toDataPacket_mapsMeshPacketFields() { val payload = "mesh payload".encodeUtf8() val packet = - MeshPacket( - from = 0x12345678, - to = 0x90ABCDEF.toInt(), - rx_time = 123, - id = 456, - decoded = Data(portnum = PortNum.TEXT_MESSAGE_APP, payload = payload, reply_id = 789, emoji = 321), - hop_limit = 3, - channel = 4, - want_ack = true, - hop_start = 5, - rx_snr = 6.5f, - rx_rssi = -70, - relay_node = 77, - via_mqtt = true, - transport_mechanism = MeshPacket.TransportMechanism.TRANSPORT_MQTT, - ) + MeshPacket.Builder() + .also { wb -> + wb.from = 0x12345678 + wb.to = 0x90ABCDEF.toInt() + wb.rx_time = 123 + wb.id = 456 + wb.decoded = + Data.Builder() + .also { wb -> + wb.portnum = PortNum.TEXT_MESSAGE_APP + wb.payload = payload + wb.reply_id = 789 + wb.emoji = 321 + } + .build() + wb.hop_limit = 3 + wb.channel = 4 + wb.want_ack = true + wb.hop_start = 5 + wb.rx_snr = 6.5f + wb.rx_rssi = -70 + wb.relay_node = 77 + wb.via_mqtt = true + wb.transport_mechanism = MeshPacket.TransportMechanism.TRANSPORT_MQTT + } + .build() val mapped = mapper.toDataPacket(packet) @@ -92,7 +102,14 @@ class MeshDataMapperTest { @Test fun toDataPacket_preservesAbsentRssiRatherThanCoercingToZero() { - val packet = MeshPacket(from = 1, to = 2, decoded = Data(portnum = PortNum.TEXT_MESSAGE_APP)) + val packet = + MeshPacket.Builder() + .also { wb -> + wb.from = 1 + wb.to = 2 + wb.decoded = Data.Builder().also { wb -> wb.portnum = PortNum.TEXT_MESSAGE_APP }.build() + } + .build() val mapped = mapper.toDataPacket(packet) @@ -102,7 +119,15 @@ class MeshDataMapperTest { @Test fun toDataPacket_keepsAReportedZeroRssiDistinctFromAbsent() { - val packet = MeshPacket(from = 1, to = 2, rx_rssi = 0, decoded = Data(portnum = PortNum.TEXT_MESSAGE_APP)) + val packet = + MeshPacket.Builder() + .also { wb -> + wb.from = 1 + wb.to = 2 + wb.rx_rssi = 0 + wb.decoded = Data.Builder().also { wb -> wb.portnum = PortNum.TEXT_MESSAGE_APP }.build() + } + .build() val mapped = mapper.toDataPacket(packet) @@ -113,13 +138,15 @@ class MeshDataMapperTest { @Test fun toDataPacket_usesPkcChannelWhenPacketIsPkiEncrypted() { val packet = - MeshPacket( - from = 1, - to = 2, - channel = 2, - pki_encrypted = true, - decoded = Data(portnum = PortNum.PRIVATE_APP), - ) + MeshPacket.Builder() + .also { wb -> + wb.from = 1 + wb.to = 2 + wb.channel = 2 + wb.pki_encrypted = true + wb.decoded = Data.Builder().also { wb -> wb.portnum = PortNum.PRIVATE_APP }.build() + } + .build() val mapped = mapper.toDataPacket(packet) @@ -130,14 +157,16 @@ class MeshDataMapperTest { @Test fun meshUser_mapsProtoFields() { val proto = - User( - id = "!cafebabe", - long_name = "Meshtastic User", - short_name = "MU", - hw_model = HardwareModel.TLORA_V2, - is_licensed = true, - role = Config.DeviceConfig.Role.ROUTER, - ) + User.Builder() + .also { wb -> + wb.id = "!cafebabe" + wb.long_name = "Meshtastic User" + wb.short_name = "MU" + wb.hw_model = HardwareModel.TLORA_V2 + wb.is_licensed = true + wb.role = Config.DeviceConfig.Role.ROUTER + } + .build() val user = MeshUser(proto) @@ -151,7 +180,7 @@ class MeshDataMapperTest { @Test fun meshUser_defaultsEmptyFieldsFromEmptyProto() { - val user = MeshUser(User()) + val user = MeshUser(User.Builder().build()) assertEquals("", user.id) assertEquals("", user.longName) @@ -164,16 +193,18 @@ class MeshDataMapperTest { @Test fun position_mapsScaledCoordinatesAndProvidedTime() { val proto = - Position( - latitude_i = 377749000, - longitude_i = -1224194000, - altitude = 15, - time = 456, - sats_in_view = 9, - ground_speed = 12, - ground_track = 180, - precision_bits = 7, - ) + Position.Builder() + .also { wb -> + wb.latitude_i = 377749000 + wb.longitude_i = -1224194000 + wb.altitude = 15 + wb.time = 456 + wb.sats_in_view = 9 + wb.ground_speed = 12 + wb.ground_track = 180 + wb.precision_bits = 7 + } + .build() val position = DomainPosition(proto, defaultTime = 123) @@ -189,7 +220,7 @@ class MeshDataMapperTest { @Test fun position_usesDefaultTimeAndZeroValuesForUnsetProtoFields() { - val position = DomainPosition(Position(), defaultTime = 789) + val position = DomainPosition(Position.Builder().build(), defaultTime = 789) assertEquals(0.0, position.latitude) assertEquals(0.0, position.longitude) @@ -204,13 +235,15 @@ class MeshDataMapperTest { @Test fun deviceMetrics_mapsProtoFields() { val proto = - DeviceMetrics( - battery_level = 87, - voltage = 4.12f, - channel_utilization = 32.5f, - air_util_tx = 7.75f, - uptime_seconds = 3600, - ) + DeviceMetrics.Builder() + .also { wb -> + wb.battery_level = 87 + wb.voltage = 4.12f + wb.channel_utilization = 32.5f + wb.air_util_tx = 7.75f + wb.uptime_seconds = 3600 + } + .build() val metrics = DomainDeviceMetrics(proto, telemetryTime = 123) @@ -224,7 +257,7 @@ class MeshDataMapperTest { @Test fun deviceMetrics_defaultsUnsetFieldsToZero() { - val metrics = DomainDeviceMetrics(DeviceMetrics(), telemetryTime = 222) + val metrics = DomainDeviceMetrics(DeviceMetrics.Builder().build(), telemetryTime = 222) assertEquals(222, metrics.time) assertEquals(0, metrics.batteryLevel) @@ -237,19 +270,21 @@ class MeshDataMapperTest { @Test fun environmentMetrics_mapsTelemetryFields() { val proto = - EnvironmentMetrics( - temperature = 24.5f, - relative_humidity = 55.5f, - soil_temperature = 18.25f, - soil_moisture = 44, - barometric_pressure = 1013.2f, - gas_resistance = 10.5f, - voltage = 3.7f, - current = 0.8f, - iaq = 42, - lux = 321.5f, - uv_lux = 4.2f, - ) + EnvironmentMetrics.Builder() + .also { wb -> + wb.temperature = 24.5f + wb.relative_humidity = 55.5f + wb.soil_temperature = 18.25f + wb.soil_moisture = 44 + wb.barometric_pressure = 1013.2f + wb.gas_resistance = 10.5f + wb.voltage = 3.7f + wb.current = 0.8f + wb.iaq = 42 + wb.lux = 321.5f + wb.uv_lux = 4.2f + } + .build() val metrics = DomainEnvironmentMetrics.fromTelemetryProto(proto, time = 999) @@ -270,19 +305,21 @@ class MeshDataMapperTest { @Test fun environmentMetrics_filtersSentinelAndInvalidValues() { val proto = - EnvironmentMetrics( - temperature = Float.NaN, - relative_humidity = 0.0f, - soil_temperature = Float.NaN, - soil_moisture = Int.MIN_VALUE, - barometric_pressure = Float.NaN, - gas_resistance = Float.NaN, - voltage = Float.NaN, - current = Float.NaN, - iaq = Int.MIN_VALUE, - lux = Float.NaN, - uv_lux = Float.NaN, - ) + EnvironmentMetrics.Builder() + .also { wb -> + wb.temperature = Float.NaN + wb.relative_humidity = 0.0f + wb.soil_temperature = Float.NaN + wb.soil_moisture = Int.MIN_VALUE + wb.barometric_pressure = Float.NaN + wb.gas_resistance = Float.NaN + wb.voltage = Float.NaN + wb.current = Float.NaN + wb.iaq = Int.MIN_VALUE + wb.lux = Float.NaN + wb.uv_lux = Float.NaN + } + .build() val metrics = DomainEnvironmentMetrics.fromTelemetryProto(proto, time = 111) diff --git a/core/model/src/commonTest/kotlin/org/meshtastic/core/model/util/RxTimeExtensionsTest.kt b/core/model/src/commonTest/kotlin/org/meshtastic/core/model/util/RxTimeExtensionsTest.kt index c6a3834947..d9a35b4899 100644 --- a/core/model/src/commonTest/kotlin/org/meshtastic/core/model/util/RxTimeExtensionsTest.kt +++ b/core/model/src/commonTest/kotlin/org/meshtastic/core/model/util/RxTimeExtensionsTest.kt @@ -25,12 +25,14 @@ import kotlin.test.assertTrue class RxTimeExtensionsTest { - private fun loraPacket(rxTime: Int?) = MeshPacket( - rx_time = rxTime, - hop_start = 3, - hop_limit = 3, - transport_mechanism = MeshPacket.TransportMechanism.TRANSPORT_LORA, - ) + private fun loraPacket(rxTime: Int?) = MeshPacket.Builder() + .also { wb -> + wb.rx_time = rxTime + wb.hop_start = 3 + wb.hop_limit = 3 + wb.transport_mechanism = MeshPacket.TransportMechanism.TRANSPORT_LORA + } + .build() @Test fun `rxTimeOrNull returns the stamped time`() { diff --git a/core/model/src/commonTest/kotlin/org/meshtastic/core/model/util/SnrExtensionsTest.kt b/core/model/src/commonTest/kotlin/org/meshtastic/core/model/util/SnrExtensionsTest.kt index 4a34b38d81..d5268232bd 100644 --- a/core/model/src/commonTest/kotlin/org/meshtastic/core/model/util/SnrExtensionsTest.kt +++ b/core/model/src/commonTest/kotlin/org/meshtastic/core/model/util/SnrExtensionsTest.kt @@ -34,13 +34,15 @@ import kotlin.test.assertNotNull */ class SnrExtensionsTest { - private fun loraPacket(snr: Float) = MeshPacket( - rx_time = 1_700_000_000, - rx_snr = snr, - hop_start = 3, - hop_limit = 3, - transport_mechanism = MeshPacket.TransportMechanism.TRANSPORT_LORA, - ) + private fun loraPacket(snr: Float) = MeshPacket.Builder() + .also { wb -> + wb.rx_time = 1_700_000_000 + wb.rx_snr = snr + wb.hop_start = 3 + wb.hop_limit = 3 + wb.transport_mechanism = MeshPacket.TransportMechanism.TRANSPORT_LORA + } + .build() @Test fun `snrOrNull reports a negative reading`() { @@ -68,6 +70,6 @@ class SnrExtensionsTest { fun `snrOrNull does not conflate a zero reading with an unknown one`() { // Regression guard for the rx_time seam's pattern being copied over verbatim. assertEquals(0f, loraPacket(0f).snrOrNull()) - assertEquals(null, MeshPacket().rxTimeOrNull()) + assertEquals(null, MeshPacket.Builder().build().rxTimeOrNull()) } } diff --git a/core/model/src/commonTest/kotlin/org/meshtastic/core/model/util/TelemetryChannelsTest.kt b/core/model/src/commonTest/kotlin/org/meshtastic/core/model/util/TelemetryChannelsTest.kt index 6a3e58efe8..eb87530f01 100644 --- a/core/model/src/commonTest/kotlin/org/meshtastic/core/model/util/TelemetryChannelsTest.kt +++ b/core/model/src/commonTest/kotlin/org/meshtastic/core/model/util/TelemetryChannelsTest.kt @@ -30,16 +30,18 @@ class TelemetryChannelsTest { @Test fun oneWireAccessorReadsEveryChannelInOrder() { val metrics = - EnvironmentMetrics( - one_wire_temperature_ch0 = 0f, - one_wire_temperature_ch1 = 1f, - one_wire_temperature_ch2 = 2f, - one_wire_temperature_ch3 = 3f, - one_wire_temperature_ch4 = 4f, - one_wire_temperature_ch5 = 5f, - one_wire_temperature_ch6 = 6f, - one_wire_temperature_ch7 = 7f, - ) + EnvironmentMetrics.Builder() + .also { wb -> + wb.one_wire_temperature_ch0 = 0f + wb.one_wire_temperature_ch1 = 1f + wb.one_wire_temperature_ch2 = 2f + wb.one_wire_temperature_ch3 = 3f + wb.one_wire_temperature_ch4 = 4f + wb.one_wire_temperature_ch5 = 5f + wb.one_wire_temperature_ch6 = 6f + wb.one_wire_temperature_ch7 = 7f + } + .build() for (channel in 0 until TELEMETRY_CHANNEL_COUNT) { assertEquals(channel.toFloat(), metrics.oneWireTemperature(channel)) @@ -49,16 +51,18 @@ class TelemetryChannelsTest { @Test fun adcAccessorReadsEveryChannelInOrder() { val metrics = - EnvironmentMetrics( - adc_voltage_ch0 = 0f, - adc_voltage_ch1 = 1f, - adc_voltage_ch2 = 2f, - adc_voltage_ch3 = 3f, - adc_voltage_ch4 = 4f, - adc_voltage_ch5 = 5f, - adc_voltage_ch6 = 6f, - adc_voltage_ch7 = 7f, - ) + EnvironmentMetrics.Builder() + .also { wb -> + wb.adc_voltage_ch0 = 0f + wb.adc_voltage_ch1 = 1f + wb.adc_voltage_ch2 = 2f + wb.adc_voltage_ch3 = 3f + wb.adc_voltage_ch4 = 4f + wb.adc_voltage_ch5 = 5f + wb.adc_voltage_ch6 = 6f + wb.adc_voltage_ch7 = 7f + } + .build() for (channel in 0 until TELEMETRY_CHANNEL_COUNT) { assertEquals(channel.toFloat(), metrics.adcVoltage(channel)) @@ -67,17 +71,29 @@ class TelemetryChannelsTest { @Test fun measuredZeroIsDistinctFromAbsentChannel() { - val reported = EnvironmentMetrics(one_wire_temperature_ch3 = 0f, adc_voltage_ch3 = 0f) + val reported = + EnvironmentMetrics.Builder() + .also { wb -> + wb.one_wire_temperature_ch3 = 0f + wb.adc_voltage_ch3 = 0f + } + .build() assertEquals(0f, reported.oneWireTemperature(3)) assertEquals(0f, reported.adcVoltage(3)) - assertNull(EnvironmentMetrics().oneWireTemperature(3)) - assertNull(EnvironmentMetrics().adcVoltage(3)) + assertNull(EnvironmentMetrics.Builder().build().oneWireTemperature(3)) + assertNull(EnvironmentMetrics.Builder().build().adcVoltage(3)) } @Test fun outOfRangeChannelIsNullRatherThanAnError() { - val metrics = EnvironmentMetrics(one_wire_temperature_ch0 = 1f, adc_voltage_ch0 = 1f) + val metrics = + EnvironmentMetrics.Builder() + .also { wb -> + wb.one_wire_temperature_ch0 = 1f + wb.adc_voltage_ch0 = 1f + } + .build() assertNull(metrics.oneWireTemperature(TELEMETRY_CHANNEL_COUNT)) assertNull(metrics.adcVoltage(TELEMETRY_CHANNEL_COUNT)) @@ -87,7 +103,7 @@ class TelemetryChannelsTest { @Test fun withAccessorsSetOnlyTheTargetChannel() { - val metrics = EnvironmentMetrics().withOneWireTemperature(2, 12.5f).withAdcVoltage(5, 1.8f) + val metrics = EnvironmentMetrics.Builder().build().withOneWireTemperature(2, 12.5f).withAdcVoltage(5, 1.8f) assertEquals(12.5f, metrics.oneWireTemperature(2)) assertEquals(1.8f, metrics.adcVoltage(5)) @@ -97,7 +113,7 @@ class TelemetryChannelsTest { @Test fun withAccessorsIgnoreOutOfRangeChannels() { - val metrics = EnvironmentMetrics() + val metrics = EnvironmentMetrics.Builder().build() assertEquals(metrics, metrics.withOneWireTemperature(TELEMETRY_CHANNEL_COUNT, 1f)) assertEquals(metrics, metrics.withAdcVoltage(TELEMETRY_CHANNEL_COUNT, 1f)) @@ -108,7 +124,7 @@ class TelemetryChannelsTest { @Suppress("DEPRECATION") @Test fun legacyListIsLiftedOntoPerChannelFields() { - val stored = EnvironmentMetrics(one_wire_temperature = listOf(10f, 0f, 30f)) + val stored = EnvironmentMetrics.Builder().also { wb -> wb.one_wire_temperature = listOf(10f, 0f, 30f) }.build() val lifted = stored.withLegacyOneWireTemperatures() @@ -122,7 +138,13 @@ class TelemetryChannelsTest { @Suppress("DEPRECATION") @Test fun perChannelValuesWinOverLegacyList() { - val mixed = EnvironmentMetrics(one_wire_temperature = listOf(10f, 20f), one_wire_temperature_ch0 = 99f) + val mixed = + EnvironmentMetrics.Builder() + .also { wb -> + wb.one_wire_temperature = listOf(10f, 20f) + wb.one_wire_temperature_ch0 = 99f + } + .build() val lifted = mixed.withLegacyOneWireTemperatures() @@ -133,7 +155,8 @@ class TelemetryChannelsTest { @Suppress("DEPRECATION") @Test fun legacyListLongerThanTheChannelRangeIsTruncated() { - val stored = EnvironmentMetrics(one_wire_temperature = List(12) { it.toFloat() }) + val stored = + EnvironmentMetrics.Builder().also { wb -> wb.one_wire_temperature = List(12) { it.toFloat() } }.build() val lifted = stored.withLegacyOneWireTemperatures() @@ -143,7 +166,7 @@ class TelemetryChannelsTest { @Test fun absentLegacyListLeavesMetricsUnchanged() { - val metrics = EnvironmentMetrics(temperature = 21f) + val metrics = EnvironmentMetrics.Builder().also { wb -> wb.temperature = 21f }.build() assertEquals(metrics, metrics.withLegacyOneWireTemperatures()) } diff --git a/core/model/src/commonTest/kotlin/org/meshtastic/core/model/util/TelemetryExtensionsTest.kt b/core/model/src/commonTest/kotlin/org/meshtastic/core/model/util/TelemetryExtensionsTest.kt new file mode 100644 index 0000000000..b94221d001 --- /dev/null +++ b/core/model/src/commonTest/kotlin/org/meshtastic/core/model/util/TelemetryExtensionsTest.kt @@ -0,0 +1,68 @@ +/* + * 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 . + */ +package org.meshtastic.core.model.util + +import org.meshtastic.proto.EnvironmentMetrics +import org.meshtastic.proto.Telemetry +import kotlin.test.Test +import kotlin.test.assertFalse +import kotlin.test.assertTrue + +/** Pins which environment readings admit a telemetry row to the Environment log. */ +class TelemetryExtensionsTest { + + private fun telemetry(build: EnvironmentMetrics.Builder.() -> Unit): Telemetry = Telemetry.Builder() + .also { wb -> wb.environment_metrics = EnvironmentMetrics.Builder().also(build).build() } + .build() + + @Test + fun `no environment metrics is not valid`() { + assertFalse(Telemetry.Builder().build().hasValidEnvironmentMetrics()) + assertFalse(telemetry {}.hasValidEnvironmentMetrics()) + } + + @Test + fun `temperature and humidity together are valid`() { + assertTrue( + telemetry { + temperature = 21.5f + relative_humidity = 40f + } + .hasValidEnvironmentMetrics(), + ) + } + + @Test + fun `temperature alone or a NaN temperature is not valid`() { + assertFalse(telemetry { temperature = 21.5f }.hasValidEnvironmentMetrics()) + assertFalse( + telemetry { + temperature = Float.NaN + relative_humidity = 40f + } + .hasValidEnvironmentMetrics(), + ) + } + + /** An AS3935 on its own reports only lightning; 0 strikes is a reading. */ + @Test + fun `lightning alone is valid`() { + assertTrue(telemetry { lightning_strike_count_1h = 0 }.hasValidEnvironmentMetrics()) + assertTrue(telemetry { lightning_distance_km = 12f }.hasValidEnvironmentMetrics()) + assertFalse(telemetry { lightning_distance_km = Float.NaN }.hasValidEnvironmentMetrics()) + } +} diff --git a/core/model/src/commonTest/kotlin/org/meshtastic/core/model/util/ToSharedContactTest.kt b/core/model/src/commonTest/kotlin/org/meshtastic/core/model/util/ToSharedContactTest.kt new file mode 100644 index 0000000000..dc70dced65 --- /dev/null +++ b/core/model/src/commonTest/kotlin/org/meshtastic/core/model/util/ToSharedContactTest.kt @@ -0,0 +1,61 @@ +/* + * 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 . + */ +package org.meshtastic.core.model.util + +import org.meshtastic.core.model.Node +import org.meshtastic.proto.User +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertFalse +import kotlin.test.assertTrue + +class ToSharedContactTest { + + private fun node(manuallyVerified: Boolean = false) = Node( + num = 7, + user = + User.Builder() + .also { wb -> + wb.id = "!7" + wb.long_name = "Seven" + } + .build(), + manuallyVerified = manuallyVerified, + ) + + @Test + fun `sharing your own contact marks it manually verified`() { + assertTrue(node().toSharedContact(isOwnContact = true).manually_verified) + } + + @Test + fun `relaying someone else's contact asserts nothing on their behalf`() { + assertFalse(node().toSharedContact(isOwnContact = false).manually_verified) + } + + @Test + fun `a contact already verified in person stays verified when relayed`() { + assertTrue(node(manuallyVerified = true).toSharedContact(isOwnContact = false).manually_verified) + } + + @Test + fun `carries the node number and user through`() { + val shared = node().toSharedContact() + assertEquals(7, shared.node_num) + assertEquals("Seven", shared.user?.long_name) + } +} diff --git a/core/navigation/build.gradle.kts b/core/navigation/build.gradle.kts index ff848a8271..d06be78e49 100644 --- a/core/navigation/build.gradle.kts +++ b/core/navigation/build.gradle.kts @@ -28,6 +28,9 @@ kotlin { implementation(projects.core.resources) implementation(libs.kotlinx.serialization.core) implementation(libs.jetbrains.navigation3.ui) + // navigation3-ui drags in navigation3-runtime 1.2.0-beta01; the app resolves the + // catalog's rc01, so declare it directly to compile against what it runs against. + implementation(libs.androidx.navigation3.runtime) implementation(libs.kermit) } diff --git a/core/navigation/src/commonMain/kotlin/org/meshtastic/core/navigation/DeepLinkRouter.kt b/core/navigation/src/commonMain/kotlin/org/meshtastic/core/navigation/DeepLinkRouter.kt index 1ea79eb2a7..efe8aa2911 100644 --- a/core/navigation/src/commonMain/kotlin/org/meshtastic/core/navigation/DeepLinkRouter.kt +++ b/core/navigation/src/commonMain/kotlin/org/meshtastic/core/navigation/DeepLinkRouter.kt @@ -17,15 +17,29 @@ package org.meshtastic.core.navigation import androidx.navigation3.runtime.NavKey +import androidx.navigation3.runtime.deeplink.BackStackMatcher +import androidx.navigation3.runtime.deeplink.DeepLinkRequest +import androidx.navigation3.runtime.deeplink.DeepLinkUri +import androidx.navigation3.runtime.deeplink.UriDeepLinkMatcher +import androidx.navigation3.runtime.deeplink.UriMatchResult +import androidx.navigation3.runtime.deeplink.withBackStack import co.touchlab.kermit.Logger +import kotlinx.serialization.KSerializer import org.meshtastic.core.common.util.CommonUri +/** Placeholder values a pattern captured, keyed by placeholder name. */ +private typealias DeepLinkArgs = Map> + /** - * Type-safe deep link parser for KMP Navigation 3. + * Deep-link router for KMP Navigation 3. * - * Maps an incoming OS intent URI to a list of NavKeys representing the target backstack. This ensures that when a user - * deep links into a detail view, the logical "up" hierarchy is synthesized and correctly populated in the user-owned - * NavBackStack list. + * Maps an incoming OS intent URI to the back stack it should open, so a link into a detail view lands with its logical + * "up" hierarchy already populated. Every supported link is a [UriDeepLinkMatcher] pattern whose placeholders decode + * straight into the target route's fields, wrapped with [withBackStack] to synthesize the parents. + * + * Path literals match case-insensitively. The scheme and host are not part of the contract: `meshtastic://meshtastic` + * and `http(s)://meshtastic.org` links are routed identically, because the OS intent-filter already decides which hosts + * reach the app. * * Supports both legacy query-parameter URIs and modern RESTful path patterns: * - `/nodes` -> List of all nodes @@ -44,8 +58,8 @@ import org.meshtastic.core.common.util.CommonUri object DeepLinkRouter { /** * Canonical set of top-level path segments this router dispatches on. [route] refuses segments outside this set, so - * a new `when` branch stays dead (and its feature tests fail) until its segment is added here. Every entry must - * also be declared as an `android:pathPrefix` in the https App Links intent-filter in + * a new pattern stays dead (and its feature tests fail) until its segment is added here. Every entry must also be + * declared as an `android:pathPrefix` in the https App Links intent-filter in * `androidApp/src/main/AndroidManifest.xml` — DeepLinkManifestConsistencyTest (androidApp unit tests) enforces that * directly from this set. */ @@ -71,6 +85,9 @@ object DeepLinkRouter { */ private val legacyImportSegments = setOf("e", "v") + private val baseScheme = DEEP_LINK_BASE_URI.substringBefore("://") + private val baseAuthority = DEEP_LINK_BASE_URI.substringAfter("://") + /** * Synthesizes a backstack list from an incoming Meshtastic URI. * @@ -78,8 +95,7 @@ object DeepLinkRouter { * @return A list of strongly-typed NavKeys representing the backstack, or null if the URI is not recognized. */ fun route(uri: CommonUri): List? { - val pathSegments = uri.pathSegments.filter { it.isNotBlank() } - val firstSegment = pathSegments.firstOrNull()?.lowercase() + val firstSegment = uri.pathSegments.firstOrNull { it.isNotBlank() }?.lowercase() if (firstSegment !in topLevelPathSegments) { // /e/ and /v/ are channel-set/contact import links, not navigation routes: returning null here lets @@ -90,151 +106,112 @@ object DeepLinkRouter { return null } - return when (firstSegment) { - "share", - "messages", - "quickchat", - -> routeContacts(uri, pathSegments) - - "connections" -> listOf(ConnectionsRoute.Connections(uri.getQueryParameter("address"))) - - "discovery" -> listOf(DiscoveryRoute.DiscoveryGraph) - - "map" -> routeMap(uri, pathSegments) - - "nodes" -> routeNodes(uri, pathSegments) - - "settings" -> routeSettings(pathSegments) - - "channels" -> listOf(ChannelsRoute.Channels) - - "firmware" -> routeFirmware(pathSegments) - - "wifi-provision" -> routeWifiProvision(uri) - - // Unreachable: gated on topLevelPathSegments above. - else -> null - } + // Patterns are written against DEEP_LINK_BASE_URI only; the matcher compares scheme and authority. + val canonical = uri.buildUpon().scheme(baseScheme).encodedAuthority(baseAuthority).build() + val request = DeepLinkRequest(DeepLinkUri(canonical.toString())) + val backStack = matchers.firstNotNullOfOrNull { it.match(request) }?.backStack + if (backStack == null) Logger.w { "No deep link pattern matches under /$firstSegment" } + return backStack } - private fun routeContacts(uri: CommonUri, segments: List): List { - val firstSegment = segments[0].lowercase() - return when (firstSegment) { - "share" -> { - val message = uri.getQueryParameter("message") ?: "" - listOf(ContactsRoute.Contacts, ContactsRoute.Share(message)) - } - - "quickchat" -> { - listOf(ContactsRoute.Contacts, ContactsRoute.QuickChat) - } - - "messages" -> { - val contactKey = if (segments.size > 1) segments[1] else uri.getQueryParameter("contactKey") ?: "" - val message = uri.getQueryParameter("message") ?: "" - if (contactKey.isNotBlank()) { - listOf(ContactsRoute.Contacts, ContactsRoute.Messages(contactKey = contactKey, message = message)) - } else { - listOf(ContactsRoute.Contacts) - } - } - - else -> listOf(ContactsRoute.Contacts) - } + /** + * Ordered: the first pattern that matches wins, so a specific pattern precedes any fallback that also matches it. A + * placeholder decodes into the same-named field of the matcher's key; a placeholder the key has no field for + * (`{page}`, `{metric}`, `{pageId}`, `{sessionId}`) reaches the builder through its args. + * + * Built on first [route]: a pattern is a platform URI, so reading [topLevelPathSegments] alone never parses one. + */ + private val matchers: List> by lazy { + listOf( + pattern("share?message={message}", ContactsRoute.Share.serializer()) { key, _ -> + listOf(ContactsRoute.Contacts, key) + }, + pattern("share", ContactsRoute.Contacts.serializer()) { key, _ -> listOf(key, ContactsRoute.Share("")) }, + pattern("quickchat", ContactsRoute.Contacts.serializer()) { key, _ -> + listOf(key, ContactsRoute.QuickChat) + }, + pattern("messages/{contactKey}?message={message}", ContactsRoute.Messages.serializer()) { key, _ -> + conversation(key) + }, + pattern("messages?contactKey={contactKey}&message={message}", ContactsRoute.Messages.serializer()) { + key, + _, + -> + conversation(key) + }, + pattern("messages", ContactsRoute.Contacts.serializer()) { key, _ -> listOf(key) }, + pattern("connections?address={address}", ConnectionsRoute.Connections.serializer()) { key, _ -> + listOf(key) + }, + pattern("discovery", DiscoveryRoute.DiscoveryGraph.serializer()) { key, _ -> listOf(key) }, + pattern("map/{waypointId}", MapRoute.Map.serializer()) { key, _ -> listOf(key) }, + pattern("map?waypointId={waypointId}", MapRoute.Map.serializer()) { key, _ -> listOf(key) }, + // A non-numeric waypointId fails to decode above and opens the plain map. + pattern("map/.*", MapRoute.Map.serializer()) { key, _ -> listOf(key) }, + pattern("nodes/{destNum}/{metric}", NodesRoute.NodeDetail.serializer()) { key, args -> + val metric = key.destNum?.let { nodeDetailSubRoutes[args.value("metric").lowercase()]?.invoke(it) } + listOf(NodesRoute.Nodes, key) + listOfNotNull(metric) + }, + pattern("nodes/{destNum}", NodesRoute.NodeDetail.serializer()) { key, _ -> listOf(NodesRoute.Nodes, key) }, + pattern("nodes?destNum={destNum}", NodesRoute.NodeDetail.serializer()) { key, _ -> + if (key.destNum == null) listOf(NodesRoute.Nodes) else listOf(NodesRoute.Nodes, key) + }, + // A non-numeric destNum fails to decode above and opens the node list. + pattern("nodes/.*", NodesRoute.Nodes.serializer()) { key, _ -> listOf(key) }, + pattern("channels", ChannelsRoute.Channels.serializer()) { key, _ -> listOf(key) }, + pattern("firmware/update", FirmwareRoute.FirmwareUpdate.serializer()) { key, _ -> + listOf(FirmwareRoute.FirmwareGraph, key) + }, + pattern("firmware", FirmwareRoute.FirmwareGraph.serializer()) { key, _ -> listOf(key) }, + pattern("wifi-provision?address={address}", WifiProvisionRoute.WifiProvision.serializer()) { key, _ -> + listOf(key) + }, + ) + + // `/settings/{destNum}` must precede `/settings/{page}`: a page slug fails the Int decode and falls + // through. + settings("") { root, _ -> listOf(root) } + + settings("{page}") { root, args -> + listOfNotNull(root, settingsSubRoutes[args.value("page").lowercase()]) + } + + settings("helpdocs/{pageId}") { root, args -> helpDocPage(root, args) } + + settings("help-docs/{pageId}") { root, args -> helpDocPage(root, args) } + + settings("local-mesh-discovery/session/{sessionId}") { root, args -> discoverySession(root, args) } + + settings("localmeshdiscovery/session/{sessionId}") { root, args -> discoverySession(root, args) } } - private fun routeMap(uri: CommonUri, segments: List): List { - val waypointIdStr = if (segments.size > 1) segments[1] else uri.getQueryParameter("waypointId") - val waypointId = waypointIdStr?.toIntOrNull() - return listOf(MapRoute.Map(waypointId)) + /** + * Registers [tail] under both `/settings/{destNum}/` and `/settings/`; the root carries the destNum. + */ + private fun settings( + tail: String, + backStack: (root: SettingsRoute.Settings, args: DeepLinkArgs) -> List, + ): List> = listOf("settings/{destNum}", "settings").map { prefix -> + pattern("$prefix/$tail".trimEnd('/'), SettingsRoute.Settings.serializer(), backStack) } - private fun routeNodes(uri: CommonUri, segments: List): List { - val destNumStr = if (segments.size > 1) segments[1] else uri.getQueryParameter("destNum") - val destNum = destNumStr?.toIntOrNull() - - return if (destNum == null) { - listOf(NodesRoute.Nodes) - } else if (segments.size > 2) { - val subRouteStr = segments[2].lowercase() - val detailRouteFn = nodeDetailSubRoutes[subRouteStr] - if (detailRouteFn != null) { - listOf(NodesRoute.Nodes, NodesRoute.NodeDetail(destNum), detailRouteFn(destNum)) - } else { - listOf(NodesRoute.Nodes, NodesRoute.NodeDetail(destNum)) - } - } else { - listOf(NodesRoute.Nodes, NodesRoute.NodeDetail(destNum)) - } - } - - @Suppress("MagicNumber", "ReturnCount") - private fun routeSettings(segments: List): List { - var destNum: Int? = null - var subRouteStr: String? = null - - if (segments.size > 1) { - val secondSegment = segments[1] - val parsedNum = secondSegment.toIntOrNull() - if (parsedNum != null) { - destNum = parsedNum - if (segments.size > 2) { - subRouteStr = segments[2].lowercase() - } - } else { - subRouteStr = secondSegment.lowercase() - } + private fun pattern( + path: String, + serializer: KSerializer, + backStack: (key: T, args: DeepLinkArgs) -> List, + ): BackStackMatcher = + UriDeepLinkMatcher(DeepLinkUri("$DEEP_LINK_BASE_URI/$path"), serializer).withBackStack { result -> + // A UriDeepLinkMatcher only ever yields a UriMatchResult; its arguments hold the placeholders the key + // has no field for. + backStack(result.key, (result as UriMatchResult<*>).arguments) } - if (subRouteStr == null) { - return listOf(SettingsRoute.Settings(destNum)) - } + private fun DeepLinkArgs.value(name: String): String = this[name]?.firstOrNull().orEmpty() - // Handle helpDocs/{pageId} pattern - if (subRouteStr == "helpdocs" || subRouteStr == "help-docs") { - val pageIdSegmentIndex = if (destNum != null) 3 else 2 - return if (segments.size > pageIdSegmentIndex) { - val pageId = segments[pageIdSegmentIndex] - listOf(SettingsRoute.Settings(destNum), SettingsRoute.HelpDocs, SettingsRoute.HelpDocPage(pageId)) - } else { - listOf(SettingsRoute.Settings(destNum), SettingsRoute.HelpDocs) - } - } + private fun conversation(key: ContactsRoute.Messages): List = + if (key.contactKey.isBlank()) listOf(ContactsRoute.Contacts) else listOf(ContactsRoute.Contacts, key) - // Handle discovery session deep links: /settings/local-mesh-discovery/session/{sessionId} - if (subRouteStr in discoveryAliases && segments.size > 3 && segments[2].lowercase() == "session") { - val sessionId = segments[3].toLongOrNull() - return if (sessionId != null) { - listOf( - SettingsRoute.Settings(destNum), - DiscoveryRoute.DiscoveryGraph, - DiscoveryRoute.DiscoverySummary(sessionId), - ) - } else { - listOf(SettingsRoute.Settings(destNum), DiscoveryRoute.DiscoveryGraph) - } - } + private fun helpDocPage(root: SettingsRoute.Settings, args: DeepLinkArgs): List = + listOf(root, SettingsRoute.HelpDocs, SettingsRoute.HelpDocPage(args.value("pageId"))) - val subRoute = settingsSubRoutes[subRouteStr] - return if (subRoute != null) { - listOf(SettingsRoute.Settings(destNum), subRoute) - } else { - listOf(SettingsRoute.Settings(destNum)) - } - } - - private fun routeWifiProvision(uri: CommonUri): List { - val address = uri.getQueryParameter("address") - return listOf(WifiProvisionRoute.WifiProvision(address)) - } - - private fun routeFirmware(segments: List): List { - val update = if (segments.size > 1) segments[1].lowercase() == "update" else false - return if (update) { - listOf(FirmwareRoute.FirmwareGraph, FirmwareRoute.FirmwareUpdate) - } else { - listOf(FirmwareRoute.FirmwareGraph) - } + /** A session id that is not a Long still opens the discovery graph. */ + private fun discoverySession(root: SettingsRoute.Settings, args: DeepLinkArgs): List { + val summary = args.value("sessionId").toLongOrNull()?.let { DiscoveryRoute.DiscoverySummary(it) } + return listOf(root, DiscoveryRoute.DiscoveryGraph) + listOfNotNull(summary) } private val settingsSubRoutes: Map = @@ -280,9 +257,6 @@ object DeepLinkRouter { "localmeshdiscovery" to DiscoveryRoute.DiscoveryGraph, ) - /** URL path segments that map to the discovery feature. */ - private val discoveryAliases = setOf("local-mesh-discovery", "localmeshdiscovery") - private val nodeDetailSubRoutes: Map Route> = mapOf( "device-metrics" to { destNum -> NodeDetailRoute.DeviceMetrics(destNum) }, diff --git a/core/navigation/src/commonTest/kotlin/org/meshtastic/core/navigation/DeepLinkRouterTest.kt b/core/navigation/src/commonTest/kotlin/org/meshtastic/core/navigation/DeepLinkRouterTest.kt index 5bcf07d3a2..d4b637a3d2 100644 --- a/core/navigation/src/commonTest/kotlin/org/meshtastic/core/navigation/DeepLinkRouterTest.kt +++ b/core/navigation/src/commonTest/kotlin/org/meshtastic/core/navigation/DeepLinkRouterTest.kt @@ -40,6 +40,34 @@ class DeepLinkRouterTest { assertNull(route("/unknown-page")) } + @Test + fun `channel set and shared contact import links return null for the import fallback`() { + assertNull(route("/e/#CgMSAQE")) + assertNull(route("/E/#CgMSAQE")) + assertNull(route("/v/#CNar9vwDEi0KCSEzZjgyOTVkNhIP")) + assertNull(CommonUri.parse("https://meshtastic.org/v/#CNar9vwDEi0KCSEzZjgyOTVkNhIP").let(DeepLinkRouter::route)) + } + + // endregion + + // region scheme and host + + @Test + fun `app link hosts route the same as the custom scheme`() { + val expected = listOf(NodesRoute.Nodes, NodesRoute.NodeDetail(destNum = 1234)) + assertEquals(expected, route("/nodes/1234")) + assertEquals(expected, DeepLinkRouter.route(CommonUri.parse("https://meshtastic.org/nodes/1234"))) + assertEquals(expected, DeepLinkRouter.route(CommonUri.parse("http://meshtastic.org/nodes/1234"))) + } + + @Test + fun `app link query parameters survive the host rewrite`() { + assertEquals( + listOf(ContactsRoute.Contacts, ContactsRoute.Share("camp & ridge #4")), + DeepLinkRouter.route(CommonUri.parse("https://meshtastic.org/share?message=camp%20%26%20ridge%20%234")), + ) + } + // endregion // region contacts / messages diff --git a/core/network/src/commonMain/kotlin/org/meshtastic/core/network/HttpClientDefaults.kt b/core/network/src/commonMain/kotlin/org/meshtastic/core/network/HttpClientDefaults.kt index 74fa5ccb7c..6e1e2a0bb9 100644 --- a/core/network/src/commonMain/kotlin/org/meshtastic/core/network/HttpClientDefaults.kt +++ b/core/network/src/commonMain/kotlin/org/meshtastic/core/network/HttpClientDefaults.kt @@ -41,11 +41,11 @@ object HttpClientDefaults { /** * Base URL for the Meshtastic public API, installed via the `DefaultRequest` plugin. * - * The Cloudflare R2-backed v2 host, not `api.meshtastic.org`. v2 is CDN-cached with `stale-if-error`, and it - * answers cross-origin requests: v1 replies HTTP 500 to any `Origin` outside a hardcoded allowlist, so it cannot be - * reached from a browser at all (meshtastic/api#134). + * The production Worker: R2-backed, CDN-cached with `stale-if-error`, and cross-origin readable. Never + * `apiv2.meshtastic.org` — that hostname is the Worker's staging route and redeploys only on a push to the dormant + * `v2` branch, so it serves a frozen copy of the JSON documents compiled into the bundle. */ - const val API_BASE_URL = "https://apiv2.meshtastic.org/" + const val API_BASE_URL = "https://api.meshtastic.org/" /** * Base URL for the nightly firmware channel. Absolute, and not routed through [API_BASE_URL] — the nightly build is diff --git a/core/network/src/commonMain/kotlin/org/meshtastic/core/network/radio/MockRadioTransport.kt b/core/network/src/commonMain/kotlin/org/meshtastic/core/network/radio/MockRadioTransport.kt index 25b4154a62..83753a5124 100644 --- a/core/network/src/commonMain/kotlin/org/meshtastic/core/network/radio/MockRadioTransport.kt +++ b/core/network/src/commonMain/kotlin/org/meshtastic/core/network/radio/MockRadioTransport.kt @@ -57,15 +57,23 @@ import org.meshtastic.proto.MyNodeInfo as ProtoMyNodeInfo import org.meshtastic.proto.Position as ProtoPosition private val defaultLoRaConfig = - Config.LoRaConfig( - use_preset = true, - modem_preset = Config.LoRaConfig.ModemPreset.LONG_FAST, - region = Config.LoRaConfig.RegionCode.US, - hop_limit = 3, - tx_enabled = true, - ) + Config.LoRaConfig.Builder() + .also { wb -> + wb.use_preset = true + wb.modem_preset = Config.LoRaConfig.ModemPreset.LONG_FAST + wb.region = Config.LoRaConfig.RegionCode.US + wb.hop_limit = 3 + wb.tx_enabled = true + } + .build() -private val defaultChannel = ProtoChannel(settings = Channel.default.settings, role = ProtoChannel.Role.PRIMARY) +private val defaultChannel = + ProtoChannel.Builder() + .also { wb -> + wb.settings = Channel.default.settings + wb.role = ProtoChannel.Role.PRIMARY + } + .build() /** * A simulated radio, selected as "Demo Mode" in the Connections screen. @@ -174,32 +182,49 @@ class MockRadioTransport( when { d.get_config_request == AdminMessage.ConfigType.LORA_CONFIG -> sendAdmin(packet.to, packet.from, packet.id) { - copy(get_config_response = Config(lora = defaultLoRaConfig)) + this.newBuilder() + .also { wb -> + wb.get_config_response = Config.Builder().also { wb -> wb.lora = defaultLoRaConfig }.build() + } + .build() } (d.get_channel_request ?: 0) != 0 -> sendAdmin(packet.to, packet.from, packet.id) { - copy( - get_channel_response = - ProtoChannel( - index = (d.get_channel_request ?: 0) - 1, // 0 based on the response - settings = if (d.get_channel_request == 1) Channel.default.settings else null, - role = - if (d.get_channel_request == 1) { - ProtoChannel.Role.PRIMARY - } else { - ProtoChannel.Role.DISABLED - }, - ), - ) + this.newBuilder() + .also { wb -> + wb.get_channel_response = + ProtoChannel.Builder() + .also { wb -> + wb.index = (d.get_channel_request ?: 0) - 1 // 0 based on the response + wb.settings = if (d.get_channel_request == 1) Channel.default.settings else null + wb.role = + if (d.get_channel_request == 1) { + ProtoChannel.Role.PRIMARY + } else { + ProtoChannel.Role.DISABLED + } + } + .build() + } + .build() } d.get_module_config_request == AdminMessage.ModuleConfigType.STATUSMESSAGE_CONFIG -> sendAdmin(packet.to, packet.from, packet.id) { - copy( - get_module_config_response = - ModuleConfig(statusmessage = ModuleConfig.StatusMessageConfig(node_status = MY_NODE_STATUS)), - ) + this.newBuilder() + .also { wb -> + wb.get_module_config_response = + ModuleConfig.Builder() + .also { wb -> + wb.statusmessage = + ModuleConfig.StatusMessageConfig.Builder() + .also { wb -> wb.node_status = MY_NODE_STATUS } + .build() + } + .build() + } + .build() } else -> Logger.i { "Ignoring admin sent to mock transport $d" } @@ -220,26 +245,62 @@ class MockRadioTransport( /** Stage 1: our own identity, device metadata, config and channels. Deliberately carries no `node_info`. */ private fun sendConfigStage() { Logger.d { "Mock transport answering config stage" } - val metadata = DeviceMetadata(firmware_version = FIRMWARE_VERSION, hw_model = HardwareModel.ANDROID_SIM) + val metadata = + DeviceMetadata.Builder() + .also { wb -> + wb.firmware_version = FIRMWARE_VERSION + wb.hw_model = HardwareModel.ANDROID_SIM + } + .build() val frames = listOf( - FromRadio(my_info = ProtoMyNodeInfo(my_node_num = MY_NODE, reboot_count = 3)), - FromRadio(metadata = metadata), - FromRadio(config = Config(lora = defaultLoRaConfig)), - FromRadio(config = Config(device = Config.DeviceConfig(role = Config.DeviceConfig.Role.CLIENT))), - FromRadio( - config = - Config( - position = - Config.PositionConfig( - position_broadcast_secs = 900, - position_broadcast_smart_enabled = true, - gps_enabled = true, - ), - ), - ), - FromRadio(channel = defaultChannel), - FromRadio(config_complete_id = HandshakeConstants.CONFIG_NONCE), + FromRadio.Builder() + .also { wb -> + wb.my_info = + ProtoMyNodeInfo.Builder() + .also { wb -> + wb.my_node_num = MY_NODE + wb.reboot_count = 3 + } + .build() + } + .build(), + FromRadio.Builder().also { wb -> wb.metadata = metadata }.build(), + FromRadio.Builder() + .also { wb -> wb.config = Config.Builder().also { wb -> wb.lora = defaultLoRaConfig }.build() } + .build(), + FromRadio.Builder() + .also { wb -> + wb.config = + Config.Builder() + .also { wb -> + wb.device = + Config.DeviceConfig.Builder() + .also { wb -> wb.role = Config.DeviceConfig.Role.CLIENT } + .build() + } + .build() + } + .build(), + FromRadio.Builder() + .also { wb -> + wb.config = + Config.Builder() + .also { wb -> + wb.position = + Config.PositionConfig.Builder() + .also { wb -> + wb.position_broadcast_secs = 900 + wb.position_broadcast_smart_enabled = true + wb.gps_enabled = true + } + .build() + } + .build() + } + .build(), + FromRadio.Builder().also { wb -> wb.channel = defaultChannel }.build(), + FromRadio.Builder().also { wb -> wb.config_complete_id = HandshakeConstants.CONFIG_NONCE }.build(), ) frames.forEach { callback.handleFromRadio(it.encode()) } } @@ -247,58 +308,84 @@ class MockRadioTransport( /** Stage 2: the node database. This is the only window in which the app accepts `node_info`. */ private fun sendNodeInfoStage() { Logger.d { "Mock transport answering node-info stage with ${SIM_PEERS.size + 1} nodes" } - callback.handleFromRadio(FromRadio(node_info = localNodeInfo()).encode()) - SIM_PEERS.forEach { peer -> callback.handleFromRadio(FromRadio(node_info = peer.toNodeInfo()).encode()) } - callback.handleFromRadio(FromRadio(config_complete_id = HandshakeConstants.NODE_INFO_NONCE).encode()) + callback.handleFromRadio(FromRadio.Builder().also { wb -> wb.node_info = localNodeInfo() }.build().encode()) + SIM_PEERS.forEach { peer -> + callback.handleFromRadio( + FromRadio.Builder().also { wb -> wb.node_info = peer.toNodeInfo() }.build().encode(), + ) + } + callback.handleFromRadio( + FromRadio.Builder() + .also { wb -> wb.config_complete_id = HandshakeConstants.NODE_INFO_NONCE } + .build() + .encode(), + ) if (trafficStarted.compareAndSet(expect = false, update = true)) { transportScope.handledLaunch { seedTraffic() } } } - private fun localNodeInfo() = NodeInfo( - num = MY_NODE, - last_heard = nowSeconds.toInt(), - user = - User( - id = NodeAddress.numToDefaultId(MY_NODE), - long_name = "Demo Handset", - short_name = "DEMO", - hw_model = HardwareModel.ANDROID_SIM, - role = Config.DeviceConfig.Role.CLIENT, - ), - position = MY_POSITION.toProto(), - device_metrics = - DeviceMetrics( - battery_level = 78, - voltage = 3.98f, - channel_utilization = 8.4f, - air_util_tx = 1.9f, - uptime_seconds = 7_240, - ), - hops_away = 0, - ) + private fun localNodeInfo() = NodeInfo.Builder() + .also { wb -> + wb.num = MY_NODE + wb.last_heard = nowSeconds.toInt() + wb.user = + User.Builder() + .also { wb -> + wb.id = NodeAddress.numToDefaultId(MY_NODE) + wb.long_name = "Demo Handset" + wb.short_name = "DEMO" + wb.hw_model = HardwareModel.ANDROID_SIM + wb.role = Config.DeviceConfig.Role.CLIENT + } + .build() + wb.position = MY_POSITION.toProto() + wb.device_metrics = + DeviceMetrics.Builder() + .also { wb -> + wb.battery_level = 78 + wb.voltage = 3.98f + wb.channel_utilization = 8.4f + wb.air_util_tx = 1.9f + wb.uptime_seconds = 7_240 + } + .build() + wb.hops_away = 0 + } + .build() - private fun SimPeer.toNodeInfo() = NodeInfo( - num = num, - // Without last_heard every simulated node reads as offline and disappears the moment the user turns on the - // node list's "online only" filter. - last_heard = nowSeconds.toInt() - secondsSinceHeard, - user = - User( - id = NodeAddress.numToDefaultId(num), - long_name = longName, - short_name = shortName, - hw_model = hwModel, - role = role, - ), - position = SimPosition(latitude, longitude, altitude).toProto(), - device_metrics = - DeviceMetrics(battery_level = batteryLevel, voltage = voltage, uptime_seconds = uptimeSeconds), - snr = snr, - hops_away = hops, - channel = 0, - ) + private fun SimPeer.toNodeInfo() = NodeInfo.Builder() + .also { wb -> + wb.num = num + // Without last_heard every simulated node reads as offline and disappears the moment the user turns on + // the + // node list's "online only" filter. + wb.last_heard = nowSeconds.toInt() - secondsSinceHeard + wb.user = + User.Builder() + .also { wb -> + wb.id = NodeAddress.numToDefaultId(num) + wb.long_name = longName + wb.short_name = shortName + wb.hw_model = hwModel + wb.role = role + } + .build() + wb.position = SimPosition(latitude, longitude, altitude).toProto() + wb.device_metrics = + DeviceMetrics.Builder() + .also { wb -> + wb.battery_level = batteryLevel + wb.voltage = voltage + wb.uptime_seconds = uptimeSeconds + } + .build() + wb.snr = snr + wb.hops_away = hops + wb.channel = 0 + } + .build() // ── Simulated traffic ───────────────────────────────────────────────────────────────────── @@ -430,43 +517,58 @@ class MockRadioTransport( * `transport_mechanism` and the matching `hop_start`/`hop_limit` are load-bearing, not decoration: the app only * harvests SNR, RSSI and hop count from packets that pass its direct-LoRa test. */ - private fun SimPeer.packet(id: Int, to: Int, ageSeconds: Int, data: Data) = MeshPacket( - id = id, - from = num, - to = to, - channel = 0, - rx_time = (nowSeconds - ageSeconds).toInt(), - rx_snr = snr, - rx_rssi = rssi, - hop_start = DEFAULT_HOP_START, - hop_limit = DEFAULT_HOP_START - hops, - transport_mechanism = MeshPacket.TransportMechanism.TRANSPORT_LORA, - decoded = data, - ) + private fun SimPeer.packet(id: Int, to: Int, ageSeconds: Int, data: Data) = MeshPacket.Builder() + .also { wb -> + wb.id = id + wb.from = num + wb.to = to + wb.channel = 0 + wb.rx_time = (nowSeconds - ageSeconds).toInt() + wb.rx_snr = snr + wb.rx_rssi = rssi + wb.hop_start = DEFAULT_HOP_START + wb.hop_limit = DEFAULT_HOP_START - hops + wb.transport_mechanism = MeshPacket.TransportMechanism.TRANSPORT_LORA + wb.decoded = data + } + .build() - private fun SimPeer.textPacket(id: Int, to: Int, text: String, ageSeconds: Int) = FromRadio( - packet = - packet( - id = id, - to = to, - ageSeconds = ageSeconds, - data = Data(portnum = PortNum.TEXT_MESSAGE_APP, payload = text.encodeUtf8()), - ), - ) + private fun SimPeer.textPacket(id: Int, to: Int, text: String, ageSeconds: Int) = FromRadio.Builder() + .also { wb -> + wb.packet = + packet( + id = id, + to = to, + ageSeconds = ageSeconds, + data = + Data.Builder() + .also { wb -> + wb.portnum = PortNum.TEXT_MESSAGE_APP + wb.payload = text.encodeUtf8() + } + .build(), + ) + } + .build() - private fun SimPeer.positionPacket(id: Int) = FromRadio( - packet = - packet( - id = id, - to = BROADCAST_ADDR, - ageSeconds = 0, - data = - Data( - portnum = PortNum.POSITION_APP, - payload = SimPosition(latitude, longitude, altitude).toProto().encode().toByteString(), - ), - ), - ) + private fun SimPeer.positionPacket(id: Int) = FromRadio.Builder() + .also { wb -> + wb.packet = + packet( + id = id, + to = BROADCAST_ADDR, + ageSeconds = 0, + data = + Data.Builder() + .also { wb -> + wb.portnum = PortNum.POSITION_APP + wb.payload = + SimPosition(latitude, longitude, altitude).toProto().encode().toByteString() + } + .build(), + ) + } + .build() /** * Device telemetry for [tick], drifted so the charts show a trend rather than a flat line. @@ -478,134 +580,203 @@ class MockRadioTransport( private fun SimPeer.deviceTelemetryPacket(id: Int, tick: Int): FromRadio { val driftedBattery = (batteryLevel - tick).coerceIn(MIN_BATTERY_PERCENT, MAX_BATTERY_PERCENT) val driftedVoltage = (voltage - tick * VOLTAGE_DRIFT_PER_TICK).coerceAtLeast(MIN_CELL_VOLTAGE) - return FromRadio( - packet = - packet( - id = id, - to = BROADCAST_ADDR, - ageSeconds = 0, - data = - Data( - portnum = PortNum.TELEMETRY_APP, - payload = - Telemetry( - device_metrics = - DeviceMetrics( - battery_level = driftedBattery, - voltage = driftedVoltage, - channel_utilization = 6f + (tick % 5) * 1.5f, - air_util_tx = 1.2f + (tick % 4) * 0.4f, - uptime_seconds = uptimeSeconds + tick * (LIVE_TICK_MS / 1000).toInt(), - ), + return FromRadio.Builder() + .also { wb -> + wb.packet = + packet( + id = id, + to = BROADCAST_ADDR, + ageSeconds = 0, + data = + Data.Builder() + .also { wb -> + wb.portnum = PortNum.TELEMETRY_APP + wb.payload = + Telemetry.Builder() + .also { wb -> + wb.device_metrics = + DeviceMetrics.Builder() + .also { wb -> + wb.battery_level = driftedBattery + wb.voltage = driftedVoltage + wb.channel_utilization = 6f + (tick % 5) * 1.5f + wb.air_util_tx = 1.2f + (tick % 4) * 0.4f + wb.uptime_seconds = + uptimeSeconds + tick * (LIVE_TICK_MS / 1000).toInt() + } + .build() + } + .build() + .encode() + .toByteString() + } + .build(), ) - .encode() - .toByteString(), - ), - ), - ) + } + .build() } - private fun SimPeer.environmentTelemetryPacket(id: Int, tick: Int) = FromRadio( - packet = - packet( - id = id, - to = BROADCAST_ADDR, - ageSeconds = 0, - data = - Data( - portnum = PortNum.TELEMETRY_APP, - payload = - Telemetry( - environment_metrics = - EnvironmentMetrics( - // Temperature AND humidity must both be present or the Environment tab - // stays empty. - temperature = 18.5f + (tick % 7) * 0.4f, - relative_humidity = 47f + (tick % 5) * 1.5f, - barometric_pressure = 1013.2f + (tick % 3) * 0.3f, - ), + private fun SimPeer.environmentTelemetryPacket(id: Int, tick: Int) = FromRadio.Builder() + .also { wb -> + wb.packet = + packet( + id = id, + to = BROADCAST_ADDR, + ageSeconds = 0, + data = + Data.Builder() + .also { wb -> + wb.portnum = PortNum.TELEMETRY_APP + wb.payload = + Telemetry.Builder() + .also { wb -> + wb.environment_metrics = + EnvironmentMetrics.Builder() + .also { wb -> + // Temperature AND humidity must both be present or the + // Environment tab + // stays empty. + wb.temperature = 18.5f + (tick % 7) * 0.4f + wb.relative_humidity = 47f + (tick % 5) * 1.5f + wb.barometric_pressure = 1013.2f + (tick % 3) * 0.3f + } + .build() + } + .build() + .encode() + .toByteString() + } + .build(), ) - .encode() - .toByteString(), - ), - ), - ) + } + .build() - private fun SimPeer.neighborInfoPacket(id: Int) = FromRadio( - packet = - packet( - id = id, - to = BROADCAST_ADDR, - ageSeconds = 0, - data = - Data( - portnum = PortNum.NEIGHBORINFO_APP, - payload = - NeighborInfo( - node_id = num, - last_sent_by_id = num, - node_broadcast_interval_secs = 900, - neighbors = - SIM_PEERS.drop(1).take(3).map { neighbor -> - Neighbor( - node_id = neighbor.num, - snr = neighbor.snr, - last_rx_time = nowSeconds.toInt(), - node_broadcast_interval_secs = 900, - ) - }, + private fun SimPeer.neighborInfoPacket(id: Int) = FromRadio.Builder() + .also { wb -> + wb.packet = + packet( + id = id, + to = BROADCAST_ADDR, + ageSeconds = 0, + data = + Data.Builder() + .also { wb -> + wb.portnum = PortNum.NEIGHBORINFO_APP + wb.payload = + NeighborInfo.Builder() + .also { wb -> + wb.node_id = num + wb.last_sent_by_id = num + wb.node_broadcast_interval_secs = 900 + wb.neighbors = + SIM_PEERS.drop(1).take(3).map { neighbor -> + Neighbor.Builder() + .also { wb -> + wb.node_id = neighbor.num + wb.snr = neighbor.snr + wb.last_rx_time = nowSeconds.toInt() + wb.node_broadcast_interval_secs = 900 + } + .build() + } + } + .build() + .encode() + .toByteString() + } + .build(), ) - .encode() - .toByteString(), - ), - ), - ) + } + .build() - private fun SimPeer.nodeStatusPacket(id: Int) = FromRadio( - packet = - packet( - id = id, - to = BROADCAST_ADDR, - ageSeconds = 0, - data = - Data( - portnum = PortNum.NODE_STATUS_APP, - payload = StatusMessage(status = PEER_NODE_STATUS).encode().toByteString(), - ), - ), - ) + private fun SimPeer.nodeStatusPacket(id: Int) = FromRadio.Builder() + .also { wb -> + wb.packet = + packet( + id = id, + to = BROADCAST_ADDR, + ageSeconds = 0, + data = + Data.Builder() + .also { wb -> + wb.portnum = PortNum.NODE_STATUS_APP + wb.payload = + StatusMessage.Builder() + .also { wb -> wb.status = PEER_NODE_STATUS } + .build() + .encode() + .toByteString() + } + .build(), + ) + } + .build() - private fun makeDataPacket(fromIn: Int, toIn: Int, data: Data) = FromRadio( - packet = - MeshPacket( - id = nextPacketId(), - from = fromIn, - to = toIn, - rx_time = nowSeconds.toInt(), - rx_snr = 1.5f, - decoded = data, - ), - ) + private fun makeDataPacket(fromIn: Int, toIn: Int, data: Data) = FromRadio.Builder() + .also { wb -> + wb.packet = + MeshPacket.Builder() + .also { wb -> + wb.id = nextPacketId() + wb.from = fromIn + wb.to = toIn + wb.rx_time = nowSeconds.toInt() + wb.rx_snr = 1.5f + wb.decoded = data + } + .build() + } + .build() private fun makeAck(fromIn: Int, toIn: Int, msgId: Int) = makeDataPacket( fromIn, toIn, - Data(portnum = PortNum.ROUTING_APP, payload = Routing().encode().toByteString(), request_id = msgId), + Data.Builder() + .also { wb -> + wb.portnum = PortNum.ROUTING_APP + wb.payload = Routing.Builder().build().encode().toByteString() + wb.request_id = msgId + } + .build(), ) private fun sendQueueStatus(msgId: Int) = callback.handleFromRadio( - FromRadio(queueStatus = QueueStatus(res = 0, free = 16, mesh_packet_id = msgId)).encode(), + FromRadio.Builder() + .also { wb -> + wb.queueStatus = + QueueStatus.Builder() + .also { wb -> + wb.res = 0 + wb.free = 16 + wb.mesh_packet_id = msgId + } + .build() + } + .build() + .encode(), ) private fun sendAdmin(fromIn: Int, toIn: Int, reqId: Int, initFn: AdminMessage.() -> AdminMessage) { // Embed a deterministic 8-byte fake passkey so SessionManager can record a session refresh — mirrors what real // firmware always attaches to admin responses (see firmware/src/modules/AdminModule.cpp:1460-1481). - val adminMsg = AdminMessage().initFn().copy(session_passkey = FAKE_SESSION_PASSKEY) + val adminMsg = + AdminMessage.Builder() + .build() + .initFn() + .newBuilder() + .also { wb -> wb.session_passkey = FAKE_SESSION_PASSKEY } + .build() val p = makeDataPacket( fromIn, toIn, - Data(portnum = PortNum.ADMIN_APP, payload = adminMsg.encode().toByteString(), request_id = reqId), + Data.Builder() + .also { wb -> + wb.portnum = PortNum.ADMIN_APP + wb.payload = adminMsg.encode().toByteString() + wb.request_id = reqId + } + .build(), ) callback.handleFromRadio(p.encode()) } @@ -642,17 +813,20 @@ class MockRadioTransport( /** Latitude/longitude/altitude triple, converted to the proto's scaled-integer representation on demand. */ private data class SimPosition(val latitude: Double, val longitude: Double, val altitude: Int) { - fun toProto() = ProtoPosition( - latitude_i = org.meshtastic.core.model.Position.degI(latitude), - longitude_i = org.meshtastic.core.model.Position.degI(longitude), - altitude = altitude, - time = nowSeconds.toInt(), - // 32 bits is "full precision"; the coarse end of the scale draws a large uncertainty circle instead of - // placing the node where it actually is. - precision_bits = 32, - sats_in_view = 9, - location_source = ProtoPosition.LocSource.LOC_INTERNAL, - ) + fun toProto() = ProtoPosition.Builder() + .also { wb -> + wb.latitude_i = org.meshtastic.core.model.Position.degI(latitude) + wb.longitude_i = org.meshtastic.core.model.Position.degI(longitude) + wb.altitude = altitude + wb.time = nowSeconds.toInt() + // 32 bits is "full precision"; the coarse end of the scale draws a large uncertainty circle instead + // of + // placing the node where it actually is. + wb.precision_bits = 32 + wb.sats_in_view = 9 + wb.location_source = ProtoPosition.LocSource.LOC_INTERNAL + } + .build() } private companion object { diff --git a/core/network/src/commonMain/kotlin/org/meshtastic/core/network/radio/ReplayRadioTransport.kt b/core/network/src/commonMain/kotlin/org/meshtastic/core/network/radio/ReplayRadioTransport.kt index 58e801d184..7b8c7f4ef2 100644 --- a/core/network/src/commonMain/kotlin/org/meshtastic/core/network/radio/ReplayRadioTransport.kt +++ b/core/network/src/commonMain/kotlin/org/meshtastic/core/network/radio/ReplayRadioTransport.kt @@ -166,7 +166,9 @@ class ReplayRadioTransport( } private fun complete(nonce: Int) { - lifecycle.runIfOpen { callback.handleFromRadio(FromRadio(config_complete_id = nonce).encode()) } + lifecycle.runIfOpen { + callback.handleFromRadio(FromRadio.Builder().also { wb -> wb.config_complete_id = nonce }.build().encode()) + } } private suspend fun streamPackets() { diff --git a/core/network/src/commonMain/kotlin/org/meshtastic/core/network/repository/MQTTRepositoryImpl.kt b/core/network/src/commonMain/kotlin/org/meshtastic/core/network/repository/MQTTRepositoryImpl.kt index 8e1a24bc67..398be02257 100644 --- a/core/network/src/commonMain/kotlin/org/meshtastic/core/network/repository/MQTTRepositoryImpl.kt +++ b/core/network/src/commonMain/kotlin/org/meshtastic/core/network/repository/MQTTRepositoryImpl.kt @@ -302,7 +302,15 @@ class MQTTRepositoryImpl( try { val jsonStr = payload.decodeToString() json.decodeFromString(jsonStr) - trySend(MqttClientProxyMessage(topic = topic, text = jsonStr, retained = msg.retain)) + trySend( + MqttClientProxyMessage.Builder() + .also { wb -> + wb.topic = topic + wb.text = jsonStr + wb.retained = msg.retain + } + .build(), + ) } catch (e: JsonDecodingException) { // Warn, not error: a non-conforming payload recurs for as long as a busy public broker // stays subscribed, not an app defect worth a non-fatal per message. @@ -327,7 +335,15 @@ class MQTTRepositoryImpl( } return } - trySend(MqttClientProxyMessage(topic = topic, data_ = payload.toByteString(), retained = msg.retain)) + trySend( + MqttClientProxyMessage.Builder() + .also { wb -> + wb.topic = topic + wb.data_ = payload.toByteString() + wb.retained = msg.retain + } + .build(), + ) } } diff --git a/core/network/src/commonMain/kotlin/org/meshtastic/core/network/transport/HeartbeatSender.kt b/core/network/src/commonMain/kotlin/org/meshtastic/core/network/transport/HeartbeatSender.kt index b2ea7ad7ef..88b5a3fd41 100644 --- a/core/network/src/commonMain/kotlin/org/meshtastic/core/network/transport/HeartbeatSender.kt +++ b/core/network/src/commonMain/kotlin/org/meshtastic/core/network/transport/HeartbeatSender.kt @@ -81,7 +81,13 @@ private constructor( nonceMutex.withLock { val n = nonce.load() Logger.v { "[$logTag] Sending ToRadio heartbeat (nonce=$n)" } - val admitted = sendToRadio(ToRadio(heartbeat = Heartbeat(nonce = n)).encode()) + val admitted = + sendToRadio( + ToRadio.Builder() + .also { wb -> wb.heartbeat = Heartbeat.Builder().also { wb -> wb.nonce = n }.build() } + .build() + .encode(), + ) if (admitted) nonce.fetchAndAdd(1) admitted to rejectionLogPolicy.record(admitted) } diff --git a/core/network/src/commonMain/kotlin/org/meshtastic/core/network/transport/TcpTransport.kt b/core/network/src/commonMain/kotlin/org/meshtastic/core/network/transport/TcpTransport.kt index bd5f85315b..b3c1cfb434 100644 --- a/core/network/src/commonMain/kotlin/org/meshtastic/core/network/transport/TcpTransport.kt +++ b/core/network/src/commonMain/kotlin/org/meshtastic/core/network/transport/TcpTransport.kt @@ -188,7 +188,12 @@ class TcpTransport( */ suspend fun sendHeartbeat() { val nonce = heartbeatNonce.getAndIncrement() - val heartbeat = ToRadio(heartbeat = org.meshtastic.proto.Heartbeat(nonce = nonce)) + val heartbeat = + ToRadio.Builder() + .also { wb -> + wb.heartbeat = org.meshtastic.proto.Heartbeat.Builder().also { wb -> wb.nonce = nonce }.build() + } + .build() sendPacket(heartbeat.encode()) } diff --git a/core/network/src/commonTest/kotlin/org/meshtastic/core/network/radio/MockRadioTransportTest.kt b/core/network/src/commonTest/kotlin/org/meshtastic/core/network/radio/MockRadioTransportTest.kt index 1b1277c6f9..1e39ee8777 100644 --- a/core/network/src/commonTest/kotlin/org/meshtastic/core/network/radio/MockRadioTransportTest.kt +++ b/core/network/src/commonTest/kotlin/org/meshtastic/core/network/radio/MockRadioTransportTest.kt @@ -107,7 +107,7 @@ class MockRadioTransportTest { try { val transport = MockRadioTransport(callback, scope, address = "") - transport.handleSendToRadio(ToRadio(want_config_id = HandshakeConstants.CONFIG_NONCE).encode()) + transport.handleSendToRadio(wantConfig(HandshakeConstants.CONFIG_NONCE)) assertEquals(1, callback.received.count { it.my_info != null }, "stage 1 must announce our node") assertNotNull(callback.received.firstNotNullOfOrNull { it.metadata }, "stage 1 must send metadata") @@ -126,10 +126,10 @@ class MockRadioTransportTest { val scope = transportScope() try { val transport = MockRadioTransport(callback, scope, address = "") - transport.handleSendToRadio(ToRadio(want_config_id = HandshakeConstants.CONFIG_NONCE).encode()) + transport.handleSendToRadio(wantConfig(HandshakeConstants.CONFIG_NONCE)) callback.received.clear() - transport.handleSendToRadio(ToRadio(want_config_id = HandshakeConstants.NODE_INFO_NONCE).encode()) + transport.handleSendToRadio(wantConfig(HandshakeConstants.NODE_INFO_NONCE)) assertTrue(callback.received.none { it.my_info != null }, "re-sending my_info breaks stage 2") assertEquals(listOf(HandshakeConstants.NODE_INFO_NONCE), callback.completions) @@ -144,10 +144,10 @@ class MockRadioTransportTest { val scope = transportScope() try { val transport = MockRadioTransport(callback, scope, address = "") - transport.handleSendToRadio(ToRadio(want_config_id = HandshakeConstants.CONFIG_NONCE).encode()) + transport.handleSendToRadio(wantConfig(HandshakeConstants.CONFIG_NONCE)) callback.received.clear() - transport.handleSendToRadio(ToRadio(want_config_id = HandshakeConstants.NODE_INFO_NONCE).encode()) + transport.handleSendToRadio(wantConfig(HandshakeConstants.NODE_INFO_NONCE)) val nodes = callback.nodeInfos assertTrue(nodes.size >= MIN_DEMO_NODES, "expected a populated node list, got ${nodes.size}") @@ -188,7 +188,7 @@ class MockRadioTransportTest { try { val transport = MockRadioTransport(callback, scope, address = "") - transport.handleSendToRadio(ToRadio(want_config_id = 1234).encode()) + transport.handleSendToRadio(wantConfig(1234)) assertTrue(callback.received.isEmpty()) } finally { @@ -202,9 +202,9 @@ class MockRadioTransportTest { val scope = transportScope() try { val transport = MockRadioTransport(callback, scope, address = "") - transport.handleSendToRadio(ToRadio(want_config_id = HandshakeConstants.CONFIG_NONCE).encode()) + transport.handleSendToRadio(wantConfig(HandshakeConstants.CONFIG_NONCE)) val myNodeNum = assertNotNull(callback.received.firstNotNullOfOrNull { it.my_info }).my_node_num - transport.handleSendToRadio(ToRadio(want_config_id = HandshakeConstants.NODE_INFO_NONCE).encode()) + transport.handleSendToRadio(wantConfig(HandshakeConstants.NODE_INFO_NONCE)) callback.received.clear() testScheduler.advanceTimeBy(SEED_WINDOW_MS) @@ -243,8 +243,8 @@ class MockRadioTransportTest { val scope = transportScope() try { val transport = MockRadioTransport(callback, scope, address = "") - transport.handleSendToRadio(ToRadio(want_config_id = HandshakeConstants.CONFIG_NONCE).encode()) - transport.handleSendToRadio(ToRadio(want_config_id = HandshakeConstants.NODE_INFO_NONCE).encode()) + transport.handleSendToRadio(wantConfig(HandshakeConstants.CONFIG_NONCE)) + transport.handleSendToRadio(wantConfig(HandshakeConstants.NODE_INFO_NONCE)) callback.received.clear() testScheduler.advanceTimeBy(SEED_WINDOW_MS) @@ -293,8 +293,8 @@ class MockRadioTransportTest { val scope = transportScope() try { val transport = MockRadioTransport(callback, scope, address = "") - transport.handleSendToRadio(ToRadio(want_config_id = HandshakeConstants.CONFIG_NONCE).encode()) - transport.handleSendToRadio(ToRadio(want_config_id = HandshakeConstants.NODE_INFO_NONCE).encode()) + transport.handleSendToRadio(wantConfig(HandshakeConstants.CONFIG_NONCE)) + transport.handleSendToRadio(wantConfig(HandshakeConstants.NODE_INFO_NONCE)) // Drop the handshake frames before timing the seed pass. Asserting on a recorder that still holds them // would pass even if the simulator went silent the moment the handshake ended, which would leave close() // with nothing to stop and this test proving nothing. @@ -309,15 +309,25 @@ class MockRadioTransportTest { // A text with want_ack leaves both a delayed ack and a delayed reply pending, so close() has more than the // telemetry ticker to cancel. transport.handleSendToRadio( - ToRadio( - packet = - MeshPacket( - id = 1, - to = BROADCAST_ADDR, - want_ack = true, - decoded = Data(portnum = PortNum.TEXT_MESSAGE_APP, payload = "ping".encodeUtf8()), - ), - ) + ToRadio.Builder() + .also { wb -> + wb.packet = + MeshPacket.Builder() + .also { wb -> + wb.id = 1 + wb.to = BROADCAST_ADDR + wb.want_ack = true + wb.decoded = + Data.Builder() + .also { wb -> + wb.portnum = PortNum.TEXT_MESSAGE_APP + wb.payload = "ping".encodeUtf8() + } + .build() + } + .build() + } + .build() .encode(), ) @@ -343,7 +353,20 @@ class MockRadioTransportTest { val scope = transportScope() try { val transport = MockRadioTransport(callback = callback, scope = scope, address = "mock") - val outbound = ToRadio(packet = MeshPacket(id = 77, from = 1234, want_ack = true)).encode() + val outbound = + ToRadio.Builder() + .also { wb -> + wb.packet = + MeshPacket.Builder() + .also { wb -> + wb.id = 77 + wb.from = 1234 + wb.want_ack = true + } + .build() + } + .build() + .encode() transport.start() assertTrue(transport.handleSendToRadio(outbound)) @@ -366,7 +389,20 @@ class MockRadioTransportTest { val scope = transportScope() try { val transport = MockRadioTransport(callback = callback, scope = scope, address = "mock") - val outbound = ToRadio(packet = MeshPacket(id = 77, from = 1234, want_ack = true)).encode() + val outbound = + ToRadio.Builder() + .also { wb -> + wb.packet = + MeshPacket.Builder() + .also { wb -> + wb.id = 77 + wb.from = 1234 + wb.want_ack = true + } + .build() + } + .build() + .encode() transport.start() assertTrue(transport.handleSendToRadio(outbound)) @@ -409,14 +445,23 @@ class MockRadioTransportTest { val callback = RecordingCallback() val transport = MockRadioTransport(callback = callback, scope = scope, address = "mock") val outbound = - ToRadio( - packet = - MeshPacket( - id = 77, - decoded = - Data(portnum = PortNum.ADMIN_APP, payload = byteArrayOf(0x80.toByte()).toByteString()), - ), - ) + ToRadio.Builder() + .also { wb -> + wb.packet = + MeshPacket.Builder() + .also { wb -> + wb.id = 77 + wb.decoded = + Data.Builder() + .also { wb -> + wb.portnum = PortNum.ADMIN_APP + wb.payload = byteArrayOf(0x80.toByte()).toByteString() + } + .build() + } + .build() + } + .build() transport.start() assertTrue(transport.handleSendToRadio(outbound.encode())) @@ -428,15 +473,28 @@ class MockRadioTransportTest { ) val validRequestId = 78 - val validAdmin = AdminMessage(get_config_request = AdminMessage.ConfigType.LORA_CONFIG) + val validAdmin = + AdminMessage.Builder() + .also { wb -> wb.get_config_request = AdminMessage.ConfigType.LORA_CONFIG } + .build() val validOutbound = - ToRadio( - packet = - MeshPacket( - id = validRequestId, - decoded = Data(portnum = PortNum.ADMIN_APP, payload = validAdmin.encode().toByteString()), - ), - ) + ToRadio.Builder() + .also { wb -> + wb.packet = + MeshPacket.Builder() + .also { wb -> + wb.id = validRequestId + wb.decoded = + Data.Builder() + .also { wb -> + wb.portnum = PortNum.ADMIN_APP + wb.payload = validAdmin.encode().toByteString() + } + .build() + } + .build() + } + .build() assertTrue(transport.handleSendToRadio(validOutbound.encode())) advanceTimeBy(ACK_WINDOW_MS) runCurrent() @@ -464,3 +522,5 @@ class MockRadioTransportTest { const val ACK_WINDOW_MS = 3_000L } } + +private fun wantConfig(id: Int) = ToRadio.Builder().also { wb -> wb.want_config_id = id }.build().encode() diff --git a/core/network/src/commonTest/kotlin/org/meshtastic/core/network/radio/ReplayFuzz.kt b/core/network/src/commonTest/kotlin/org/meshtastic/core/network/radio/ReplayFuzz.kt index eea307fd9d..b37e985373 100644 --- a/core/network/src/commonTest/kotlin/org/meshtastic/core/network/radio/ReplayFuzz.kt +++ b/core/network/src/commonTest/kotlin/org/meshtastic/core/network/radio/ReplayFuzz.kt @@ -95,36 +95,51 @@ object ReplayFuzz { /** A valid `FromRadio` carrying hostile content. Variant chosen by [random]. Corpus for handler/UI fuzzing. */ fun adversarialFromRadio(random: Random): FromRadio = if (random.nextBoolean()) { - FromRadio(node_info = adversarialNode(random)) + FromRadio.Builder().also { wb -> wb.node_info = adversarialNode(random) }.build() } else { - FromRadio(packet = adversarialPacket(random)) + FromRadio.Builder().also { wb -> wb.packet = adversarialPacket(random) }.build() } /** A NodeInfo with extreme identity, signal, and location values — the kind a malicious node could advertise. */ - fun adversarialNode(random: Random): NodeInfo = NodeInfo( - num = random.nextInt(), - snr = HOSTILE_FLOATS.random(random), - last_heard = random.nextInt(), - hops_away = random.nextInt(), - user = - User( - id = "!${random.nextInt().toUInt().toString(16)}", - long_name = hostileString(random), - short_name = hostileString(random), - ), - // Unconstrained, so trivially outside the valid +/-90 deg / +/-180 deg ranges (stored x 1e7). - position = Position(latitude_i = random.nextInt(), longitude_i = random.nextInt()), - ) + fun adversarialNode(random: Random): NodeInfo = NodeInfo.Builder() + .also { wb -> + wb.num = random.nextInt() + wb.snr = HOSTILE_FLOATS.random(random) + wb.last_heard = random.nextInt() + wb.hops_away = random.nextInt() + wb.user = + User.Builder() + .also { wb -> + wb.id = "!${random.nextInt().toUInt().toString(16)}" + wb.long_name = hostileString(random) + wb.short_name = hostileString(random) + } + .build() + // Unconstrained, so trivially outside the valid +/-90 deg / +/-180 deg ranges (stored x 1e7). + wb.position = + Position.Builder() + .also { wb -> + wb.latitude_i = random.nextInt() + wb.longitude_i = random.nextInt() + } + .build() + } + .build() /** A MeshPacket with extreme envelope fields and a random (malformed-for-its-portnum) decoded payload. */ - fun adversarialPacket(random: Random): MeshPacket = MeshPacket( - id = random.nextInt(), - to = random.nextInt(), - channel = random.nextInt(), - rx_time = random.nextInt(), - hop_limit = random.nextInt(), - decoded = Data(payload = randomBytes(random, MAX_PAYLOAD_LEN).toByteString()), - ) + fun adversarialPacket(random: Random): MeshPacket = MeshPacket.Builder() + .also { wb -> + wb.id = random.nextInt() + wb.to = random.nextInt() + wb.channel = random.nextInt() + wb.rx_time = random.nextInt() + wb.hop_limit = random.nextInt() + wb.decoded = + Data.Builder() + .also { wb -> wb.payload = randomBytes(random, MAX_PAYLOAD_LEN).toByteString() } + .build() + } + .build() /** Encodes the three replay sections (mirrors `replay_server.py --export` / [ReplayRadioTransport]'s reader). */ fun asset(config: List, nodes: List, packets: List): ByteArray { diff --git a/core/network/src/commonTest/kotlin/org/meshtastic/core/network/radio/ReplayFuzzTest.kt b/core/network/src/commonTest/kotlin/org/meshtastic/core/network/radio/ReplayFuzzTest.kt index 55cb92829b..ee3f4e8242 100644 --- a/core/network/src/commonTest/kotlin/org/meshtastic/core/network/radio/ReplayFuzzTest.kt +++ b/core/network/src/commonTest/kotlin/org/meshtastic/core/network/radio/ReplayFuzzTest.kt @@ -56,9 +56,21 @@ class ReplayFuzzTest { } } - private val sampleConfig = listOf(FromRadio(my_info = MyNodeInfo(my_node_num = 1))) - private val sampleNodes = listOf(FromRadio(node_info = NodeInfo(num = 1))) - private val samplePackets = listOf(FromRadio(packet = MeshPacket(id = 1))) + private val sampleConfig = + listOf( + FromRadio.Builder() + .also { wb -> wb.my_info = MyNodeInfo.Builder().also { wb -> wb.my_node_num = 1 }.build() } + .build(), + ) + private val sampleNodes = + listOf( + FromRadio.Builder() + .also { wb -> wb.node_info = NodeInfo.Builder().also { nb -> nb.num = 1 }.build() } + .build(), + ) + private val samplePacket = MeshPacket.Builder().also { wb -> wb.id = 1 }.build() + + private val samplePackets = listOf(FromRadio.Builder().also { wb -> wb.packet = samplePacket }.build()) private fun validAsset() = ReplayFuzz.asset(sampleConfig, sampleNodes, samplePackets) @@ -91,7 +103,7 @@ class ReplayFuzzTest { ReplayFuzz.forSeeds { random, seed -> val error = runCatching { - transport.handleSendToRadio(ReplayFuzz.mutate(random, ToRadio(want_config_id = 1).encode())) + transport.handleSendToRadio(ReplayFuzz.mutate(random, wantConfig(1))) transport.handleSendToRadio(ReplayFuzz.randomBytes(random)) } .exceptionOrNull() @@ -108,7 +120,11 @@ class ReplayFuzzTest { */ @Test fun `decode of bit-flipped frames never raises an Error`() = runTest { - val seedFrame = FromRadio(node_info = NodeInfo(num = 7)).encode() + val seedFrame = + FromRadio.Builder() + .also { wb -> wb.node_info = NodeInfo.Builder().also { wb -> wb.num = 7 }.build() } + .build() + .encode() ReplayFuzz.forSeeds { random, seed -> val mutated = ReplayFuzz.mutate(random, seedFrame) val error = runCatching { FromRadio.ADAPTER.decode(mutated) }.exceptionOrNull() @@ -120,8 +136,14 @@ class ReplayFuzzTest { @Test fun `adversarial frames replay through the transport without crashing it`() = runTest { val sink = Sink() - val nodes = (0 until 25).map { FromRadio(node_info = ReplayFuzz.adversarialNode(Random(it))) } - val packets = (0 until 75).map { FromRadio(packet = ReplayFuzz.adversarialPacket(Random(it))) } + val nodes = + (0 until 25).map { + FromRadio.Builder().also { wb -> wb.node_info = ReplayFuzz.adversarialNode(Random(it)) }.build() + } + val packets = + (0 until 75).map { + FromRadio.Builder().also { wb -> wb.packet = ReplayFuzz.adversarialPacket(Random(it)) }.build() + } val transport = ReplayRadioTransport( callback = sink, @@ -133,7 +155,7 @@ class ReplayFuzzTest { try { transport.start() - transport.handleSendToRadio(ToRadio(want_config_id = HandshakeConstants.NODE_INFO_NONCE).encode()) + transport.handleSendToRadio(wantConfig(HandshakeConstants.NODE_INFO_NONCE)) testScheduler.runCurrent() assertTrue(sink.frames.isNotEmpty()) @@ -143,3 +165,5 @@ class ReplayFuzzTest { } } } + +private fun wantConfig(id: Int) = ToRadio.Builder().also { wb -> wb.want_config_id = id }.build().encode() diff --git a/core/network/src/commonTest/kotlin/org/meshtastic/core/network/radio/ReplayRadioTransportTest.kt b/core/network/src/commonTest/kotlin/org/meshtastic/core/network/radio/ReplayRadioTransportTest.kt index feb0287aba..9c76de23bd 100644 --- a/core/network/src/commonTest/kotlin/org/meshtastic/core/network/radio/ReplayRadioTransportTest.kt +++ b/core/network/src/commonTest/kotlin/org/meshtastic/core/network/radio/ReplayRadioTransportTest.kt @@ -34,10 +34,27 @@ import kotlin.test.assertTrue class ReplayRadioTransportTest { - private val configFrames = listOf(FromRadio(my_info = MyNodeInfo(my_node_num = 1))) + private val configFrames = + listOf( + FromRadio.Builder() + .also { wb -> wb.my_info = MyNodeInfo.Builder().also { wb -> wb.my_node_num = 1 }.build() } + .build(), + ) private val nodeFrames = - listOf(FromRadio(node_info = NodeInfo(num = 42)), FromRadio(node_info = NodeInfo(num = 43))) - private val packetFrames = (1..3).map { FromRadio(packet = MeshPacket(id = it)) } + listOf( + FromRadio.Builder() + .also { wb -> wb.node_info = NodeInfo.Builder().also { wb -> wb.num = 42 }.build() } + .build(), + FromRadio.Builder() + .also { wb -> wb.node_info = NodeInfo.Builder().also { wb -> wb.num = 43 }.build() } + .build(), + ) + private val packetFrames = + (1..3).map { + FromRadio.Builder() + .also { wb -> wb.packet = MeshPacket.Builder().also { wb -> wb.id = it }.build() } + .build() + } private class RecordingCallback : RadioTransportCallback { var connects = 0 @@ -83,7 +100,7 @@ class ReplayRadioTransportTest { ReplayRadioTransport(callback, backgroundScope, address = "", frames = asset(), packetDelayMs = 0) assertEquals(0, callback.connects, "construction must not publish lifecycle callbacks") - assertFalse(transport.handleSendToRadio(ToRadio(want_config_id = HandshakeConstants.CONFIG_NONCE).encode())) + assertFalse(transport.handleSendToRadio(wantConfig(HandshakeConstants.CONFIG_NONCE))) transport.start() assertEquals(1, callback.connects) @@ -97,11 +114,11 @@ class ReplayRadioTransportTest { ReplayRadioTransport(callback, backgroundScope, address = "", frames = asset(), packetDelayMs = 0) transport.start() - val accepted = transport.handleSendToRadio(ToRadio(want_config_id = HandshakeConstants.CONFIG_NONCE).encode()) + val accepted = transport.handleSendToRadio(wantConfig(HandshakeConstants.CONFIG_NONCE)) testScheduler.runCurrent() assertTrue(accepted) - assertEquals(configFrames + FromRadio(config_complete_id = HandshakeConstants.CONFIG_NONCE), callback.received) + assertEquals(configFrames + configComplete(HandshakeConstants.CONFIG_NONCE), callback.received) } @Test @@ -111,10 +128,10 @@ class ReplayRadioTransportTest { ReplayRadioTransport(callback, backgroundScope, address = "", frames = asset(), packetDelayMs = 0) transport.start() - assertTrue(transport.handleSendToRadio(ToRadio(want_config_id = HandshakeConstants.NODE_INFO_NONCE).encode())) + assertTrue(transport.handleSendToRadio(wantConfig(HandshakeConstants.NODE_INFO_NONCE))) testScheduler.runCurrent() - val expected = nodeFrames + FromRadio(config_complete_id = HandshakeConstants.NODE_INFO_NONCE) + packetFrames + val expected = nodeFrames + configComplete(HandshakeConstants.NODE_INFO_NONCE) + packetFrames assertEquals(expected, callback.received) } @@ -125,15 +142,15 @@ class ReplayRadioTransportTest { ReplayRadioTransport(callback, backgroundScope, address = "", frames = asset(), packetDelayMs = 0) transport.start() - assertTrue(transport.handleSendToRadio(ToRadio(want_config_id = HandshakeConstants.CONFIG_NONCE).encode())) - assertTrue(transport.handleSendToRadio(ToRadio(want_config_id = HandshakeConstants.NODE_INFO_NONCE).encode())) + assertTrue(transport.handleSendToRadio(wantConfig(HandshakeConstants.CONFIG_NONCE))) + assertTrue(transport.handleSendToRadio(wantConfig(HandshakeConstants.NODE_INFO_NONCE))) testScheduler.runCurrent() val expected = configFrames + - FromRadio(config_complete_id = HandshakeConstants.CONFIG_NONCE) + + configComplete(HandshakeConstants.CONFIG_NONCE) + nodeFrames + - FromRadio(config_complete_id = HandshakeConstants.NODE_INFO_NONCE) + + configComplete(HandshakeConstants.NODE_INFO_NONCE) + packetFrames assertEquals(expected, callback.received) } @@ -145,9 +162,9 @@ class ReplayRadioTransportTest { ReplayRadioTransport(callback, backgroundScope, address = "", frames = asset(), packetDelayMs = 0) transport.start() - assertTrue(transport.handleSendToRadio(ToRadio(want_config_id = HandshakeConstants.NODE_INFO_NONCE).encode())) + assertTrue(transport.handleSendToRadio(wantConfig(HandshakeConstants.NODE_INFO_NONCE))) testScheduler.runCurrent() - assertTrue(transport.handleSendToRadio(ToRadio(want_config_id = HandshakeConstants.NODE_INFO_NONCE).encode())) + assertTrue(transport.handleSendToRadio(wantConfig(HandshakeConstants.NODE_INFO_NONCE))) testScheduler.runCurrent() val packetsSent = callback.received.count { it.packet != null } @@ -161,9 +178,9 @@ class ReplayRadioTransportTest { ReplayRadioTransport(callback, backgroundScope, address = "", frames = asset(), packetDelayMs = 1_000) transport.start() - assertTrue(transport.handleSendToRadio(ToRadio(want_config_id = HandshakeConstants.NODE_INFO_NONCE).encode())) + assertTrue(transport.handleSendToRadio(wantConfig(HandshakeConstants.NODE_INFO_NONCE))) testScheduler.runCurrent() - assertTrue(transport.handleSendToRadio(ToRadio(want_config_id = HandshakeConstants.CONFIG_NONCE).encode())) + assertTrue(transport.handleSendToRadio(wantConfig(HandshakeConstants.CONFIG_NONCE))) testScheduler.runCurrent() assertTrue( @@ -180,7 +197,13 @@ class ReplayRadioTransportTest { ReplayRadioTransport(callback, backgroundScope, address = "", frames = asset(), packetDelayMs = 0) transport.start() - val accepted = transport.handleSendToRadio(ToRadio(packet = MeshPacket(id = 99)).encode()) + val accepted = + transport.handleSendToRadio( + ToRadio.Builder() + .also { wb -> wb.packet = MeshPacket.Builder().also { wb -> wb.id = 99 }.build() } + .build() + .encode(), + ) testScheduler.runCurrent() assertTrue(accepted, "a live replay transport accepts ordinary traffic before intentionally discarding it") @@ -194,7 +217,7 @@ class ReplayRadioTransportTest { ReplayRadioTransport(callback, backgroundScope, address = "", frames = asset(), packetDelayMs = 1_000) transport.start() - assertTrue(transport.handleSendToRadio(ToRadio(want_config_id = HandshakeConstants.NODE_INFO_NONCE).encode())) + assertTrue(transport.handleSendToRadio(wantConfig(HandshakeConstants.NODE_INFO_NONCE))) testScheduler.runCurrent() val receivedBeforeClose = callback.received.toList() assertTrue( @@ -207,7 +230,7 @@ class ReplayRadioTransportTest { testScheduler.runCurrent() assertEquals(receivedBeforeClose, callback.received, "close must cancel the in-flight packet stream") - assertFalse(transport.handleSendToRadio(ToRadio(want_config_id = HandshakeConstants.CONFIG_NONCE).encode())) + assertFalse(transport.handleSendToRadio(wantConfig(HandshakeConstants.CONFIG_NONCE))) transport.start() assertEquals(1, callback.connects, "a closed replay transport must not reconnect") } @@ -261,10 +284,14 @@ class ReplayRadioTransportTest { ReplayRadioTransport(callback, backgroundScope, address = "", frames = frames, packetDelayMs = 0) transport.start() - assertTrue(transport.handleSendToRadio(ToRadio(want_config_id = HandshakeConstants.NODE_INFO_NONCE).encode())) + assertTrue(transport.handleSendToRadio(wantConfig(HandshakeConstants.NODE_INFO_NONCE))) testScheduler.runCurrent() // Only the injected config_complete — no nodes, no packets — proves zero-length sections parse cleanly. - assertEquals(listOf(FromRadio(config_complete_id = HandshakeConstants.NODE_INFO_NONCE)), callback.received) + assertEquals(listOf(configComplete(HandshakeConstants.NODE_INFO_NONCE)), callback.received) } } + +private fun wantConfig(id: Int) = ToRadio.Builder().also { wb -> wb.want_config_id = id }.build().encode() + +private fun configComplete(id: Int) = FromRadio.Builder().also { wb -> wb.config_complete_id = id }.build() diff --git a/core/network/src/commonTest/kotlin/org/meshtastic/core/network/repository/MQTTRepositoryImplTest.kt b/core/network/src/commonTest/kotlin/org/meshtastic/core/network/repository/MQTTRepositoryImplTest.kt index dbb2612848..4de981d3de 100644 --- a/core/network/src/commonTest/kotlin/org/meshtastic/core/network/repository/MQTTRepositoryImplTest.kt +++ b/core/network/src/commonTest/kotlin/org/meshtastic/core/network/repository/MQTTRepositoryImplTest.kt @@ -190,7 +190,16 @@ class MQTTRepositoryImplTest { @Test fun `empty address substitutes the public broker's well-known credentials`() { // Mirrors firmware PubSubConfig: lockdown-redacted (zeroed) configs must not connect anonymously. - val creds = effectiveCredentials(ModuleConfig.MQTTConfig(address = "", username = "", password = "")) + val creds = + effectiveCredentials( + ModuleConfig.MQTTConfig.Builder() + .also { wb -> + wb.address = "" + wb.username = "" + wb.password = "" + } + .build(), + ) assertEquals("meshdev" to "large4cats", creds) } @@ -201,19 +210,42 @@ class MQTTRepositoryImplTest { @Test fun `empty address ignores stored credentials entirely - firmware parity`() { - val creds = effectiveCredentials(ModuleConfig.MQTTConfig(address = "", username = "custom", password = "pw")) + val creds = + effectiveCredentials( + ModuleConfig.MQTTConfig.Builder() + .also { wb -> + wb.address = "" + wb.username = "custom" + wb.password = "pw" + } + .build(), + ) assertEquals("meshdev" to "large4cats", creds) } @Test fun `explicit address uses the stored credentials as-is`() { - val config = ModuleConfig.MQTTConfig(address = "broker.example.com", username = "user", password = "pass") + val config = + ModuleConfig.MQTTConfig.Builder() + .also { wb -> + wb.address = "broker.example.com" + wb.username = "user" + wb.password = "pass" + } + .build() assertEquals("user" to "pass", effectiveCredentials(config)) } @Test fun `explicit default server address uses the stored credentials as-is - firmware parity`() { - val config = ModuleConfig.MQTTConfig(address = "mqtt.meshtastic.org", username = "user", password = "pass") + val config = + ModuleConfig.MQTTConfig.Builder() + .also { wb -> + wb.address = "mqtt.meshtastic.org" + wb.username = "user" + wb.password = "pass" + } + .build() assertEquals("user" to "pass", effectiveCredentials(config)) } @@ -253,29 +285,47 @@ class MQTTRepositoryImplTest { val radioConfigRepository = FakeRadioConfigRepository().apply { setChannelSet( - ChannelSet( - settings = - listOf( - ChannelSettings( - name = "alpha", - downlink_enabled = true, - psk = byteArrayOf(1).toByteString(), - ), - ChannelSettings( - name = "beta", - downlink_enabled = false, - psk = byteArrayOf(2).toByteString(), - ), - ChannelSettings( - name = "gamma", - downlink_enabled = true, - psk = byteArrayOf(3).toByteString(), - ), - ), - ), + ChannelSet.Builder() + .also { wb -> + wb.settings = + listOf( + ChannelSettings.Builder() + .also { wb -> + wb.name = "alpha" + wb.downlink_enabled = true + wb.psk = byteArrayOf(1).toByteString() + } + .build(), + ChannelSettings.Builder() + .also { wb -> + wb.name = "beta" + wb.downlink_enabled = false + wb.psk = byteArrayOf(2).toByteString() + } + .build(), + ChannelSettings.Builder() + .also { wb -> + wb.name = "gamma" + wb.downlink_enabled = true + wb.psk = byteArrayOf(3).toByteString() + } + .build(), + ) + } + .build(), ) setLocalModuleConfigDirect( - LocalModuleConfig(mqtt = ModuleConfig.MQTTConfig(root = "custom", json_enabled = true)), + LocalModuleConfig.Builder() + .also { wb -> + wb.mqtt = + ModuleConfig.MQTTConfig.Builder() + .also { wb -> + wb.root = "custom" + wb.json_enabled = true + } + .build() + } + .build(), ) } val harness = createHarness(radioConfigRepository = radioConfigRepository) @@ -314,7 +364,12 @@ class MQTTRepositoryImplTest { radioConfigRepository = FakeRadioConfigRepository().apply { setLocalModuleConfigDirect( - LocalModuleConfig(mqtt = ModuleConfig.MQTTConfig(json_enabled = true)), + LocalModuleConfig.Builder() + .also { wb -> + wb.mqtt = + ModuleConfig.MQTTConfig.Builder().also { wb -> wb.json_enabled = true }.build() + } + .build(), ) }, ) @@ -658,50 +713,73 @@ class MQTTRepositoryImplTest { private fun envelopeBytes( channelId: String = "LongFast", gatewayId: String = "!aabbccdd", - packet: MeshPacket? = MeshPacket(), - ): ByteArray = - ServiceEnvelope.ADAPTER.encode(ServiceEnvelope(packet = packet, channel_id = channelId, gateway_id = gatewayId)) + packet: MeshPacket? = MeshPacket.Builder().build(), + ): ByteArray = ServiceEnvelope.ADAPTER.encode( + ServiceEnvelope.Builder() + .also { wb -> + wb.packet = packet + wb.channel_id = channelId + wb.gateway_id = gatewayId + } + .build(), + ) @Test fun `payload-less packet is undeliverable`() { // The observed LongFast flood: a packet with neither decoded nor encrypted set. - val bytes = envelopeBytes(packet = MeshPacket(from = 1, to = 2)) + val bytes = + envelopeBytes( + packet = + MeshPacket.Builder() + .also { wb -> + wb.from = 1 + wb.to = 2 + } + .build(), + ) assertTrue(isUndeliverableDownlink(bytes, myId = "!12345678")) } @Test fun `decoded packet is deliverable`() { - val bytes = envelopeBytes(packet = MeshPacket(decoded = Data())) + val bytes = + envelopeBytes(packet = MeshPacket.Builder().also { wb -> wb.decoded = Data.Builder().build() }.build()) assertFalse(isUndeliverableDownlink(bytes, myId = "!12345678")) } @Test fun `encrypted packet is deliverable`() { - val bytes = envelopeBytes(packet = MeshPacket(encrypted = byteArrayOf(1, 2, 3).toByteString())) + val bytes = + envelopeBytes( + packet = MeshPacket.Builder().also { wb -> wb.encrypted = byteArrayOf(1, 2, 3).toByteString() }.build(), + ) assertFalse(isUndeliverableDownlink(bytes, myId = "!12345678")) } @Test fun `PKI payload-less packet is deliverable - guard`() { - val bytes = envelopeBytes(channelId = "PKI", packet = MeshPacket(from = 1)) + val bytes = envelopeBytes(channelId = "PKI", packet = MeshPacket.Builder().also { wb -> wb.from = 1 }.build()) assertFalse(isUndeliverableDownlink(bytes, myId = "!12345678")) } @Test fun `own echo payload-less packet is deliverable - guard`() { - val bytes = envelopeBytes(gatewayId = "!12345678", packet = MeshPacket(from = 1)) + val bytes = + envelopeBytes(gatewayId = "!12345678", packet = MeshPacket.Builder().also { wb -> wb.from = 1 }.build()) assertFalse(isUndeliverableDownlink(bytes, myId = "!12345678")) } @Test fun `a different gateway's payload-less packet is undeliverable`() { - val bytes = envelopeBytes(gatewayId = "!deadbeef", packet = MeshPacket(from = 1)) + val bytes = + envelopeBytes(gatewayId = "!deadbeef", packet = MeshPacket.Builder().also { wb -> wb.from = 1 }.build()) assertTrue(isUndeliverableDownlink(bytes, myId = "!12345678")) } @Test fun `null myId does not suppress dropping`() { - val bytes = envelopeBytes(gatewayId = "!deadbeef", packet = MeshPacket(from = 1)) + val bytes = + envelopeBytes(gatewayId = "!deadbeef", packet = MeshPacket.Builder().also { wb -> wb.from = 1 }.build()) assertTrue(isUndeliverableDownlink(bytes, myId = null)) } @@ -721,8 +799,20 @@ class MQTTRepositoryImplTest { @Test fun `payload-less downlink stubs are dropped before forwarding`() = runTest { val harness = createHarness() - val stub = envelopeBytes(packet = MeshPacket(from = 1, to = 2)) // no payload → dropped - val real = envelopeBytes(packet = MeshPacket(decoded = Data())) // has payload → forwarded + val stub = + envelopeBytes( + packet = + MeshPacket.Builder() + .also { wb -> + wb.from = 1 + wb.to = 2 + } + .build(), + ) // no payload → dropped + val real = + envelopeBytes( + packet = MeshPacket.Builder().also { wb -> wb.decoded = Data.Builder().build() }.build(), + ) // has payload → forwarded val nextMessage = backgroundScope.async { harness.repository.proxyMessageFlow.first() } runCurrent() @@ -762,16 +852,20 @@ class MQTTRepositoryImplTest { private fun defaultRadioConfigRepository(): FakeRadioConfigRepository = FakeRadioConfigRepository().apply { setChannelSet( - ChannelSet( - settings = - listOf( - ChannelSettings( - name = "alpha", - downlink_enabled = true, - psk = byteArrayOf(1).toByteString(), - ), - ), - ), + ChannelSet.Builder() + .also { wb -> + wb.settings = + listOf( + ChannelSettings.Builder() + .also { wb -> + wb.name = "alpha" + wb.downlink_enabled = true + wb.psk = byteArrayOf(1).toByteString() + } + .build(), + ) + } + .build(), ) } diff --git a/core/nfc/src/androidMain/kotlin/org/meshtastic/core/nfc/MeshtasticHostApduService.kt b/core/nfc/src/androidMain/kotlin/org/meshtastic/core/nfc/MeshtasticHostApduService.kt new file mode 100644 index 0000000000..9be4ada102 --- /dev/null +++ b/core/nfc/src/androidMain/kotlin/org/meshtastic/core/nfc/MeshtasticHostApduService.kt @@ -0,0 +1,166 @@ +/* + * 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 . + */ +package org.meshtastic.core.nfc + +import android.nfc.cardemulation.HostApduService +import android.os.Bundle +import co.touchlab.kermit.Logger + +/** + * Presents the armed share URL as an NFC Forum Type 4 Tag, so a reader - another phone, a Flipper, any NDEF reader - + * can take a contact or channel by tapping this phone, with no physical tag. + * + * Read-only by construction: the capability container advertises no write access and no write command is answered. + * Serves nothing unless [NfcTagEmulation] is armed, so an idle phone presents no tag at all. + * + * Implements only what a Type 4 read needs: SELECT application, SELECT file, READ BINARY. + */ +class MeshtasticHostApduService : HostApduService() { + + private var selectedFile = FILE_NONE + + override fun processCommandApdu(commandApdu: ByteArray?, extras: Bundle?): ByteArray { + val ndef = NfcTagEmulation.armedNdefMessage() + // Unarmed, every command is refused, so a reader sees no tag rather than an empty one. + return if (commandApdu == null || ndef == null) SW_FILE_NOT_FOUND else dispatch(commandApdu, ndef) + } + + override fun onDeactivated(reason: Int) { + // The next reader starts unselected; without this a second tap could read whatever file + // the previous one left selected. + selectedFile = FILE_NONE + } + + private fun dispatch(apdu: ByteArray, ndef: ByteArray): ByteArray = when { + apdu.contentEquals(SELECT_APPLICATION) -> { + selectedFile = FILE_NONE + SW_OK + } + + isSelectFile(apdu) -> selectFile(apdu) + + isReadBinary(apdu) -> readBinary(apdu, ndef) + + else -> SW_INS_NOT_SUPPORTED + } + + private fun isSelectFile(apdu: ByteArray) = apdu.size == SELECT_FILE_LENGTH && + apdu[OFFSET_CLA] == CLA_ISO && + apdu[OFFSET_INS] == INS_SELECT && + apdu[OFFSET_P1] == SELECT_BY_FILE_ID && + apdu[OFFSET_P2] == SELECT_FIRST_OCCURRENCE + + private fun isReadBinary(apdu: ByteArray) = + apdu.size >= READ_BINARY_LENGTH && apdu[OFFSET_CLA] == CLA_ISO && apdu[OFFSET_INS] == INS_READ_BINARY + + private fun selectFile(apdu: ByteArray): ByteArray { + val fileId = twoByteValue(apdu, SELECT_FILE_ID_OFFSET) + return if (fileId == FILE_CAPABILITY_CONTAINER || fileId == FILE_NDEF) { + selectedFile = fileId + SW_OK + } else { + Logger.w { "Reader selected unknown file 0x${fileId.toString(HEX_RADIX)}" } + SW_FILE_NOT_FOUND + } + } + + private fun readBinary(apdu: ByteArray, ndef: ByteArray): ByteArray { + val content = + when (selectedFile) { + FILE_CAPABILITY_CONTAINER -> CAPABILITY_CONTAINER + FILE_NDEF -> ndefFile(ndef) + else -> null + } + val offset = twoByteValue(apdu, OFFSET_P1) + // Le == 0 means "as much as you can", which for a short APDU is 256 bytes. + val requested = (apdu[OFFSET_LE].toInt() and BYTE_MASK).let { if (it == 0) MAX_SHORT_LE else it } + + return when { + content == null -> SW_FILE_NOT_FOUND + offset > content.size -> SW_WRONG_PARAMETERS + else -> content.copyOfRange(offset, minOf(offset + requested, content.size)) + SW_OK + } + } + + /** + * The NDEF file as a reader sees it: a two-byte big-endian length, then the message. A message too large for what + * [CAPABILITY_CONTAINER] advertises is served as empty rather than truncated, since a truncated NDEF message is + * worse than none. + */ + private fun ndefFile(ndef: ByteArray): ByteArray { + val body = + if (ndef.size > MAX_NDEF_FILE_SIZE - NLEN_SIZE) { + Logger.w { "Armed NDEF message too large to emulate: ${ndef.size} bytes" } + ByteArray(0) + } else { + ndef + } + val nlen = byteArrayOf((body.size shr BITS_PER_BYTE and BYTE_MASK).toByte(), (body.size and BYTE_MASK).toByte()) + return nlen + body + } + + private fun twoByteValue(apdu: ByteArray, offset: Int): Int = + (apdu[offset].toInt() and BYTE_MASK shl BITS_PER_BYTE) or (apdu[offset + 1].toInt() and BYTE_MASK) + + private companion object { + fun hex(spaced: String): ByteArray = + spaced.filterNot(Char::isWhitespace).chunked(2).map { it.toInt(HEX_RADIX).toByte() }.toByteArray() + + val SW_OK = hex("90 00") + val SW_FILE_NOT_FOUND = hex("6A 82") + val SW_WRONG_PARAMETERS = hex("6B 00") + val SW_INS_NOT_SUPPORTED = hex("6D 00") + + /** SELECT (by name) of the NFC Forum NDEF Tag Application, AID D2760000850101. */ + val SELECT_APPLICATION = hex("00 A4 04 00 07 D2 76 00 00 85 01 01 00") + + /** + * CCLEN 000F, mapping version 2.0, MLe 00FF, MLc 0034, then the NDEF File Control TLV: type 04, length 06, file + * id E104, max size 0400, read access free, write access none. MLe is the largest response a reader may ask for + * in one READ BINARY; 255 keeps a share URL to a single exchange. + */ + val CAPABILITY_CONTAINER = hex("000F 20 00FF 0034 04 06 E104 0400 00 FF") + + const val CLA_ISO: Byte = 0x00 + const val INS_SELECT: Byte = 0xA4.toByte() + const val INS_READ_BINARY: Byte = 0xB0.toByte() + const val SELECT_BY_FILE_ID: Byte = 0x00 + const val SELECT_FIRST_OCCURRENCE: Byte = 0x0C + + const val OFFSET_CLA = 0 + const val OFFSET_INS = 1 + const val OFFSET_P1 = 2 + const val OFFSET_P2 = 3 + const val OFFSET_LE = 4 + const val SELECT_FILE_ID_OFFSET = 5 + + const val SELECT_FILE_LENGTH = 7 + const val READ_BINARY_LENGTH = 5 + + const val FILE_NONE = -1 + const val FILE_CAPABILITY_CONTAINER = 0xE103 + const val FILE_NDEF = 0xE104 + + const val MAX_NDEF_FILE_SIZE = 1024 + const val NLEN_SIZE = 2 + const val MAX_SHORT_LE = 256 + + const val BYTE_MASK = 0xFF + const val BITS_PER_BYTE = 8 + const val HEX_RADIX = 16 + } +} diff --git a/core/nfc/src/androidMain/kotlin/org/meshtastic/core/nfc/NfcScanner.kt b/core/nfc/src/androidMain/kotlin/org/meshtastic/core/nfc/NfcScanner.kt index 17ab92fb7c..b8fb2434b6 100644 --- a/core/nfc/src/androidMain/kotlin/org/meshtastic/core/nfc/NfcScanner.kt +++ b/core/nfc/src/androidMain/kotlin/org/meshtastic/core/nfc/NfcScanner.kt @@ -23,6 +23,8 @@ import android.nfc.NdefRecord import android.nfc.NfcAdapter import android.nfc.Tag import android.nfc.tech.Ndef +import android.nfc.tech.NdefFormatable +import android.nfc.tech.TagTechnology import androidx.compose.runtime.Composable import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.getValue @@ -104,64 +106,93 @@ fun NfcWriterEffect(url: String, onResult: (Boolean) -> Unit, onNfcDisabled: (() } private fun writeNdefUrl(tag: Tag, url: String): Boolean { - val ndef = Ndef.get(tag) - if (ndef == null) { - Logger.w { "Tag does not support NDEF" } - return false - } val message = NdefMessage(NdefRecord.createUri(url)) - return try { - ndef.connect() - when { - !ndef.isWritable -> { - Logger.w { "NDEF tag is read-only" } - false - } + val ndef = Ndef.get(tag) + // A tag that has never been NDEF-formatted exposes NdefFormatable and no Ndef: blank + // Ultralight/Classic, or an NTAG whose capability container was zeroed. format() writes both. + val formatable = if (ndef == null) NdefFormatable.get(tag) else null - ndef.maxSize < message.byteArrayLength -> { - Logger.w { "NDEF tag too small: ${ndef.maxSize} < ${message.byteArrayLength}" } - false - } + return when { + ndef != null -> writeToNdef(ndef, message) - else -> { - ndef.writeNdefMessage(message) - true - } - } - } catch (e: IOException) { - Logger.w(e) { "Error writing NDEF tag" } - false - } catch (e: FormatException) { - Logger.w(e) { "Malformed NDEF message" } - false - } finally { - try { - ndef.close() - } catch (e: IOException) { - Logger.w(e) { "Error closing NDEF" } + formatable != null -> formatAndWrite(formatable, message) + + else -> { + Logger.w { "Tag supports neither NDEF nor NDEF formatting" } + false } } } +private fun writeToNdef(ndef: Ndef, message: NdefMessage): Boolean = withTag(ndef) { + when { + !ndef.isWritable -> { + Logger.w { "NDEF tag is read-only" } + false + } + + ndef.maxSize < message.byteArrayLength -> { + Logger.w { "NDEF tag too small: ${ndef.maxSize} < ${message.byteArrayLength}" } + false + } + + else -> { + ndef.writeNdefMessage(message) + true + } + } +} + +private fun formatAndWrite(formatable: NdefFormatable, message: NdefMessage): Boolean = withTag(formatable) { + // format() lays down the capability container and the NDEF message in one pass; a tag too + // small for the message fails here as IOException, so there is no maxSize to check first. + formatable.format(message) + Logger.i { "Formatted unformatted tag and wrote NDEF" } + true +} + +/** Connects [tech], runs [block], and always closes. Reports every failure mode as `false`. */ +private inline fun withTag(tech: TagTechnology, block: () -> Boolean): Boolean = try { + tech.connect() + block() +} catch (e: IOException) { + Logger.w(e) { "Error writing NDEF tag" } + false +} catch (e: FormatException) { + Logger.w(e) { "Malformed NDEF message" } + false +} catch (e: SecurityException) { + // The framework invalidates a Tag handle once the tag leaves the field, and every call here + // then throws this rather than IOException. Letting it escape strands the caller's armed-write + // UI with no result, so it reports as an ordinary write failure. + Logger.w(e) { "Tag went out of range before the write completed" } + false +} finally { + closeQuietly(tech) +} + private fun handleNfcTag(tag: Tag, onResult: (String?) -> Unit) { val ndef = Ndef.get(tag) ?: return try { ndef.connect() - val ndefMessage = ndef.ndefMessage ?: return - for (record in ndefMessage.records) { - val payload = record.toUri()?.toString() - if (payload != null) { - onResult(payload) - break - } - } + ndef.ndefMessage?.records?.firstNotNullOfOrNull { it.toUri()?.toString() }?.let(onResult) } catch (e: IOException) { Logger.w(e) { "Error reading NDEF tag" } + } catch (e: SecurityException) { + // Runs on a binder thread, so an escaping exception takes the process with it. + Logger.w(e) { "Tag went out of range before the read completed" } } finally { - try { - ndef.close() - } catch (e: IOException) { - Logger.w(e) { "Error closing NDEF" } - } + closeQuietly(ndef) + } +} + +/** Closes [tech], swallowing the failures a stale tag handle produces. */ +private fun closeQuietly(tech: TagTechnology) { + try { + tech.close() + } catch (e: IOException) { + Logger.w(e) { "Error closing NDEF" } + } catch (e: SecurityException) { + Logger.w(e) { "Tag already gone when closing NDEF" } } } diff --git a/core/nfc/src/androidMain/kotlin/org/meshtastic/core/nfc/NfcTagEmulation.kt b/core/nfc/src/androidMain/kotlin/org/meshtastic/core/nfc/NfcTagEmulation.kt new file mode 100644 index 0000000000..49978688ad --- /dev/null +++ b/core/nfc/src/androidMain/kotlin/org/meshtastic/core/nfc/NfcTagEmulation.kt @@ -0,0 +1,61 @@ +/* + * 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 . + */ +package org.meshtastic.core.nfc + +import android.nfc.NdefMessage +import android.nfc.NdefRecord +import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect + +/** + * The share URL this phone is currently offering to NFC readers, as an encoded NDEF message. + * + * Card emulation is served by the system, in a service this app does not own the lifetime of, so the armed payload + * lives here rather than in the service. Nothing is offered unless a share surface has armed it: a reader that taps an + * unarmed phone gets no tag at all. Channel URLs carry channel PSKs, so "armed only while the share dialog is open" is + * the privacy boundary, the same exposure as the QR code being on screen. + */ +object NfcTagEmulation { + + @Volatile private var armed: ByteArray? = null + + /** The encoded NDEF message to serve, or null when nothing is armed. */ + fun armedNdefMessage(): ByteArray? = armed + + fun arm(url: String) { + armed = NdefMessage(NdefRecord.createUri(url)).toByteArray() + } + + fun disarm() { + armed = null + } +} + +/** + * Offers [url] to NFC readers for as long as this effect stays composed. + * + * The effect does not police the foreground itself. Callers compose it only while the share surface is actually on + * screen and resumed; `MainActivity` holds that gate, because a dialog left open behind a home-press stays composed and + * arming through that would offer a channel PSK to any reader while the phone sits in a pocket. + */ +@Composable +fun NfcEmulatorEffect(url: String) { + DisposableEffect(url) { + NfcTagEmulation.arm(url) + onDispose { NfcTagEmulation.disarm() } + } +} diff --git a/core/prefs/src/commonMain/kotlin/org/meshtastic/core/prefs/map/MapPrefsImpl.kt b/core/prefs/src/commonMain/kotlin/org/meshtastic/core/prefs/map/MapPrefsImpl.kt index 76e0331560..8973b4b45f 100644 --- a/core/prefs/src/commonMain/kotlin/org/meshtastic/core/prefs/map/MapPrefsImpl.kt +++ b/core/prefs/src/commonMain/kotlin/org/meshtastic/core/prefs/map/MapPrefsImpl.kt @@ -16,6 +16,8 @@ */ package org.meshtastic.core.prefs.map +import androidx.datastore.preferences.core.MutablePreferences +import androidx.datastore.preferences.core.Preferences import androidx.datastore.preferences.core.booleanPreferencesKey import androidx.datastore.preferences.core.doublePreferencesKey import androidx.datastore.preferences.core.edit @@ -34,6 +36,7 @@ import org.koin.core.annotation.Single import org.meshtastic.core.di.CoroutineDispatchers import org.meshtastic.core.prefs.di.MapDataStore import org.meshtastic.core.repository.MapCameraPosition +import org.meshtastic.core.repository.MapFilterPrefs import org.meshtastic.core.repository.MapPrefs @Single @@ -50,83 +53,48 @@ class MapPrefsImpl(private val dataStore: MapDataStore, dispatchers: CoroutineDi override suspend fun awaitMapStyle(): Int = dataStore.data.map { it[KEY_MAP_STYLE_PREF] ?: 0 }.first() - override val showOnlyFavorites: StateFlow = - dataStore.data.map { it[KEY_SHOW_ONLY_FAVORITES_PREF] ?: false }.stateIn(scope, SharingStarted.Eagerly, false) + override val mapFilters: StateFlow = + dataStore.data.map { it.toMapFilterPrefs() }.stateIn(scope, SharingStarted.Eagerly, MapFilterPrefs()) - override fun setShowOnlyFavorites(show: Boolean) { - scope.launch { dataStore.edit { it[KEY_SHOW_ONLY_FAVORITES_PREF] = show } } + override fun updateMapFilters(transform: (MapFilterPrefs) -> MapFilterPrefs) { + // Read-modify-write inside edit{}, which is transactional — the same lost-update guard updateHiddenLayerUrls + // uses. Writing every key rather than the changed one keeps this a single pure transform. + scope.launch { dataStore.edit { it.writeMapFilterPrefs(transform(it.toMapFilterPrefs())) } } } - override val showWaypointsOnMap: StateFlow = - dataStore.data.map { it[KEY_SHOW_WAYPOINTS_PREF] ?: true }.stateIn(scope, SharingStarted.Eagerly, true) - - override fun setShowWaypointsOnMap(show: Boolean) { - scope.launch { dataStore.edit { it[KEY_SHOW_WAYPOINTS_PREF] = show } } + private fun Preferences.toMapFilterPrefs(): MapFilterPrefs { + val defaults = MapFilterPrefs() + return MapFilterPrefs( + onlyFavorites = this[KEY_SHOW_ONLY_FAVORITES_PREF] ?: defaults.onlyFavorites, + showWaypoints = this[KEY_SHOW_WAYPOINTS_PREF] ?: defaults.showWaypoints, + showPrecisionCircle = this[KEY_SHOW_PRECISION_CIRCLE_PREF] ?: defaults.showPrecisionCircle, + onlyOnline = this[KEY_ONLY_ONLINE_PREF] ?: defaults.onlyOnline, + onlyDirect = this[KEY_ONLY_DIRECT_PREF] ?: defaults.onlyDirect, + onlySigned = this[KEY_ONLY_SIGNED_PREF] ?: defaults.onlySigned, + onlyEncrypted = this[KEY_ONLY_ENCRYPTED_PREF] ?: defaults.onlyEncrypted, + excludeMqtt = this[KEY_EXCLUDE_MQTT_PREF] ?: defaults.excludeMqtt, + showIgnored = this[KEY_SHOW_IGNORED_PREF] ?: defaults.showIgnored, + includeUnknown = this[KEY_INCLUDE_UNKNOWN_PREF] ?: defaults.includeUnknown, + lastHeardSeconds = this[KEY_LAST_HEARD_FILTER_PREF] ?: defaults.lastHeardSeconds, + lastHeardTrackSeconds = this[KEY_LAST_HEARD_TRACK_FILTER_PREF] ?: defaults.lastHeardTrackSeconds, + excludedRoles = this[KEY_EXCLUDED_ROLES_PREF] ?: defaults.excludedRoles, + ) } - override val showPrecisionCircleOnMap: StateFlow = - dataStore.data.map { it[KEY_SHOW_PRECISION_CIRCLE_PREF] ?: true }.stateIn(scope, SharingStarted.Eagerly, true) - - override fun setShowPrecisionCircleOnMap(show: Boolean) { - scope.launch { dataStore.edit { it[KEY_SHOW_PRECISION_CIRCLE_PREF] = show } } - } - - override val lastHeardFilter: StateFlow = - dataStore.data.map { it[KEY_LAST_HEARD_FILTER_PREF] ?: 0L }.stateIn(scope, SharingStarted.Eagerly, 0L) - - override fun setLastHeardFilter(seconds: Long) { - scope.launch { dataStore.edit { it[KEY_LAST_HEARD_FILTER_PREF] = seconds } } - } - - override val lastHeardTrackFilter: StateFlow = - dataStore.data.map { it[KEY_LAST_HEARD_TRACK_FILTER_PREF] ?: 0L }.stateIn(scope, SharingStarted.Eagerly, 0L) - - override fun setLastHeardTrackFilter(seconds: Long) { - scope.launch { dataStore.edit { it[KEY_LAST_HEARD_TRACK_FILTER_PREF] = seconds } } - } - - override val onlyOnlineOnMap: StateFlow = - dataStore.data.map { it[KEY_ONLY_ONLINE_PREF] ?: false }.stateIn(scope, SharingStarted.Eagerly, false) - - override fun setOnlyOnlineOnMap(only: Boolean) { - scope.launch { dataStore.edit { it[KEY_ONLY_ONLINE_PREF] = only } } - } - - override val onlyDirectOnMap: StateFlow = - dataStore.data.map { it[KEY_ONLY_DIRECT_PREF] ?: false }.stateIn(scope, SharingStarted.Eagerly, false) - - override fun setOnlyDirectOnMap(only: Boolean) { - scope.launch { dataStore.edit { it[KEY_ONLY_DIRECT_PREF] = only } } - } - - override val excludeMqttOnMap: StateFlow = - dataStore.data.map { it[KEY_EXCLUDE_MQTT_PREF] ?: false }.stateIn(scope, SharingStarted.Eagerly, false) - - override fun setExcludeMqttOnMap(exclude: Boolean) { - scope.launch { dataStore.edit { it[KEY_EXCLUDE_MQTT_PREF] = exclude } } - } - - override val showIgnoredOnMap: StateFlow = - dataStore.data.map { it[KEY_SHOW_IGNORED_PREF] ?: false }.stateIn(scope, SharingStarted.Eagerly, false) - - override fun setShowIgnoredOnMap(show: Boolean) { - scope.launch { dataStore.edit { it[KEY_SHOW_IGNORED_PREF] = show } } - } - - override val includeUnknownOnMap: StateFlow = - dataStore.data.map { it[KEY_INCLUDE_UNKNOWN_PREF] ?: true }.stateIn(scope, SharingStarted.Eagerly, true) - - override fun setIncludeUnknownOnMap(include: Boolean) { - scope.launch { dataStore.edit { it[KEY_INCLUDE_UNKNOWN_PREF] = include } } - } - - override val excludedMapRoles: StateFlow> = - dataStore.data - .map { it[KEY_EXCLUDED_ROLES_PREF] ?: emptySet() } - .stateIn(scope, SharingStarted.Eagerly, emptySet()) - - override fun setExcludedMapRoles(roles: Set) { - scope.launch { dataStore.edit { it[KEY_EXCLUDED_ROLES_PREF] = roles } } + private fun MutablePreferences.writeMapFilterPrefs(prefs: MapFilterPrefs) { + this[KEY_SHOW_ONLY_FAVORITES_PREF] = prefs.onlyFavorites + this[KEY_SHOW_WAYPOINTS_PREF] = prefs.showWaypoints + this[KEY_SHOW_PRECISION_CIRCLE_PREF] = prefs.showPrecisionCircle + this[KEY_ONLY_ONLINE_PREF] = prefs.onlyOnline + this[KEY_ONLY_DIRECT_PREF] = prefs.onlyDirect + this[KEY_ONLY_SIGNED_PREF] = prefs.onlySigned + this[KEY_ONLY_ENCRYPTED_PREF] = prefs.onlyEncrypted + this[KEY_EXCLUDE_MQTT_PREF] = prefs.excludeMqtt + this[KEY_SHOW_IGNORED_PREF] = prefs.showIgnored + this[KEY_INCLUDE_UNKNOWN_PREF] = prefs.includeUnknown + this[KEY_LAST_HEARD_FILTER_PREF] = prefs.lastHeardSeconds + this[KEY_LAST_HEARD_TRACK_FILTER_PREF] = prefs.lastHeardTrackSeconds + this[KEY_EXCLUDED_ROLES_PREF] = prefs.excludedRoles } override val hiddenLayerUrls: StateFlow> = @@ -203,6 +171,8 @@ class MapPrefsImpl(private val dataStore: MapDataStore, dispatchers: CoroutineDi val KEY_LAYER_OPACITY_PREF = stringSetPreferencesKey("layer_opacity") val KEY_ONLY_ONLINE_PREF = booleanPreferencesKey("map_only_online") val KEY_ONLY_DIRECT_PREF = booleanPreferencesKey("map_only_direct") + val KEY_ONLY_SIGNED_PREF = booleanPreferencesKey("map_only_signed") + val KEY_ONLY_ENCRYPTED_PREF = booleanPreferencesKey("map_only_encrypted") val KEY_EXCLUDE_MQTT_PREF = booleanPreferencesKey("map_exclude_mqtt") val KEY_SHOW_IGNORED_PREF = booleanPreferencesKey("map_show_ignored") val KEY_INCLUDE_UNKNOWN_PREF = booleanPreferencesKey("map_include_unknown") diff --git a/core/prefs/src/commonMain/kotlin/org/meshtastic/core/prefs/ui/UiPrefsImpl.kt b/core/prefs/src/commonMain/kotlin/org/meshtastic/core/prefs/ui/UiPrefsImpl.kt index 16efc1328a..7a6f7da324 100644 --- a/core/prefs/src/commonMain/kotlin/org/meshtastic/core/prefs/ui/UiPrefsImpl.kt +++ b/core/prefs/src/commonMain/kotlin/org/meshtastic/core/prefs/ui/UiPrefsImpl.kt @@ -16,6 +16,7 @@ */ package org.meshtastic.core.prefs.ui +import androidx.datastore.preferences.core.MutablePreferences import androidx.datastore.preferences.core.Preferences import androidx.datastore.preferences.core.booleanPreferencesKey import androidx.datastore.preferences.core.edit @@ -35,6 +36,7 @@ import org.meshtastic.core.di.CoroutineDispatchers import org.meshtastic.core.model.DeviceType import org.meshtastic.core.prefs.cachedFlow import org.meshtastic.core.prefs.di.UiDataStore +import org.meshtastic.core.repository.NodeFilterPrefs import org.meshtastic.core.repository.UiPrefs @Single @@ -83,53 +85,13 @@ class UiPrefsImpl(private val dataStore: UiDataStore, dispatchers: CoroutineDisp } // Defaults on so nodes heard before their NodeInfo arrives stay visible and messageable (design#16). - override val includeUnknown: StateFlow = - dataStore.data.map { it[KEY_INCLUDE_UNKNOWN] ?: true }.stateIn(scope, SharingStarted.Lazily, true) + override val nodeFilters: StateFlow = + dataStore.data.map { it.toNodeFilterPrefs() }.stateIn(scope, SharingStarted.Lazily, NodeFilterPrefs()) - override fun setIncludeUnknown(value: Boolean) { - scope.launch { dataStore.edit { it[KEY_INCLUDE_UNKNOWN] = value } } - } - - override val excludeInfrastructure: StateFlow = - dataStore.data.map { it[KEY_EXCLUDE_INFRASTRUCTURE] ?: false }.stateIn(scope, SharingStarted.Lazily, false) - - override fun setExcludeInfrastructure(value: Boolean) { - scope.launch { dataStore.edit { it[KEY_EXCLUDE_INFRASTRUCTURE] = value } } - } - - override val onlyOnline: StateFlow = - dataStore.data.map { it[KEY_ONLY_ONLINE] ?: false }.stateIn(scope, SharingStarted.Lazily, false) - - override fun setOnlyOnline(value: Boolean) { - scope.launch { dataStore.edit { it[KEY_ONLY_ONLINE] = value } } - } - - override val onlyDirect: StateFlow = - dataStore.data.map { it[KEY_ONLY_DIRECT] ?: false }.stateIn(scope, SharingStarted.Lazily, false) - - override fun setOnlyDirect(value: Boolean) { - scope.launch { dataStore.edit { it[KEY_ONLY_DIRECT] = value } } - } - - override val showIgnored: StateFlow = - dataStore.data.map { it[KEY_SHOW_IGNORED] ?: false }.stateIn(scope, SharingStarted.Lazily, false) - - override fun setShowIgnored(value: Boolean) { - scope.launch { dataStore.edit { it[KEY_SHOW_IGNORED] = value } } - } - - override val excludeMqtt: StateFlow = - dataStore.data.map { it[KEY_EXCLUDE_MQTT] ?: false }.stateIn(scope, SharingStarted.Lazily, false) - - override fun setExcludeMqtt(value: Boolean) { - scope.launch { dataStore.edit { it[KEY_EXCLUDE_MQTT] = value } } - } - - override val excludeUnheard: StateFlow = - dataStore.data.map { it[KEY_EXCLUDE_UNHEARD] ?: false }.stateIn(scope, SharingStarted.Lazily, false) - - override fun setExcludeUnheard(value: Boolean) { - scope.launch { dataStore.edit { it[KEY_EXCLUDE_UNHEARD] = value } } + override fun updateNodeFilters(transform: (NodeFilterPrefs) -> NodeFilterPrefs) { + // Read-modify-write inside edit{}, which is transactional — the same lost-update guard updateHiddenLayerUrls + // uses. Writing every key rather than the changed one keeps this a single pure transform. + scope.launch { dataStore.edit { it.writeNodeFilterPrefs(transform(it.toNodeFilterPrefs())) } } } override val hasShownNotPairedWarning: StateFlow = @@ -318,6 +280,30 @@ class UiPrefsImpl(private val dataStore: UiDataStore, dispatchers: CoroutineDisp scope.launch { dataStore.edit { it[NodeListLayoutPreferences.KEY_SHOW_TELEMETRY] = value } } } + private fun Preferences.toNodeFilterPrefs() = NodeFilterPrefs( + includeUnknown = this[KEY_INCLUDE_UNKNOWN] ?: NodeFilterPrefs().includeUnknown, + excludeInfrastructure = this[KEY_EXCLUDE_INFRASTRUCTURE] ?: false, + onlyOnline = this[KEY_ONLY_ONLINE] ?: false, + onlyDirect = this[KEY_ONLY_DIRECT] ?: false, + showIgnored = this[KEY_SHOW_IGNORED] ?: false, + onlySigned = this[KEY_ONLY_SIGNED] ?: false, + onlyEncrypted = this[KEY_ONLY_ENCRYPTED] ?: false, + excludeMqtt = this[KEY_EXCLUDE_MQTT] ?: false, + excludeUnheard = this[KEY_EXCLUDE_UNHEARD] ?: false, + ) + + private fun MutablePreferences.writeNodeFilterPrefs(prefs: NodeFilterPrefs) { + this[KEY_INCLUDE_UNKNOWN] = prefs.includeUnknown + this[KEY_EXCLUDE_INFRASTRUCTURE] = prefs.excludeInfrastructure + this[KEY_ONLY_ONLINE] = prefs.onlyOnline + this[KEY_ONLY_DIRECT] = prefs.onlyDirect + this[KEY_SHOW_IGNORED] = prefs.showIgnored + this[KEY_ONLY_SIGNED] = prefs.onlySigned + this[KEY_ONLY_ENCRYPTED] = prefs.onlyEncrypted + this[KEY_EXCLUDE_MQTT] = prefs.excludeMqtt + this[KEY_EXCLUDE_UNHEARD] = prefs.excludeUnheard + } + companion object { val KEY_HAS_SHOWN_NOT_PAIRED_WARNING_PREF = booleanPreferencesKey("has_shown_not_paired_warning") val KEY_SHOW_QUICK_CHAT_PREF = booleanPreferencesKey("show-quick-chat") @@ -333,6 +319,8 @@ class UiPrefsImpl(private val dataStore: UiDataStore, dispatchers: CoroutineDisp val KEY_EXCLUDE_INFRASTRUCTURE = booleanPreferencesKey("exclude-infrastructure") val KEY_ONLY_ONLINE = booleanPreferencesKey("only-online") val KEY_ONLY_DIRECT = booleanPreferencesKey("only-direct") + val KEY_ONLY_SIGNED = booleanPreferencesKey("only-signed") + val KEY_ONLY_ENCRYPTED = booleanPreferencesKey("only-encrypted") val KEY_SHOW_IGNORED = booleanPreferencesKey("show-ignored") val KEY_EXCLUDE_MQTT = booleanPreferencesKey("exclude-mqtt") val KEY_EXCLUDE_UNHEARD = booleanPreferencesKey("exclude-unheard") diff --git a/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/AppPreferences.kt b/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/AppPreferences.kt index 841b93bca0..f97acd2a2b 100644 --- a/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/AppPreferences.kt +++ b/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/AppPreferences.kt @@ -105,34 +105,16 @@ interface UiPrefs { fun setNodeSort(value: Int) - val includeUnknown: StateFlow + /** + * Every node-list filter, as one value. + * + * One flow rather than one per toggle: the store already holds them together, and splitting them apart only to + * recombine them downstream costs a `combine` argument per filter — a cap the typed overloads hit at five. + */ + val nodeFilters: StateFlow - fun setIncludeUnknown(value: Boolean) - - val excludeInfrastructure: StateFlow - - fun setExcludeInfrastructure(value: Boolean) - - val onlyOnline: StateFlow - - fun setOnlyOnline(value: Boolean) - - val onlyDirect: StateFlow - - fun setOnlyDirect(value: Boolean) - - val showIgnored: StateFlow - - fun setShowIgnored(value: Boolean) - - val excludeMqtt: StateFlow - - /** Hide nodes not heard since the radio's current LoRa config took effect. */ - val excludeUnheard: StateFlow - - fun setExcludeMqtt(value: Boolean) - - fun setExcludeUnheard(value: Boolean) + /** Applies [transform] to the stored filters. Read-modify-write happens inside the store's own transaction. */ + fun updateNodeFilters(transform: (NodeFilterPrefs) -> NodeFilterPrefs) val hasShownNotPairedWarning: StateFlow @@ -280,60 +262,14 @@ interface MapPrefs { */ suspend fun awaitMapStyle(): Int - val showOnlyFavorites: StateFlow - - fun setShowOnlyFavorites(show: Boolean) - - val showWaypointsOnMap: StateFlow - - fun setShowWaypointsOnMap(show: Boolean) - - val showPrecisionCircleOnMap: StateFlow - - fun setShowPrecisionCircleOnMap(show: Boolean) - - val lastHeardFilter: StateFlow - - fun setLastHeardFilter(seconds: Long) - - val lastHeardTrackFilter: StateFlow - - fun setLastHeardTrackFilter(seconds: Long) - /** - * Node filters shared with the node list's vocabulary, persisted separately: a user filtering the map to routers - * has not asked for the same of their contact list. + * Every map filter, as one value — the same shape, and for the same reason, as [UiPrefs.nodeFilters]. The map keeps + * its own copy of the node-list vocabulary: filtering the map to routers is not a statement about the contact list. */ - val onlyOnlineOnMap: StateFlow + val mapFilters: StateFlow - fun setOnlyOnlineOnMap(only: Boolean) - - val onlyDirectOnMap: StateFlow - - fun setOnlyDirectOnMap(only: Boolean) - - val excludeMqttOnMap: StateFlow - - fun setExcludeMqttOnMap(exclude: Boolean) - - val showIgnoredOnMap: StateFlow - - fun setShowIgnoredOnMap(show: Boolean) - - val includeUnknownOnMap: StateFlow - - fun setIncludeUnknownOnMap(include: Boolean) - - /** - * Names of the device roles the user has switched off on the map. - * - * Excluded rather than included, and by name rather than ordinal: an included set would make nodes reporting a role - * added by future firmware invisible with no way to discover why, and `ROUTER_CLIENT = 3` is already a deprecated - * slot. - */ - val excludedMapRoles: StateFlow> - - fun setExcludedMapRoles(roles: Set) + /** Applies [transform] to the stored filters. Read-modify-write happens inside the store's own transaction. */ + fun updateMapFilters(transform: (MapFilterPrefs) -> MapFilterPrefs) /** URIs of imported map layers the user has toggled off; a layer is visible unless its URI is in this set. */ val hiddenLayerUrls: StateFlow> @@ -533,7 +469,15 @@ interface DiscoveryPrefs { fun setTopologyOverlayEnabled(enabled: Boolean) companion object { - const val DEFAULT_DWELL_MINUTES = 15 + /** + * Minutes to dwell on each preset by default. + * + * Deliberately short. The dwell is per preset, so this multiplies: at the previous default of 15 a three-preset + * sweep ran for 45 minutes, which outlived the wake lock that keeps the CPU up for it. Longer dwells hear more, + * so every previous option is still offered in the picker — this only changes what an untouched first scan + * costs. + */ + const val DEFAULT_DWELL_MINUTES = 5 } } diff --git a/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/FixedPositionAdminMessage.kt b/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/FixedPositionAdminMessage.kt index 3a007163c4..7344fc205b 100644 --- a/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/FixedPositionAdminMessage.kt +++ b/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/FixedPositionAdminMessage.kt @@ -21,12 +21,17 @@ import org.meshtastic.proto.AdminMessage import org.meshtastic.proto.Position as ProtoPosition /** Converts the model position to the exact protobuf payload used by fixed-position admin commands. */ -fun Position.toFixedPositionProto(): ProtoPosition = - ProtoPosition(latitude_i = Position.degI(latitude), longitude_i = Position.degI(longitude), altitude = altitude) +fun Position.toFixedPositionProto(): ProtoPosition = ProtoPosition.Builder() + .also { wb -> + wb.latitude_i = Position.degI(latitude) + wb.longitude_i = Position.degI(longitude) + wb.altitude = altitude + } + .build() /** Builds the device admin command for setting or removing a fixed position. */ fun Position.toFixedPositionAdminMessage(): AdminMessage = if (isFixedPositionRemoval()) { - AdminMessage(remove_fixed_position = true) + AdminMessage.Builder().also { wb -> wb.remove_fixed_position = true }.build() } else { - AdminMessage(set_fixed_position = toFixedPositionProto()) + AdminMessage.Builder().also { wb -> wb.set_fixed_position = toFixedPositionProto() }.build() } diff --git a/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/NodeFilterPrefs.kt b/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/NodeFilterPrefs.kt new file mode 100644 index 0000000000..026ba7aa62 --- /dev/null +++ b/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/NodeFilterPrefs.kt @@ -0,0 +1,70 @@ +/* + * 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 . + */ +package org.meshtastic.core.repository + +/** + * The stored node-list filters, as one value. + * + * The defaults here are what a fresh install reads back, so they are the app's real defaults, not an "unset" + * placeholder. Adding a filter is one property here plus one key in the store. + */ +data class NodeFilterPrefs( + /** Show nodes whose user info has not arrived yet. On by default: hiding them looks like packet loss. */ + val includeUnknown: Boolean = true, + /** Hide routers, repeaters and other nodes that are infrastructure rather than people. */ + val excludeInfrastructure: Boolean = false, + val onlyOnline: Boolean = false, + /** Show only nodes heard with no intervening hop. */ + val onlyDirect: Boolean = false, + val showIgnored: Boolean = false, + /** Show only nodes whose signed broadcasts the radio has verified (design#149). */ + val onlySigned: Boolean = false, + /** Show only nodes a public key is on file for, so direct messages to them are PKI-encrypted. */ + val onlyEncrypted: Boolean = false, + val excludeMqtt: Boolean = false, + /** Hide nodes not heard since the radio's current LoRa config took effect. */ + val excludeUnheard: Boolean = false, +) + +/** + * The stored map filters, as one value. The map keeps its own copy of the node-list vocabulary on purpose: filtering + * the map to routers is not a statement about the contact list. + */ +data class MapFilterPrefs( + val onlyFavorites: Boolean = false, + val showWaypoints: Boolean = true, + val showPrecisionCircle: Boolean = true, + val onlyOnline: Boolean = false, + val onlyDirect: Boolean = false, + val onlySigned: Boolean = false, + val onlyEncrypted: Boolean = false, + val excludeMqtt: Boolean = false, + val showIgnored: Boolean = false, + val includeUnknown: Boolean = true, + /** Seconds; `0` means no last-heard limit. */ + val lastHeardSeconds: Long = 0, + /** Seconds; `0` means no limit on how far back a node's track is drawn. */ + val lastHeardTrackSeconds: Long = 0, + /** + * Names of the device roles switched off. + * + * Excluded rather than included, and by name rather than ordinal: an included set would make nodes reporting a role + * added by future firmware invisible with no way to discover why, and `ROUTER_CLIENT = 3` is already a deprecated + * slot. + */ + val excludedRoles: Set = emptySet(), +) diff --git a/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/NodeRepository.kt b/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/NodeRepository.kt index e816b1ca68..5ef9643c10 100644 --- a/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/NodeRepository.kt +++ b/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/NodeRepository.kt @@ -105,7 +105,7 @@ interface NodeRepository { * @param filter A search string for filtering by name or ID. * @param includeUnknown Whether to include nodes with unset hardware models. * @param onlyOnline Whether to include only nodes currently considered online. - * @param onlyDirect Whether to include only nodes heard directly (0 hops away). + * @param onlyDirect Whether to include only nodes our radio heard directly: 0 hops away and not via MQTT. */ fun getNodes( sort: NodeSortOption = NodeSortOption.LAST_HEARD, diff --git a/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/PacketRepository.kt b/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/PacketRepository.kt index 1d14069ea5..51941a7102 100644 --- a/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/PacketRepository.kt +++ b/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/PacketRepository.kt @@ -24,7 +24,6 @@ import org.meshtastic.core.model.Message import org.meshtastic.core.model.MessageStatus import org.meshtastic.core.model.Node import org.meshtastic.core.model.Reaction -import org.meshtastic.proto.ChannelSettings import org.meshtastic.proto.MeshPacket /** Stable identity of one persisted packet row within its owning node database. */ @@ -245,9 +244,6 @@ interface PacketRepository { /** Clears all packet and message history from the database. */ suspend fun clearPacketDB() - /** Migrates channel-specific message history when encryption keys change. */ - suspend fun migrateChannelsByPSK(oldSettings: List, newSettings: List) - /** Marks all messages from a specific sender as filtered or unfiltered. */ suspend fun updateFilteredBySender(senderId: String, filtered: Boolean) diff --git a/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/RadioConfigRepository.kt b/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/RadioConfigRepository.kt index bff29300fb..a27b2b556d 100644 --- a/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/RadioConfigRepository.kt +++ b/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/RadioConfigRepository.kt @@ -48,6 +48,15 @@ interface RadioConfigRepository { */ suspend fun updateChannelSet(settingsList: List?, loraConfig: Config.LoRaConfig?) + /** + * Re-keys stored conversations onto the current channel set. + * + * Conversations are keyed by channel index, so a slot that changed occupant shows the previous channel's history + * under the new channel until this runs. The app's own whole-set writes reconcile themselves; this exists for the + * handshake, which streams channels in one slot at a time and so must reconcile once at the end. + */ + suspend fun reconcileConversations() + /** Updates the [ChannelSettings] list with the provided channel. */ suspend fun updateChannelSettings(channel: Channel) diff --git a/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/usecase/SendMessageUseCase.kt b/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/usecase/SendMessageUseCase.kt index 236479cea1..f687791ec1 100644 --- a/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/usecase/SendMessageUseCase.kt +++ b/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/usecase/SendMessageUseCase.kt @@ -74,6 +74,12 @@ class SendMessageUseCaseImpl( @Suppress("NestedBlockDepth", "LongMethod", "CyclomaticComplexMethod") override suspend operator fun invoke(text: String, contactKey: String, replyId: Int?): Int { val parsedKey = ContactKey(contactKey) + // A retired conversation's channel is no longer on the radio, so there is no slot to send on. Without this + // the key's absent channel prefix would read as 0 and the message would go out on the primary channel. + if (parsedKey.isRetired) { + Logger.w { "Refusing to send to a retired conversation" } + return 0 + } val channel = parsedKey.channelOrNull val dest = parsedKey.addressString diff --git a/core/repository/src/commonTest/kotlin/org/meshtastic/core/repository/ClientNotificationIdentityTest.kt b/core/repository/src/commonTest/kotlin/org/meshtastic/core/repository/ClientNotificationIdentityTest.kt index 87a705f58f..6d8a93fae3 100644 --- a/core/repository/src/commonTest/kotlin/org/meshtastic/core/repository/ClientNotificationIdentityTest.kt +++ b/core/repository/src/commonTest/kotlin/org/meshtastic/core/repository/ClientNotificationIdentityTest.kt @@ -27,29 +27,63 @@ class ClientNotificationIdentityTest { @Test fun `identity is stable across reply_id and time on an otherwise identical repeat`() { - val first = ClientNotification(message = "Location sharing is disabled", reply_id = 100, time = 1_000) - val second = ClientNotification(message = "Location sharing is disabled", reply_id = 200, time = 2_000) + val first = + ClientNotification.Builder() + .also { wb -> + wb.message = "Location sharing is disabled" + wb.reply_id = 100 + wb.time = 1_000 + } + .build() + val second = + ClientNotification.Builder() + .also { wb -> + wb.message = "Location sharing is disabled" + wb.reply_id = 200 + wb.time = 2_000 + } + .build() assertEquals(first.notificationId(), second.notificationId()) } @Test fun `identity differs when the message differs`() { - val first = ClientNotification(message = "Location sharing is disabled", reply_id = 100) - val second = ClientNotification(message = "Quota exceeded", reply_id = 100) + val first = + ClientNotification.Builder() + .also { wb -> + wb.message = "Location sharing is disabled" + wb.reply_id = 100 + } + .build() + val second = + ClientNotification.Builder() + .also { wb -> + wb.message = "Quota exceeded" + wb.reply_id = 100 + } + .build() assertNotEquals(first.notificationId(), second.notificationId()) } @Test fun `identity differs when the payload variant differs`() { - val generic = ClientNotification(message = "Compromised keys detected", level = LogRecord.Level.WARNING) + val generic = + ClientNotification.Builder() + .also { wb -> + wb.message = "Compromised keys detected" + wb.level = LogRecord.Level.WARNING + } + .build() val structured = - ClientNotification( - message = "Compromised keys detected", - level = LogRecord.Level.WARNING, - duplicated_public_key = DuplicatedPublicKey(), - ) + ClientNotification.Builder() + .also { wb -> + wb.message = "Compromised keys detected" + wb.level = LogRecord.Level.WARNING + wb.duplicated_public_key = DuplicatedPublicKey.Builder().build() + } + .build() assertNotEquals(generic.notificationId(), structured.notificationId()) } diff --git a/core/repository/src/commonTest/kotlin/org/meshtastic/core/repository/usecase/SendMessageUseCaseTest.kt b/core/repository/src/commonTest/kotlin/org/meshtastic/core/repository/usecase/SendMessageUseCaseTest.kt index 6704e766f5..379bc8e9d0 100644 --- a/core/repository/src/commonTest/kotlin/org/meshtastic/core/repository/usecase/SendMessageUseCaseTest.kt +++ b/core/repository/src/commonTest/kotlin/org/meshtastic/core/repository/usecase/SendMessageUseCaseTest.kt @@ -76,7 +76,7 @@ class SendMessageUseCaseTest { @Test fun `invoke with broadcast message simply sends data packet`() = runTest { // Arrange - val ourNode = Node(num = 1, user = User(id = "!1234")) + val ourNode = Node(num = 1, user = User.Builder().also { wb -> wb.id = "!1234" }.build()) nodeRepository.setOurNode(ourNode) appPreferences.homoglyph.setHomoglyphEncodingEnabled(false) @@ -91,7 +91,7 @@ class SendMessageUseCaseTest { @Test fun `invoke reports a message_send analytics action`() = runTest { // Arrange - val ourNode = Node(num = 1, user = User(id = "!1234")) + val ourNode = Node(num = 1, user = User.Builder().also { wb -> wb.id = "!1234" }.build()) nodeRepository.setOurNode(ourNode) appPreferences.homoglyph.setHomoglyphEncodingEnabled(false) @@ -118,12 +118,18 @@ class SendMessageUseCaseTest { val ourNode = Node( num = 1, - user = User(id = "!local", role = Config.DeviceConfig.Role.CLIENT), - metadata = DeviceMetadata(firmware_version = "2.0.0"), + user = + User.Builder() + .also { wb -> + wb.id = "!local" + wb.role = Config.DeviceConfig.Role.CLIENT + } + .build(), + metadata = DeviceMetadata.Builder().also { wb -> wb.firmware_version = "2.0.0" }.build(), ) nodeRepository.setOurNode(ourNode) - val destNode = Node(num = 12345, user = User(id = "!dest")) + val destNode = Node(num = 12345, user = User.Builder().also { wb -> wb.id = "!dest" }.build()) nodeRepository.upsert(destNode) appPreferences.homoglyph.setHomoglyphEncodingEnabled(false) @@ -142,12 +148,18 @@ class SendMessageUseCaseTest { val ourNode = Node( num = 1, - user = User(id = "!local", role = Config.DeviceConfig.Role.CLIENT), - metadata = DeviceMetadata(firmware_version = "2.7.12"), + user = + User.Builder() + .also { wb -> + wb.id = "!local" + wb.role = Config.DeviceConfig.Role.CLIENT + } + .build(), + metadata = DeviceMetadata.Builder().also { wb -> wb.firmware_version = "2.7.12" }.build(), ) nodeRepository.setOurNode(ourNode) - val destNode = Node(num = 67890, user = User(id = "!dest")) + val destNode = Node(num = 67890, user = User.Builder().also { wb -> wb.id = "!dest" }.build()) nodeRepository.upsert(destNode) appPreferences.homoglyph.setHomoglyphEncodingEnabled(false) @@ -182,12 +194,18 @@ class SendMessageUseCaseTest { val ourNode = Node( num = 1, - user = User(id = "!local", role = Config.DeviceConfig.Role.CLIENT), - metadata = DeviceMetadata(firmware_version = "2.7.12"), + user = + User.Builder() + .also { wb -> + wb.id = "!local" + wb.role = Config.DeviceConfig.Role.CLIENT + } + .build(), + metadata = DeviceMetadata.Builder().also { wb -> wb.firmware_version = "2.7.12" }.build(), ) nodeRepository.setOurNode(ourNode) - val destNode = Node(num = 0x70fdde9b.toInt(), user = User(id = "!70fdde9b")) + val destNode = Node(num = 0x70fdde9b.toInt(), user = User.Builder().also { wb -> wb.id = "!70fdde9b" }.build()) nodeRepository.upsert(destNode) appPreferences.homoglyph.setHomoglyphEncodingEnabled(false) @@ -207,12 +225,18 @@ class SendMessageUseCaseTest { val ourNode = Node( num = 1, - user = User(id = "!local", role = Config.DeviceConfig.Role.CLIENT), - metadata = DeviceMetadata(firmware_version = "2.7.12"), + user = + User.Builder() + .also { wb -> + wb.id = "!local" + wb.role = Config.DeviceConfig.Role.CLIENT + } + .build(), + metadata = DeviceMetadata.Builder().also { wb -> wb.firmware_version = "2.7.12" }.build(), ) nodeRepository.setOurNode(ourNode) - val destNode = Node(num = 0x12345678, user = User(id = "!12345678")) + val destNode = Node(num = 0x12345678, user = User.Builder().also { wb -> wb.id = "!12345678" }.build()) nodeRepository.upsert(destNode) appPreferences.homoglyph.setHomoglyphEncodingEnabled(false) @@ -231,12 +255,18 @@ class SendMessageUseCaseTest { val ourNode = Node( num = 1, - user = User(id = "!local", role = Config.DeviceConfig.Role.CLIENT), - metadata = DeviceMetadata(firmware_version = "2.0.0"), + user = + User.Builder() + .also { wb -> + wb.id = "!local" + wb.role = Config.DeviceConfig.Role.CLIENT + } + .build(), + metadata = DeviceMetadata.Builder().also { wb -> wb.firmware_version = "2.0.0" }.build(), ) nodeRepository.setOurNode(ourNode) - val destNode = Node(num = 0xABCDEF01.toInt(), user = User(id = "!abcdef01")) + val destNode = Node(num = 0xABCDEF01.toInt(), user = User.Builder().also { wb -> wb.id = "!abcdef01" }.build()) nodeRepository.upsert(destNode) appPreferences.homoglyph.setHomoglyphEncodingEnabled(false) diff --git a/core/resources/src/commonMain/composeResources/values-bg/strings.xml b/core/resources/src/commonMain/composeResources/values-bg/strings.xml index bf1164be4d..75cb59e30a 100644 --- a/core/resources/src/commonMain/composeResources/values-bg/strings.xml +++ b/core/resources/src/commonMain/composeResources/values-bg/strings.xml @@ -19,6 +19,7 @@ %1$s: %2$s Съобщение от %1$s: %2$s + батерия %1$d% любим последно чут %1$s офлайн @@ -131,12 +132,14 @@ Удебелен заглавен шрифт Сдвояването не е успешно. Предоставете разрешения на устройството наблизо и опитайте отново. + Сдвояването не завърши. Опитайте да сдвоите отново. Настройки Интервал на излъчване GPIO за бутон GPIO за зумер Изчисляване… Опознавателен знак + Вашият радиолюбителски опознавателен знак, до 7 знака Разрешение за камерата Разрешаване на достъп до камерата за сканиране на QR кодове. Отказ @@ -166,6 +169,7 @@ Ето някои страници с документация, които биха могли да помогнат: Ниво на контраста Избор на тема + Изберете единици Това ще премахне %1$d възела от вашата база данни. Това действие не може да бъде отменено. Почистване на базата данни с възлите @@ -214,6 +218,7 @@ Налични предварително зададени настройки на модема, по подразбиране е Дълъг Бърз. Регионът, където ще използвате радиостанциите си. Активирането на Ethernet ще деактивира Bluetooth връзката с приложението. TCP връзки с възли не са налични на устройства на Apple. + Активирането на Wi-Fi ще деактивира Bluetooth връзката с приложението. Максималният интервал, който може да изтече, без възела да излъчи позиция. Най-бързо ще бъдат изпратени актуализации на позицията, ако е спазено минималното разстояние. Възелът се рестартира и ще бъде за кратко недостъпен. @@ -333,6 +338,7 @@ Напредък на сканирането Бутонът за стартиране на сканиране е деактивиран. %1$s радио хардуерът не поддържа 2.4 GHz + устройството не е свързано Анализ Средно използване на канала Използване на канала @@ -365,6 +371,9 @@ DNS Изчистване на търсенето + Зареждане на документацията… + Няма налична документация + Няма намерени резултати Търсене в документацията… Ръководство за разработчици Ръководство за потребители @@ -385,6 +394,7 @@ Интеграция с TAK Телеметрия & Сензори Превод на приложението + Документация Готово Да не се показва отново за това устройство Съобщенията от шлюза към публичния интернет се препращат към локалната mesh мрежа. Поради политиката за нулев отскок, трафикът от MQTT сървъра по подразбиране няма да се разпространява по-далеч от това устройство. @@ -412,7 +422,6 @@ Несъответствие на публичния ключ Публичният ключ не съвпада със записания ключ. Можете да премахнете възела и да го оставите да обмени ключове отново, но това може да показва по-сериозен проблем със сигурността. Свържете се с потребителя чрез друг надежден канал, за да определите дали промяната на ключа се дължи на фабрично нулиране или друго умишлено действие. Криптиране с публичния ключ - Директните съобщения използват новата инфраструктура с публичен ключ за криптиране. Показатели на околната среда Околна среда @@ -429,6 +438,7 @@ Ethernet IP: Размяна на позиция Разгъване на диаграмата + Разгъната Изтича Експортиране на конфигурацията @@ -459,6 +469,7 @@ Версия на фърмуера %1$s приключи. Върнете се към стандартния фърмуер на Meshtastic, за да възстановите нормалните функции. Актуализиране на фърмуера + Това устройство не поддържа изтриване от приложението. Фърмуерът на радиото е твърде стар, за да общува с това приложение. За повече информация относно това вижте нашето ръководство за инсталиране на фърмуер. Възобновяване на актуализацията на фърмуера Необходима е актуализация на фърмуера. @@ -491,16 +502,20 @@ BLE OTA Актуализиране чрез %1$s Прехвърляне на файл през USB + Wi-Fi OTA Нощен Няма свързано устройство Липсва информация за потребителя на възела. Не е намерен фърмуер за %1$s в изданието. Отваряне на актуализациите на фърмуера OTA актуализацията не е успешна: %1$s + Подготовка на устройството: изтриване на флаша. Това може да отнеме до минута... Рестартиране в DFU... Бележки за изданието Опитайте отново + Устройството не отговори. Повторен опит за качване (опит %1$d/%2$d)... Моля, запазете .uf2 файла на DFU устройството си. + Търсене на OTA устройство в мрежата... Изберете локален файл Източник: Локален файл Стабилен @@ -545,6 +560,7 @@ Премахване на областта Задаване на област на картата Започнете + GitHub хранилище Добър GPIO @@ -599,6 +615,7 @@ Порт: Режим на IPv4 %1$s + %1$s +%2$d Проверката на ключа е завършена Заявка за проверка на ключ @@ -676,6 +693,7 @@ Карта на Mesh Капацитет: %1$d MB\nИзползвани: %2$d MB Мениджър на кеш + %1$s MB Текущ размер на свалените данни %1$d плочки Изчистване на свалените карти @@ -683,6 +701,7 @@ Свалянето приключи с %1$d грешки Сваляне на регион Пълно + Изтегляне Филтър на картата\n Всички Дисплей @@ -690,6 +709,9 @@ Слоевете на картата поддържат формати .kml, .kmz или GeoJSON. %1$s<br>Последно чут: %2$s<br>Последна позиция: %3$s<br>Батерия: %4$s Управление извън линия + %1$s, %2$s %3$s + Показване на картата + Контури Изчистването на SQL кеша е неуспешно, вижте logcat за подробности Свалените SQL данни бяха изчистени успешно за %1$s Съгласие за споделяне на некриптирани данни от възела чрез MQTT @@ -700,14 +722,17 @@ Избор на стил на картата посока: %1$d° разстояние: %2$s Прогноза за изтегляне на картинки: + %1$s %2$s %3$s Източник на карти Хибриден Нормален Сателит Терен Маркиране като прочетено + Маркиране като непрочетено Макс + Канал %1$d Слушане за маяци Добавяне на канал Канал: %1$s diff --git a/core/resources/src/commonMain/composeResources/values-cs/strings.xml b/core/resources/src/commonMain/composeResources/values-cs/strings.xml index eeb33bc507..344e5b69cd 100644 --- a/core/resources/src/commonMain/composeResources/values-cs/strings.xml +++ b/core/resources/src/commonMain/composeResources/values-cs/strings.xml @@ -404,6 +404,7 @@ Maximální počet databází zařízení uchovávaných v tomto telefonu GPS zařízení Odkazy na zařízení + Odkazy na produkty mohou být partnerské odkazy – z nákupů může Meshtastic získat provizi. Chci jeden taky Otevřít v prohlížeči %1$s: %2$s @@ -551,7 +552,7 @@ Neshoda veřejného klíče Veřejný klíč neodpovídá uloženému klíči. Můžete uzel odebrat a nechat jej znovu vyměnit klíče, ale může to znamenat bezpečnostní problém. Kontaktujte uživatele jiným důvěryhodným kanálem a ověřte, zda ke změně klíče došlo v důsledku továrního resetu nebo jiné úmyslné akce. Šifrování veřejného klíče - Přímé zprávy používají novou infrastrukturu veřejných klíčů pro šifrování. + Pro tento uzel je uložen veřejný klíč, takže komunikace s ním používá šifrování veřejným klíčem. Metriky prostředí Životní prostředí @@ -832,6 +833,8 @@ Stav LED Starý kanál správce %1$d knihoven + Licence + Svobodný software licencovaný pod GNU General Public License v3, bez záruky. Můžete jej dále šířit za stejných licenčních podmínek. Licencované amatérské rádio (Ham) Povolení této možnosti zruší šifrování a není kompatibilní se základním nastavením Meshtastic sítě. @@ -874,7 +877,6 @@ Zadejte přístupovou frázi pro vypnutí režimu uzamčení. Zařízení dešifruje své úložiště a restartuje se. Povolit uzamčení Rozumím - Pozor: povolením režimu uzamčení se na podporovaném hardwaru uzamkne ladicí port (SWD). Režim uzamčení můžete kdykoli vypnout pomocí přístupové fráze a v případě potřeby vše obnovit úplným vymazáním zařízení. Zadejte přístupovou frázi Skrýt Hodiny do vypršení platnosti @@ -1135,7 +1137,7 @@ Nenalezena žádná síťová zařízení Metriky PAX nejsou k dispozici. Žádný veřejný klíč - Tento uzel nesdílel veřejný klíč, takže mu nelze posílat přímé zprávy. Požádejte o informace o uživateli. + Tento uzel nesdílel veřejný klíč, takže šifrovaná komunikace s ním není možná. Požádejte o informace o uživateli a vyžádejte si jeho veřejný klíč. Nenalezena žádná USB zařízení Připojte zařízení datovým USB kabelem, chcete-li použít sériové připojení. Nebyla nalezena žádná USB zařízení @@ -1146,10 +1148,13 @@ %1$d celkem Vyloučit infrastrukturu Vyloučit MQTT + Skrýt neslyšené uzly Prohlížíte ignorované uzly,\nStiskněte pro návrat do seznamu uzlů. Včetně neznámých Zobrazit jen přímé uzly + Pouze šifrované Skrýt offline uzly + Pouze podepsané Filtr Zobrazit jen ignorované uzly Filtrovat podle @@ -1182,6 +1187,7 @@ Detaily uzlu Nápověda pro seznam uzlů Možnosti uzlu + Neslyšen při vašem aktuálním nastavení LoRa Číslo uzlu Požadavek se nepodařilo odeslat. Zkuste to znovu. Restartování… @@ -1204,6 +1210,9 @@ Zde se budou postupně zobrazovat nalezené uzly v okolí. Vyhledávání uzlů %1$d uzlů zařazeno k odstranění: + %1$d uzlů nebylo slyšeno při vašem aktuálním nastavení LoRa + 1 uzel nebyl slyšen při vašem aktuálním nastavení LoRa + Ponechat Odstranit Úroveň šumu Úroveň okolního rádiového šumu naměřená přijímačem. Nižší hodnoty obvykle znamenají klidnější rádiové prostředí; −85 dBm je orientační hodnota pro silné rušení, nikoli pevná hranice. @@ -1361,6 +1370,7 @@ Primární Pravidelné vysílání pozice a telemetrie Soukromý klíč + Informace o projektu Poskytnout polohu síti Proxy na klienta povoleno PSK @@ -1524,11 +1534,16 @@ Nezabezpečený kanál, přesná poloha Bezpečný Varování: Nezabezpečeno, přesná poloha a MQTT uplink + Zobrazeno v libovolné verzi firmwaru. + Zobrazeno u uzlů s firmwarem starším než 2.8 nebo u uzlů, které neuvádějí verzi. + Zobrazeno u uzlů s firmwarem 2.8 nebo novějším a u všech uzlů, jejichž podpis rádio ověřilo. Ověřeno klíčem odesílatele. Podepsaný uzel Ověřeno automaticky - Tento uzel podepisuje své vysílání pomocí XEdDSA. Vysílání, která od něj přijímáte, rádio ověřuje pomocí jeho identitního klíče. + Rádio ověřilo podepsaná vysílání tohoto uzlu pomocí jeho identifikačního klíče, takže jeho identita zůstává v čase konzistentní, ale osobně jste ji neověřili. Každý uzel s firmwarem 2.8 nebo novějším podepisuje svá vysílání. Podepsáno · ověřeno + Ověřený kontakt + Tento klíč uzlu jste osobně ověřili výměnou kontaktních QR kódů, takže jeho identita je potvrzena. Jde o nejvyšší úroveň důvěry, kterou seznam zobrazuje, a zobrazuje se u všech verzí firmwaru. Vybrat Vybrat vše Vybrané @@ -1553,11 +1568,14 @@ nastavení Sdílet - Sdílet QR kód kanálů + Sdílet odkaz + Dokud je tato obrazovka otevřená, může ji převzít i jiný telefon přiložením k vašemu telefonu. + Sdílet kanály Sdílet připojený uzel Sdílet kontakt Sdílet polohu Použijte GPS telefonu k odesílání polohy do svého uzlu namísto použití hardwarového GPS přímo na uzlu. + Naskenujte tento kód pomocí aplikace Meshtastic na jiném zařízení a přidejte jej tam. Sdílet do… Sdílejte svou polohu v reálném čase a udržujte svou skupinu koordinovanou díky integrovaným funkcím GPS. Tento kontakt nemá žádný veřejný klíč, takže jej nelze importovat. Kontakt bez něj vymaže všechny klíče, které vaše rádio již pro daný uzel drží. Požádejte odesílatele, aby jej znovu sdílel, jakmile si jeho uzel vymění klíče. @@ -1855,7 +1873,7 @@ Zapsat na NFC tag Nelze zapsat do tagu. Použijte zapisovatelný, prázdný NFC tag a podržte jej stabilně. - Zápisem se tento odkaz pro sdílení uloží na NFC štítek. Kdokoli může přiložit telefon ke štítku a otevřít odkaz. + Zápisem se tento odkaz uloží na NFC štítek, ke kterému může kdokoli přiložit telefon. Zapsáno do tagu Přidržte zapisovatelný NFC tag proti zadní straně telefonu. Vy diff --git a/core/resources/src/commonMain/composeResources/values-de/strings.xml b/core/resources/src/commonMain/composeResources/values-de/strings.xml index f90220f348..5f9b0f5175 100644 --- a/core/resources/src/commonMain/composeResources/values-de/strings.xml +++ b/core/resources/src/commonMain/composeResources/values-de/strings.xml @@ -534,7 +534,6 @@ Öffentlicher Schlüssel stimmt nicht überein Der öffentliche Schlüssel stimmt nicht mit dem gespeicherten Schlüssel überein. Sie können den Knoten entfernen und den Schlüsselaustausch erneut durchführen lassen. Dies könnte jedoch auf ein schwerwiegenderes Sicherheitsproblem hindeuten. Kontaktieren Sie den Benutzer über einen anderen vertrauenswürdigen Kanal, um zu klären, ob die Schlüsseländerung auf ein Zurücksetzen auf Werkseinstellungen oder eine andere absichtliche Handlung zurückzuführen ist. Verschlüsselung mit öffentlichen Schlüssel - Direktnachrichten verwenden zur den öffentlichen Knotenschlüssel zur Verschlüsselung. Umweltdaten Umgebung @@ -813,7 +812,6 @@ Versuche es erneut in %1$d Sekunden. Passwort bestätigen Ich verstehe - Hinweis: Die Aktivierung des Lockdown-Modus sperrt den Debug-Port (SWD) auf unterstützter Hardware. Sie können den Lockdown jederzeit mithilfe Ihrer Passphrase aufheben; zudem stellt ein vollständiges Zurücksetzen des Geräts bei Bedarf den ursprünglichen Zustand wieder her. Passwort eingeben Ausblenden Stunden bis zum Ablauf @@ -1413,7 +1411,6 @@ Einstellungen Teilen - QR-Code für Kanäle teilen Kontakt teilen Standort teilen Nutze das GPS deines Handys, um Positionen an deinen Knoten zu senden, anstatt eines GPS-Empfängers an deinem Knoten. diff --git a/core/resources/src/commonMain/composeResources/values-es/strings.xml b/core/resources/src/commonMain/composeResources/values-es/strings.xml index 031f105c25..ee51e02ec4 100644 --- a/core/resources/src/commonMain/composeResources/values-es/strings.xml +++ b/core/resources/src/commonMain/composeResources/values-es/strings.xml @@ -285,7 +285,6 @@ Permitir Encripción Clave pública no coincide Cifrado de Clave Pública - Los mensajes directos están utilizando la nueva infraestructura de clave pública para el cifrado. Métricas de Entorno Ambiente diff --git a/core/resources/src/commonMain/composeResources/values-et/strings.xml b/core/resources/src/commonMain/composeResources/values-et/strings.xml index 8175797f98..095cd6aac0 100644 --- a/core/resources/src/commonMain/composeResources/values-et/strings.xml +++ b/core/resources/src/commonMain/composeResources/values-et/strings.xml @@ -607,7 +607,6 @@ Kokkusobimatu avalik võti Avalikvõti ei ühti salvestatud võtmele. Võid sõlme eemaldada ja lasta uuesti võtmeid vahetada, kuid see võib viidata tõsisemale turvaprobleemile. Võtke kasutajaga ühendust mõne muu usaldusväärse kanali kaudu, et teha kindlaks, kas võtmevahetus oli tingitud tehaseseadete taastamisest või muust tahtlikust toimingust. Avaliku võtme krüpteerimine - Otsesõnumid kasutavad krüpteerimiseks uut avaliku võtme infrastruktuuri. Keskkonnamõõdikud Keskkond @@ -968,7 +967,6 @@ Lukustuse väljalülitamiseks sisesta salasõna. Seade dekrüpteerib oma salvestusruumi ja taaskäivitub. Luba lukustus Saan aru - Tähelepanu: lukustuse lubamine lukustab arendajapordi (SWD) toetatud riistvara. Lukustuse saab igal ajal salasõnaga välja lülitada ja seadme täielik kustutamine taastab vajadusel kõik. Sisesta salasõna Peida Tunde kuni kehtivusaja lõpuni @@ -1641,7 +1639,6 @@ Tõendatud saatja võtmega. Registreeritud sõlm Tõendatud automaatselt - See sõlm allkirjastab oma leviedastused XEdDSA-ga. Raadio kontrollib sealt nähtavaid leviedastusi oma identiteedivõtme abil. Registreeritud - tõendatud Vali Vali kõik @@ -1669,7 +1666,6 @@ sätted Jaga - Jaga kanalite QR koodi Jaga ühendatud sõlme Jaga kontakti Jaga asukohta @@ -2006,7 +2002,6 @@ Kirjuta NFC sildile Sildile ei õnnestunud kirjutada. Kasuta kirjutatavat, tühja NFC silti ja hoia seda paigal. - Kirjutamine salvestab selle jagamislingi füüsilisele NFC-sildile. Igaüks saab silti oma telefoniga puudutates, et seda avada. Kirjutatud sildile Hoia kirjutatavat NFC silti telefoni tagakülje vastas. Sina diff --git a/core/resources/src/commonMain/composeResources/values-fi/strings.xml b/core/resources/src/commonMain/composeResources/values-fi/strings.xml index 342f434e09..8ff5e642f4 100644 --- a/core/resources/src/commonMain/composeResources/values-fi/strings.xml +++ b/core/resources/src/commonMain/composeResources/values-fi/strings.xml @@ -620,7 +620,6 @@ Julkinen avain ei täsmää Julkinen avain ei vastaa tallennettua avainta. Voit poistaa laitteen ja antaa sen vaihtaa avaimet uudelleen, mutta tämä saattaa viitata vakavampaan tietoturvaongelmaan. Ota yhteyttä käyttäjään toista luotettua kanavaa pitkin selvittääksesi, johtuuko avaimen vaihtuminen tehdasasetusten palautuksesta tai muusta tarkoituksellisesta toimenpiteestä. Julkisen avaimen salaus - Yksityisviesteissä käytetään uutta julkisen avaimen infrastruktuuria salaukseen. Ympäristöarvot Ympäristö @@ -988,7 +987,6 @@ Anna salasana poistaaksesi lukituksen käytöstä. Laite purkaa tallennustilan salauksen ja käynnistyy uudelleen. Poista lukitus Ymmärrän - Huom: lukitustilan käyttöönotto lukitsee debug-portin (SWD) tuetuilla laitteilla. Voit poistaa lukituksen milloin tahansa salasanallasi ja laitteen täydellinen tyhjennys palauttaa kaiken tarvittaessa. Syötä salasana Piilota Tuntia vanhenemiseen saakka @@ -1686,7 +1684,6 @@ Varmennettu lähettäjän avaimella. Allekirjoitettu radio Varmennettu automaattisesti - Tämä radio allekirjoittaa lähetyksensä XEdDSA:lla. Tämän radion lähetykset, jotka näet, radio varmentaa sen identiteettiavaimella. Allekirjoitettu · varmennettu Valitse Valitse kaikki @@ -1714,7 +1711,6 @@ asetukset Jaa - Jaa kanavien QR-koodi Jaa yhdistetty radio Jaa yhteystieto Jaa sijainti @@ -2063,7 +2059,6 @@ Kirjoita NFC-tunnisteeseen Tunnisteeseen kirjoittaminen epäonnistui. Käytä tyhjää, kirjoitettavaa NFC-tunnistetta ja pidä sitä paikallaan. - Kirjoittaminen tallentaa tämän jakolinkin fyysiselle NFC-tunnisteelle. Kuka tahansa voi avata linkin napauttamalla tunnistetta puhelimellaan. Kirjoitettu tunnisteeseen Pidä kirjoitettavaa NFC-tunnistetta puhelimesi takapuolta vasten. Sinä diff --git a/core/resources/src/commonMain/composeResources/values-fr/strings.xml b/core/resources/src/commonMain/composeResources/values-fr/strings.xml index 8853a976dc..4433aba117 100644 --- a/core/resources/src/commonMain/composeResources/values-fr/strings.xml +++ b/core/resources/src/commonMain/composeResources/values-fr/strings.xml @@ -548,7 +548,6 @@ Non-concordance de clé publique La clé publique ne correspond pas à la clé enregistrée. Vous pouvez supprimer le nœud et le laisser à nouveau échanger les clés, mais cela peut indiquer un problème de sécurité plus grave. Contactez l'utilisateur à travers un autre canal de confiance, pour déterminer si le changement de clé est dû à une réinitialisation d'usine ou à une autre action intentionnelle. Chiffrement de clé publique - Les messages directs utilisent la nouvelle infrastructure de clé publique pour le chiffrement. Métriques d'environnement Environnement @@ -844,7 +843,6 @@ Entrez votre phrase de passe pour désactiver le verrouillage. L'appareil déchiffrera son stockage et redémarrera. Activer le verrouillage Je comprends - Attention : l'activation du verrouillage bloque le port de débogage (SWD) sur le matériel pris en charge. Vous pouvez désactiver le verrouillage à tout moment avec votre phrase de passe. Une restauration d'usine effacera l'entièreté de l'appareil si besoin. Entrez la phrase de passe Masquer Heures jusqu'à expiration @@ -1372,7 +1370,6 @@ paramètres Partager - Partager le code QR des canaux Partager le contact Localisation partagée Partager vers… diff --git a/core/resources/src/commonMain/composeResources/values-hu/strings.xml b/core/resources/src/commonMain/composeResources/values-hu/strings.xml index cfec14e5cb..d6eadf9a43 100644 --- a/core/resources/src/commonMain/composeResources/values-hu/strings.xml +++ b/core/resources/src/commonMain/composeResources/values-hu/strings.xml @@ -289,7 +289,6 @@ Titkosítás engedélyezve Publikus kulcs nem egyezik Publikus Kulcs Titkosítás - A közvetlen üzenetek az új nyilvános kulcsú infrastruktúrát használják titkosításhoz. Környezeti metrikák Környezet diff --git a/core/resources/src/commonMain/composeResources/values-it/strings.xml b/core/resources/src/commonMain/composeResources/values-it/strings.xml index bb268ebbf9..324204f4c3 100644 --- a/core/resources/src/commonMain/composeResources/values-it/strings.xml +++ b/core/resources/src/commonMain/composeResources/values-it/strings.xml @@ -570,7 +570,6 @@ Chiave pubblica errata La chiave pubblica non corrisponde alla chiave salvata. È possibile rimuovere il nodo e lasciarlo scambiare le chiavi nuovamente, ma questo può indicare un problema di sicurezza più serio. Contattare l'utente attraverso un altro canale attendibile, per determinare se il cambiamento di chiave è dovuto a un ripristino di fabbrica o ad altre azioni intenzionali. Crittografia a Chiave Pubblica - I messaggi diretti stanno usando la crittografia basata sulla nuova infrastruttura a chiave pubblica. Metriche Ambientali Ambiente @@ -892,7 +891,6 @@ Inserisci la passphrase per disattivare il lockdown. Il dispositivo decifrerà la sua memoria e si riavvierà. Attiva il lockdown Ho capito - Attenzione: attivando il lockdown viene bloccata la porta di debug (SWD) sull'hardware supportato. Puoi disattivare il lockdown in qualsiasi momento con la tua passphrase, e una cancellazione completa del dispositivo ripristina tutto se necessario. Inserisci Passphrase Nascondi Ore alla scadenza @@ -1494,7 +1492,6 @@ Verificato con la chiave del mittente. Nodo firmato Verificato automaticamente - Questo nodo firma le sue trasmissioni con XEdDSA. Le trasmissioni che ricevi da esso vengono verificate dalla radio tramite la sua chiave d'identità. Firmato · verificato Seleziona Seleziona tutti @@ -1522,7 +1519,6 @@ impostazioni Condividi - Condividi QR code dei canali Condividi il nodo connesso Condividi contatto Condividi posizione @@ -1821,7 +1817,6 @@ Scrivi su tag NFC Impossibile scrivere sul tag. Usa un tag NFC scrivibile e vuoto e tienilo fermo. - La scrittura salva questo link di condivisione su un tag NFC fisico. Chiunque può toccare il tag con il telefono per aprirlo. Scritto sul tag Tieni un tag NFC scrivibile contro il retro del telefono. Tu diff --git a/core/resources/src/commonMain/composeResources/values-ja/strings.xml b/core/resources/src/commonMain/composeResources/values-ja/strings.xml index dd0c92a177..264ade2b58 100644 --- a/core/resources/src/commonMain/composeResources/values-ja/strings.xml +++ b/core/resources/src/commonMain/composeResources/values-ja/strings.xml @@ -564,7 +564,6 @@ 公開鍵が一致しません 公開鍵が記録されている鍵と一致しません。ノードを削除して鍵を再交換させることもできますが、これはより深刻なセキュリティ上の問題を示している可能性があります。鍵の変更が工場出荷時リセットやその他の意図的な操作によるものかどうかを確認するため、別の信頼できる手段でユーザーに連絡してください。 公開鍵暗号化 - ダイレクトメッセージは、暗号化に新しい公開鍵基盤を使用しています。 環境メトリクス 環境 @@ -886,7 +885,6 @@ パスフレーズを入力してロックダウンを解除します。デバイスはストレージを復号して再起動します。 ロックダウンを有効化 理解しました - 注意:ロックダウンを有効にすると、対応ハードウェアではデバッグポート(SWD)がロックされます。パスフレーズがあればいつでもロックダウンを解除でき、必要に応じてデバイスを完全に消去すればすべて元に戻せます。 パスフレーズを入力 非表示 有効期限までの時間 @@ -1486,7 +1484,6 @@ 送信者の鍵で検証済み。 署名済みノード 自動的に検証済み - このノードは XEdDSA でブロードキャストに署名します。表示されるブロードキャストは、無線機がそのアイデンティティ鍵を使って検証します。 署名済み・検証済み 選択 すべてを選択 @@ -1514,7 +1511,6 @@ 設定 シェア - チャンネルのQRコードを共有 接続中のノードを共有 連絡先を共有 位置情報を共有 @@ -1812,7 +1808,6 @@ NFC タグに書き込む タグに書き込めませんでした。書き込み可能な空の NFC タグを使い、動かさずに近づけてください。 - 書き込むと、この共有リンクが物理的な NFC タグに保存されます。誰でもスマートフォンでタグにタッチすると開けます。 タグに書き込みました 書き込み可能な NFC タグをスマートフォンの背面に近づけてください。 あなた diff --git a/core/resources/src/commonMain/composeResources/values-pl/strings.xml b/core/resources/src/commonMain/composeResources/values-pl/strings.xml index 2dc827e6ff..b4d21584f1 100644 --- a/core/resources/src/commonMain/composeResources/values-pl/strings.xml +++ b/core/resources/src/commonMain/composeResources/values-pl/strings.xml @@ -264,7 +264,6 @@ Niezgodność klucza publicznego Klucz publiczny nie pasuje do zapisanego klucza. Możesz usunąć węzeł i pozwolić mu na ponowną wymianę kluczy, ale może to oznaczać poważniejszy problem z bezpieczeństwem. Skontaktuj się z użytkownikiem przez inny zaufany kanał, żeby sprawdzić, czy zmiana klucza była spowodowana przywróceniem ustawień fabrycznych lub innym celowym działaniem. Szyfrowanie klucza publicznego - Bezpośrednie wiadomości wykorzystują nową infrastrukturę klucza publicznego do szyfrowania. Metryki środowiskowe Środowisko diff --git a/core/resources/src/commonMain/composeResources/values-ro/strings.xml b/core/resources/src/commonMain/composeResources/values-ro/strings.xml index 46170175e0..e4bb88477f 100644 --- a/core/resources/src/commonMain/composeResources/values-ro/strings.xml +++ b/core/resources/src/commonMain/composeResources/values-ro/strings.xml @@ -338,7 +338,6 @@ Nepotrivire cheie publică Cheia publică nu corespunde cu cheia înregistrată. Puteți elimina nodul și permiteți schimbul de chei din nou, dar acest lucru poate indica o problemă de securitate mai gravă. Contactați utilizatorul printr-un alt canal de încredere, pentru a determina dacă schimbarea cheii s-a datorat unei resetări la setările din fabrică sau unei alte acțiuni intenționate. Criptare cu cheie publică - Mesajele directe utilizează noua infrastructură cu cheie publică pentru criptare. Indicatori de mediu Mediu @@ -1023,7 +1022,6 @@ setari Distribuie - Distribuie codul QR al canalelor Împărtășește contacte Partajați locația Distribuire către… diff --git a/core/resources/src/commonMain/composeResources/values-ru/strings.xml b/core/resources/src/commonMain/composeResources/values-ru/strings.xml index de61b3272f..17c903f381 100644 --- a/core/resources/src/commonMain/composeResources/values-ru/strings.xml +++ b/core/resources/src/commonMain/composeResources/values-ru/strings.xml @@ -125,6 +125,7 @@ Вредный для чувствительных групп %1$d (%2$s) Очень вредный + Архивный канал Вы уверены? Вы уверены, что хотите перейти на канал по умолчанию? Звук @@ -433,6 +434,7 @@ Максимальное количество баз данных для этого устройства GPS устройства Ссылки на устройства + Ссылки на продукты могут быть партнерскими — покупки могут приносить Meshtastic комиссию. Я хочу один Открыть в браузере %1$s: %2$s @@ -566,6 +568,9 @@ translate,crowdin,localization,language,i18n,contribute units,locale,metric,imperial,temperature,distance widget,home screen,local stats,glance,battery,utilization + Загрузка документации… + Документация недоступна + Ничего не найдено Поиск по документации… Руководство разработчика Руководство пользователя @@ -626,7 +631,7 @@ Несоответствие публичного ключа Открытый ключ не соответствует записанному ключу. Вы можете удалить ноду и позволить ей снова обменяться ключами, но это может указывать на серьезную проблему с безопасностью. Свяжитесь с пользователем по другому надежному каналу чтобы определить, произошла ли смена ключа в результате сброса настроек или другого преднамеренного действия. Общий ключ шифрования - Прямые сообщения используют новую инфраструктуру открытого ключа для шифрования. + Для этой ноды есть открытый ключ, поэтому взаимодействие с ним происходит с использованием шифрования с открытым ключом. Метрики окружения Окружающая среда @@ -984,6 +989,7 @@ Доступ к местоположению отключен, и Android больше не будет спрашивать. Включи его в настройках приложения, когда захочешь снова пользоваться картой и делиться местоположением. Разрешение на определение местоположения выключено. Включи его в настройках приложения, чтобы поделиться своей позицией. Без доступа к местоположению карта не покажет где ты находишься, и Meshtastic не сможет делиться твоей позицией в сети. Всё остальное будет работать, и ты сможешь включить это позже. + Meshtastic использует твоё местоположение, чтобы показать тебя на карте, измерять расстояние до нод и — только если ты включишь эту функцию — делиться своим местоположением с сетью. Отказ от этого ничего другого не ломает. Поделиться местоположением Попробуй снова через %1$d секунд. @@ -993,7 +999,6 @@ Введи пароль для отключения блокировки. Устройство расшифрует хранилище и перезагрузится. Включить блокировку Понимаю - Внимание: включение режима блокировки закрывает отладочный порт (SWD) на поддерживаемом оборудовании. Ты можешь отключить блокировку в любое время с помощью пароля, а полное стирание устройства восстанавливает всё при необходимости. Введи парольную фразу Скрыть Часов до истечения @@ -1061,6 +1066,7 @@ Загрузка Пауза Карта недоступна на этом устройстве + Эта сборка приложения не включает рендер карты для процессора твоего телефона. Всё остальное работает нормально. Фильтр карты\n Всё Дисплей @@ -1072,12 +1078,21 @@ Прозрачность: %1$d% %1$s<br>Последняя активность: %2$s<br>Последняя позиция: %3$s<br>Заряд: %4$s Оффлайн — отображение кэшированных данных + Сейчас нет данных офлайн-карты — попробуй позже + Ошибка загрузки — проверь соединение и попробуй снова + Слишком много офлайн-районов уже загружено — сначала удали один + Загруженные регионы занимают слишком много памяти — сначала удали один + Выбранная область слишком большая — увеличь масштаб или выбери меньший участок Скачать ландшафт Загрузка… %1$d / %2$d плиток Оффлайн менеджер %1$s, %2$s %3$s Показать на карте Контуры + Загрузка местности завершена + Загрузка местности не удалась — проверь подключение и попробуй снова + Выбранная область слишком большая для местности — увеличь масштаб или выбери меньший участок + Загрузка местности… %1$d / %2$d плиток Тень на склоне Осадки Метеорадары @@ -1111,15 +1126,19 @@ Периодически рекламировать эту сеть на близлежащие ноды Канал %1$d Каналы маяка + Период трансляции + Выбери интервал как минимум на один час Приглашения в сеть Слушать маяки Захват приглашений, рекламируемых ближайшими сетями Сообщение маяка Максимум %1$d байт + Нет доступных каналов Ближайшая сеть пригласила тебя присоединиться Приглашение в сеть Добавить канал Канал: %1$s + Предложенный канал Обнаруживать Отменить С неизвестной ноды @@ -1256,6 +1275,7 @@ Имя не может быть пустым. Вернуться Перейти в + Разрешение ближайших устройств Нужно оборудование? Meshtastic требует совместимое устройство. Наши спонсоры и партнёры предлагают готовое к использованию оборудование. Вот некоторые из самых популярных вариантов тут. Информация об окружении @@ -1281,6 +1301,7 @@ Убедитесь, что вы подключены к той же сети, что и устройство. Сетевые устройства не обнаружены Метрики прохожих недоступны + Нет открытого ключа Устройства USB не найдены Подключи устройство с помощью USB-кабеля для передачи данных, чтобы использовать последовательное соединение. USB-устройства не обнаружены @@ -1365,6 +1386,7 @@ NTP-сервер Количество записей + Офлайн-карты ОК в MQTT Лады Тип OLED @@ -1695,11 +1717,15 @@ Небезопасный канал, точное местоположение Безопасный Предупреждение: Небезопасно, точное местоположение; Uplink MQTT + Показывается на любой версии прошивки. + Показывается для нод с прошивкой до версии 2.8 или тех, которые не указывают версию. + Показывается для нод с прошивкой 2.8 или новее, а также для любой ноды, чью подпись радио подтвердило. Проверено с помощью ключа отправителя. Подписанная нода Автоматически проверено - Эта нода подписывает свои трансляции с помощью XEdDSA. Трансляции, которые ты видишь от неё, проверяются радио с использованием его ключа идентификации. + Радио проверило подписанные трансляции этой ноды с помощью её ключа идентификации, так что его личность постоянна, но ты сам её не проверял. Каждая нода с прошивкой 2.8 или выше подписывает свои трансляции. Подписано · проверено + Подтверждённый контакт Выбрать Выбрать все Выбрано @@ -1726,11 +1752,13 @@ настройки Поделиться - Поделиться QR-кодом каналов + Поделиться ссылкой + Поделиться каналами Поделиться подключенной нодой Отправить контакт Поделиться геопозицией Используй GPS твоего телефона для отправки координат на твою ноду вместо аппаратного GPS на самой ноде. + Отсканируй этот код с помощью приложения Meshtastic на другом устройстве, чтобы добавить его туда. Поделиться… Делитесь своим местоположением в режиме реального времени и поддерживайте работу группы с функциями GPS. У этого контакта нет публичного ключа, поэтому его нельзя импортировать. Контакт без ключа стирает любой ключ, который твоя радиостанция уже хранит для этой ноды. Попроси отправителя снова поделиться им, когда его нода обменяется ключами. @@ -2081,7 +2109,7 @@ Записать в NFC-метку Не удалось записать в метку. Используй пустую NFC-метку, которую можно записывать, и держи её неподвижно. - Запись сохраняет эту ссылку для общего доступа на физический NFC-тег. Любой может коснуться тега своим телефоном, чтобы открыть его. + Запись сохраняет эту ссылку на NFC-метку, к которой может прикоснуться любой. Написано для метки Поднеси записываемую NFC-метку к задней части своего телефона. Вы diff --git a/core/resources/src/commonMain/composeResources/values-sv/strings.xml b/core/resources/src/commonMain/composeResources/values-sv/strings.xml index c2c2e64a83..4014b5f128 100644 --- a/core/resources/src/commonMain/composeResources/values-sv/strings.xml +++ b/core/resources/src/commonMain/composeResources/values-sv/strings.xml @@ -423,7 +423,6 @@ Kryptering aktiverad Publik nyckel matchar inte Kryptering med Publik nyckel - Direktmeddelanden använder den nya publika nyckelinfrastrukturen (PKI) för kryptering. Miljövärden Miljö @@ -1170,7 +1169,6 @@ inställningar Dela - Dela kanalens QR-kod Dela ansluten nod Dela kontakt Dela plats diff --git a/core/resources/src/commonMain/composeResources/values-uk/strings.xml b/core/resources/src/commonMain/composeResources/values-uk/strings.xml index 6efe1b7c62..33bedee7bf 100644 --- a/core/resources/src/commonMain/composeResources/values-uk/strings.xml +++ b/core/resources/src/commonMain/composeResources/values-uk/strings.xml @@ -392,7 +392,6 @@ Не збігаються відкритий ключ Публічний ключ не збігається із записаним ключем. Ви можете видалити вузол і дозволити йому обмінятися ключами знову, але це може свідчити про проблему з безпекою. Зв’яжіться з користувачем через інший довірений канал, щоб з’ясувати, чи була зміна ключа наслідком скидання до заводських налаштувань або іншої навмисної дії. Шифрування з відкритим ключем - Для шифрування особистих повідомлень використовується нова система публічних ключів. Показники довкілля Середовище diff --git a/core/resources/src/commonMain/composeResources/values-zh-rCN/strings.xml b/core/resources/src/commonMain/composeResources/values-zh-rCN/strings.xml index 0b6a39f5b7..641bb8e71b 100644 --- a/core/resources/src/commonMain/composeResources/values-zh-rCN/strings.xml +++ b/core/resources/src/commonMain/composeResources/values-zh-rCN/strings.xml @@ -571,7 +571,6 @@ 公钥不匹配 公钥与输入的密钥不匹配。 您可以移除该节点并让它再次交换密钥,但这可能会出现密钥泄露问题。 请通过另一个受信任的频道来联系用户,以确定密钥更改是否由于出厂重置或其他故意操作。 公钥加密 - 私信消息正在使用新的公用钥匙加密。 传感器指标 环境 @@ -901,7 +900,6 @@ 请输入您的密码短语以关闭锁定状态.设备将解密其存储空间并重启。 启用锁定功能模式 我明白了 - 高能预警:在支持的硬件上启用锁定模式将会锁死调试端口 (SWD) 你可以随时通过输入密码短语来关闭锁定模式如果情况紧急执行一次全设备擦除也能恢复一切。 输入密码短语 隐藏 距离失效还有多少小时 @@ -1502,7 +1500,6 @@ 已通过发送方的密钥进行验证。 已签名节点 自动验证通过 - 该节点使用 XEdDSA 算法对其广播进行签名。你看到的来自该节点的广播,均通过其身份密钥由无线电进行了验证。 已签名 · 已验证 选择 选择全部 @@ -1530,7 +1527,6 @@ 设置 分享 - 共享频道二维码 分享已连接的节点 分享联系人 分享位置 @@ -1833,7 +1829,6 @@ 写入 NFC 标签 无法写入标签请使用一张可写入的空白 NFC 标签,并保持手机与标签贴合稳定。 - 写入后,此共享链接将保存到实体 NFC 标签中。任何人都可以使用手机轻触该标签以打开链接。 数据已成功写入标签 请将一张可写入的 NFC 标签贴在手机背面。 你 diff --git a/core/resources/src/commonMain/composeResources/values-zh-rTW/strings.xml b/core/resources/src/commonMain/composeResources/values-zh-rTW/strings.xml index c64f903c9c..7b76fe5c88 100644 --- a/core/resources/src/commonMain/composeResources/values-zh-rTW/strings.xml +++ b/core/resources/src/commonMain/composeResources/values-zh-rTW/strings.xml @@ -428,7 +428,6 @@ 公鑰不相符 公開金鑰與先前記錄不符。您可以移除此節點並重新進行金鑰交換,但這可能代表有更嚴重的安全性問題。建議透過其他可靠的通訊方式聯繫該使用者,確認金鑰改變是否為重設裝置或其他有意的操作。 加密公鑰 - 私訊功能已改用新的公開金鑰基礎建設加密。 環境計量資料 環境 @@ -1193,7 +1192,6 @@ 設定 分享 - 分享頻道 QR Code 分享聯絡人 分享位置 分享至… diff --git a/core/resources/src/commonMain/composeResources/values/strings.xml b/core/resources/src/commonMain/composeResources/values/strings.xml index ee9ad87931..ed20ff928d 100644 --- a/core/resources/src/commonMain/composeResources/values/strings.xml +++ b/core/resources/src/commonMain/composeResources/values/strings.xml @@ -90,6 +90,7 @@ Always point north Ambient Lighting Ambient Lighting Config + Ammonium Analytics are collected to help us improve the Android app (thank you), we will receive anonymized information about user behavior. This includes crash reports, screens used in the app, etc. Allow analytics and crash reporting. Analytics platforms: @@ -125,6 +126,8 @@ Unhealthy for Sensitive Groups %1$d (%2$s) Very Unhealthy + Archived channel + This channel is no longer set up on your radio. Its messages are kept here, but you can't send or react. Are you sure? Are you sure you want to change to the default channel? Audio @@ -144,6 +147,7 @@ Baro Battery Battery INA_2XX I2C address + BOD Bluetooth Devices BLE RSSI threshold (defaults to -80) Bluetooth scanning also needs location services switched on for this version of Android. Your position is not used. @@ -220,6 +224,7 @@ Channel URL ChUtil Channels + COD Chirpy Chirpy Assistant @@ -482,6 +487,7 @@ Session complete Scan failed: %1$s Scan History + Scanning LoRa presets… Session incomplete Scan Progress Scan Summary @@ -533,6 +539,7 @@ Display mode When enabled, the device will display the time in 12-hour format on screen. Display units + Dissolved O₂ Distance Distance Filters @@ -564,6 +571,9 @@ translate,crowdin,localization,language,i18n,contribute units,locale,metric,imperial,temperature,distance widget,home screen,local stats,glance,battery,utilization + Loading documentation… + No documentation available + No results found Search documentation… Developer Guide User Guide @@ -604,6 +614,7 @@ Edit Edit Network Tile Source 8 Hours + Conductivity MSL Activities @@ -625,7 +636,7 @@ Public key mismatch The public key does not match the recorded key. You may remove the node and let it exchange keys again, but this may indicate a more security problem. Contact the user through another trusted channel, to determine if the key change was due to a factory reset or other intentional action. Public Key Encryption - Direct messages are using the new public key infrastructure for encryption. + A public key is on file for this node, so interactions with it use public key encryption. Environment Metrics Environment @@ -737,6 +748,7 @@ Flashing device, please wait... Hang tight, we are working on it... Firmware hash rejected. Device may require hash provisioning or bootloader update. + Updating firmware… The selected file "%1$s" does not match %2$s (%3$s). Choose the firmware file for this target. Keep your device close to your phone. Update To: %1$s @@ -945,6 +957,8 @@ LED state Legacy Admin channel %1$d libraries + License + Free software under the GNU General Public License v3, with no warranty. You may redistribute it under the same license. Licensed amateur radio (Ham) Enabling this option disables encryption and is not compatible with the default Meshtastic network. @@ -952,6 +966,8 @@ Enable licensed (Ham) mode? Licensed mode removes channel encryption keys and disables the admin channel, so all traffic is sent as plaintext that anyone can read — and this firmware cannot sign it, so other nodes cannot verify it came from you. This is not compatible with the default Meshtastic network, and you remain responsible for meeting your amateur radio license requirements and local regulations. Licensed mode removes channel encryption keys and disables the admin channel, so all traffic is sent as plaintext that anyone can read — but it is digitally signed, letting other nodes verify it came from you. Your node number may change once to match your identity key, so favorites, message history, remote admin, and other nodes may initially treat it as new; you remain responsible for meeting your amateur radio license requirements and local regulations. + Storm Distance + Lightning (1h) Load Load 15m @@ -997,7 +1013,7 @@ Enter your passphrase to turn off lockdown. The device will decrypt its storage and reboot. Enable lockdown I understand - Heads up: enabling lockdown locks the debug port (SWD) on supported hardware. You can turn lockdown off any time with your passphrase, and a full device erase restores everything if needed. + Enabling lockdown locks the debug port (SWD) on hardware where the lockout takes effect. Turning lockdown off later decrypts your data, but the port stays locked. Reopening it takes a full chip erase with a debug probe, which destroys everything on the device. Enter Passphrase Hide Hours until expiry @@ -1306,6 +1322,8 @@ New Nodes Next NFC is disabled. Please enable it in system settings. + Nitrate + Nitrogen Ensure you're within range of the device. No Bluetooth devices seen @@ -1320,7 +1338,7 @@ No network devices seen No PAX metrics available. No public key - This node has not shared a public key, so direct messages to it cannot be sent. Request User Info to ask for one. + This node has not shared a public key, so encrypted interactions with it are not possible. Request User Info to ask for one. No USB devices found Connect a device with a USB data cable to use serial. No USB devices detected @@ -1335,7 +1353,9 @@ You are viewing ignored nodes,\nPress to return to the node list. Include unknown Only show direct nodes + Encrypted only Hide offline nodes + Signed only Filter Only show ignored Nodes Filter by @@ -1368,7 +1388,7 @@ Node Details Node List Help Node options - Not heard since you changed settings + Not heard on your current LoRa settings Node Number Couldn't send request. Try again. Restarting… @@ -1391,8 +1411,8 @@ Nearby nodes will appear here as they're discovered. Searching for nodes %1$d nodes queued for deletion: - %1$d nodes not heard since you changed settings - 1 node not heard since you changed settings + %1$d nodes not heard on your current LoRa settings + 1 node not heard on your current LoRa settings Keep Remove Noise Floor @@ -1445,6 +1465,7 @@ Open Wi-Fi settings Options Orient north + ORP Output buzzer (GPIO) Output duration (milliseconds) @@ -1510,6 +1531,7 @@ Show permissions Phone Location Meshtastic uses your phone's location to enable a number of features. You can update your location permissions at any time from settings. + Phosphorus Pin Play @@ -1524,6 +1546,16 @@ %1$d second %1$d seconds + + Sensor Status + CO₂ error + Fan error + Fan speed warning + Gas sensor error + HCHO error + PM error + RH&T error + Unknown flags 0x%1$s PM1.0 PM10 PM2.5 @@ -1534,6 +1566,7 @@ Position Flags Position Position Packet + Potassium Power Power Config @@ -1708,6 +1741,7 @@ RSSI Received Signal Strength Indicator, a measurement used to determine the power level being received by the antenna. A higher RSSI value generally indicates a stronger and more stable connection. rsyslog server + Salinity hey I found the cache, it is over here next to the big tiger. I'm kinda scared. Sats @@ -1752,11 +1786,16 @@ Insecure Channel, Precise Location Secure Warning: Insecure, Precise Location & MQTT Uplink + Shown at any firmware version. + Shown for nodes on firmware before 2.8, or that report no version. + Shown for nodes on firmware 2.8 or later, and for any node whose signature the radio has verified. Verified with the sender's key. Signed node Verified automatically - This node signs its broadcasts with XEdDSA. Broadcasts you see from it are verified by the radio using its identity key. + The radio verified this node's signed broadcasts using its identity key, so its identity is consistent over time, but you have not verified it in person. Every node on firmware 2.8 or later signs its broadcasts. Signed · verified + Verified contact + You verified this node's key in person, by exchanging contact QR codes, so its identity is confirmed. The strongest trust the list shows, and shown at any firmware version. Select Select all Selected @@ -1783,11 +1822,18 @@ settings Share - Share Channels QR Code + Share link + While this is open, another phone can also take it by tapping yours. + Share Channels + Adding keeps the channels they already have and appends yours. + Replacing overwrites every channel on their radio with yours. + Meshtastic channels Share Connected Node Share Contact + Meshtastic contact Share Location Use your phone GPS to send locations to your node instead of using a hardware GPS on your node. + Scan this code with another device's Meshtastic app to add it there. Share to… Share your location in real-time and keep your group coordinated with integrated GPS features. This contact has no public key, so it cannot be imported. A contact without one clears any key your radio already holds for that node. Ask the sender to share it again once their node has exchanged keys. @@ -1845,7 +1891,10 @@ SNR Signal-to-Noise Ratio, a measure used in communications to quantify the level of a desired signal to the level of background noise. In Meshtastic and other wireless systems, a higher SNR indicates a clearer signal that can enhance the reliability and quality of data transmission. Soil Moist + Soil pH Soil Temp + Soil & Water + Solar Irradiance Speed %1$d Km/h %1$d mph @@ -2006,6 +2055,7 @@ UDP USB Triple Click Ad Hoc Ping + Turbidity 24H 48 Hours 2W @@ -2077,6 +2127,7 @@ Wait for Bluetooth duration Wake on tap or motion Warning + Water pH Delete waypoint? Edit waypoint New waypoint @@ -2137,7 +2188,7 @@ Write to NFC tag Couldn't write to tag. Use a writable, empty NFC tag and hold it steady. - Writing saves this share link to a physical NFC tag. Anyone can tap the tag with their phone to open it. + Writing saves this link to an NFC tag anyone can tap. Written to tag Hold a writable NFC tag against the back of your phone. You diff --git a/core/service/src/androidHostTest/kotlin/org/meshtastic/core/service/AndroidNotificationManagerTest.kt b/core/service/src/androidHostTest/kotlin/org/meshtastic/core/service/AndroidNotificationManagerTest.kt index 852d2b6d28..e221b90897 100644 --- a/core/service/src/androidHostTest/kotlin/org/meshtastic/core/service/AndroidNotificationManagerTest.kt +++ b/core/service/src/androidHostTest/kotlin/org/meshtastic/core/service/AndroidNotificationManagerTest.kt @@ -199,7 +199,13 @@ class AndroidNotificationManagerTest { @Test fun `client notification identity cancels the notification posted with that identity`() = runTest { val manager = AndroidNotificationManager(context) - val clientNotification = ClientNotification(message = "Protected position advisory", reply_id = 123) + val clientNotification = + ClientNotification.Builder() + .also { wb -> + wb.message = "Protected position advisory" + wb.reply_id = 123 + } + .build() val id = clientNotification.notificationId() manager.dispatch( @@ -229,16 +235,28 @@ class AndroidNotificationManagerTest { val advisory = protectedPositionAdvisory(replyId = 123, time = 1_000) val nearMisses = listOf( - advisory.copy(message = "Location sharing is disabled"), - advisory.copy(level = LogRecord.Level.INFO), - advisory.copy(reply_id = 0), - advisory.copy(reply_id = null), - advisory.copy(key_verification_number_inform = KeyVerificationNumberInform()), - advisory.copy(key_verification_number_request = KeyVerificationNumberRequest()), - advisory.copy(key_verification_final = KeyVerificationFinal()), - advisory.copy(duplicated_public_key = DuplicatedPublicKey()), - advisory.copy(low_entropy_key = LowEntropyKey()), - advisory.copy(message = "Rebooting to WiFi OTA"), + advisory.newBuilder().also { wb -> wb.message = "Location sharing is disabled" }.build(), + advisory.newBuilder().also { wb -> wb.level = LogRecord.Level.INFO }.build(), + advisory.newBuilder().also { wb -> wb.reply_id = 0 }.build(), + advisory.newBuilder().also { wb -> wb.reply_id = null }.build(), + advisory + .newBuilder() + .also { wb -> wb.key_verification_number_inform = KeyVerificationNumberInform.Builder().build() } + .build(), + advisory + .newBuilder() + .also { wb -> wb.key_verification_number_request = KeyVerificationNumberRequest.Builder().build() } + .build(), + advisory + .newBuilder() + .also { wb -> wb.key_verification_final = KeyVerificationFinal.Builder().build() } + .build(), + advisory + .newBuilder() + .also { wb -> wb.duplicated_public_key = DuplicatedPublicKey.Builder().build() } + .build(), + advisory.newBuilder().also { wb -> wb.low_entropy_key = LowEntropyKey.Builder().build() }.build(), + advisory.newBuilder().also { wb -> wb.message = "Rebooting to WiFi OTA" }.build(), ) nearMisses.forEach { notification -> @@ -275,8 +293,22 @@ class AndroidNotificationManagerTest { val manager = AndroidNotificationManager(context) // A near-miss of the protected-position predicate (message differs slightly), so it takes the plain // dispatch path — but reply_id/time still change on every firmware reply, exactly like the real advisory. - val first = ClientNotification(message = "Location sharing is disabled", reply_id = 100, time = 1_000) - val second = ClientNotification(message = "Location sharing is disabled", reply_id = 200, time = 2_000) + val first = + ClientNotification.Builder() + .also { wb -> + wb.message = "Location sharing is disabled" + wb.reply_id = 100 + wb.time = 1_000 + } + .build() + val second = + ClientNotification.Builder() + .also { wb -> + wb.message = "Location sharing is disabled" + wb.reply_id = 200 + wb.time = 2_000 + } + .build() listOf(first, second).forEach { cn -> manager.dispatchClientNotification( @@ -296,7 +328,13 @@ class AndroidNotificationManagerTest { @Test fun `generic client notification does not enable only-alert-once`() = runTest { val manager = AndroidNotificationManager(context) - val clientNotification = ClientNotification(message = "Generic warning", reply_id = 123) + val clientNotification = + ClientNotification.Builder() + .also { wb -> + wb.message = "Generic warning" + wb.reply_id = 123 + } + .build() manager.dispatchClientNotification( Notification( @@ -365,10 +403,12 @@ class AndroidNotificationManagerTest { channelIds.forEach { channelId -> systemNotificationManager.deleteNotificationChannel(channelId) } } - private fun protectedPositionAdvisory(replyId: Int, time: Int) = ClientNotification( - message = "Location sharing is disabled on this channel", - reply_id = replyId, - time = time, - level = LogRecord.Level.WARNING, - ) + private fun protectedPositionAdvisory(replyId: Int, time: Int) = ClientNotification.Builder() + .also { wb -> + wb.message = "Location sharing is disabled on this channel" + wb.reply_id = replyId + wb.time = time + wb.level = LogRecord.Level.WARNING + } + .build() } diff --git a/core/service/src/androidHostTest/kotlin/org/meshtastic/core/service/ConversationShortcutPublisherTest.kt b/core/service/src/androidHostTest/kotlin/org/meshtastic/core/service/ConversationShortcutPublisherTest.kt index 3a54da384e..c29a1b9dcc 100644 --- a/core/service/src/androidHostTest/kotlin/org/meshtastic/core/service/ConversationShortcutPublisherTest.kt +++ b/core/service/src/androidHostTest/kotlin/org/meshtastic/core/service/ConversationShortcutPublisherTest.kt @@ -59,7 +59,18 @@ class ConversationShortcutPublisherTest { private val context: Context = ApplicationProvider.getApplicationContext() private val shortcutManager = context.getSystemService(ShortcutManager::class.java)!! - private val hawk = Node(num = 7, user = User(id = "!00000007", long_name = "Hawk Ridge", short_name = "HAWK")) + private val hawk = + Node( + num = 7, + user = + User.Builder() + .also { wb -> + wb.id = "!00000007" + wb.long_name = "Hawk Ridge" + wb.short_name = "HAWK" + } + .build(), + ) private val packetRepository: PacketRepository = mock(MockMode.autofill) private val nodeRepository: NodeRepository = mock(MockMode.autofill) @@ -81,10 +92,16 @@ class ConversationShortcutPublisherTest { // Primary channel with an empty name (resolves to the preset name) plus one named secondary channel. every { radioConfigRepository.channelSetFlow } returns flowOf( - ChannelSet( - settings = listOf(MeshChannel.default.settings, ChannelSettings(name = "Beta")), - lora_config = MeshChannel.default.loraConfig, - ), + ChannelSet.Builder() + .also { wb -> + wb.settings = + listOf( + MeshChannel.default.settings, + ChannelSettings.Builder().also { wb -> wb.name = "Beta" }.build(), + ) + wb.lora_config = MeshChannel.default.loraConfig + } + .build(), ) } diff --git a/core/service/src/androidHostTest/kotlin/org/meshtastic/core/service/MeshNotificationManagerImplConversationTest.kt b/core/service/src/androidHostTest/kotlin/org/meshtastic/core/service/MeshNotificationManagerImplConversationTest.kt index b4da9cd397..3c388d43a9 100644 --- a/core/service/src/androidHostTest/kotlin/org/meshtastic/core/service/MeshNotificationManagerImplConversationTest.kt +++ b/core/service/src/androidHostTest/kotlin/org/meshtastic/core/service/MeshNotificationManagerImplConversationTest.kt @@ -36,6 +36,7 @@ import org.junit.Before import org.junit.Test import org.junit.runner.RunWith import org.meshtastic.core.common.di.asServiceScope +import org.meshtastic.core.common.state.RadioOperationLock import org.meshtastic.core.di.CoroutineDispatchers import org.meshtastic.core.model.ConnectionState import org.meshtastic.core.model.Message @@ -66,8 +67,30 @@ class MeshNotificationManagerImplConversationTest { private val context: Context = ApplicationProvider.getApplicationContext() private val systemNotificationManager = context.getSystemService(NotificationManager::class.java)!! - private val sender = Node(num = 7, user = User(id = "!00000007", long_name = "Hawk Ridge", short_name = "HAWK")) - private val me = Node(num = 42, user = User(id = "!0000002a", long_name = "Me Node", short_name = "ME")) + private val sender = + Node( + num = 7, + user = + User.Builder() + .also { wb -> + wb.id = "!00000007" + wb.long_name = "Hawk Ridge" + wb.short_name = "HAWK" + } + .build(), + ) + private val me = + Node( + num = 42, + user = + User.Builder() + .also { wb -> + wb.id = "!0000002a" + wb.long_name = "Me Node" + wb.short_name = "ME" + } + .build(), + ) private val packetRepository: PacketRepository = mock(MockMode.autofill) private val nodeRepository: NodeRepository = mock(MockMode.autofill) @@ -92,6 +115,7 @@ class MeshNotificationManagerImplConversationTest { ) }, radioConfigRepository = lazy { radioConfigRepository }, + radioOperationLock = RadioOperationLock(), scope = scope.asServiceScope(), ) @@ -101,15 +125,17 @@ class MeshNotificationManagerImplConversationTest { systemNotificationManager.cancelAll() every { nodeRepository.ourNodeInfo } returns MutableStateFlow(me) every { nodeRepository.myNodeInfo } returns MutableStateFlow(null) - every { nodeRepository.localStats } returns MutableStateFlow(org.meshtastic.proto.LocalStats()) + every { nodeRepository.localStats } returns MutableStateFlow(org.meshtastic.proto.LocalStats.Builder().build()) every { nodeRepository.nodeDBbyNum } returns MutableStateFlow(mapOf(7 to sender, 42 to me)) everySuspend { nodeRepository.getNode(any()) } returns sender every { radioConfigRepository.channelSetFlow } returns flowOf( - ChannelSet( - settings = listOf(MeshChannel.default.settings), - lora_config = MeshChannel.default.loraConfig, - ), + ChannelSet.Builder() + .also { wb -> + wb.settings = listOf(MeshChannel.default.settings) + wb.lora_config = MeshChannel.default.loraConfig + } + .build(), ) } diff --git a/core/service/src/androidHostTest/kotlin/org/meshtastic/core/service/MeshNotificationManagerImplTest.kt b/core/service/src/androidHostTest/kotlin/org/meshtastic/core/service/MeshNotificationManagerImplTest.kt index b7f24deaba..102147b91f 100644 --- a/core/service/src/androidHostTest/kotlin/org/meshtastic/core/service/MeshNotificationManagerImplTest.kt +++ b/core/service/src/androidHostTest/kotlin/org/meshtastic/core/service/MeshNotificationManagerImplTest.kt @@ -34,6 +34,7 @@ import org.junit.Before import org.junit.Test import org.junit.runner.RunWith import org.meshtastic.core.common.di.asServiceScope +import org.meshtastic.core.common.state.RadioOperationLock import org.meshtastic.core.model.ConnectionState import org.meshtastic.core.model.MyNodeInfo import org.meshtastic.core.repository.NodeRepository @@ -162,7 +163,14 @@ class MeshNotificationManagerImplTest { @Test fun `service state seeds local stats before the local node row is available`() = runWithRenderScope { renderScope -> - val stats = LocalStats(uptime_seconds = 1, num_online_nodes = 2, num_total_nodes = 3) + val stats = + LocalStats.Builder() + .also { wb -> + wb.uptime_seconds = 1 + wb.num_online_nodes = 2 + wb.num_total_nodes = 3 + } + .build() every { nodeRepository.localStats } returns MutableStateFlow(stats) val notifications = createManager(renderScope) notifications.initChannels() @@ -181,11 +189,22 @@ class MeshNotificationManagerImplTest { nodeRepository = lazy { nodeRepository }, conversationShortcutPublisher = lazy { error("Not used in this test") }, radioConfigRepository = lazy { error("Not used in this test") }, + radioOperationLock = RadioOperationLock(), scope = scope.asServiceScope(), ) - private fun populatedTelemetry() = - Telemetry(local_stats = LocalStats(uptime_seconds = 1, num_online_nodes = 1, num_total_nodes = 1)) + private fun populatedTelemetry() = Telemetry.Builder() + .also { wb -> + wb.local_stats = + LocalStats.Builder() + .also { wb -> + wb.uptime_seconds = 1 + wb.num_online_nodes = 1 + wb.num_total_nodes = 1 + } + .build() + } + .build() private fun activeServiceNotification() = systemNotificationManager.activeNotifications.singleOrNull { it.id == SERVICE_NOTIFY_ID } diff --git a/core/service/src/androidHostTest/kotlin/org/meshtastic/core/service/NotificationTitleResourceTest.kt b/core/service/src/androidHostTest/kotlin/org/meshtastic/core/service/NotificationTitleResourceTest.kt new file mode 100644 index 0000000000..d87b297070 --- /dev/null +++ b/core/service/src/androidHostTest/kotlin/org/meshtastic/core/service/NotificationTitleResourceTest.kt @@ -0,0 +1,66 @@ +/* + * 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 . + */ +package org.meshtastic.core.service + +import org.meshtastic.core.common.state.RadioOperation +import org.meshtastic.core.resources.Res +import org.meshtastic.core.resources.discovery_scan_in_progress +import org.meshtastic.core.resources.firmware_update_in_progress +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertNull + +class NotificationTitleResourceTest { + + @Test + fun noOperationLeavesTheConnectionStateTitleAlone() { + assertNull(emptySet().notificationTitleResource()) + } + + @Test + fun firmwareUpdateIsReported() { + assertEquals( + Res.string.firmware_update_in_progress, + setOf(RadioOperation.FirmwareUpdate).notificationTitleResource(), + ) + } + + @Test + fun firmwareMaintenanceIsReportedAsAFirmwareUpdate() { + assertEquals( + Res.string.firmware_update_in_progress, + setOf(RadioOperation.FirmwareMaintenance).notificationTitleResource(), + ) + } + + @Test + fun discoveryScanIsReported() { + assertEquals( + Res.string.discovery_scan_in_progress, + setOf(RadioOperation.DiscoveryScan).notificationTitleResource(), + ) + } + + @Test + fun firmwareWorkOutranksAScan() { + // The one the user must not interrupt wins the title. + assertEquals( + Res.string.firmware_update_in_progress, + setOf(RadioOperation.DiscoveryScan, RadioOperation.FirmwareUpdate).notificationTitleResource(), + ) + } +} diff --git a/core/service/src/androidMain/kotlin/org/meshtastic/core/service/AndroidMeshLocationManager.kt b/core/service/src/androidMain/kotlin/org/meshtastic/core/service/AndroidMeshLocationManager.kt index acf155dc2d..54d13f6e21 100644 --- a/core/service/src/androidMain/kotlin/org/meshtastic/core/service/AndroidMeshLocationManager.kt +++ b/core/service/src/androidMain/kotlin/org/meshtastic/core/service/AndroidMeshLocationManager.kt @@ -51,21 +51,23 @@ class AndroidMeshLocationManager(private val context: Application, private val l .getLocations() .onEach { location -> sendPositionFn( - ProtoPosition( - latitude_i = Position.degI(location.latitude), - longitude_i = Position.degI(location.longitude), - altitude = - if (LocationCompat.hasMslAltitude(location)) { - LocationCompat.getMslAltitudeMeters(location).toInt() - } else { - null - }, - altitude_hae = location.altitude.toInt(), - time = (location.time.milliseconds.inWholeSeconds).toInt(), - ground_speed = location.speed.toInt(), - ground_track = location.bearing.toInt(), - location_source = ProtoPosition.LocSource.LOC_EXTERNAL, - ), + ProtoPosition.Builder() + .also { wb -> + wb.latitude_i = Position.degI(location.latitude) + wb.longitude_i = Position.degI(location.longitude) + wb.altitude = + if (LocationCompat.hasMslAltitude(location)) { + LocationCompat.getMslAltitudeMeters(location).toInt() + } else { + null + } + wb.altitude_hae = location.altitude.toInt() + wb.time = (location.time.milliseconds.inWholeSeconds).toInt() + wb.ground_speed = location.speed.toInt() + wb.ground_track = location.bearing.toInt() + wb.location_source = ProtoPosition.LocSource.LOC_EXTERNAL + } + .build(), ) } .launchIn(scope) diff --git a/core/service/src/androidMain/kotlin/org/meshtastic/core/service/MeshNotificationManagerImpl.kt b/core/service/src/androidMain/kotlin/org/meshtastic/core/service/MeshNotificationManagerImpl.kt index 5df0ff54aa..6faf14418a 100644 --- a/core/service/src/androidMain/kotlin/org/meshtastic/core/service/MeshNotificationManagerImpl.kt +++ b/core/service/src/androidMain/kotlin/org/meshtastic/core/service/MeshNotificationManagerImpl.kt @@ -42,6 +42,8 @@ import kotlinx.coroutines.launch import org.jetbrains.compose.resources.StringResource import org.koin.core.annotation.Single import org.meshtastic.core.common.di.ServiceScope +import org.meshtastic.core.common.state.RadioOperation +import org.meshtastic.core.common.state.RadioOperationLock import org.meshtastic.core.common.util.NumberFormatter import org.meshtastic.core.common.util.nowMillis import org.meshtastic.core.common.util.safeCatching @@ -67,6 +69,8 @@ import org.meshtastic.core.resources.connected import org.meshtastic.core.resources.connecting import org.meshtastic.core.resources.device_sleeping import org.meshtastic.core.resources.disconnected +import org.meshtastic.core.resources.discovery_scan_in_progress +import org.meshtastic.core.resources.firmware_update_in_progress import org.meshtastic.core.resources.getString import org.meshtastic.core.resources.getStringSuspend import org.meshtastic.core.resources.local_stats_bad @@ -123,6 +127,7 @@ class MeshNotificationManagerImpl( private val nodeRepository: Lazy, private val conversationShortcutPublisher: Lazy, private val radioConfigRepository: Lazy, + private val radioOperationLock: RadioOperationLock, private val scope: ServiceScope, ) : MeshNotificationManager { @@ -171,6 +176,7 @@ class MeshNotificationManagerImpl( val deviceMetrics: DeviceMetrics?, val previousMessage: String?, val nextUpdateAt: Long, + val activeOperations: Set = emptySet(), ) private data class RenderedServiceNotification(val notification: Notification, val message: String) @@ -370,6 +376,16 @@ class MeshNotificationManagerImpl( init { scope.launch { serviceNotificationSnapshots.collectLatest(::renderAndPostServiceNotification) } + // A long operation changes the notification without any connection-state change to piggyback on — a + // firmware update in particular deselects the device, so the state alone would read "Disconnected". + scope.launch { + radioOperationLock.holders.collect { holders -> + synchronized(serviceNotificationLock) { + val last = serviceNotificationSnapshots.replayCache.lastOrNull() ?: return@synchronized + serviceNotificationSnapshots.tryEmit(last.copy(activeOperations = holders.values.toSet())) + } + } + } } /** @@ -451,21 +467,25 @@ class MeshNotificationManagerImpl( deviceMetrics = cachedDeviceMetrics, previousMessage = cachedMessage, nextUpdateAt = nextStatsUpdateMillis, + activeOperations = radioOperationLock.activeOperations, ) } private suspend fun renderServiceNotification(snapshot: ServiceNotificationSnapshot): RenderedServiceNotification { + // A held operation outranks the connection state. During a firmware update the device is deliberately + // deselected, so the state alone would report "Disconnected" over a flash that is running perfectly. val title = - when (snapshot.state) { - is ConnectionState.Connected -> - getStringSuspend(Res.string.meshtastic_app_name) + ": " + getStringSuspend(Res.string.connected) + snapshot.activeOperations.notificationTitleResource()?.let { getStringSuspend(it) } + ?: when (snapshot.state) { + is ConnectionState.Connected -> + getStringSuspend(Res.string.meshtastic_app_name) + ": " + getStringSuspend(Res.string.connected) - is ConnectionState.Disconnected -> getStringSuspend(Res.string.disconnected) + is ConnectionState.Disconnected -> getStringSuspend(Res.string.disconnected) - is ConnectionState.DeviceSleep -> getStringSuspend(Res.string.device_sleeping) + is ConnectionState.DeviceSleep -> getStringSuspend(Res.string.device_sleeping) - is ConnectionState.Connecting -> getStringSuspend(Res.string.connecting) - } + is ConnectionState.Connecting -> getStringSuspend(Res.string.connecting) + } val freshMessage = when { @@ -1201,3 +1221,17 @@ class MeshNotificationManagerImpl( // endregion } + +/** + * The notification title for the most significant operation in flight, or null when none is. + * + * Firmware work outranks a discovery scan: it is the one the user must not interrupt. + */ +internal fun Set.notificationTitleResource(): StringResource? = when { + isEmpty() -> null + + contains(RadioOperation.FirmwareUpdate) || contains(RadioOperation.FirmwareMaintenance) -> + Res.string.firmware_update_in_progress + + else -> Res.string.discovery_scan_in_progress +} diff --git a/core/service/src/androidMain/kotlin/org/meshtastic/core/service/MeshService.kt b/core/service/src/androidMain/kotlin/org/meshtastic/core/service/MeshService.kt index a0e9cd53db..580fd93055 100644 --- a/core/service/src/androidMain/kotlin/org/meshtastic/core/service/MeshService.kt +++ b/core/service/src/androidMain/kotlin/org/meshtastic/core/service/MeshService.kt @@ -35,6 +35,7 @@ import kotlinx.coroutines.launch import kotlinx.coroutines.withTimeoutOrNull import org.koin.android.ext.android.inject import org.meshtastic.core.common.hasLocationPermission +import org.meshtastic.core.common.state.RadioOperationLock import org.meshtastic.core.common.util.isValidDeviceAddress import org.meshtastic.core.model.DeviceVersion import org.meshtastic.core.model.util.anonymize @@ -65,6 +66,8 @@ class MeshService : Service() { private val conversationShortcutPublisher: ConversationShortcutPublisher by inject() + private val radioOperationLock: RadioOperationLock by inject() + private var isServiceInitialized = false /** @@ -87,6 +90,9 @@ class MeshService : Service() { */ private var wakeLock: PowerManager.WakeLock? = null + /** Keeps the CPU up for exactly as long as a long radio operation is in flight. See [OperationWakeLock]. */ + private val operationWakeLock by lazy { OperationWakeLock(this, serviceScope) } + companion object { fun createIntent(context: Context) = Intent(context, MeshService::class.java) @@ -119,6 +125,13 @@ class MeshService : Service() { // Keep conversation shortcuts published for the whole service lifetime (not only during an Android Auto // session) so message notifications can link to them and get Conversations-section treatment on phones. conversationShortcutPublisher.startObserving(serviceScope) + + // Hold a wake lock for as long as — and no longer than — some radio operation is in flight. + serviceScope.launch { + radioOperationLock.holders.collect { holders -> + if (holders.isEmpty()) operationWakeLock.stop() else operationWakeLock.start() + } + } } @Suppress("ReturnCount") @@ -192,12 +205,21 @@ class MeshService : Service() { withTimeoutOrNull(DEVICE_ADDRESS_SETTLE_MS) { radioInterfaceService.currentDeviceAddressFlow.first(::isValidDeviceAddress) } - if (isValidDeviceAddress(resolved)) { - Logger.i { "MeshService: selected device resolved (${resolved.anonymize}) after address-flow wait" } - acquireWakeLock() - } else { - Logger.i { "MeshService: no device selected after address flow settled; stopping" } - stopServiceCleanly() + when (serviceStayAliveDecision(resolved, radioOperationLock.isActive)) { + ServiceStayAliveDecision.STAY_FOR_DEVICE -> { + Logger.i { + "MeshService: selected device resolved (${resolved.anonymize}) after address-flow wait" + } + acquireWakeLock() + } + + ServiceStayAliveDecision.STAY_FOR_OPERATION -> + Logger.i { "MeshService: no device selected, but a radio operation is in flight; staying up" } + + ServiceStayAliveDecision.STOP -> { + Logger.i { "MeshService: no device selected after address flow settled; stopping" } + stopServiceCleanly() + } } } } @@ -205,6 +227,7 @@ class MeshService : Service() { /** Releases everything this service holds and stops it. Safe to call before it ever reached the foreground. */ private fun stopServiceCleanly() { releaseWakeLock() + operationWakeLock.stop() ServiceCompat.stopForeground(this, ServiceCompat.STOP_FOREGROUND_REMOVE) stopSelf() } @@ -256,6 +279,7 @@ class MeshService : Service() { addressWaitJob = null serviceScope.cancel() releaseWakeLock() + operationWakeLock.stop() ServiceCompat.stopForeground(this, ServiceCompat.STOP_FOREGROUND_REMOVE) if (isServiceInitialized) { orchestrator.stop() diff --git a/core/service/src/androidMain/kotlin/org/meshtastic/core/service/OperationWakeLock.kt b/core/service/src/androidMain/kotlin/org/meshtastic/core/service/OperationWakeLock.kt new file mode 100644 index 0000000000..103e2a3bbc --- /dev/null +++ b/core/service/src/androidMain/kotlin/org/meshtastic/core/service/OperationWakeLock.kt @@ -0,0 +1,121 @@ +/* + * 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 . + */ +@file:Suppress("TooGenericExceptionCaught") + +package org.meshtastic.core.service + +import android.content.Context +import android.os.PowerManager +import co.touchlab.kermit.Logger +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Job +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch + +/** + * The partial wake lock held while a long radio operation — a firmware flash, a discovery sweep — is in flight. + * + * A foreground service keeps the process alive but does not keep the CPU awake, so without this a screen-off flash or + * sweep is left to whatever the OEM's battery optimizations allow. Separate from the mesh service's own connection wake + * lock because it answers a different question: that one tracks having a device selected, and a firmware update + * deselects the device precisely so it can flash. + * + * Owns its renewal. The platform releases a timed wake lock on expiry and nothing re-emits while the same operation + * continues, so a USB maintenance sequence waiting on the user's file picker, or a long user-configured sweep, would + * otherwise lose the CPU partway through. + */ +internal class OperationWakeLock(private val context: Context, private val scope: CoroutineScope) { + + private var wakeLock: PowerManager.WakeLock? = null + private var renewal: Job? = null + + /** Takes the lock and keeps it re-armed until [stop]. Safe to call while already running. */ + fun start() { + acquire() + if (renewal?.isActive == true) return + renewal = + scope.launch { + while (true) { + delay(RENEW_INTERVAL_MS) + acquire() + } + } + } + + /** Ends the renewal and drops the lock. Safe to call when nothing is held. */ + fun stop() { + renewal?.cancel() + renewal = null + release() + } + + /** + * Acquires or re-arms the lock, per the documented pattern: a tag naming this class, an acquire timeout as a safety + * net, and an explicit release when the work ends. + * + * Re-arming an already-held lock is deliberate — it is not reference counted, so a further acquire restarts the + * timeout rather than nesting. + */ + private fun acquire() { + wakeLock?.let { existing -> + try { + existing.acquire(TIMEOUT_MS) + } catch (e: Exception) { + Logger.w(e) { "Failed to re-arm the radio operation wake lock" } + } + return + } + try { + val powerManager = context.getSystemService(Context.POWER_SERVICE) as PowerManager + val lock = + powerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, WAKE_LOCK_TAG).apply { + setReferenceCounted(false) + } + lock.acquire(TIMEOUT_MS) + wakeLock = lock + Logger.i { "Acquired partial wake lock for a radio operation" } + } catch (e: SecurityException) { + Logger.w(e) { "Failed to acquire the radio operation wake lock — WAKE_LOCK permission missing?" } + } catch (e: Exception) { + Logger.w(e) { "Failed to acquire the radio operation wake lock" } + } + } + + private fun release() { + val lock = wakeLock ?: return + try { + if (lock.isHeld) { + lock.release() + Logger.i { "Released the radio operation wake lock" } + } + } catch (e: Exception) { + Logger.w(e) { "Failed to release the radio operation wake lock" } + } finally { + wakeLock = null + } + } + + private companion object { + private const val WAKE_LOCK_TAG = "Meshtastic::MeshServiceRadioOperation" + + /** Safety net, not the mechanism that ends the lock: [stop] is what normally releases it. */ + private const val TIMEOUT_MS = 60L * 60L * 1_000L // 1 hour + + /** Comfortably inside [TIMEOUT_MS], so a missed tick cannot strand the lock. */ + private const val RENEW_INTERVAL_MS = 10L * 60L * 1_000L // 10 minutes + } +} diff --git a/core/service/src/commonMain/kotlin/org/meshtastic/core/service/AdminControllerImpl.kt b/core/service/src/commonMain/kotlin/org/meshtastic/core/service/AdminControllerImpl.kt index 38802fdacf..b9ca37408c 100644 --- a/core/service/src/commonMain/kotlin/org/meshtastic/core/service/AdminControllerImpl.kt +++ b/core/service/src/commonMain/kotlin/org/meshtastic/core/service/AdminControllerImpl.kt @@ -89,12 +89,14 @@ internal class AdminControllerImpl( // ── Owner ─────────────────────────────────────────────────────────────── override suspend fun setOwner(destNum: Int, user: User, packetId: Int) { - commandSender.sendAdmin(destNum, packetId) { AdminMessage(set_owner = user) } + commandSender.sendAdmin(destNum, packetId) { AdminMessage.Builder().also { wb -> wb.set_owner = user }.build() } nodeManager.handleReceivedUser(destNum, user) } override suspend fun getOwner(destNum: Int, packetId: Int) { - commandSender.sendAdmin(destNum, packetId, wantResponse = true) { AdminMessage(get_owner_request = true) } + commandSender.sendAdmin(destNum, packetId, wantResponse = true) { + AdminMessage.Builder().also { wb -> wb.get_owner_request = true }.build() + } } override suspend fun setHamMode(destNum: Int, hamParameters: HamParameters, packetId: Int) { @@ -104,24 +106,36 @@ internal class AdminControllerImpl( } // Firmware applies tx_power/frequency to the LoRa config verbatim, so echo the node's current // values to keep a re-send (e.g. a callsign edit while already licensed) from wiping overrides. - val lora = radioConfigRepository.localConfigFlow.firstOrNull()?.lora ?: Config.LoRaConfig() - val params = hamParameters.copy(tx_power = lora.tx_power, frequency = lora.override_frequency) - commandSender.sendAdmin(destNum, packetId) { AdminMessage(set_ham_mode = params) } - val currentUser = nodeManager.nodeDBbyNodeNum[destNum]?.user ?: User() + val lora = radioConfigRepository.localConfigFlow.firstOrNull()?.lora ?: Config.LoRaConfig.Builder().build() + val params = + hamParameters + .newBuilder() + .also { wb -> + wb.tx_power = lora.tx_power + wb.frequency = lora.override_frequency + } + .build() + commandSender.sendAdmin(destNum, packetId) { + AdminMessage.Builder().also { wb -> wb.set_ham_mode = params }.build() + } + val currentUser = nodeManager.nodeDBbyNodeNum[destNum]?.user ?: User.Builder().build() nodeManager.handleReceivedUser( destNum, - currentUser.copy( - long_name = HamName.compose(hamParameters.call_sign, hamParameters.long_name), - short_name = hamParameters.short_name, - is_licensed = true, - ), + currentUser + .newBuilder() + .also { wb -> + wb.long_name = HamName.compose(hamParameters.call_sign, hamParameters.long_name) + wb.short_name = hamParameters.short_name + wb.is_licensed = true + } + .build(), ) } // ── Configuration ───────────────────────────────────────────────────────── override suspend fun setLocalConfig(config: Config) { - commandSender.sendAdmin(myNodeNum) { AdminMessage(set_config = config) } + commandSender.sendAdmin(myNodeNum) { AdminMessage.Builder().also { wb -> wb.set_config = config }.build() } scope.handledLaunch { radioConfigRepository.setLocalConfig(config) } } @@ -148,7 +162,9 @@ internal class AdminControllerImpl( } override suspend fun setConfig(destNum: Int, config: Config, packetId: Int) { - commandSender.sendAdmin(destNum, packetId) { AdminMessage(set_config = config) } + commandSender.sendAdmin(destNum, packetId) { + AdminMessage.Builder().also { wb -> wb.set_config = config }.build() + } if (destNum == nodeManager.myNodeNum.value) { scope.handledLaunch { radioConfigRepository.setLocalConfig(config) } } @@ -157,15 +173,19 @@ internal class AdminControllerImpl( override suspend fun getConfig(destNum: Int, configType: Int, packetId: Int) { commandSender.sendAdmin(destNum, packetId, wantResponse = true) { if (configType == AdminMessage.ConfigType.SESSIONKEY_CONFIG.value) { - AdminMessage(get_device_metadata_request = true) + AdminMessage.Builder().also { wb -> wb.get_device_metadata_request = true }.build() } else { - AdminMessage(get_config_request = AdminMessage.ConfigType.fromValue(configType)) + AdminMessage.Builder() + .also { wb -> wb.get_config_request = AdminMessage.ConfigType.fromValue(configType) } + .build() } } } override suspend fun setModuleConfig(destNum: Int, config: ModuleConfig, packetId: Int) { - commandSender.sendAdmin(destNum, packetId) { AdminMessage(set_module_config = config) } + commandSender.sendAdmin(destNum, packetId) { + AdminMessage.Builder().also { wb -> wb.set_module_config = config }.build() + } if (destNum == nodeManager.myNodeNum.value) { config.statusmessage?.let { sm -> nodeManager.updateNodeStatus(destNum, sm.node_status) } scope.handledLaunch { radioConfigRepository.setLocalModuleConfig(config) } @@ -174,19 +194,23 @@ internal class AdminControllerImpl( override suspend fun getModuleConfig(destNum: Int, moduleConfigType: Int, packetId: Int) { commandSender.sendAdmin(destNum, packetId, wantResponse = true) { - AdminMessage(get_module_config_request = AdminMessage.ModuleConfigType.fromValue(moduleConfigType)) + AdminMessage.Builder() + .also { wb -> wb.get_module_config_request = AdminMessage.ModuleConfigType.fromValue(moduleConfigType) } + .build() } } // ── Channels ──────────────────────────────────────────────────────────── override suspend fun setLocalChannel(channel: Channel) { - commandSender.sendAdmin(myNodeNum) { AdminMessage(set_channel = channel) } + commandSender.sendAdmin(myNodeNum) { AdminMessage.Builder().also { wb -> wb.set_channel = channel }.build() } scope.handledLaunch { radioConfigRepository.updateChannelSettings(channel) } } override suspend fun setRemoteChannel(destNum: Int, channel: Channel, packetId: Int) { - commandSender.sendAdmin(destNum, packetId) { AdminMessage(set_channel = channel) } + commandSender.sendAdmin(destNum, packetId) { + AdminMessage.Builder().also { wb -> wb.set_channel = channel }.build() + } if (destNum == nodeManager.myNodeNum.value) { scope.handledLaunch { radioConfigRepository.updateChannelSettings(channel) } } @@ -194,33 +218,41 @@ internal class AdminControllerImpl( override suspend fun getChannel(destNum: Int, index: Int, packetId: Int) { commandSender.sendAdmin(destNum, packetId, wantResponse = true) { - AdminMessage(get_channel_request = index + 1) + AdminMessage.Builder().also { wb -> wb.get_channel_request = index + 1 }.build() } } // ── Ringtone & Canned Messages ───────────────────────────────────────── override suspend fun setRingtone(destNum: Int, ringtone: String) { - commandSender.sendAdmin(destNum) { AdminMessage(set_ringtone_message = ringtone) } + commandSender.sendAdmin(destNum) { + AdminMessage.Builder().also { wb -> wb.set_ringtone_message = ringtone }.build() + } } override suspend fun getRingtone(destNum: Int, packetId: Int) { - commandSender.sendAdmin(destNum, packetId, wantResponse = true) { AdminMessage(get_ringtone_request = true) } + commandSender.sendAdmin(destNum, packetId, wantResponse = true) { + AdminMessage.Builder().also { wb -> wb.get_ringtone_request = true }.build() + } } override suspend fun setCannedMessages(destNum: Int, messages: String) { - commandSender.sendAdmin(destNum) { AdminMessage(set_canned_message_module_messages = messages) } + commandSender.sendAdmin(destNum) { + AdminMessage.Builder().also { wb -> wb.set_canned_message_module_messages = messages }.build() + } } override suspend fun setTime(destNum: Int, packetId: Int) { Logger.i { "Set time requested for node $destNum" } // Resolve the timestamp at send time so the value is as fresh as possible when it leaves the phone. - commandSender.sendAdmin(destNum, packetId) { AdminMessage(set_time_only = nowSeconds.toInt()) } + commandSender.sendAdmin(destNum, packetId) { + AdminMessage.Builder().also { wb -> wb.set_time_only = nowSeconds.toInt() }.build() + } } override suspend fun getCannedMessages(destNum: Int, packetId: Int) { commandSender.sendAdmin(destNum, packetId, wantResponse = true) { - AdminMessage(get_canned_message_module_messages_request = true) + AdminMessage.Builder().also { wb -> wb.get_canned_message_module_messages_request = true }.build() } } @@ -234,37 +266,54 @@ internal class AdminControllerImpl( override suspend fun getDeviceConnectionStatus(destNum: Int, packetId: Int) { commandSender.sendAdmin(destNum, packetId, wantResponse = true) { - AdminMessage(get_device_connection_status_request = true) + AdminMessage.Builder().also { wb -> wb.get_device_connection_status_request = true }.build() } } override suspend fun reboot(destNum: Int, packetId: Int) { Logger.i { "Reboot requested for node $destNum" } - commandSender.sendAdmin(destNum, packetId) { AdminMessage(reboot_seconds = DEFAULT_DELAY_SECONDS) } + commandSender.sendAdmin(destNum, packetId) { + AdminMessage.Builder().also { wb -> wb.reboot_seconds = DEFAULT_DELAY_SECONDS }.build() + } } override suspend fun rebootToDfu(nodeNum: Int) { - commandSender.sendAdmin(nodeNum) { AdminMessage(enter_dfu_mode_request = true) } + commandSender.sendAdmin(nodeNum) { + AdminMessage.Builder().also { wb -> wb.enter_dfu_mode_request = true }.build() + } } override suspend fun requestRebootOta(requestId: Int, destNum: Int, mode: Int, hash: ByteArray?) { val otaMode = OTAMode.fromValue(mode) ?: OTAMode.NO_REBOOT_OTA val otaEvent = - AdminMessage.OTAEvent(reboot_ota_mode = otaMode, ota_hash = hash?.toByteString() ?: ByteString.EMPTY) - commandSender.sendAdmin(destNum, requestId) { AdminMessage(ota_request = otaEvent) } + AdminMessage.OTAEvent.Builder() + .also { wb -> + wb.reboot_ota_mode = otaMode + wb.ota_hash = hash?.toByteString() ?: ByteString.EMPTY + } + .build() + commandSender.sendAdmin(destNum, requestId) { + AdminMessage.Builder().also { wb -> wb.ota_request = otaEvent }.build() + } } override suspend fun shutdown(destNum: Int, packetId: Int) { - commandSender.sendAdmin(destNum, packetId) { AdminMessage(shutdown_seconds = DEFAULT_DELAY_SECONDS) } + commandSender.sendAdmin(destNum, packetId) { + AdminMessage.Builder().also { wb -> wb.shutdown_seconds = DEFAULT_DELAY_SECONDS }.build() + } } override suspend fun factoryReset(destNum: Int, packetId: Int) { Logger.i { "Factory reset requested for node $destNum" } - commandSender.sendAdmin(destNum, packetId) { AdminMessage(factory_reset_device = 1) } + commandSender.sendAdmin(destNum, packetId) { + AdminMessage.Builder().also { wb -> wb.factory_reset_device = 1 }.build() + } } override suspend fun nodedbReset(destNum: Int, packetId: Int, preserveFavorites: Boolean) { - commandSender.sendAdmin(destNum, packetId) { AdminMessage(nodedb_reset = preserveFavorites) } + commandSender.sendAdmin(destNum, packetId) { + AdminMessage.Builder().also { wb -> wb.nodedb_reset = preserveFavorites }.build() + } } // ── Edit Settings (transactional) ─────────────────────────────────────── @@ -323,18 +372,22 @@ internal class AdminControllerImpl( private val stagedProjections = mutableListOf Unit>() override suspend fun setOwner(user: User) { - commandSender.sendAdmin(destNum, commandSender.generatePacketId()) { AdminMessage(set_owner = user) } + commandSender.sendAdmin(destNum, commandSender.generatePacketId()) { + AdminMessage.Builder().also { wb -> wb.set_owner = user }.build() + } stageProjection { nodeManager.handleReceivedUser(destNum, user) } } override suspend fun setConfig(config: Config) { - commandSender.sendAdmin(destNum, commandSender.generatePacketId()) { AdminMessage(set_config = config) } + commandSender.sendAdmin(destNum, commandSender.generatePacketId()) { + AdminMessage.Builder().also { wb -> wb.set_config = config }.build() + } if (isLocalDestination) stageProjection { radioConfigRepository.setLocalConfig(config) } } override suspend fun setModuleConfig(config: ModuleConfig) { commandSender.sendAdmin(destNum, commandSender.generatePacketId()) { - AdminMessage(set_module_config = config) + AdminMessage.Builder().also { wb -> wb.set_module_config = config }.build() } if (isLocalDestination) { stageProjection { @@ -348,7 +401,9 @@ internal class AdminControllerImpl( // set, so the operation adding channel writes must reconcile that set after the transaction; mirroring slots // here could expose a partial cache if a later write or commit fails. override suspend fun setChannel(channel: Channel) = - commandSender.sendAdmin(destNum, commandSender.generatePacketId()) { AdminMessage(set_channel = channel) } + commandSender.sendAdmin(destNum, commandSender.generatePacketId()) { + AdminMessage.Builder().also { wb -> wb.set_channel = channel }.build() + } override suspend fun setFixedPosition(position: Position) { val removesFixedPosition = position.isFixedPositionRemoval() @@ -391,7 +446,11 @@ internal class AdminControllerImpl( /** Requires the begin boundary to be admitted before any transactional settings writes are issued. */ private suspend fun requireBeginBoundaryAccepted(destNum: Int) { - if (!commandSender.sendAdminAwait(destNum) { AdminMessage(begin_edit_settings = true) }) { + if ( + !commandSender.sendAdminAwait(destNum) { + AdminMessage.Builder().also { wb -> wb.begin_edit_settings = true }.build() + } + ) { throw EditSettingsTransactionException(editSettingsBoundaryFailureMessage("begin")) } } @@ -402,7 +461,10 @@ internal class AdminControllerImpl( * prevents a rebooting transport write from overtaking an uncommitted settings transaction. */ private suspend fun requireCommitBoundaryAccepted(destNum: Int, isLocalDestination: Boolean) { - val result = commandSender.sendAdminAwaitResult(destNum) { AdminMessage(commit_edit_settings = true) } + val result = + commandSender.sendAdminAwaitResult(destNum) { + AdminMessage.Builder().also { wb -> wb.commit_edit_settings = true }.build() + } val uncertainAfterDispatch = isLocalDestination && result.dispatched && diff --git a/core/service/src/commonMain/kotlin/org/meshtastic/core/service/MessagingControllerImpl.kt b/core/service/src/commonMain/kotlin/org/meshtastic/core/service/MessagingControllerImpl.kt index 9d992c5b8c..da3a4ff58e 100644 --- a/core/service/src/commonMain/kotlin/org/meshtastic/core/service/MessagingControllerImpl.kt +++ b/core/service/src/commonMain/kotlin/org/meshtastic/core/service/MessagingControllerImpl.kt @@ -73,6 +73,11 @@ internal class MessagingControllerImpl( override suspend fun sendReaction(emoji: String, replyId: Int, contactKey: String) { val myNum = nodeManager.myNodeNum.value ?: return val parsedKey = ContactKey(contactKey) + // A retired conversation has no live slot; its absent channel prefix would otherwise read as the primary. + if (parsedKey.isRetired) { + Logger.w { "Refusing to react in a retired conversation" } + return + } val channel = parsedKey.channel val destId = parsedKey.addressString val dataPacket = @@ -88,7 +93,9 @@ internal class MessagingControllerImpl( .apply { from = nodeManager.getMyId().takeIf { it.isNotEmpty() } ?: NodeAddress.ID_LOCAL } commandSender.sendData(dataPacket) analytics.trackAction("reaction_send") - val user = nodeManager.nodeDBbyNodeNum[myNum]?.user ?: User(id = nodeManager.getMyId()) + val user = + nodeManager.nodeDBbyNodeNum[myNum]?.user + ?: User.Builder().also { wb -> wb.id = nodeManager.getMyId() }.build() packetRepository.value.insertReaction( Reaction( replyId = replyId, @@ -117,8 +124,18 @@ internal class MessagingControllerImpl( false } else { val contact = - SharedContact(node_num = nodeDef.num, user = nodeDef.user, manually_verified = nodeDef.manuallyVerified) - safeCatching { commandSender.sendAdminAwait(myNum) { AdminMessage(add_contact = contact) } } + SharedContact.Builder() + .also { wb -> + wb.node_num = nodeDef.num + wb.user = nodeDef.user + wb.manually_verified = nodeDef.manuallyVerified + } + .build() + safeCatching { + commandSender.sendAdminAwait(myNum) { + AdminMessage.Builder().also { wb -> wb.add_contact = contact }.build() + } + } .getOrDefault(false) } } @@ -133,7 +150,7 @@ internal class MessagingControllerImpl( } // Cross-platform policy: honor the verification state encoded by the sharer as-is // (see meshtastic/design standards/audits/nfc-alignment-audit.md). - commandSender.sendAdmin(myNum) { AdminMessage(add_contact = contact) } + commandSender.sendAdmin(myNum) { AdminMessage.Builder().also { wb -> wb.add_contact = contact }.build() } nodeManager.handleReceivedUser(contact.node_num, user, manuallyVerified = contact.manually_verified) } diff --git a/core/service/src/commonMain/kotlin/org/meshtastic/core/service/NodeControllerImpl.kt b/core/service/src/commonMain/kotlin/org/meshtastic/core/service/NodeControllerImpl.kt index da10a6c748..5235444f9a 100644 --- a/core/service/src/commonMain/kotlin/org/meshtastic/core/service/NodeControllerImpl.kt +++ b/core/service/src/commonMain/kotlin/org/meshtastic/core/service/NodeControllerImpl.kt @@ -45,9 +45,9 @@ internal class NodeControllerImpl( if (node.isFavorite != favorite) { commandSender.sendAdmin(myNum) { if (favorite) { - AdminMessage(set_favorite_node = node.num) + AdminMessage.Builder().also { wb -> wb.set_favorite_node = node.num }.build() } else { - AdminMessage(remove_favorite_node = node.num) + AdminMessage.Builder().also { wb -> wb.remove_favorite_node = node.num }.build() } } nodeManager.updateNode(node.num) { it.copy(isFavorite = favorite) } @@ -59,7 +59,11 @@ internal class NodeControllerImpl( val node = nodeManager.nodeDBbyNodeNum[nodeNum] ?: return if (node.isIgnored != ignored) { commandSender.sendAdmin(myNum) { - if (ignored) AdminMessage(set_ignored_node = node.num) else AdminMessage(remove_ignored_node = node.num) + if (ignored) { + AdminMessage.Builder().also { wb -> wb.set_ignored_node = node.num }.build() + } else { + AdminMessage.Builder().also { wb -> wb.remove_ignored_node = node.num }.build() + } } nodeManager.updateNode(node.num) { it.copy(isIgnored = ignored) } scope.handledLaunch { packetRepository.value.updateFilteredBySender(node.user.id, ignored) } @@ -69,7 +73,7 @@ internal class NodeControllerImpl( override suspend fun toggleMuted(nodeNum: Int) { val myNum = nodeManager.myNodeNum.value ?: return val node = nodeManager.nodeDBbyNodeNum[nodeNum] ?: return - commandSender.sendAdmin(myNum) { AdminMessage(toggle_muted_node = node.num) } + commandSender.sendAdmin(myNum) { AdminMessage.Builder().also { wb -> wb.toggle_muted_node = node.num }.build() } nodeManager.updateNode(node.num) { it.copy(isMuted = !node.isMuted) } } @@ -77,7 +81,9 @@ internal class NodeControllerImpl( nodeManager.removeByNodenum(nodeNum) val myNum = nodeManager.myNodeNum.value ?: return try { - commandSender.sendAdmin(myNum, packetId) { AdminMessage(remove_by_nodenum = nodeNum) } + commandSender.sendAdmin(myNum, packetId) { + AdminMessage.Builder().also { wb -> wb.remove_by_nodenum = nodeNum }.build() + } } catch (e: PacketQueueRejectedException) { // Node removal has always been local-first and is allowed while disconnected. Preserve that contract when // the connected transport is transitioning and cannot admit the best-effort radio cleanup command. diff --git a/core/service/src/commonMain/kotlin/org/meshtastic/core/service/QueryControllerImpl.kt b/core/service/src/commonMain/kotlin/org/meshtastic/core/service/QueryControllerImpl.kt index 8eb8def189..96425ef4b7 100644 --- a/core/service/src/commonMain/kotlin/org/meshtastic/core/service/QueryControllerImpl.kt +++ b/core/service/src/commonMain/kotlin/org/meshtastic/core/service/QueryControllerImpl.kt @@ -38,7 +38,9 @@ internal class QueryControllerImpl( get() = nodeManager.myNodeNum.value ?: 0 override suspend fun refreshMetadata(destNum: Int) { - commandSender.sendAdmin(destNum, wantResponse = true) { AdminMessage(get_device_metadata_request = true) } + commandSender.sendAdmin(destNum, wantResponse = true) { + AdminMessage.Builder().also { wb -> wb.get_device_metadata_request = true }.build() + } } override suspend fun requestPosition(destNum: Int, currentPosition: Position) { diff --git a/core/service/src/commonMain/kotlin/org/meshtastic/core/service/ServiceStayAliveDecision.kt b/core/service/src/commonMain/kotlin/org/meshtastic/core/service/ServiceStayAliveDecision.kt new file mode 100644 index 0000000000..baf25cb14f --- /dev/null +++ b/core/service/src/commonMain/kotlin/org/meshtastic/core/service/ServiceStayAliveDecision.kt @@ -0,0 +1,53 @@ +/* + * 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 . + */ +package org.meshtastic.core.service + +import org.meshtastic.core.common.util.isValidDeviceAddress + +/** What the mesh service should do once the selected-device address has settled. */ +enum class ServiceStayAliveDecision { + /** A device is selected; run as a normal foreground service. */ + STAY_FOR_DEVICE, + + /** + * No device is selected, but a long-running radio operation is in flight and needs this process alive. + * + * The firmware update flow deselects the device (`setDeviceAddress("n")`) to free the transport before it flashes. + * That deselect is what makes this case real: without it the service would stop roughly five seconds into a flash + * that runs for minutes, taking the foreground notification and the wake lock with it. + */ + STAY_FOR_OPERATION, + + /** Nothing is selected and nothing is running, so the service has no reason to exist. */ + STOP, +} + +/** + * Decides whether the mesh service keeps running once the address flow has settled. + * + * Pure and platform-agnostic so the gate is unit-testable without standing up a `Service`, in the same shape as + * [bootReconnectDecision]. + * + * Deliberately not consulted by the service's other two teardown paths. A held operation must not keep alive a service + * that failed dependency injection or could not enter the foreground — in the latter case ActivityManager has already + * armed the pending-start watchdog, and refusing to stop would turn a recoverable refusal into a process kill. + */ +fun serviceStayAliveDecision(address: String?, hasActiveRadioOperation: Boolean): ServiceStayAliveDecision = when { + isValidDeviceAddress(address) -> ServiceStayAliveDecision.STAY_FOR_DEVICE + hasActiveRadioOperation -> ServiceStayAliveDecision.STAY_FOR_OPERATION + else -> ServiceStayAliveDecision.STOP +} diff --git a/core/service/src/commonMain/kotlin/org/meshtastic/core/service/SharedRadioInterfaceService.kt b/core/service/src/commonMain/kotlin/org/meshtastic/core/service/SharedRadioInterfaceService.kt index b7f27ff5e6..b2817151df 100644 --- a/core/service/src/commonMain/kotlin/org/meshtastic/core/service/SharedRadioInterfaceService.kt +++ b/core/service/src/commonMain/kotlin/org/meshtastic/core/service/SharedRadioInterfaceService.kt @@ -60,7 +60,7 @@ import org.koin.core.annotation.Named import org.koin.core.annotation.Single import org.meshtastic.core.ble.BluetoothRepository import org.meshtastic.core.common.di.PROCESS_LIFECYCLE -import org.meshtastic.core.common.state.FirmwareMaintenanceLock +import org.meshtastic.core.common.state.RadioOperationLock import org.meshtastic.core.common.util.ignoreExceptionSuspend import org.meshtastic.core.common.util.nowMillis import org.meshtastic.core.common.util.safeCatching @@ -172,7 +172,7 @@ class SharedRadioInterfaceService( private val radioPrefs: RadioPrefs, private val transportFactory: RadioTransportFactory, private val analytics: PlatformAnalytics, - private val firmwareMaintenanceLock: FirmwareMaintenanceLock, + private val radioOperationLock: RadioOperationLock, ) : RadioInterfaceService { override val supportedDeviceTypes: List @@ -590,7 +590,7 @@ class SharedRadioInterfaceService( // explicitly disconnected from. stopTransportLocked() below still fires on // BLE-disabled to tear down a running BLE link, but we deliberately do NOT // clear connectionRequested here — that is disconnect()'s job. - if (connectionRequested && !firmwareMaintenanceLock.isActive) { + if (connectionRequested && !radioOperationLock.suppressesTransport) { startTransportLocked() } } else if (runningTransportId == InterfaceId.BLUETOOTH) { @@ -606,7 +606,7 @@ class SharedRadioInterfaceService( transportMutex.withLock { if (state) { // Environmental recovery only — see the BLE listener above for rationale. - if (connectionRequested && !firmwareMaintenanceLock.isActive) { + if (connectionRequested && !radioOperationLock.suppressesTransport) { startTransportLocked() } } else if (runningTransportId == InterfaceId.TCP) { @@ -648,7 +648,7 @@ class SharedRadioInterfaceService( if (!connectionRequested) return@withLock // A USB firmware-maintenance sequence owns the port: the device is enumerating as bare erase or // bootloader firmware, and binding a mesh transport to it would claim the port the flow needs. - if (firmwareMaintenanceLock.isActive) return@withLock + if (radioOperationLock.suppressesTransport) return@withLock if (runningTransportId != InterfaceId.SERIAL) return@withLock // Race-defense: the combine snapshot may be stale by the time we acquire // transportMutex — another path (setDeviceAddress, BLE liveness restart) may @@ -971,7 +971,9 @@ class SharedRadioInterfaceService( _connectionState.value != ConnectionState.Disconnected ) { ignoreExceptionSuspend { - currentTransport.handleSendToRadio(ToRadio(disconnect = true).encode()) + currentTransport.handleSendToRadio( + ToRadio.Builder().also { wb -> wb.disconnect = true }.build().encode(), + ) delay(POLITE_DISCONNECT_DRAIN_MS) } } diff --git a/core/service/src/commonTest/kotlin/org/meshtastic/core/service/MeshServiceOrchestratorTest.kt b/core/service/src/commonTest/kotlin/org/meshtastic/core/service/MeshServiceOrchestratorTest.kt index 7b26c2a1e8..8e2132b9d0 100644 --- a/core/service/src/commonTest/kotlin/org/meshtastic/core/service/MeshServiceOrchestratorTest.kt +++ b/core/service/src/commonTest/kotlin/org/meshtastic/core/service/MeshServiceOrchestratorTest.kt @@ -121,7 +121,7 @@ class MeshServiceOrchestratorTest { isSessionActive(it.args[0] as RadioSessionContext) } every { serviceRepository.meshPacketFlow } returns MutableSharedFlow() - every { meshConfigHandler.moduleConfig } returns MutableStateFlow(LocalModuleConfig()) + every { meshConfigHandler.moduleConfig } returns MutableStateFlow(LocalModuleConfig.Builder().build()) every { takPrefs.isTakServerEnabled } returns takEnabledFlow every { takPrefs.isMeshToCotEnabled } returns MutableStateFlow(false) every { takPrefs.takServerChannel } returns MutableStateFlow(0) @@ -414,7 +414,11 @@ class MeshServiceOrchestratorTest { activeSession = activeSession, ) every { nodeManager.myNodeNum } returns MutableStateFlow(null) - val payload = FromRadio(my_info = MyNodeInfo(my_node_num = 42)).encode() + val payload = + FromRadio.Builder() + .also { wb -> wb.my_info = MyNodeInfo.Builder().also { wb -> wb.my_node_num = 42 }.build() } + .build() + .encode() val staleFrame = frame(payload, RadioSessionContext(generation = 1L, address = DEFAULT_ADDRESS)) val freshFrame = frame(payload, RadioSessionContext(generation = 2L, address = DEFAULT_ADDRESS)) diff --git a/core/service/src/commonTest/kotlin/org/meshtastic/core/service/RadioControllerImplTest.kt b/core/service/src/commonTest/kotlin/org/meshtastic/core/service/RadioControllerImplTest.kt index 480140646b..f70f191196 100644 --- a/core/service/src/commonTest/kotlin/org/meshtastic/core/service/RadioControllerImplTest.kt +++ b/core/service/src/commonTest/kotlin/org/meshtastic/core/service/RadioControllerImplTest.kt @@ -374,7 +374,7 @@ class RadioControllerImplTest { fun connectionStateAndClientNotificationDelegateToServiceRepository() = runTest { val serviceRepository = ServiceRepositoryImpl() val controller = createController(scope = backgroundScope, serviceRepository = serviceRepository) - val notification = ClientNotification() + val notification = ClientNotification.Builder().build() assertSame(serviceRepository.connectionState, controller.connectionState) assertSame(serviceRepository.clientNotification, controller.clientNotification) @@ -417,7 +417,14 @@ class RadioControllerImplTest { @Test fun localChannelDoesNotPersistWhenQueueRejects() = runTest { val controller = createController(scope = backgroundScope, myNodeNum = 456) - val channel = Channel(index = 0, role = Channel.Role.PRIMARY, settings = ChannelSettings(name = "Primary")) + val channel = + Channel.Builder() + .also { wb -> + wb.index = 0 + wb.role = Channel.Role.PRIMARY + wb.settings = ChannelSettings.Builder().also { wb -> wb.name = "Primary" }.build() + } + .build() val rejection = PacketQueueRejectedException("Admin command") everySuspend { commandSender.sendAdmin(any(), any(), any(), any()) } throws rejection @@ -445,12 +452,14 @@ class RadioControllerImplTest { val controller = createController(scope = backgroundScope) val nodeNum = 321 val user = - User( - id = NodeAddress.numToDefaultId(nodeNum), - long_name = "Remote Node", - short_name = "RN", - public_key = TEST_PUBLIC_KEY, - ) + User.Builder() + .also { wb -> + wb.id = NodeAddress.numToDefaultId(nodeNum) + wb.long_name = "Remote Node" + wb.short_name = "RN" + wb.public_key = TEST_PUBLIC_KEY + } + .build() val node = Node(num = nodeNum, user = user, manuallyVerified = true) every { nodeRepository.getNode(NodeAddress.numToDefaultId(nodeNum)) } returns node everySuspend { commandSender.sendAdminAwait(any(), any(), any(), any()) } returns true @@ -674,7 +683,14 @@ class RadioControllerImplTest { @Test fun sendReactionPersistsToDatabase() = runTest { val controller = createController(scope = backgroundScope) - val user = User(id = "!abcd1234", long_name = "Test", short_name = "T") + val user = + User.Builder() + .also { wb -> + wb.id = "!abcd1234" + wb.long_name = "Test" + wb.short_name = "T" + } + .build() val node = Node(num = 1234, user = user) every { nodeManager.nodeDBbyNodeNum } returns mapOf(1234 to node) every { nodeManager.getMyId() } returns "!abcd1234" @@ -700,7 +716,14 @@ class RadioControllerImplTest { @Test fun sendReactionDoesNotPersistWhenQueueRejects() = runTest { val controller = createController(scope = backgroundScope) - val user = User(id = "!abcd1234", long_name = "Test", short_name = "T") + val user = + User.Builder() + .also { wb -> + wb.id = "!abcd1234" + wb.long_name = "Test" + wb.short_name = "T" + } + .build() every { nodeManager.nodeDBbyNodeNum } returns mapOf(1234 to Node(num = 1234, user = user)) every { nodeManager.getMyId() } returns "!abcd1234" val rejection = PacketQueueRejectedException("Reaction") @@ -722,7 +745,14 @@ class RadioControllerImplTest { @Test fun sendReactionFallsBackToQueuedWhenSenderLeavesStatusNull() = runTest { val controller = createController(scope = backgroundScope) - val user = User(id = "!abcd1234", long_name = "Test", short_name = "T") + val user = + User.Builder() + .also { wb -> + wb.id = "!abcd1234" + wb.long_name = "Test" + wb.short_name = "T" + } + .build() every { nodeManager.nodeDBbyNodeNum } returns mapOf(1234 to Node(num = 1234, user = user)) every { nodeManager.getMyId() } returns "!abcd1234" everySuspend { commandSender.sendData(any()) } calls { (it.args[0] as DataPacket).status = null } @@ -737,7 +767,7 @@ class RadioControllerImplTest { @Test fun setFavoriteSendsAdminAndUpdatesState() = runTest { val controller = createController(scope = backgroundScope) - val node = Node(num = 99, user = User(id = "!node99"), isFavorite = false) + val node = Node(num = 99, user = User.Builder().also { wb -> wb.id = "!node99" }.build(), isFavorite = false) every { nodeManager.nodeDBbyNodeNum } returns mapOf(99 to node) controller.setFavorite(99, favorite = true) @@ -749,7 +779,7 @@ class RadioControllerImplTest { @Test fun setFavoriteIsNoOpWhenAlreadyInRequestedState() = runTest { val controller = createController(scope = backgroundScope) - val node = Node(num = 99, user = User(id = "!node99"), isFavorite = true) + val node = Node(num = 99, user = User.Builder().also { wb -> wb.id = "!node99" }.build(), isFavorite = true) every { nodeManager.nodeDBbyNodeNum } returns mapOf(99 to node) controller.setFavorite(99, favorite = true) @@ -761,7 +791,7 @@ class RadioControllerImplTest { @Test fun setIgnoredSendsAdminUpdatesStateAndFiltersPackets() = runTest { val controller = createController(scope = backgroundScope) - val node = Node(num = 99, user = User(id = "!node99"), isIgnored = false) + val node = Node(num = 99, user = User.Builder().also { wb -> wb.id = "!node99" }.build(), isIgnored = false) every { nodeManager.nodeDBbyNodeNum } returns mapOf(99 to node) controller.setIgnored(99, ignored = true) @@ -775,7 +805,7 @@ class RadioControllerImplTest { @Test fun toggleMutedSendsAdminAndUpdatesState() = runTest { val controller = createController(scope = backgroundScope) - val node = Node(num = 99, user = User(id = "!node99"), isMuted = false) + val node = Node(num = 99, user = User.Builder().also { wb -> wb.id = "!node99" }.build(), isMuted = false) every { nodeManager.nodeDBbyNodeNum } returns mapOf(99 to node) controller.toggleMuted(99) @@ -904,8 +934,24 @@ class RadioControllerImplTest { everySuspend { commandSender.sendAdminAwaitResult(any(), any(), any(), any()) } returns acceptedSendResult() controller.editLocalSettings { - setChannel(Channel(index = 0, role = Channel.Role.PRIMARY, settings = ChannelSettings(name = "A"))) - setChannel(Channel(index = 1, role = Channel.Role.SECONDARY, settings = ChannelSettings(name = "B"))) + setChannel( + Channel.Builder() + .also { wb -> + wb.index = 0 + wb.role = Channel.Role.PRIMARY + wb.settings = ChannelSettings.Builder().also { wb -> wb.name = "A" }.build() + } + .build(), + ) + setChannel( + Channel.Builder() + .also { wb -> + wb.index = 1 + wb.role = Channel.Role.SECONDARY + wb.settings = ChannelSettings.Builder().also { wb -> wb.name = "B" }.build() + } + .build(), + ) } advanceUntilIdle() @@ -943,9 +989,23 @@ class RadioControllerImplTest { } controller.editLocalSettings { - setChannel(Channel(index = 0, role = Channel.Role.PRIMARY, settings = ChannelSettings(name = "Primary"))) setChannel( - Channel(index = 1, role = Channel.Role.SECONDARY, settings = ChannelSettings(name = "Secondary")), + Channel.Builder() + .also { wb -> + wb.index = 0 + wb.role = Channel.Role.PRIMARY + wb.settings = ChannelSettings.Builder().also { wb -> wb.name = "Primary" }.build() + } + .build(), + ) + setChannel( + Channel.Builder() + .also { wb -> + wb.index = 1 + wb.role = Channel.Role.SECONDARY + wb.settings = ChannelSettings.Builder().also { wb -> wb.name = "Secondary" }.build() + } + .build(), ) } @@ -961,9 +1021,21 @@ class RadioControllerImplTest { val controller = createController(scope = backgroundScope, myNodeNum = 1234) val commitStarted = CompletableDeferred() val releaseCommit = CompletableDeferred() - val user = User(id = "!000004d2", long_name = "Committed owner") - val config = Config(device = Config.DeviceConfig()) - val moduleConfig = ModuleConfig(statusmessage = ModuleConfig.StatusMessageConfig(node_status = "Ready")) + val user = + User.Builder() + .also { wb -> + wb.id = "!000004d2" + wb.long_name = "Committed owner" + } + .build() + val config = Config.Builder().also { wb -> wb.device = Config.DeviceConfig.Builder().build() }.build() + val moduleConfig = + ModuleConfig.Builder() + .also { wb -> + wb.statusmessage = + ModuleConfig.StatusMessageConfig.Builder().also { wb -> wb.node_status = "Ready" }.build() + } + .build() val fixedPosition = Position(latitude = 47.6, longitude = -122.3, altitude = 42) everySuspend { commandSender.sendAdminAwait(any(), any(), any(), any()) } returns true everySuspend { commandSender.sendAdminAwaitResult(any(), any(), any(), any()) } calls @@ -1003,9 +1075,21 @@ class RadioControllerImplTest { @Test fun editSettingsRetainsInterleavedStagedLocalProjections() = runTest { val controller = createController(scope = backgroundScope, myNodeNum = 1234) - val user = User(id = "!000004d2", long_name = "Concurrent owner") - val config = Config(device = Config.DeviceConfig()) - val moduleConfig = ModuleConfig(statusmessage = ModuleConfig.StatusMessageConfig(node_status = "Ready")) + val user = + User.Builder() + .also { wb -> + wb.id = "!000004d2" + wb.long_name = "Concurrent owner" + } + .build() + val config = Config.Builder().also { wb -> wb.device = Config.DeviceConfig.Builder().build() }.build() + val moduleConfig = + ModuleConfig.Builder() + .also { wb -> + wb.statusmessage = + ModuleConfig.StatusMessageConfig.Builder().also { wb -> wb.node_status = "Ready" }.build() + } + .build() everySuspend { commandSender.sendAdminAwait(any(), any(), any(), any()) } returns true everySuspend { commandSender.sendAdminAwaitResult(any(), any(), any(), any()) } returns acceptedSendResult() everySuspend { commandSender.sendAdmin(any(), any(), any(), any()) } returns Unit @@ -1067,8 +1151,14 @@ class RadioControllerImplTest { @Test fun editSettingsContinuesLocalProjectionReconciliationAfterProjectionFailure() = runTest { val controller = createController(scope = backgroundScope, myNodeNum = 1234) - val user = User(id = "!000004d2", long_name = "Committed owner") - val config = Config(device = Config.DeviceConfig()) + val user = + User.Builder() + .also { wb -> + wb.id = "!000004d2" + wb.long_name = "Committed owner" + } + .build() + val config = Config.Builder().also { wb -> wb.device = Config.DeviceConfig.Builder().build() }.build() everySuspend { commandSender.sendAdminAwait(any(), any(), any(), any()) } returns true everySuspend { commandSender.sendAdminAwaitResult(any(), any(), any(), any()) } returns acceptedSendResult() every { nodeManager.handleReceivedUser(any(), any(), any(), any(), any()) } throws @@ -1092,9 +1182,25 @@ class RadioControllerImplTest { assertFailsWith { controller.editLocalSettings { - setOwner(User(id = "!000004d2", long_name = "Uncommitted owner")) - setConfig(Config(device = Config.DeviceConfig())) - setModuleConfig(ModuleConfig(statusmessage = ModuleConfig.StatusMessageConfig(node_status = "Stale"))) + setOwner( + User.Builder() + .also { wb -> + wb.id = "!000004d2" + wb.long_name = "Uncommitted owner" + } + .build(), + ) + setConfig(Config.Builder().also { wb -> wb.device = Config.DeviceConfig.Builder().build() }.build()) + setModuleConfig( + ModuleConfig.Builder() + .also { wb -> + wb.statusmessage = + ModuleConfig.StatusMessageConfig.Builder() + .also { wb -> wb.node_status = "Stale" } + .build() + } + .build(), + ) setFixedPosition(Position(latitude = 1.0, longitude = 2.0, altitude = 3)) } } @@ -1117,10 +1223,24 @@ class RadioControllerImplTest { val failure = assertFailsWith { controller.editLocalSettings { - setOwner(User(id = "!000004d2", long_name = "Rolled back owner")) - setConfig(Config(device = Config.DeviceConfig())) + setOwner( + User.Builder() + .also { wb -> + wb.id = "!000004d2" + wb.long_name = "Rolled back owner" + } + .build(), + ) + setConfig(Config.Builder().also { wb -> wb.device = Config.DeviceConfig.Builder().build() }.build()) setModuleConfig( - ModuleConfig(statusmessage = ModuleConfig.StatusMessageConfig(node_status = "Rolled back")), + ModuleConfig.Builder() + .also { wb -> + wb.statusmessage = + ModuleConfig.StatusMessageConfig.Builder() + .also { wb -> wb.node_status = "Rolled back" } + .build() + } + .build(), ) setFixedPosition(Position(latitude = 4.0, longitude = 5.0, altitude = 6)) throw blockFailure @@ -1163,7 +1283,13 @@ class RadioControllerImplTest { assertFailsWith { controller.editLocalSettings { setChannel( - Channel(index = 0, role = Channel.Role.PRIMARY, settings = ChannelSettings(name = "Primary")), + Channel.Builder() + .also { wb -> + wb.index = 0 + wb.role = Channel.Role.PRIMARY + wb.settings = ChannelSettings.Builder().also { wb -> wb.name = "Primary" }.build() + } + .build(), ) } } @@ -1188,7 +1314,15 @@ class RadioControllerImplTest { val failure = assertFailsWith { controller.editLocalSettings { - setChannel(Channel(index = 0, role = Channel.Role.PRIMARY, settings = ChannelSettings(name = "A"))) + setChannel( + Channel.Builder() + .also { wb -> + wb.index = 0 + wb.role = Channel.Role.PRIMARY + wb.settings = ChannelSettings.Builder().also { wb -> wb.name = "A" }.build() + } + .build(), + ) } } @@ -1495,7 +1629,14 @@ class RadioControllerImplTest { val job = launch { controller.editLocalSettings { - setOwner(User(id = "!000004d2", long_name = "Cancelled owner")) + setOwner( + User.Builder() + .also { wb -> + wb.id = "!000004d2" + wb.long_name = "Cancelled owner" + } + .build(), + ) blockStarted.complete(Unit) keepBlockOpen.await() } @@ -1515,10 +1656,19 @@ class RadioControllerImplTest { val controller = createController(scope = backgroundScope) // A QR-scanned contact arrives with manually_verified = false (proto default). val contact = - SharedContact( - node_num = 42, - user = User(id = "!0000002a", long_name = "Test", public_key = TEST_PUBLIC_KEY), - ) + SharedContact.Builder() + .also { wb -> + wb.node_num = 42 + wb.user = + User.Builder() + .also { wb -> + wb.id = "!0000002a" + wb.long_name = "Test" + wb.public_key = TEST_PUBLIC_KEY + } + .build() + } + .build() var sentMessage: AdminMessage? = null everySuspend { commandSender.sendAdmin(any(), any(), any(), any()) } calls @@ -1540,11 +1690,20 @@ class RadioControllerImplTest { val controller = createController(scope = backgroundScope) // A contact shared as already verified stays verified on import. val contact = - SharedContact( - node_num = 42, - user = User(id = "!0000002a", long_name = "Test", public_key = TEST_PUBLIC_KEY), - manually_verified = true, - ) + SharedContact.Builder() + .also { wb -> + wb.node_num = 42 + wb.user = + User.Builder() + .also { wb -> + wb.id = "!0000002a" + wb.long_name = "Test" + wb.public_key = TEST_PUBLIC_KEY + } + .build() + wb.manually_verified = true + } + .build() var sentMessage: AdminMessage? = null everySuspend { commandSender.sendAdmin(any(), any(), any(), any()) } calls @@ -1563,10 +1722,29 @@ class RadioControllerImplTest { @Test fun setHamModeSendsAdminWithEchoedLoraValuesAndUpdatesUser() = runTest { val controller = createController(scope = backgroundScope, myNodeNum = 123) - val existingUser = User(id = "!0000007b", long_name = "Old Name", short_name = "OLD") + val existingUser = + User.Builder() + .also { wb -> + wb.id = "!0000007b" + wb.long_name = "Old Name" + wb.short_name = "OLD" + } + .build() every { nodeManager.nodeDBbyNodeNum } returns mapOf(123 to Node(num = 123, user = existingUser)) every { radioConfigRepository.localConfigFlow } returns - MutableStateFlow(LocalConfig(lora = Config.LoRaConfig(tx_power = 20, override_frequency = 915.5f))) + MutableStateFlow( + LocalConfig.Builder() + .also { wb -> + wb.lora = + Config.LoRaConfig.Builder() + .also { wb -> + wb.tx_power = 20 + wb.override_frequency = 915.5f + } + .build() + } + .build(), + ) var sentMessage: AdminMessage? = null everySuspend { commandSender.sendAdmin(any(), any(), any(), any()) } calls @@ -1577,7 +1755,13 @@ class RadioControllerImplTest { controller.setHamMode( 123, - HamParameters(call_sign = "KK7ABC", short_name = "KK7A", long_name = "Attic Heltec"), + HamParameters.Builder() + .also { wb -> + wb.call_sign = "KK7ABC" + wb.short_name = "KK7A" + wb.long_name = "Attic Heltec" + } + .build(), 42, ) @@ -1593,7 +1777,14 @@ class RadioControllerImplTest { verify { nodeManager.handleReceivedUser( 123, - existingUser.copy(long_name = "KK7ABC//Attic Heltec", short_name = "KK7A", is_licensed = true), + existingUser + .newBuilder() + .also { wb -> + wb.long_name = "KK7ABC//Attic Heltec" + wb.short_name = "KK7A" + wb.is_licensed = true + } + .build(), 0, false, ) @@ -1603,18 +1794,41 @@ class RadioControllerImplTest { @Test fun setHamModeWithoutALongNameNamesTheNodeAfterTheCallSignAlone() = runTest { val controller = createController(scope = backgroundScope, myNodeNum = 123) - val existingUser = User(id = "!0000007b", long_name = "Old Name", short_name = "OLD") + val existingUser = + User.Builder() + .also { wb -> + wb.id = "!0000007b" + wb.long_name = "Old Name" + wb.short_name = "OLD" + } + .build() every { nodeManager.nodeDBbyNodeNum } returns mapOf(123 to Node(num = 123, user = existingUser)) - every { radioConfigRepository.localConfigFlow } returns MutableStateFlow(LocalConfig()) + every { radioConfigRepository.localConfigFlow } returns MutableStateFlow(LocalConfig.Builder().build()) everySuspend { commandSender.sendAdmin(any(), any(), any(), any()) } returns Unit - controller.setHamMode(123, HamParameters(call_sign = "KK7ABC", short_name = "KK7A"), 42) + controller.setHamMode( + 123, + HamParameters.Builder() + .also { wb -> + wb.call_sign = "KK7ABC" + wb.short_name = "KK7A" + } + .build(), + 42, + ) // long_name is optional: firmware leaves the bare call sign rather than a dangling "//". verify { nodeManager.handleReceivedUser( 123, - existingUser.copy(long_name = "KK7ABC", short_name = "KK7A", is_licensed = true), + existingUser + .newBuilder() + .also { wb -> + wb.long_name = "KK7ABC" + wb.short_name = "KK7A" + wb.is_licensed = true + } + .build(), 0, false, ) @@ -1625,7 +1839,7 @@ class RadioControllerImplTest { fun setHamModeWithNoCachedLoraConfigSendsProtoDefaults() = runTest { val controller = createController(scope = backgroundScope, myNodeNum = 123) every { nodeManager.nodeDBbyNodeNum } returns emptyMap() - every { radioConfigRepository.localConfigFlow } returns MutableStateFlow(LocalConfig()) + every { radioConfigRepository.localConfigFlow } returns MutableStateFlow(LocalConfig.Builder().build()) var sentMessage: AdminMessage? = null everySuspend { commandSender.sendAdmin(any(), any(), any(), any()) } calls @@ -1634,7 +1848,16 @@ class RadioControllerImplTest { sentMessage = (it.args[3] as () -> AdminMessage)() } - controller.setHamMode(123, HamParameters(call_sign = "KK7ABC", short_name = "KK7A"), 42) + controller.setHamMode( + 123, + HamParameters.Builder() + .also { wb -> + wb.call_sign = "KK7ABC" + wb.short_name = "KK7A" + } + .build(), + 42, + ) val ham = sentMessage?.set_ham_mode assertEquals(0, ham?.tx_power) @@ -1643,7 +1866,13 @@ class RadioControllerImplTest { verify { nodeManager.handleReceivedUser( 123, - User(long_name = "KK7ABC", short_name = "KK7A", is_licensed = true), + User.Builder() + .also { wb -> + wb.long_name = "KK7ABC" + wb.short_name = "KK7A" + wb.is_licensed = true + } + .build(), 0, false, ) @@ -1654,7 +1883,16 @@ class RadioControllerImplTest { fun setHamModeIgnoresRemoteDestinations() = runTest { val controller = createController(scope = backgroundScope, myNodeNum = 123) - controller.setHamMode(456, HamParameters(call_sign = "KK7ABC", short_name = "KK7A"), 42) + controller.setHamMode( + 456, + HamParameters.Builder() + .also { wb -> + wb.call_sign = "KK7ABC" + wb.short_name = "KK7A" + } + .build(), + 42, + ) verifySuspend(exactly(0)) { commandSender.sendAdmin(any(), any(), any(), any()) } verify(exactly(0)) { nodeManager.handleReceivedUser(any(), any(), any(), any()) } @@ -1663,7 +1901,13 @@ class RadioControllerImplTest { @Test fun importContactReturnsEarlyWhenDisconnected() = runTest { val controller = createController(scope = backgroundScope, myNodeNum = null) - val contact = SharedContact(node_num = 42, user = User(id = "!0000002a")) + val contact = + SharedContact.Builder() + .also { wb -> + wb.node_num = 42 + wb.user = User.Builder().also { wb -> wb.id = "!0000002a" }.build() + } + .build() controller.importContact(contact) @@ -1674,7 +1918,14 @@ class RadioControllerImplTest { fun sendSharedContactRejectsNodeWithNoPublicKey() = runTest { val controller = createController(scope = backgroundScope) val nodeNum = 321 - val user = User(id = NodeAddress.numToDefaultId(nodeNum), long_name = "Heard Only", short_name = "HO") + val user = + User.Builder() + .also { wb -> + wb.id = NodeAddress.numToDefaultId(nodeNum) + wb.long_name = "Heard Only" + wb.short_name = "HO" + } + .build() every { nodeRepository.getNode(NodeAddress.numToDefaultId(nodeNum)) } returns Node(num = nodeNum, user = user) val result = controller.sendSharedContact(nodeNum) @@ -1686,7 +1937,19 @@ class RadioControllerImplTest { @Test fun importContactRejectsEmptyPublicKey() = runTest { val controller = createController(scope = backgroundScope) - val contact = SharedContact(node_num = 42, user = User(id = "!0000002a", long_name = "Test")) + val contact = + SharedContact.Builder() + .also { wb -> + wb.node_num = 42 + wb.user = + User.Builder() + .also { wb -> + wb.id = "!0000002a" + wb.long_name = "Test" + } + .build() + } + .build() controller.importContact(contact) diff --git a/core/service/src/commonTest/kotlin/org/meshtastic/core/service/RadioControllerRestoreTest.kt b/core/service/src/commonTest/kotlin/org/meshtastic/core/service/RadioControllerRestoreTest.kt index c687f690b1..7b7a83171b 100644 --- a/core/service/src/commonTest/kotlin/org/meshtastic/core/service/RadioControllerRestoreTest.kt +++ b/core/service/src/commonTest/kotlin/org/meshtastic/core/service/RadioControllerRestoreTest.kt @@ -116,7 +116,12 @@ class RadioControllerRestoreTest { val restored = controller.restoreLocalConfiguration( expectedDeviceAddress = "x:STALE", - config = Config(lora = Config.LoRaConfig(use_preset = true)), + config = + Config.Builder() + .also { wb -> + wb.lora = Config.LoRaConfig.Builder().also { wb -> wb.use_preset = true }.build() + } + .build(), ) assertFalse(restored) @@ -131,7 +136,12 @@ class RadioControllerRestoreTest { val restored = controller.restoreLocalConfiguration( expectedDeviceAddress = null, - config = Config(lora = Config.LoRaConfig(use_preset = true)), + config = + Config.Builder() + .also { wb -> + wb.lora = Config.LoRaConfig.Builder().also { wb -> wb.use_preset = true }.build() + } + .build(), ) assertFalse(restored) @@ -140,8 +150,18 @@ class RadioControllerRestoreTest { @Test fun restoreLocalConfigurationUsesOneEditTransactionInChannelThenConfigOrder() = runTest { - val primaryChannel = Channel(index = 0, role = Channel.Role.PRIMARY, settings = ChannelSettings()) - val config = Config(lora = Config.LoRaConfig(use_preset = true)) + val primaryChannel = + Channel.Builder() + .also { wb -> + wb.index = 0 + wb.role = Channel.Role.PRIMARY + wb.settings = ChannelSettings.Builder().build() + } + .build() + val config = + Config.Builder() + .also { wb -> wb.lora = Config.LoRaConfig.Builder().also { wb -> wb.use_preset = true }.build() } + .build() val operations = mutableListOf() val editScope = mock(MockMode.autofill) everySuspend { editScope.setChannel(any()) } calls { operations += "channel" } @@ -191,7 +211,12 @@ class RadioControllerRestoreTest { val restore = async { controller.restoreLocalConfiguration( expectedDeviceAddress = "x:OLD", - config = Config(lora = Config.LoRaConfig(use_preset = true)), + config = + Config.Builder() + .also { wb -> + wb.lora = Config.LoRaConfig.Builder().also { wb -> wb.use_preset = true }.build() + } + .build(), ) } runCurrent() diff --git a/core/service/src/commonTest/kotlin/org/meshtastic/core/service/ServiceStayAliveDecisionTest.kt b/core/service/src/commonTest/kotlin/org/meshtastic/core/service/ServiceStayAliveDecisionTest.kt new file mode 100644 index 0000000000..92ac52db20 --- /dev/null +++ b/core/service/src/commonTest/kotlin/org/meshtastic/core/service/ServiceStayAliveDecisionTest.kt @@ -0,0 +1,76 @@ +/* + * 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 . + */ +package org.meshtastic.core.service + +import kotlin.test.Test +import kotlin.test.assertEquals + +class ServiceStayAliveDecisionTest { + + @Test + fun selectedDeviceKeepsServiceAlive() { + assertEquals( + ServiceStayAliveDecision.STAY_FOR_DEVICE, + serviceStayAliveDecision(address = "x11:22:33:44:55:66", hasActiveRadioOperation = false), + ) + } + + @Test + fun selectedDeviceWinsOverOperation() { + assertEquals( + ServiceStayAliveDecision.STAY_FOR_DEVICE, + serviceStayAliveDecision(address = "t10.0.2.2:4403", hasActiveRadioOperation = true), + ) + } + + @Test + fun deselectedDuringAnOperationStaysAlive() { + // The exact firmware-update case: the flow sets the address to the "none" sentinel to free the transport. + assertEquals( + ServiceStayAliveDecision.STAY_FOR_OPERATION, + serviceStayAliveDecision(address = "n", hasActiveRadioOperation = true), + ) + } + + @Test + fun noAddressDuringAnOperationStaysAlive() { + assertEquals( + ServiceStayAliveDecision.STAY_FOR_OPERATION, + serviceStayAliveDecision(address = null, hasActiveRadioOperation = true), + ) + } + + @Test + fun deselectedWithNothingRunningStops() { + assertEquals( + ServiceStayAliveDecision.STOP, + serviceStayAliveDecision(address = "n", hasActiveRadioOperation = false), + ) + } + + @Test + fun blankAddressWithNothingRunningStops() { + assertEquals( + ServiceStayAliveDecision.STOP, + serviceStayAliveDecision(address = "", hasActiveRadioOperation = false), + ) + assertEquals( + ServiceStayAliveDecision.STOP, + serviceStayAliveDecision(address = null, hasActiveRadioOperation = false), + ) + } +} diff --git a/core/service/src/commonTest/kotlin/org/meshtastic/core/service/SharedRadioInterfaceServiceLivenessTest.kt b/core/service/src/commonTest/kotlin/org/meshtastic/core/service/SharedRadioInterfaceServiceLivenessTest.kt index 2d32aac00d..ebd2d67a99 100644 --- a/core/service/src/commonTest/kotlin/org/meshtastic/core/service/SharedRadioInterfaceServiceLivenessTest.kt +++ b/core/service/src/commonTest/kotlin/org/meshtastic/core/service/SharedRadioInterfaceServiceLivenessTest.kt @@ -42,7 +42,7 @@ import kotlinx.coroutines.test.advanceTimeBy import kotlinx.coroutines.test.resetMain import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.setMain -import org.meshtastic.core.common.state.FirmwareMaintenanceLock +import org.meshtastic.core.common.state.RadioOperationLock import org.meshtastic.core.di.CoroutineDispatchers import org.meshtastic.core.model.ConnectionState import org.meshtastic.core.model.DeviceType @@ -152,7 +152,7 @@ class SharedRadioInterfaceServiceLivenessTest { private val networkRepository: NetworkRepository = mock(MockMode.autofill) private val analytics: PlatformAnalytics = mock(MockMode.autofill) - private val firmwareMaintenanceLock = FirmwareMaintenanceLock() + private val radioOperationLock = RadioOperationLock() /** * Minimal [LifecycleOwner] for tests that avoids [LifecycleRegistry], which enforces main-thread checks and throws @@ -306,7 +306,7 @@ class SharedRadioInterfaceServiceLivenessTest { radioPrefs = radioPrefs, transportFactory = transportFactory, analytics = analytics, - firmwareMaintenanceLock = firmwareMaintenanceLock, + radioOperationLock = radioOperationLock, ) service.clockMillis = { clock } // Register the service so tearDown can disconnect it deterministically (the heartbeat loop diff --git a/core/takserver/src/commonMain/kotlin/org/meshtastic/core/takserver/TAKPacketConversion.kt b/core/takserver/src/commonMain/kotlin/org/meshtastic/core/takserver/TAKPacketConversion.kt index effb020139..db7768106d 100644 --- a/core/takserver/src/commonMain/kotlin/org/meshtastic/core/takserver/TAKPacketConversion.kt +++ b/core/takserver/src/commonMain/kotlin/org/meshtastic/core/takserver/TAKPacketConversion.kt @@ -52,14 +52,18 @@ object TAKPacketConversion { fun CoTMessage.toTAKPacket(): TAKPacket? { val group = this.group?.let { - Group( - role = - MemberRole.fromValue(TakConversionHelpers.getMemberRoleValue(it.role)) ?: MemberRole.Unspecifed, - team = Team.fromValue(TakConversionHelpers.getTeamValue(it.name)) ?: Team.Unspecifed_Color, - ) + Group.Builder() + .also { wb -> + wb.role = + MemberRole.fromValue(TakConversionHelpers.getMemberRoleValue(it.role)) + ?: MemberRole.Unspecifed + wb.team = Team.fromValue(TakConversionHelpers.getTeamValue(it.name)) ?: Team.Unspecifed_Color + } + .build() } - val status = this.status?.let { Status(battery = it.battery.coerceAtLeast(0)) } + val status = + this.status?.let { Status.Builder().also { wb -> wb.battery = it.battery.coerceAtLeast(0) }.build() } if (type.startsWith("a-f-G") || type.startsWith("a-f-g")) { return createPliPacket(group, status) @@ -74,17 +78,35 @@ object TAKPacketConversion { } private fun CoTMessage.createPliPacket(group: Group?, status: Status?): TAKPacket { - val contact = this.contact?.let { Contact(callsign = it.callsign, device_callsign = this.uid) } + val contact = + this.contact?.let { + Contact.Builder() + .also { wb -> + wb.callsign = it.callsign + wb.device_callsign = this.uid + } + .build() + } val pli = - PLI( - latitude_i = (latitude * TAK_COORDINATE_SCALE).toInt(), - longitude_i = (longitude * TAK_COORDINATE_SCALE).toInt(), - altitude = if (hae >= TAK_UNKNOWN_POINT_VALUE || hae.isNaN()) 0 else hae.toInt(), - speed = track?.speed?.coerceAtLeast(0.0)?.toInt() ?: 0, - course = track?.course?.coerceAtLeast(0.0)?.toInt() ?: 0, - ) + PLI.Builder() + .also { wb -> + wb.latitude_i = (latitude * TAK_COORDINATE_SCALE).toInt() + wb.longitude_i = (longitude * TAK_COORDINATE_SCALE).toInt() + wb.altitude = if (hae >= TAK_UNKNOWN_POINT_VALUE || hae.isNaN()) 0 else hae.toInt() + wb.speed = track?.speed?.coerceAtLeast(0.0)?.toInt() ?: 0 + wb.course = track?.course?.coerceAtLeast(0.0)?.toInt() ?: 0 + } + .build() - return TAKPacket(is_compressed = false, contact = contact, group = group, status = status, pli = pli) + return TAKPacket.Builder() + .also { wb -> + wb.is_compressed = false + wb.contact = contact + wb.group = group + wb.status = status + wb.pli = pli + } + .build() } private fun CoTMessage.createChatPacket(group: Group?, status: Status?): TAKPacket? { @@ -109,7 +131,12 @@ object TAKPacketConversion { } else { it.endpoint ?: "" } - Contact(callsign = it.callsign, device_callsign = smuggledCallsign) + Contact.Builder() + .also { wb -> + wb.callsign = it.callsign + wb.device_callsign = smuggledCallsign + } + .build() } if (localChat.chatroom.startsWith(this.uid) || this.uid.startsWith("GeoChat")) { @@ -122,13 +149,23 @@ object TAKPacketConversion { } val chat = - GeoChat( - message = chatMsg, - to = toUid ?: if (toCallsign == null) "All Chat Rooms" else null, - to_callsign = toCallsign, - ) + GeoChat.Builder() + .also { wb -> + wb.message = chatMsg + wb.to = toUid ?: if (toCallsign == null) "All Chat Rooms" else null + wb.to_callsign = toCallsign + } + .build() - return TAKPacket(is_compressed = false, contact = contact, group = group, status = status, chat = chat) + return TAKPacket.Builder() + .also { wb -> + wb.is_compressed = false + wb.contact = contact + wb.group = group + wb.status = status + wb.chat = chat + } + .build() } fun TAKPacket.toCoTMessage(): CoTMessage? { diff --git a/core/takserver/src/commonMain/kotlin/org/meshtastic/core/takserver/TAKPacketV2Conversion.kt b/core/takserver/src/commonMain/kotlin/org/meshtastic/core/takserver/TAKPacketV2Conversion.kt index c6799f2fbe..bdaa5c5d43 100644 --- a/core/takserver/src/commonMain/kotlin/org/meshtastic/core/takserver/TAKPacketV2Conversion.kt +++ b/core/takserver/src/commonMain/kotlin/org/meshtastic/core/takserver/TAKPacketV2Conversion.kt @@ -58,33 +58,35 @@ object TAKPacketV2Conversion { val callsign = contact?.callsign ?: "UNKNOWN" val deviceCallsign = uid - return TAKPacketV2( - cot_type_id = cotTypeEnum, - cot_type_str = cotTypeStr, - how = howEnum, - callsign = callsign, - device_callsign = deviceCallsign, - uid = uid, - team = teamEnum, - role = roleEnum, - latitude_i = (latitude * TAK_COORDINATE_SCALE).toInt(), - longitude_i = (longitude * TAK_COORDINATE_SCALE).toInt(), - altitude = if (hae >= TAK_UNKNOWN_POINT_VALUE || hae.isNaN()) 0 else hae.toInt(), - // V2 encodes speed as cm/s (m/s × 100) and course as deg×100. - // V1 (legacy TAKPacket, port 72) uses raw integers with no scaling. - // These two paths are ALWAYS separate (different portnums) and must - // never cross-feed: a V1 packet decoded in TAKMeshIntegration goes - // through convertV1ToCoT() → CoTMessage.pli() → toXml(), NOT through - // toTAKPacketV2(). If this invariant is ever broken, speed/course - // would be silently off by ×100. - speed = (track?.speed?.coerceAtLeast(0.0)?.times(100))?.toInt() ?: 0, // m/s -> cm/s - course = (track?.course?.coerceAtLeast(0.0)?.times(100))?.toInt() ?: 0, // deg -> deg*100 - battery = battery, - geo_src = GeoPointSource.GeoPointSource_GPS, - alt_src = GeoPointSource.GeoPointSource_GPS, - // v0.4.0: PLI is implicit — no payload_variant is set (the bool pli - // oneof arm was removed). An a-f-* packet with no variant IS a PLI. - ) + return TAKPacketV2.Builder() + .also { wb -> + wb.cot_type_id = cotTypeEnum + wb.cot_type_str = cotTypeStr + wb.how = howEnum + wb.callsign = callsign + wb.device_callsign = deviceCallsign + wb.uid = uid + wb.team = teamEnum + wb.role = roleEnum + wb.latitude_i = (latitude * TAK_COORDINATE_SCALE).toInt() + wb.longitude_i = (longitude * TAK_COORDINATE_SCALE).toInt() + wb.altitude = if (hae >= TAK_UNKNOWN_POINT_VALUE || hae.isNaN()) 0 else hae.toInt() + // V2 encodes speed as cm/s (m/s × 100) and course as deg×100. + // V1 (legacy TAKPacket, port 72) uses raw integers with no scaling. + // These two paths are ALWAYS separate (different portnums) and must + // never cross-feed: a V1 packet decoded in TAKMeshIntegration goes + // through convertV1ToCoT() → CoTMessage.pli() → toXml(), NOT through + // toTAKPacketV2(). If this invariant is ever broken, speed/course + // would be silently off by ×100. + wb.speed = (track?.speed?.coerceAtLeast(0.0)?.times(100))?.toInt() ?: 0 // m/s -> cm/s + wb.course = (track?.course?.coerceAtLeast(0.0)?.times(100))?.toInt() ?: 0 // deg -> deg*100 + wb.battery = battery + wb.geo_src = GeoPointSource.GeoPointSource_GPS + wb.alt_src = GeoPointSource.GeoPointSource_GPS + // v0.4.0: PLI is implicit — no payload_variant is set (the bool pli + // oneof arm was removed). An a-f-* packet with no variant IS a PLI. + } + .build() } // GeoChat @@ -121,22 +123,26 @@ object TAKPacketV2Conversion { } } - return TAKPacketV2( - cot_type_id = CotType.CotType_b_t_f, - how = CotHow.CotHow_h_g_i_g_o, - callsign = callsign, - device_callsign = smuggledCallsign, - uid = uid, - team = teamEnum, - role = roleEnum, - battery = battery, - chat = - GeoChat( - message = localChat.message, - to = toUid ?: if (toCallsign == null) "All Chat Rooms" else null, - to_callsign = toCallsign, - ), - ) + return TAKPacketV2.Builder() + .also { wb -> + wb.cot_type_id = CotType.CotType_b_t_f + wb.how = CotHow.CotHow_h_g_i_g_o + wb.callsign = callsign + wb.device_callsign = smuggledCallsign + wb.uid = uid + wb.team = teamEnum + wb.role = roleEnum + wb.battery = battery + wb.chat = + GeoChat.Builder() + .also { wb -> + wb.message = localChat.message + wb.to = toUid ?: if (toCallsign == null) "All Chat Rooms" else null + wb.to_callsign = toCallsign + } + .build() + } + .build() } // Fallback: wrap the whole detail XML in raw_detail for unmapped types @@ -144,21 +150,23 @@ object TAKPacketV2Conversion { val detailBytes = parsedDetailXml?.encodeToByteArray() if (detailBytes != null) { val callsign = contact?.callsign ?: "UNKNOWN" - return TAKPacketV2( - cot_type_id = cotTypeEnum, - cot_type_str = cotTypeStr, - how = howEnum, - callsign = callsign, - device_callsign = uid, - uid = uid, - team = teamEnum, - role = roleEnum, - latitude_i = (latitude * TAK_COORDINATE_SCALE).toInt(), - longitude_i = (longitude * TAK_COORDINATE_SCALE).toInt(), - altitude = if (hae >= TAK_UNKNOWN_POINT_VALUE || hae.isNaN()) 0 else hae.toInt(), - battery = battery, - raw_detail = detailBytes.toByteString(), - ) + return TAKPacketV2.Builder() + .also { wb -> + wb.cot_type_id = cotTypeEnum + wb.cot_type_str = cotTypeStr + wb.how = howEnum + wb.callsign = callsign + wb.device_callsign = uid + wb.uid = uid + wb.team = teamEnum + wb.role = roleEnum + wb.latitude_i = (latitude * TAK_COORDINATE_SCALE).toInt() + wb.longitude_i = (longitude * TAK_COORDINATE_SCALE).toInt() + wb.altitude = if (hae >= TAK_UNKNOWN_POINT_VALUE || hae.isNaN()) 0 else hae.toInt() + wb.battery = battery + wb.raw_detail = detailBytes.toByteString() + } + .build() } Logger.w { "Cannot convert CoT to TAKPacketV2 for type $type (no parsed detail)" } diff --git a/core/takserver/src/commonMain/kotlin/org/meshtastic/core/takserver/TakV2Compressor.kt b/core/takserver/src/commonMain/kotlin/org/meshtastic/core/takserver/TakV2Compressor.kt index b373d3721c..f00f9863ac 100644 --- a/core/takserver/src/commonMain/kotlin/org/meshtastic/core/takserver/TakV2Compressor.kt +++ b/core/takserver/src/commonMain/kotlin/org/meshtastic/core/takserver/TakV2Compressor.kt @@ -330,230 +330,267 @@ internal object TakV2Compressor { org.meshtastic.proto.GeoPointSource.fromValue(data.altSrc) ?: org.meshtastic.proto.GeoPointSource.GeoPointSource_Unspecified - return TAKPacketV2( - cot_type_id = cotType, - cot_type_str = data.cotTypeStr ?: "", - how = how, - callsign = data.callsign, - team = team, - role = role, - latitude_i = data.latitudeI, - longitude_i = data.longitudeI, - altitude = data.altitude, - speed = data.speed, - course = data.course, - battery = data.battery, - geo_src = geoSrc, - alt_src = altSrc, - uid = data.uid, - device_callsign = data.deviceCallsign, - stale_seconds = data.staleSeconds, - tak_version = data.takVersion, - tak_device = data.takDevice, - tak_platform = data.takPlatform, - tak_os = data.takOs, - endpoint = data.endpoint, - phone = data.phone, - // v0.4.0: PLI is implicit — no payload_variant is set for a PLI (the - // bool pli oneof arm was removed). Pli/None simply set no oneof field. - chat = - (data.payload as? TakPacketV2Data.Payload.Chat)?.let { chat -> - WireGeoChat( - message = chat.message, - to = chat.to, - to_callsign = chat.toCallsign, - receipt_for_uid = chat.receiptForUid, - receipt_type = - WireGeoChat.ReceiptType.fromValue(chat.receiptType) - ?: WireGeoChat.ReceiptType.ReceiptType_None, - // TAKTALK sidecars. Empty SDK string → wire null (field absent) - // so non-TAKTALK chats don't carry empty sidecar bytes on every - // mesh packet. voice_profile_id stays present-but-empty when - // hasVoiceProfile=true so the receiver can re-emit ``. - lang = chat.lang.ifEmpty { null }, - room_id = chat.roomId.ifEmpty { null }, - voice_profile_id = if (chat.hasVoiceProfile) chat.voiceProfileId else null, - ) - }, - aircraft = - (data.payload as? TakPacketV2Data.Payload.Aircraft)?.let { ac -> - WireAircraftTrack( - icao = ac.icao, - registration = ac.registration, - flight = ac.flight, - aircraft_type = ac.aircraftType, - squawk = ac.squawk, - category = ac.category, - rssi_x10 = ac.rssiX10, - gps = ac.gps, - cot_host_id = ac.cotHostId, - ) - }, - shape = - (data.payload as? TakPacketV2Data.Payload.DrawnShape)?.let { s -> - WireDrawnShape( - kind = WireDrawnShape.Kind.fromValue(s.kind) ?: WireDrawnShape.Kind.Kind_Unspecified, - style = - WireDrawnShape.StyleMode.fromValue(s.style) - ?: WireDrawnShape.StyleMode.StyleMode_Unspecified, - major_cm = s.majorCm, - minor_cm = s.minorCm, - angle_deg = s.angleDeg, - stroke_color = WireTeam.fromValue(s.strokeColor) ?: WireTeam.Unspecifed_Color, - stroke_argb = s.strokeArgb, - stroke_weight_x10 = s.strokeWeightX10, - fill_color = WireTeam.fromValue(s.fillColor) ?: WireTeam.Unspecifed_Color, - fill_argb = s.fillArgb, - labels_on = s.labelsOn, - // v0.4.0: delta-encode vertices into two packed sint32 columns - // relative to the event anchor (was repeated CotGeoPoint). - vertex_lat_deltas = s.vertices.map { it.latI - data.latitudeI }, - vertex_lon_deltas = s.vertices.map { it.lonI - data.longitudeI }, - truncated = s.truncated, - bullseye_distance_dm = s.bullseyeDistanceDm, - bullseye_bearing_ref = s.bullseyeBearingRef, - bullseye_flags = s.bullseyeFlags, - bullseye_uid_ref = s.bullseyeUidRef, - ) - }, - marker = - (data.payload as? TakPacketV2Data.Payload.Marker)?.let { m -> - WireMarker( - kind = WireMarker.Kind.fromValue(m.kind) ?: WireMarker.Kind.Kind_Unspecified, - color = WireTeam.fromValue(m.color) ?: WireTeam.Unspecifed_Color, - color_argb = m.colorArgb, - readiness = m.readiness, - parent_uid = m.parentUid, - parent_type = m.parentType, - parent_callsign = m.parentCallsign, - iconset = m.iconset, - ) - }, - rab = - (data.payload as? TakPacketV2Data.Payload.RangeAndBearing)?.let { r -> - WireRangeAndBearing( - anchor = - WireCotGeoPoint( - lat_delta_i = r.anchorLatI - data.latitudeI, - lon_delta_i = r.anchorLonI - data.longitudeI, - ), - anchor_uid = r.anchorUid, - range_cm = r.rangeCm, - bearing_cdeg = r.bearingCdeg, - stroke_color = WireTeam.fromValue(r.strokeColor) ?: WireTeam.Unspecifed_Color, - stroke_argb = r.strokeArgb, - stroke_weight_x10 = r.strokeWeightX10, - ) - }, - route = - (data.payload as? TakPacketV2Data.Payload.Route)?.let { rt -> - WireRoute( - method = WireRoute.Method.fromValue(rt.method) ?: WireRoute.Method.Method_Unspecified, - direction = - WireRoute.Direction.fromValue(rt.direction) ?: WireRoute.Direction.Direction_Unspecified, - prefix = rt.prefix, - stroke_weight_x10 = rt.strokeWeightX10, - links = - rt.links.map { link -> - WireRoute.Link( - point = - WireCotGeoPoint( - lat_delta_i = link.latI - data.latitudeI, - lon_delta_i = link.lonI - data.longitudeI, - ), - uid = link.uid, - callsign = link.callsign, - link_type = link.linkType, - ) - }, - truncated = rt.truncated, - ) - }, - casevac = - (data.payload as? TakPacketV2Data.Payload.CasevacReport)?.let { c -> - WireCasevacReport( - precedence = - WireCasevacReport.Precedence.fromValue(c.precedence) - ?: WireCasevacReport.Precedence.Precedence_Unspecified, - equipment_flags = c.equipmentFlags, - litter_patients = c.litterPatients, - ambulatory_patients = c.ambulatoryPatients, - security = - WireCasevacReport.Security.fromValue(c.security) - ?: WireCasevacReport.Security.Security_Unspecified, - hlz_marking = - WireCasevacReport.HlzMarking.fromValue(c.hlzMarking) - ?: WireCasevacReport.HlzMarking.HlzMarking_Unspecified, - zone_marker = c.zoneMarker, - us_military = c.usMilitary, - us_civilian = c.usCivilian, - non_us_military = c.nonUsMilitary, - non_us_civilian = c.nonUsCivilian, - epw = c.epw, - child = c.child, - terrain_flags = c.terrainFlags, - frequency = c.frequency, - ) - }, - emergency = - (data.payload as? TakPacketV2Data.Payload.EmergencyAlert)?.let { e -> - WireEmergencyAlert( - type = WireEmergencyAlert.Type.fromValue(e.type) ?: WireEmergencyAlert.Type.Type_Unspecified, - authoring_uid = e.authoringUid, - cancel_reference_uid = e.cancelReferenceUid, - ) - }, - task = - (data.payload as? TakPacketV2Data.Payload.TaskRequest)?.let { t -> - WireTaskRequest( - task_type = t.taskType, - target_uid = t.targetUid, - assignee_uid = t.assigneeUid, - priority = - WireTaskRequest.Priority.fromValue(t.priority) - ?: WireTaskRequest.Priority.Priority_Unspecified, - status = - WireTaskRequest.Status.fromValue(t.status) ?: WireTaskRequest.Status.Status_Unspecified, - note = t.note, - ) - }, - raw_detail = (data.payload as? TakPacketV2Data.Payload.RawDetail)?.bytes?.toByteString(), - // TAKTALK voice/text message (m-t-t). Without this, m-t-t events - // would compress with no payload set, the receiver's wireToSdkData - // would fall through to Payload.None, and TAKTALK plugin would - // never see the rebuilt CoT event for TTS playback. - taktalk = - (data.payload as? TakPacketV2Data.Payload.TakTalk)?.let { tt -> - WireTakTalkMessage( - text = tt.text, - chatroom_id = tt.chatroomId, - lang = tt.lang, - from_voice = tt.fromVoice, - ) - }, - // TAKTALK room/membership broadcast (y-). Required for receivers - // to resolve TAKTALK room UUIDs to friendly names + rosters. - taktalk_room = - (data.payload as? TakPacketV2Data.Payload.TakTalkRoom)?.let { room -> - @Suppress("DEPRECATION") - WireTakTalkRoomData( - // sender_callsign deprecated in SDK v0.3.2 — the SDK - // builder reconstitutes from envelope - // packet.callsign, so we stop emitting the duplicate - // wire byte. Field stays present for one release so - // v0.3.1 receivers continue decoding cleanly. - sender_callsign = "", - room_id = room.roomId, - room_name = room.roomName, - participants = room.participants.toList(), - ) - }, - // Directed-routing recipient list (…). - // Empty list = broadcast (default); populated for TAKTALK m-t-t - // and directed b-t-f DMs. Encode an explicit Marti only when - // there is at least one destination — the wrapper costs wire - // bytes for no benefit on broadcast packets. - marti = data.marti.takeIf { it.isNotEmpty() }?.let { org.meshtastic.proto.Marti(dest_callsign = it) }, - ) + return TAKPacketV2.Builder() + .also { wb -> + wb.cot_type_id = cotType + wb.cot_type_str = data.cotTypeStr ?: "" + wb.how = how + wb.callsign = data.callsign + wb.team = team + wb.role = role + wb.latitude_i = data.latitudeI + wb.longitude_i = data.longitudeI + wb.altitude = data.altitude + wb.speed = data.speed + wb.course = data.course + wb.battery = data.battery + wb.geo_src = geoSrc + wb.alt_src = altSrc + wb.uid = data.uid + wb.device_callsign = data.deviceCallsign + wb.stale_seconds = data.staleSeconds + wb.tak_version = data.takVersion + wb.tak_device = data.takDevice + wb.tak_platform = data.takPlatform + wb.tak_os = data.takOs + wb.endpoint = data.endpoint + wb.phone = data.phone + // v0.4.0: PLI is implicit — no payload_variant is set for a PLI (the + // bool pli oneof arm was removed). Pli/None simply set no oneof field. + wb.chat = + (data.payload as? TakPacketV2Data.Payload.Chat)?.let { chat -> + WireGeoChat.Builder() + .also { wb -> + wb.message = chat.message + wb.to = chat.to + wb.to_callsign = chat.toCallsign + wb.receipt_for_uid = chat.receiptForUid + wb.receipt_type = + WireGeoChat.ReceiptType.fromValue(chat.receiptType) + ?: WireGeoChat.ReceiptType.ReceiptType_None + // TAKTALK sidecars. Empty SDK string → wire null (field absent) + // so non-TAKTALK chats don't carry empty sidecar bytes on every + // mesh packet. voice_profile_id stays present-but-empty when + // hasVoiceProfile=true so the receiver can re-emit ``. + wb.lang = chat.lang.ifEmpty { null } + wb.room_id = chat.roomId.ifEmpty { null } + wb.voice_profile_id = if (chat.hasVoiceProfile) chat.voiceProfileId else null + } + .build() + } + wb.aircraft = + (data.payload as? TakPacketV2Data.Payload.Aircraft)?.let { ac -> + WireAircraftTrack.Builder() + .also { wb -> + wb.icao = ac.icao + wb.registration = ac.registration + wb.flight = ac.flight + wb.aircraft_type = ac.aircraftType + wb.squawk = ac.squawk + wb.category = ac.category + wb.rssi_x10 = ac.rssiX10 + wb.gps = ac.gps + wb.cot_host_id = ac.cotHostId + } + .build() + } + wb.shape = + (data.payload as? TakPacketV2Data.Payload.DrawnShape)?.let { s -> + WireDrawnShape.Builder() + .also { wb -> + wb.kind = WireDrawnShape.Kind.fromValue(s.kind) ?: WireDrawnShape.Kind.Kind_Unspecified + wb.style = + WireDrawnShape.StyleMode.fromValue(s.style) + ?: WireDrawnShape.StyleMode.StyleMode_Unspecified + wb.major_cm = s.majorCm + wb.minor_cm = s.minorCm + wb.angle_deg = s.angleDeg + wb.stroke_color = WireTeam.fromValue(s.strokeColor) ?: WireTeam.Unspecifed_Color + wb.stroke_argb = s.strokeArgb + wb.stroke_weight_x10 = s.strokeWeightX10 + wb.fill_color = WireTeam.fromValue(s.fillColor) ?: WireTeam.Unspecifed_Color + wb.fill_argb = s.fillArgb + wb.labels_on = s.labelsOn + // v0.4.0: delta-encode vertices into two packed sint32 columns + // relative to the event anchor (was repeated CotGeoPoint). + wb.vertex_lat_deltas = s.vertices.map { it.latI - data.latitudeI } + wb.vertex_lon_deltas = s.vertices.map { it.lonI - data.longitudeI } + wb.truncated = s.truncated + wb.bullseye_distance_dm = s.bullseyeDistanceDm + wb.bullseye_bearing_ref = s.bullseyeBearingRef + wb.bullseye_flags = s.bullseyeFlags + wb.bullseye_uid_ref = s.bullseyeUidRef + } + .build() + } + wb.marker = + (data.payload as? TakPacketV2Data.Payload.Marker)?.let { m -> + WireMarker.Builder() + .also { wb -> + wb.kind = WireMarker.Kind.fromValue(m.kind) ?: WireMarker.Kind.Kind_Unspecified + wb.color = WireTeam.fromValue(m.color) ?: WireTeam.Unspecifed_Color + wb.color_argb = m.colorArgb + wb.readiness = m.readiness + wb.parent_uid = m.parentUid + wb.parent_type = m.parentType + wb.parent_callsign = m.parentCallsign + wb.iconset = m.iconset + } + .build() + } + wb.rab = + (data.payload as? TakPacketV2Data.Payload.RangeAndBearing)?.let { r -> + WireRangeAndBearing.Builder() + .also { wb -> + wb.anchor = + WireCotGeoPoint.Builder() + .also { wb -> + wb.lat_delta_i = r.anchorLatI - data.latitudeI + wb.lon_delta_i = r.anchorLonI - data.longitudeI + } + .build() + wb.anchor_uid = r.anchorUid + wb.range_cm = r.rangeCm + wb.bearing_cdeg = r.bearingCdeg + wb.stroke_color = WireTeam.fromValue(r.strokeColor) ?: WireTeam.Unspecifed_Color + wb.stroke_argb = r.strokeArgb + wb.stroke_weight_x10 = r.strokeWeightX10 + } + .build() + } + wb.route = + (data.payload as? TakPacketV2Data.Payload.Route)?.let { rt -> + WireRoute.Builder() + .also { wb -> + wb.method = WireRoute.Method.fromValue(rt.method) ?: WireRoute.Method.Method_Unspecified + wb.direction = + WireRoute.Direction.fromValue(rt.direction) + ?: WireRoute.Direction.Direction_Unspecified + wb.prefix = rt.prefix + wb.stroke_weight_x10 = rt.strokeWeightX10 + wb.links = + rt.links.map { link -> + WireRoute.Link.Builder() + .also { wb -> + wb.point = + WireCotGeoPoint.Builder() + .also { wb -> + wb.lat_delta_i = link.latI - data.latitudeI + wb.lon_delta_i = link.lonI - data.longitudeI + } + .build() + wb.uid = link.uid + wb.callsign = link.callsign + wb.link_type = link.linkType + } + .build() + } + wb.truncated = rt.truncated + } + .build() + } + wb.casevac = + (data.payload as? TakPacketV2Data.Payload.CasevacReport)?.let { c -> + WireCasevacReport.Builder() + .also { wb -> + wb.precedence = + WireCasevacReport.Precedence.fromValue(c.precedence) + ?: WireCasevacReport.Precedence.Precedence_Unspecified + wb.equipment_flags = c.equipmentFlags + wb.litter_patients = c.litterPatients + wb.ambulatory_patients = c.ambulatoryPatients + wb.security = + WireCasevacReport.Security.fromValue(c.security) + ?: WireCasevacReport.Security.Security_Unspecified + wb.hlz_marking = + WireCasevacReport.HlzMarking.fromValue(c.hlzMarking) + ?: WireCasevacReport.HlzMarking.HlzMarking_Unspecified + wb.zone_marker = c.zoneMarker + wb.us_military = c.usMilitary + wb.us_civilian = c.usCivilian + wb.non_us_military = c.nonUsMilitary + wb.non_us_civilian = c.nonUsCivilian + wb.epw = c.epw + wb.child = c.child + wb.terrain_flags = c.terrainFlags + wb.frequency = c.frequency + } + .build() + } + wb.emergency = + (data.payload as? TakPacketV2Data.Payload.EmergencyAlert)?.let { e -> + WireEmergencyAlert.Builder() + .also { wb -> + wb.type = + WireEmergencyAlert.Type.fromValue(e.type) + ?: WireEmergencyAlert.Type.Type_Unspecified + wb.authoring_uid = e.authoringUid + wb.cancel_reference_uid = e.cancelReferenceUid + } + .build() + } + wb.task = + (data.payload as? TakPacketV2Data.Payload.TaskRequest)?.let { t -> + WireTaskRequest.Builder() + .also { wb -> + wb.task_type = t.taskType + wb.target_uid = t.targetUid + wb.assignee_uid = t.assigneeUid + wb.priority = + WireTaskRequest.Priority.fromValue(t.priority) + ?: WireTaskRequest.Priority.Priority_Unspecified + wb.status = + WireTaskRequest.Status.fromValue(t.status) + ?: WireTaskRequest.Status.Status_Unspecified + wb.note = t.note + } + .build() + } + wb.raw_detail = (data.payload as? TakPacketV2Data.Payload.RawDetail)?.bytes?.toByteString() + // TAKTALK voice/text message (m-t-t). Without this, m-t-t events + // would compress with no payload set, the receiver's wireToSdkData + // would fall through to Payload.None, and TAKTALK plugin would + // never see the rebuilt CoT event for TTS playback. + wb.taktalk = + (data.payload as? TakPacketV2Data.Payload.TakTalk)?.let { tt -> + WireTakTalkMessage.Builder() + .also { wb -> + wb.text = tt.text + wb.chatroom_id = tt.chatroomId + wb.lang = tt.lang + wb.from_voice = tt.fromVoice + } + .build() + } + // TAKTALK room/membership broadcast (y-). Required for receivers + // to resolve TAKTALK room UUIDs to friendly names + rosters. + wb.taktalk_room = + (data.payload as? TakPacketV2Data.Payload.TakTalkRoom)?.let { room -> + @Suppress("DEPRECATION") + WireTakTalkRoomData.Builder() + .also { wb -> + // sender_callsign deprecated in SDK v0.3.2 — the SDK + // builder reconstitutes from envelope + // packet.callsign, so we stop emitting the duplicate + // wire byte. Field stays present for one release so + // v0.3.1 receivers continue decoding cleanly. + wb.sender_callsign = "" + wb.room_id = room.roomId + wb.room_name = room.roomName + wb.participants = room.participants.toList() + } + .build() + } + // Directed-routing recipient list (…). + // Empty list = broadcast (default); populated for TAKTALK m-t-t + // and directed b-t-f DMs. Encode an explicit Marti only when + // there is at least one destination — the wrapper costs wire + // bytes for no benefit on broadcast packets. + wb.marti = + data.marti + .takeIf { it.isNotEmpty() } + ?.let { org.meshtastic.proto.Marti.Builder().also { wb -> wb.dest_callsign = it }.build() } + } + .build() } } diff --git a/core/takserver/src/commonTest/kotlin/org/meshtastic/core/takserver/CoTConversionTest.kt b/core/takserver/src/commonTest/kotlin/org/meshtastic/core/takserver/CoTConversionTest.kt index fbaf9d098f..9e592b1701 100644 --- a/core/takserver/src/commonTest/kotlin/org/meshtastic/core/takserver/CoTConversionTest.kt +++ b/core/takserver/src/commonTest/kotlin/org/meshtastic/core/takserver/CoTConversionTest.kt @@ -28,14 +28,16 @@ class CoTConversionTest { @Test fun testPositionToCoTMessage() { val position = - Position( - latitude_i = 377749000, - longitude_i = -1224194000, - altitude = 15, - ground_speed = 5, - ground_track = 180, - time = 1620000000, - ) + Position.Builder() + .also { wb -> + wb.latitude_i = 377749000 + wb.longitude_i = -1224194000 + wb.altitude = 15 + wb.ground_speed = 5 + wb.ground_track = 180 + wb.time = 1620000000 + } + .build() val cot = position.toCoTMessage(uid = "!12345678", callsign = "TestUser", team = "Red", role = "HQ", battery = 85) @@ -61,12 +63,14 @@ class CoTConversionTest { @Test fun testUserToCoTMessage() { val user = - User( - id = "!87654321", - long_name = "LongName", - short_name = "SN", - macaddr = "00:11:22:33:44:55".encodeUtf8(), - ) + User.Builder() + .also { wb -> + wb.id = "!87654321" + wb.long_name = "LongName" + wb.short_name = "SN" + wb.macaddr = "00:11:22:33:44:55".encodeUtf8() + } + .build() val cot = user.toCoTMessage(position = null, team = "Blue", role = "Sniper", battery = 92) diff --git a/core/takserver/src/commonTest/kotlin/org/meshtastic/core/takserver/MeshNodeCoTConversionTest.kt b/core/takserver/src/commonTest/kotlin/org/meshtastic/core/takserver/MeshNodeCoTConversionTest.kt index bf90c0c372..e8045d6b49 100644 --- a/core/takserver/src/commonTest/kotlin/org/meshtastic/core/takserver/MeshNodeCoTConversionTest.kt +++ b/core/takserver/src/commonTest/kotlin/org/meshtastic/core/takserver/MeshNodeCoTConversionTest.kt @@ -153,15 +153,22 @@ class MeshNodeCoTConversionTest { shortName: String = "WOLF", longName: String = "Wolf Ridge Relay", hwModel: org.meshtastic.proto.HardwareModel = org.meshtastic.proto.HardwareModel.TBEAM, - ) = org.meshtastic.proto.User( - id = "!a1b2c3d4", - short_name = shortName, - long_name = longName, - hw_model = hwModel, - ) + ) = org.meshtastic.proto.User.Builder() + .also { wb -> + wb.id = "!a1b2c3d4" + wb.short_name = shortName + wb.long_name = longName + wb.hw_model = hwModel + } + .build() fun position(latitudeI: Int = 377_749_000, longitudeI: Int = -1_224_194_000) = - org.meshtastic.proto.Position(latitude_i = latitudeI, longitude_i = longitudeI) + org.meshtastic.proto.Position.Builder() + .also { wb -> + wb.latitude_i = latitudeI + wb.longitude_i = longitudeI + } + .build() @Suppress("LongParameterList") fun meshNode( @@ -188,12 +195,14 @@ class MeshNodeCoTConversionTest { snr = snr, rssi = rssi, deviceMetrics = - org.meshtastic.proto.DeviceMetrics( - battery_level = batteryLevel, - voltage = voltage, - channel_utilization = channelUtilization, - air_util_tx = airUtilTx, - ), + org.meshtastic.proto.DeviceMetrics.Builder() + .also { wb -> + wb.battery_level = batteryLevel + wb.voltage = voltage + wb.channel_utilization = channelUtilization + wb.air_util_tx = airUtilTx + } + .build(), ) } } diff --git a/core/takserver/src/commonTest/kotlin/org/meshtastic/core/takserver/MeshToCotBroadcasterTest.kt b/core/takserver/src/commonTest/kotlin/org/meshtastic/core/takserver/MeshToCotBroadcasterTest.kt index 3bef087e32..c76c1869c7 100644 --- a/core/takserver/src/commonTest/kotlin/org/meshtastic/core/takserver/MeshToCotBroadcasterTest.kt +++ b/core/takserver/src/commonTest/kotlin/org/meshtastic/core/takserver/MeshToCotBroadcasterTest.kt @@ -212,13 +212,21 @@ class MeshToCotBroadcasterTest { ) = Node( num = num, user = - org.meshtastic.proto.User( - id = "!" + num.toUInt().toString(16).padStart(8, '0'), - short_name = "WOLF", - long_name = "Wolf Ridge Relay", - hw_model = hwModel, - ), - position = org.meshtastic.proto.Position(latitude_i = latitudeI, longitude_i = longitudeI), + org.meshtastic.proto.User.Builder() + .also { wb -> + wb.id = "!" + num.toUInt().toString(16).padStart(8, '0') + wb.short_name = "WOLF" + wb.long_name = "Wolf Ridge Relay" + wb.hw_model = hwModel + } + .build(), + position = + org.meshtastic.proto.Position.Builder() + .also { wb -> + wb.latitude_i = latitudeI + wb.longitude_i = longitudeI + } + .build(), lastHeard = lastHeard, ) diff --git a/core/takserver/src/commonTest/kotlin/org/meshtastic/core/takserver/TAKDefaultsTest.kt b/core/takserver/src/commonTest/kotlin/org/meshtastic/core/takserver/TAKDefaultsTest.kt index bcb0d0b9c5..b845abf9e6 100644 --- a/core/takserver/src/commonTest/kotlin/org/meshtastic/core/takserver/TAKDefaultsTest.kt +++ b/core/takserver/src/commonTest/kotlin/org/meshtastic/core/takserver/TAKDefaultsTest.kt @@ -90,19 +90,40 @@ class TAKDefaultsTest { @Test fun `toTakCallsign prefers short_name`() { - val user = User(id = "!1234", long_name = "Long Name", short_name = "SN") + val user = + User.Builder() + .also { wb -> + wb.id = "!1234" + wb.long_name = "Long Name" + wb.short_name = "SN" + } + .build() assertEquals("SN", user.toTakCallsign()) } @Test fun `toTakCallsign falls back to long_name when short_name is blank`() { - val user = User(id = "!1234", long_name = "Long Name", short_name = "") + val user = + User.Builder() + .also { wb -> + wb.id = "!1234" + wb.long_name = "Long Name" + wb.short_name = "" + } + .build() assertEquals("Long Name", user.toTakCallsign()) } @Test fun `toTakCallsign falls back to id when both names are blank`() { - val user = User(id = "!1234", long_name = "", short_name = "") + val user = + User.Builder() + .also { wb -> + wb.id = "!1234" + wb.long_name = "" + wb.short_name = "" + } + .build() assertEquals("!1234", user.toTakCallsign()) } diff --git a/core/takserver/src/commonTest/kotlin/org/meshtastic/core/takserver/TAKMeshIntegrationTest.kt b/core/takserver/src/commonTest/kotlin/org/meshtastic/core/takserver/TAKMeshIntegrationTest.kt index 5b48e9687a..f32e0d0bda 100644 --- a/core/takserver/src/commonTest/kotlin/org/meshtastic/core/takserver/TAKMeshIntegrationTest.kt +++ b/core/takserver/src/commonTest/kotlin/org/meshtastic/core/takserver/TAKMeshIntegrationTest.kt @@ -66,8 +66,8 @@ class TAKMeshIntegrationTest { // FakeTAKServerManager lives in its own file in this source set, shared with MeshToCotBroadcasterTest. private class FakeMeshConfigHandler : MeshConfigHandler { - override val localConfig: StateFlow = MutableStateFlow(LocalConfig()) - override val moduleConfig: StateFlow = MutableStateFlow(LocalModuleConfig()) + override val localConfig: StateFlow = MutableStateFlow(LocalConfig.Builder().build()) + override val moduleConfig: StateFlow = MutableStateFlow(LocalModuleConfig.Builder().build()) override fun handleDeviceConfig(config: Config, session: RadioSessionContext): Boolean = true @@ -107,7 +107,7 @@ class TAKMeshIntegrationTest { override val ourNodeInfo: StateFlow = MutableStateFlow(null) override val myId: StateFlow = MutableStateFlow(null) - override val localStats: StateFlow = MutableStateFlow(LocalStats()) + override val localStats: StateFlow = MutableStateFlow(LocalStats.Builder().build()) override val nodeDBbyNum: StateFlow> = MutableStateFlow(emptyMap()) override suspend fun getNodeDbSnapshot(): Map = nodeDBbyNum.value @@ -121,9 +121,9 @@ class TAKMeshIntegrationTest { override fun getNode(userId: String): Node = Node(num = 0) - override fun getUser(nodeNum: Int): User = User() + override fun getUser(nodeNum: Int): User = User.Builder().build() - override fun getUser(userId: String): User = User() + override fun getUser(userId: String): User = User.Builder().build() override fun getNodes( sort: NodeSortOption, @@ -304,10 +304,17 @@ class TAKMeshIntegrationTest { h.integration.start(backgroundScope) val textPacket = - MeshPacket( - decoded = - Data(portnum = PortNum.TEXT_MESSAGE_APP, payload = "hello".encodeToByteArray().toByteString()), - ) + MeshPacket.Builder() + .also { wb -> + wb.decoded = + Data.Builder() + .also { wb -> + wb.portnum = PortNum.TEXT_MESSAGE_APP + wb.payload = "hello".encodeToByteArray().toByteString() + } + .build() + } + .build() h.serviceRepository.emitMeshPacket(textPacket) assertTrue(h.serverManager.broadcasts.isEmpty()) @@ -467,26 +474,46 @@ class TAKMeshIntegrationTest { private fun createV1PliMeshPacket(isCompressed: Boolean = false): MeshPacket { val takPacket = - TAKPacket( - is_compressed = isCompressed, - contact = org.meshtastic.proto.Contact(callsign = "BRAVO", device_callsign = "bravo-uid"), - pli = - org.meshtastic.proto.PLI( - latitude_i = 330000000, - longitude_i = -840000000, - altitude = 100, - speed = 0, - course = 0, - ), - group = - org.meshtastic.proto.Group( - team = org.meshtastic.proto.Team.Cyan, - role = org.meshtastic.proto.MemberRole.TeamMember, - ), - status = org.meshtastic.proto.Status(battery = 85), - ) - return MeshPacket( - decoded = Data(portnum = PortNum.ATAK_PLUGIN, payload = TAKPacket.ADAPTER.encode(takPacket).toByteString()), - ) + TAKPacket.Builder() + .also { wb -> + wb.is_compressed = isCompressed + wb.contact = + org.meshtastic.proto.Contact.Builder() + .also { wb -> + wb.callsign = "BRAVO" + wb.device_callsign = "bravo-uid" + } + .build() + wb.pli = + org.meshtastic.proto.PLI.Builder() + .also { wb -> + wb.latitude_i = 330000000 + wb.longitude_i = -840000000 + wb.altitude = 100 + wb.speed = 0 + wb.course = 0 + } + .build() + wb.group = + org.meshtastic.proto.Group.Builder() + .also { wb -> + wb.team = org.meshtastic.proto.Team.Cyan + wb.role = org.meshtastic.proto.MemberRole.TeamMember + } + .build() + wb.status = org.meshtastic.proto.Status.Builder().also { wb -> wb.battery = 85 }.build() + } + .build() + return MeshPacket.Builder() + .also { wb -> + wb.decoded = + Data.Builder() + .also { wb -> + wb.portnum = PortNum.ATAK_PLUGIN + wb.payload = TAKPacket.ADAPTER.encode(takPacket).toByteString() + } + .build() + } + .build() } } diff --git a/core/takserver/src/commonTest/kotlin/org/meshtastic/core/takserver/TAKPacketConversionTest.kt b/core/takserver/src/commonTest/kotlin/org/meshtastic/core/takserver/TAKPacketConversionTest.kt index 771f10cfec..2232c5a744 100644 --- a/core/takserver/src/commonTest/kotlin/org/meshtastic/core/takserver/TAKPacketConversionTest.kt +++ b/core/takserver/src/commonTest/kotlin/org/meshtastic/core/takserver/TAKPacketConversionTest.kt @@ -69,13 +69,36 @@ class TAKPacketConversionTest { @Test fun testTAKPacketToCoTMessagePLI() { val takPacket = - TAKPacket( - is_compressed = false, - contact = Contact(callsign = "Alice", device_callsign = "!5678"), - group = Group(team = Team.Cyan, role = MemberRole.HQ), - status = Status(battery = 85), - pli = PLI(latitude_i = 300000000, longitude_i = -800000000, altitude = 50, speed = 5, course = 90), - ) + TAKPacket.Builder() + .also { wb -> + wb.is_compressed = false + wb.contact = + Contact.Builder() + .also { wb -> + wb.callsign = "Alice" + wb.device_callsign = "!5678" + } + .build() + wb.group = + Group.Builder() + .also { wb -> + wb.team = Team.Cyan + wb.role = MemberRole.HQ + } + .build() + wb.status = Status.Builder().also { wb -> wb.battery = 85 }.build() + wb.pli = + PLI.Builder() + .also { wb -> + wb.latitude_i = 300000000 + wb.longitude_i = -800000000 + wb.altitude = 50 + wb.speed = 5 + wb.course = 90 + } + .build() + } + .build() val cot = takPacket.toCoTMessage() assertNotNull(cot) @@ -138,11 +161,25 @@ class TAKPacketConversionTest { @Test fun testParseSmuggledMessageId() { val takPacket = - TAKPacket( - is_compressed = false, - contact = Contact(callsign = "Alice", device_callsign = "alice-device-456|msg-789"), - chat = GeoChat(message = "Hi Bob", to = "Bob"), - ) + TAKPacket.Builder() + .also { wb -> + wb.is_compressed = false + wb.contact = + Contact.Builder() + .also { wb -> + wb.callsign = "Alice" + wb.device_callsign = "alice-device-456|msg-789" + } + .build() + wb.chat = + GeoChat.Builder() + .also { wb -> + wb.message = "Hi Bob" + wb.to = "Bob" + } + .build() + } + .build() val cot = takPacket.toCoTMessage() assertNotNull(cot) diff --git a/core/takserver/src/commonTest/kotlin/org/meshtastic/core/takserver/TakMeshTestRunnerTest.kt b/core/takserver/src/commonTest/kotlin/org/meshtastic/core/takserver/TakMeshTestRunnerTest.kt index bf1c5f9996..a1ba51a2dd 100644 --- a/core/takserver/src/commonTest/kotlin/org/meshtastic/core/takserver/TakMeshTestRunnerTest.kt +++ b/core/takserver/src/commonTest/kotlin/org/meshtastic/core/takserver/TakMeshTestRunnerTest.kt @@ -49,8 +49,8 @@ import kotlin.test.assertTrue class TakMeshTestRunnerTest { private class FakeMeshConfigHandler : MeshConfigHandler { - override val localConfig = MutableStateFlow(LocalConfig()) - override val moduleConfig = MutableStateFlow(LocalModuleConfig()) + override val localConfig = MutableStateFlow(LocalConfig.Builder().build()) + override val moduleConfig = MutableStateFlow(LocalModuleConfig.Builder().build()) override fun handleDeviceConfig(config: Config, session: RadioSessionContext) = true diff --git a/core/takserver/src/commonTest/kotlin/org/meshtastic/core/takserver/TakV2CompressorTaktalkTest.kt b/core/takserver/src/commonTest/kotlin/org/meshtastic/core/takserver/TakV2CompressorTaktalkTest.kt index e5bf68c1b8..7d4440fb5f 100644 --- a/core/takserver/src/commonTest/kotlin/org/meshtastic/core/takserver/TakV2CompressorTaktalkTest.kt +++ b/core/takserver/src/commonTest/kotlin/org/meshtastic/core/takserver/TakV2CompressorTaktalkTest.kt @@ -43,18 +43,22 @@ class TakV2CompressorTaktalkTest { @Test fun `m-t-t taktalk message round-trips with voice marker`() { val original = - TAKPacketV2( - cot_type_id = CotType.CotType_m_t_t, - callsign = "ASPEN", - uid = "TAKTALK-MESSAGE-81b44b48-21c5-4edf-aab2-b6bd2ec1f52f", - taktalk = - TakTalkMessage( - text = "Testing 123, testing 123.", - chatroom_id = "1", - lang = "English", - from_voice = true, - ), - ) + TAKPacketV2.Builder() + .also { wb -> + wb.cot_type_id = CotType.CotType_m_t_t + wb.callsign = "ASPEN" + wb.uid = "TAKTALK-MESSAGE-81b44b48-21c5-4edf-aab2-b6bd2ec1f52f" + wb.taktalk = + TakTalkMessage.Builder() + .also { wb -> + wb.text = "Testing 123, testing 123." + wb.chatroom_id = "1" + wb.lang = "English" + wb.from_voice = true + } + .build() + } + .build() val wire = TakV2Compressor.compress(original) val decompressed = TakV2Compressor.decompress(wire) @@ -73,13 +77,22 @@ class TakV2CompressorTaktalkTest { @Test fun `m-t-t taktalk text-only message has from_voice false`() { val original = - TAKPacketV2( - cot_type_id = CotType.CotType_m_t_t, - callsign = "ETHEL", - uid = "TAKTALK-MESSAGE-text-only", - taktalk = - TakTalkMessage(text = "typed message", chatroom_id = "1", lang = "English", from_voice = false), - ) + TAKPacketV2.Builder() + .also { wb -> + wb.cot_type_id = CotType.CotType_m_t_t + wb.callsign = "ETHEL" + wb.uid = "TAKTALK-MESSAGE-text-only" + wb.taktalk = + TakTalkMessage.Builder() + .also { wb -> + wb.text = "typed message" + wb.chatroom_id = "1" + wb.lang = "English" + wb.from_voice = false + } + .build() + } + .build() val decompressed = TakV2Compressor.decompress(TakV2Compressor.compress(original)) @@ -91,21 +104,25 @@ class TakV2CompressorTaktalkTest { @Test fun `y- taktalk room broadcast round-trips with participant list`() { val original = - TAKPacketV2( - cot_type_id = CotType.CotType_y, - uid = "ROOM-DATA-5eac9336-2698-4dbb-9fa2-62b100e469fe", - // v0.3.2: sender identity lives on envelope packet.callsign, - // not on the deprecated TakTalkRoomData.sender_callsign field. - // The SDK builder reconstitutes from envelope - // callsign on emit. - callsign = "ASPEN", - taktalk_room = - TakTalkRoomData( - room_id = "30b2755c-c547-44ef-a0cc-cdbd8a15616f", - room_name = "test", - participants = listOf("ETHEL", "ASPEN"), - ), - ) + TAKPacketV2.Builder() + .also { wb -> + wb.cot_type_id = CotType.CotType_y + wb.uid = "ROOM-DATA-5eac9336-2698-4dbb-9fa2-62b100e469fe" + // v0.3.2: sender identity lives on envelope packet.callsign, + // not on the deprecated TakTalkRoomData.sender_callsign field. + // The SDK builder reconstitutes from envelope + // callsign on emit. + wb.callsign = "ASPEN" + wb.taktalk_room = + TakTalkRoomData.Builder() + .also { wb -> + wb.room_id = "30b2755c-c547-44ef-a0cc-cdbd8a15616f" + wb.room_name = "test" + wb.participants = listOf("ETHEL", "ASPEN") + } + .build() + } + .build() val decompressed = TakV2Compressor.decompress(TakV2Compressor.compress(original)) @@ -124,22 +141,27 @@ class TakV2CompressorTaktalkTest { @Test fun `m-t-t with marti round-trips dest callsigns and survives compression`() { val original = - TAKPacketV2( - cot_type_id = CotType.CotType_m_t_t, - uid = "TAKTALK-MESSAGE-marti-test-uuid", - callsign = "ASPEN", - taktalk = - TakTalkMessage( - text = "Push-to-talk to ETHEL", - chatroom_id = "1", - lang = "English", - from_voice = true, - ), - // Directed-routing recipient list. TAKTALK gates voice TTS on - // this list matching the receiver's callsign — a regression - // here silently breaks voice messaging end-to-end. - marti = org.meshtastic.proto.Marti(dest_callsign = listOf("ETHEL")), - ) + TAKPacketV2.Builder() + .also { wb -> + wb.cot_type_id = CotType.CotType_m_t_t + wb.uid = "TAKTALK-MESSAGE-marti-test-uuid" + wb.callsign = "ASPEN" + wb.taktalk = + TakTalkMessage.Builder() + .also { wb -> + wb.text = "Push-to-talk to ETHEL" + wb.chatroom_id = "1" + wb.lang = "English" + wb.from_voice = true + } + .build() + // Directed-routing recipient list. TAKTALK gates voice TTS on + // this list matching the receiver's callsign — a regression + // here silently breaks voice messaging end-to-end. + wb.marti = + org.meshtastic.proto.Marti.Builder().also { wb -> wb.dest_callsign = listOf("ETHEL") }.build() + } + .build() val decompressed = TakV2Compressor.decompress(TakV2Compressor.compress(original)) @@ -156,18 +178,22 @@ class TakV2CompressorTaktalkTest { @Test fun `b-t-f chat carries TAKTALK sidecars through compressor`() { val original = - TAKPacketV2( - cot_type_id = CotType.CotType_b_t_f, - callsign = "ASPEN", - uid = "GeoChat.test", - chat = - GeoChat( - message = "Test message", - lang = "English", - room_id = "30b2755c-c547-44ef-a0cc-cdbd8a15616f", - voice_profile_id = "", // empty marker `` - ), - ) + TAKPacketV2.Builder() + .also { wb -> + wb.cot_type_id = CotType.CotType_b_t_f + wb.callsign = "ASPEN" + wb.uid = "GeoChat.test" + wb.chat = + GeoChat.Builder() + .also { wb -> + wb.message = "Test message" + wb.lang = "English" + wb.room_id = "30b2755c-c547-44ef-a0cc-cdbd8a15616f" + wb.voice_profile_id = "" // empty marker `` + } + .build() + } + .build() val decompressed = TakV2Compressor.decompress(TakV2Compressor.compress(original)) @@ -192,16 +218,20 @@ class TakV2CompressorTaktalkTest { // round-trip must not synthesize empty-string sidecars that would // change the rebuilt CoT XML for plain chat messages. val original = - TAKPacketV2( - cot_type_id = CotType.CotType_b_t_f, - callsign = "ALPHA-1", - uid = "GeoChat.plain", - chat = - GeoChat( - message = "plain chat", - // no lang, no room_id, no voice_profile_id - ), - ) + TAKPacketV2.Builder() + .also { wb -> + wb.cot_type_id = CotType.CotType_b_t_f + wb.callsign = "ALPHA-1" + wb.uid = "GeoChat.plain" + wb.chat = + GeoChat.Builder() + .also { wb -> + wb.message = "plain chat" + // no lang, no room_id, no voice_profile_id + } + .build() + } + .build() val decompressed = TakV2Compressor.decompress(TakV2Compressor.compress(original)) diff --git a/core/testing/src/commonMain/kotlin/org/meshtastic/core/testing/FakeAppPreferences.kt b/core/testing/src/commonMain/kotlin/org/meshtastic/core/testing/FakeAppPreferences.kt index 99bfc1127f..c1ecc4ab0e 100644 --- a/core/testing/src/commonMain/kotlin/org/meshtastic/core/testing/FakeAppPreferences.kt +++ b/core/testing/src/commonMain/kotlin/org/meshtastic/core/testing/FakeAppPreferences.kt @@ -18,6 +18,7 @@ package org.meshtastic.core.testing import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.update import org.meshtastic.core.model.DeviceType import org.meshtastic.core.repository.AnalyticsPrefs import org.meshtastic.core.repository.AppFunctionsPrefs @@ -27,9 +28,11 @@ import org.meshtastic.core.repository.FilterPrefs import org.meshtastic.core.repository.HomoglyphPrefs import org.meshtastic.core.repository.MapCameraPosition import org.meshtastic.core.repository.MapConsentPrefs +import org.meshtastic.core.repository.MapFilterPrefs import org.meshtastic.core.repository.MapPrefs import org.meshtastic.core.repository.MapTileProviderPrefs import org.meshtastic.core.repository.MeshPrefs +import org.meshtastic.core.repository.NodeFilterPrefs import org.meshtastic.core.repository.RadioPrefs import org.meshtastic.core.repository.UiPrefs @@ -110,46 +113,12 @@ class FakeUiPrefs : UiPrefs { nodeSort.value = value } - override val includeUnknown = MutableStateFlow(true) + override val nodeFilters = MutableStateFlow(NodeFilterPrefs()) - override fun setIncludeUnknown(value: Boolean) { - includeUnknown.value = value - } - - override val excludeInfrastructure = MutableStateFlow(false) - - override fun setExcludeInfrastructure(value: Boolean) { - excludeInfrastructure.value = value - } - - override val onlyOnline = MutableStateFlow(false) - - override fun setOnlyOnline(value: Boolean) { - onlyOnline.value = value - } - - override val onlyDirect = MutableStateFlow(false) - - override fun setOnlyDirect(value: Boolean) { - onlyDirect.value = value - } - - override val showIgnored = MutableStateFlow(false) - - override fun setShowIgnored(value: Boolean) { - showIgnored.value = value - } - - override val excludeMqtt = MutableStateFlow(false) - - override fun setExcludeMqtt(value: Boolean) { - excludeMqtt.value = value - } - - override val excludeUnheard = MutableStateFlow(false) - - override fun setExcludeUnheard(value: Boolean) { - excludeUnheard.value = value + override fun updateNodeFilters(transform: (NodeFilterPrefs) -> NodeFilterPrefs) { + // update, not read-then-assign: the real one is transactional, and a fake that loses a concurrent write + // would pass tests the production path fails. + nodeFilters.update(transform) } override val hasShownNotPairedWarning = MutableStateFlow(false) @@ -282,70 +251,10 @@ class FakeMapPrefs : MapPrefs { override suspend fun awaitMapStyle(): Int = mapStyle.value - override val showOnlyFavorites = MutableStateFlow(false) + override val mapFilters = MutableStateFlow(MapFilterPrefs()) - override fun setShowOnlyFavorites(show: Boolean) { - showOnlyFavorites.value = show - } - - override val showWaypointsOnMap = MutableStateFlow(true) - - override fun setShowWaypointsOnMap(show: Boolean) { - showWaypointsOnMap.value = show - } - - override val showPrecisionCircleOnMap = MutableStateFlow(true) - - override fun setShowPrecisionCircleOnMap(show: Boolean) { - showPrecisionCircleOnMap.value = show - } - - override val lastHeardFilter = MutableStateFlow(0L) - - override fun setLastHeardFilter(seconds: Long) { - lastHeardFilter.value = seconds - } - - override val lastHeardTrackFilter = MutableStateFlow(0L) - - override fun setLastHeardTrackFilter(seconds: Long) { - lastHeardTrackFilter.value = seconds - } - - override val onlyOnlineOnMap = MutableStateFlow(false) - - override fun setOnlyOnlineOnMap(only: Boolean) { - onlyOnlineOnMap.value = only - } - - override val onlyDirectOnMap = MutableStateFlow(false) - - override fun setOnlyDirectOnMap(only: Boolean) { - onlyDirectOnMap.value = only - } - - override val excludeMqttOnMap = MutableStateFlow(false) - - override fun setExcludeMqttOnMap(exclude: Boolean) { - excludeMqttOnMap.value = exclude - } - - override val showIgnoredOnMap = MutableStateFlow(false) - - override fun setShowIgnoredOnMap(show: Boolean) { - showIgnoredOnMap.value = show - } - - override val includeUnknownOnMap = MutableStateFlow(true) - - override fun setIncludeUnknownOnMap(include: Boolean) { - includeUnknownOnMap.value = include - } - - override val excludedMapRoles = MutableStateFlow>(emptySet()) - - override fun setExcludedMapRoles(roles: Set) { - excludedMapRoles.value = roles + override fun updateMapFilters(transform: (MapFilterPrefs) -> MapFilterPrefs) { + mapFilters.update(transform) } override val hiddenLayerUrls = MutableStateFlow>(emptySet()) diff --git a/core/testing/src/commonMain/kotlin/org/meshtastic/core/testing/FakeCommandSender.kt b/core/testing/src/commonMain/kotlin/org/meshtastic/core/testing/FakeCommandSender.kt index 7f0fd81576..8aebd56494 100644 --- a/core/testing/src/commonMain/kotlin/org/meshtastic/core/testing/FakeCommandSender.kt +++ b/core/testing/src/commonMain/kotlin/org/meshtastic/core/testing/FakeCommandSender.kt @@ -113,9 +113,9 @@ class FakeCommandSender : override fun getCurrentPacketId(): Long = nextPacketId.toLong() - override fun getCachedLocalConfig(): LocalConfig = LocalConfig() + override fun getCachedLocalConfig(): LocalConfig = LocalConfig.Builder().build() - override fun getCachedChannelSet(): ChannelSet = ChannelSet() + override fun getCachedChannelSet(): ChannelSet = ChannelSet.Builder().build() override fun generatePacketId(): Int = nextPacketId++ diff --git a/core/testing/src/commonMain/kotlin/org/meshtastic/core/testing/FakeLocalStatsDataSource.kt b/core/testing/src/commonMain/kotlin/org/meshtastic/core/testing/FakeLocalStatsDataSource.kt index 43b837c968..8e2ed15a76 100644 --- a/core/testing/src/commonMain/kotlin/org/meshtastic/core/testing/FakeLocalStatsDataSource.kt +++ b/core/testing/src/commonMain/kotlin/org/meshtastic/core/testing/FakeLocalStatsDataSource.kt @@ -24,7 +24,7 @@ import org.meshtastic.proto.LocalStats class FakeLocalStatsDataSource : BaseFake(), LocalStatsDataSource { - private val _localStatsFlow = mutableStateFlow(LocalStats()) + private val _localStatsFlow = mutableStateFlow(LocalStats.Builder().build()) override val localStatsFlow: StateFlow = _localStatsFlow override suspend fun setLocalStats(stats: LocalStats) { @@ -32,6 +32,6 @@ class FakeLocalStatsDataSource : } override suspend fun clearLocalStats() { - _localStatsFlow.value = LocalStats() + _localStatsFlow.value = LocalStats.Builder().build() } } diff --git a/core/testing/src/commonMain/kotlin/org/meshtastic/core/testing/FakeNodeRepository.kt b/core/testing/src/commonMain/kotlin/org/meshtastic/core/testing/FakeNodeRepository.kt index ded0a96461..84b233fc4f 100644 --- a/core/testing/src/commonMain/kotlin/org/meshtastic/core/testing/FakeNodeRepository.kt +++ b/core/testing/src/commonMain/kotlin/org/meshtastic/core/testing/FakeNodeRepository.kt @@ -55,7 +55,7 @@ class FakeNodeRepository : private val _myId = mutableStateFlow(null) override val myId: StateFlow = _myId - private val _localStats = mutableStateFlow(LocalStats()) + private val _localStats = mutableStateFlow(LocalStats.Builder().build()) override val localStats: StateFlow = _localStats private val _nodeDBbyNum = mutableStateFlow>(emptyMap()) @@ -70,12 +70,13 @@ class FakeNodeRepository : override fun effectiveLogNodeId(nodeNum: Int): Flow = MutableStateFlow(0) - override fun getNode(userId: String): Node = - _nodeDBbyNum.value.values.find { it.user.id == userId } ?: Node(num = 0, user = User(id = userId)) + override fun getNode(userId: String): Node = _nodeDBbyNum.value.values.find { it.user.id == userId } + ?: Node(num = 0, user = User.Builder().also { wb -> wb.id = userId }.build()) - override fun getUser(nodeNum: Int): User = _nodeDBbyNum.value[nodeNum]?.user ?: User() + override fun getUser(nodeNum: Int): User = _nodeDBbyNum.value[nodeNum]?.user ?: User.Builder().build() - override fun getUser(userId: String): User = _nodeDBbyNum.value.values.find { it.user.id == userId }?.user ?: User() + override fun getUser(userId: String): User = + _nodeDBbyNum.value.values.find { it.user.id == userId }?.user ?: User.Builder().build() override fun getNodes( sort: NodeSortOption, @@ -121,7 +122,7 @@ class FakeNodeRepository : node.user.id.contains(filter, ignoreCase = true) val matchesUnknown = includeUnknown || !node.isUnknownUser val matchesOnline = !onlyOnline || node.isOnline - val matchesDirect = !onlyDirect || node.hopsAway == 0 + val matchesDirect = !onlyDirect || (node.hopsAway == 0 && !node.viaMqtt) return matchesFilter && matchesUnknown && matchesOnline && matchesDirect } diff --git a/core/testing/src/commonMain/kotlin/org/meshtastic/core/testing/FakeRadioConfigRepository.kt b/core/testing/src/commonMain/kotlin/org/meshtastic/core/testing/FakeRadioConfigRepository.kt index c8197efefa..d0de5d0bb3 100644 --- a/core/testing/src/commonMain/kotlin/org/meshtastic/core/testing/FakeRadioConfigRepository.kt +++ b/core/testing/src/commonMain/kotlin/org/meshtastic/core/testing/FakeRadioConfigRepository.kt @@ -43,16 +43,16 @@ class FakeRadioConfigRepository : data class ChannelSetUpdate(val settingsList: List?, val loraConfig: Config.LoRaConfig?) - private val channelSetBacking = mutableStateFlow(ChannelSet()) + private val channelSetBacking = mutableStateFlow(ChannelSet.Builder().build()) override val channelSetFlow: Flow = channelSetBacking - private val localConfigBacking = mutableStateFlow(LocalConfig()) + private val localConfigBacking = mutableStateFlow(LocalConfig.Builder().build()) override val localConfigFlow: Flow = localConfigBacking - private val moduleConfigBacking = mutableStateFlow(LocalModuleConfig()) + private val moduleConfigBacking = mutableStateFlow(LocalModuleConfig.Builder().build()) override val moduleConfigFlow: Flow = moduleConfigBacking - private val deviceProfileBacking = mutableStateFlow(DeviceProfile()) + private val deviceProfileBacking = mutableStateFlow(DeviceProfile.Builder().build()) override val deviceProfileFlow: Flow = deviceProfileBacking val currentDeviceProfile: DeviceProfile get() = deviceProfileBacking.value @@ -102,12 +102,13 @@ class FakeRadioConfigRepository : registerResetAction { lastSetLocalConfig = null lastSetModuleConfig = null + reconcileConversationsCount = 0 channelSetUpdates.clear() } } override suspend fun clearChannelSet() { - channelSetBacking.value = ChannelSet() + channelSetBacking.value = ChannelSet.Builder().build() } override suspend fun replaceAllSettings(settingsList: List) { @@ -118,18 +119,32 @@ class FakeRadioConfigRepository : channelSetUpdates += ChannelSetUpdate(settingsList, loraConfig) val current = channelSetBacking.value channelSetBacking.value = - current.copy(settings = settingsList ?: current.settings, lora_config = loraConfig ?: current.lora_config) + current + .newBuilder() + .also { wb -> + wb.settings = settingsList ?: current.settings + wb.lora_config = loraConfig ?: current.lora_config + } + .build() + } + + /** Number of times [reconcileConversations] was called. */ + var reconcileConversationsCount: Int = 0 + private set + + override suspend fun reconcileConversations() { + reconcileConversationsCount++ } override suspend fun updateChannelSettings(channel: Channel) { val current = channelSetBacking.value.settings.toMutableList() - while (current.size <= channel.index) current.add(ChannelSettings()) - current[channel.index] = channel.settings ?: ChannelSettings() - channelSetBacking.value = channelSetBacking.value.copy(settings = current) + while (current.size <= channel.index) current.add(ChannelSettings.Builder().build()) + current[channel.index] = channel.settings ?: ChannelSettings.Builder().build() + channelSetBacking.value = channelSetBacking.value.newBuilder().also { wb -> wb.settings = current }.build() } override suspend fun clearLocalConfig() { - localConfigBacking.value = LocalConfig() + localConfigBacking.value = LocalConfig.Builder().build() } override suspend fun setLocalConfig(config: Config) { @@ -137,7 +152,7 @@ class FakeRadioConfigRepository : } override suspend fun clearLocalModuleConfig() { - moduleConfigBacking.value = LocalModuleConfig() + moduleConfigBacking.value = LocalModuleConfig.Builder().build() } override suspend fun setLocalModuleConfig(config: ModuleConfig) { diff --git a/core/testing/src/commonMain/kotlin/org/meshtastic/core/testing/TestDataFactory.kt b/core/testing/src/commonMain/kotlin/org/meshtastic/core/testing/TestDataFactory.kt index 55c1c7b97b..894e54565f 100644 --- a/core/testing/src/commonMain/kotlin/org/meshtastic/core/testing/TestDataFactory.kt +++ b/core/testing/src/commonMain/kotlin/org/meshtastic/core/testing/TestDataFactory.kt @@ -49,8 +49,17 @@ object TestDataFactory { hwModel: org.meshtastic.proto.HardwareModel = org.meshtastic.proto.HardwareModel.UNSET, batteryLevel: Int? = 100, ): Node { - val user = User(id = userId, long_name = longName, short_name = shortName, hw_model = hwModel) - val metrics = org.meshtastic.proto.DeviceMetrics(battery_level = batteryLevel) + val user = + User.Builder() + .also { wb -> + wb.id = userId + wb.long_name = longName + wb.short_name = shortName + wb.hw_model = hwModel + } + .build() + val metrics = + org.meshtastic.proto.DeviceMetrics.Builder().also { wb -> wb.battery_level = batteryLevel }.build() return Node( num = num, user = user, @@ -68,7 +77,14 @@ object TestDataFactory { to: Int = 0xffffffff.toInt(), decoded: org.meshtastic.proto.Data? = null, relayNode: Int = 0, - ) = org.meshtastic.proto.MeshPacket(from = from, to = to, decoded = decoded, relay_node = relayNode) + ) = org.meshtastic.proto.MeshPacket.Builder() + .also { wb -> + wb.from = from + wb.to = to + wb.decoded = decoded + wb.relay_node = relayNode + } + .build() /** Creates multiple test nodes with sequential IDs. */ fun createTestNodes(count: Int, baseNum: Int = 1): List = (0 until count).map { i -> diff --git a/core/testing/src/commonTest/kotlin/org/meshtastic/core/testing/FakeNodeRepositoryTest.kt b/core/testing/src/commonTest/kotlin/org/meshtastic/core/testing/FakeNodeRepositoryTest.kt index b12c54f8f9..796c6dde51 100644 --- a/core/testing/src/commonTest/kotlin/org/meshtastic/core/testing/FakeNodeRepositoryTest.kt +++ b/core/testing/src/commonTest/kotlin/org/meshtastic/core/testing/FakeNodeRepositoryTest.kt @@ -33,9 +33,9 @@ class FakeNodeRepositoryTest { fun `getNodes sorting by name`() = runTest { val nodes = listOf( - Node(num = 1, user = User(long_name = "Charlie")), - Node(num = 2, user = User(long_name = "Alice")), - Node(num = 3, user = User(long_name = "Bob")), + Node(num = 1, user = User.Builder().also { wb -> wb.long_name = "Charlie" }.build()), + Node(num = 2, user = User.Builder().also { wb -> wb.long_name = "Alice" }.build()), + Node(num = 3, user = User.Builder().also { wb -> wb.long_name = "Bob" }.build()), ) repository.setNodes(nodes) @@ -49,8 +49,16 @@ class FakeNodeRepositoryTest { @Test fun `getUnknownNodes returns nodes with UNSET hw_model`() = runTest { - val node1 = Node(num = 1, user = User(hw_model = org.meshtastic.proto.HardwareModel.UNSET)) - val node2 = Node(num = 2, user = User(hw_model = org.meshtastic.proto.HardwareModel.TLORA_V2)) + val node1 = + Node( + num = 1, + user = User.Builder().also { wb -> wb.hw_model = org.meshtastic.proto.HardwareModel.UNSET }.build(), + ) + val node2 = + Node( + num = 2, + user = User.Builder().also { wb -> wb.hw_model = org.meshtastic.proto.HardwareModel.TLORA_V2 }.build(), + ) repository.setNodes(listOf(node1, node2)) val result = repository.getUnknownNodes() @@ -84,11 +92,26 @@ class FakeNodeRepositoryTest { } } + @Test + fun `getNodes filtering by onlyDirect drops an mqtt node at zero hops`() = runTest { + // An MQTT-bridged node reports the hop count its uplink gateway heard, not ours, so it is not direct. + val node1 = Node(num = 1, hopsAway = 0) + val node2 = Node(num = 2, hopsAway = 0, viaMqtt = true) + repository.setNodes(listOf(node1, node2)) + + repository.getNodes(onlyDirect = true).test { + val result = awaitItem() + assertEquals(1, result.size) + assertEquals(1, result[0].num) + } + } + @Test fun `insertMetadata updates node metadata`() = runTest { val nodeNum = 1234 repository.upsert(Node(num = nodeNum)) - val metadata = org.meshtastic.proto.DeviceMetadata(firmware_version = "2.5.0") + val metadata = + org.meshtastic.proto.DeviceMetadata.Builder().also { wb -> wb.firmware_version = "2.5.0" }.build() repository.insertMetadata(nodeNum, metadata) val node = repository.nodeDBbyNum.value[nodeNum] diff --git a/core/testing/src/commonTest/kotlin/org/meshtastic/core/testing/FakeRadioControllerRestoreTest.kt b/core/testing/src/commonTest/kotlin/org/meshtastic/core/testing/FakeRadioControllerRestoreTest.kt index 93d849821a..5cd7ecfc7c 100644 --- a/core/testing/src/commonTest/kotlin/org/meshtastic/core/testing/FakeRadioControllerRestoreTest.kt +++ b/core/testing/src/commonTest/kotlin/org/meshtastic/core/testing/FakeRadioControllerRestoreTest.kt @@ -46,7 +46,7 @@ class FakeRadioControllerRestoreTest { assertTrue( controller.restoreLocalConfiguration( expectedDeviceAddress = "x:DEVICE", - config = Config(), + config = Config.Builder().build(), primaryChannel = null, ), ) @@ -55,8 +55,17 @@ class FakeRadioControllerRestoreTest { @Test fun restoreRequiresSelectedDeviceOwnership() = runTest { val controller = FakeRadioController().apply { selectedDeviceAddress = "x:CURRENT" } - val channel = Channel(index = 0, settings = ChannelSettings(name = "Primary")) - val config = Config(lora = Config.LoRaConfig(use_preset = true)) + val channel = + Channel.Builder() + .also { wb -> + wb.index = 0 + wb.settings = ChannelSettings.Builder().also { wb -> wb.name = "Primary" }.build() + } + .build() + val config = + Config.Builder() + .also { wb -> wb.lora = Config.LoRaConfig.Builder().also { wb -> wb.use_preset = true }.build() } + .build() assertFalse( controller.restoreLocalConfiguration( @@ -82,8 +91,17 @@ class FakeRadioControllerRestoreTest { @Test fun restoreRejectsMissingSelectedDeviceOwnership() = runTest { val controller = FakeRadioController() - val channel = Channel(index = 0, settings = ChannelSettings(name = "Primary")) - val config = Config(lora = Config.LoRaConfig(use_preset = true)) + val channel = + Channel.Builder() + .also { wb -> + wb.index = 0 + wb.settings = ChannelSettings.Builder().also { wb -> wb.name = "Primary" }.build() + } + .build() + val config = + Config.Builder() + .also { wb -> wb.lora = Config.LoRaConfig.Builder().also { wb -> wb.use_preset = true }.build() } + .build() val restored = controller.restoreLocalConfiguration( @@ -104,8 +122,17 @@ class FakeRadioControllerRestoreTest { selectedDeviceAddress = "x:CURRENT" throwOnSetLocalConfig = true } - val channel = Channel(index = 0, settings = ChannelSettings(name = "Primary")) - val config = Config(lora = Config.LoRaConfig(use_preset = true)) + val channel = + Channel.Builder() + .also { wb -> + wb.index = 0 + wb.settings = ChannelSettings.Builder().also { wb -> wb.name = "Primary" }.build() + } + .build() + val config = + Config.Builder() + .also { wb -> wb.lora = Config.LoRaConfig.Builder().also { wb -> wb.use_preset = true }.build() } + .build() assertFailsWith { controller.restoreLocalConfiguration( @@ -129,7 +156,10 @@ class FakeRadioControllerRestoreTest { writeEntered.complete(Unit) releaseWrite.await() } - val config = Config(lora = Config.LoRaConfig(use_preset = true)) + val config = + Config.Builder() + .also { wb -> wb.lora = Config.LoRaConfig.Builder().also { wb -> wb.use_preset = true }.build() } + .build() val restore = async { controller.restoreLocalConfiguration( diff --git a/core/testing/src/commonTest/kotlin/org/meshtastic/core/testing/RepositoryFakesTest.kt b/core/testing/src/commonTest/kotlin/org/meshtastic/core/testing/RepositoryFakesTest.kt index 9e57c120ed..d1ab367737 100644 --- a/core/testing/src/commonTest/kotlin/org/meshtastic/core/testing/RepositoryFakesTest.kt +++ b/core/testing/src/commonTest/kotlin/org/meshtastic/core/testing/RepositoryFakesTest.kt @@ -124,7 +124,11 @@ class RepositoryFakesTest { @Test fun `FakeTracerouteSnapshotRepository roundtrips positions keyed by log uuid`() = runTest { val repo = FakeTracerouteSnapshotRepository() - val positions = mapOf(1 to Position(latitude_i = 10), 2 to Position(latitude_i = 20)) + val positions = + mapOf( + 1 to Position.Builder().also { wb -> wb.latitude_i = 10 }.build(), + 2 to Position.Builder().also { wb -> wb.latitude_i = 20 }.build(), + ) repo.upsertSnapshotPositions(logUuid = "log-1", requestId = 99, positions = positions) repo.getSnapshotPositions("log-1").test { assertEquals(positions, awaitItem()) } @@ -189,9 +193,15 @@ class RepositoryFakesTest { @Test fun `FakeRadioController preserves configuration and fixed-position evidence until reset`() = runTest { val controller = FakeRadioController() - val local = Config(device = Config.DeviceConfig()) - val admin = Config(lora = Config.LoRaConfig(hop_limit = 5)) - val module = ModuleConfig(serial = ModuleConfig.SerialConfig(enabled = true)) + val local = Config.Builder().also { wb -> wb.device = Config.DeviceConfig.Builder().build() }.build() + val admin = + Config.Builder() + .also { wb -> wb.lora = Config.LoRaConfig.Builder().also { wb -> wb.hop_limit = 5 }.build() } + .build() + val module = + ModuleConfig.Builder() + .also { wb -> wb.serial = ModuleConfig.SerialConfig.Builder().also { wb -> wb.enabled = true }.build() } + .build() val position = ModelPosition(latitude = 1.0, longitude = 2.0, altitude = 3) controller.setLocalConfig(local) @@ -242,8 +252,11 @@ class RepositoryFakesTest { @Test fun `FakeRadioController uses null destination consistently for local config writes`() = runTest { val controller = FakeRadioController() - val config = Config(device = Config.DeviceConfig()) - val moduleConfig = ModuleConfig(serial = ModuleConfig.SerialConfig(enabled = true)) + val config = Config.Builder().also { wb -> wb.device = Config.DeviceConfig.Builder().build() }.build() + val moduleConfig = + ModuleConfig.Builder() + .also { wb -> wb.serial = ModuleConfig.SerialConfig.Builder().also { wb -> wb.enabled = true }.build() } + .build() controller.setConfig(destNum = 0, config = config, packetId = 1) controller.setModuleConfig(destNum = 0, config = moduleConfig, packetId = 2) @@ -319,7 +332,7 @@ class RepositoryFakesTest { @Test fun `FakeCommandSender records retryable admin and telemetry requests and clears them on reset`() = runTest { val sender = FakeCommandSender() - val adminMessage = AdminMessage(get_device_metadata_request = true) + val adminMessage = AdminMessage.Builder().also { wb -> wb.get_device_metadata_request = true }.build() sender.sendAdmin(destNum = 123, requestId = 7, wantResponse = true) { adminMessage } sender.requestTelemetry(requestId = 8, destNum = 456, typeValue = 2) @@ -359,7 +372,7 @@ class RepositoryFakesTest { fun `FakeCommandSender applies command failure before immediate command mutation`() { val failure = PacketQueueRejectedException("queue closed") val sender = FakeCommandSender().apply { commandFailure = failure } - val message = AdminMessage(set_time_only = 123) + val message = AdminMessage.Builder().also { wb -> wb.set_time_only = 123 }.build() assertSame(failure, assertFailsWith { sender.sendAdminImmediate(7) { message } }) assertSame( @@ -379,7 +392,9 @@ class RepositoryFakesTest { fun `FakeCommandSender records connection ownership on post-handshake requests`() = runTest { val sender = FakeCommandSender() - sender.sendAdminForConnection(destNum = 123, expectedConnectionVersion = 41, requestId = 7) { AdminMessage() } + sender.sendAdminForConnection(destNum = 123, expectedConnectionVersion = 41, requestId = 7) { + AdminMessage.Builder().build() + } sender.requestTelemetryForConnection( requestId = 8, destNum = 123, @@ -423,16 +438,29 @@ class RepositoryFakesTest { controller.editSettings(destNum = 7) { transactionStarted.complete(Unit) finishTransaction.await() - setConfig(Config(device = Config.DeviceConfig())) - setModuleConfig(ModuleConfig(serial = ModuleConfig.SerialConfig(enabled = true))) + setConfig(Config.Builder().also { wb -> wb.device = Config.DeviceConfig.Builder().build() }.build()) + setModuleConfig( + ModuleConfig.Builder() + .also { wb -> + wb.serial = ModuleConfig.SerialConfig.Builder().also { wb -> wb.enabled = true }.build() + } + .build(), + ) } } transactionStarted.await() - controller.setConfig(destNum = 8, config = Config(power = Config.PowerConfig()), packetId = 1) + controller.setConfig( + destNum = 8, + config = Config.Builder().also { wb -> wb.power = Config.PowerConfig.Builder().build() }.build(), + packetId = 1, + ) controller.setModuleConfig( destNum = 8, - config = ModuleConfig(mqtt = ModuleConfig.MQTTConfig(enabled = true)), + config = + ModuleConfig.Builder() + .also { wb -> wb.mqtt = ModuleConfig.MQTTConfig.Builder().also { wb -> wb.enabled = true }.build() } + .build(), packetId = 2, ) finishTransaction.complete(Unit) @@ -441,8 +469,14 @@ class RepositoryFakesTest { assertEquals(listOf("config", "module"), standaloneHooks) assertEquals( listOf( - FakeRadioController.ConfigWrite(destination = 8, config = Config(power = Config.PowerConfig())), - FakeRadioController.ConfigWrite(destination = 7, config = Config(device = Config.DeviceConfig())), + FakeRadioController.ConfigWrite( + destination = 8, + config = Config.Builder().also { wb -> wb.power = Config.PowerConfig.Builder().build() }.build(), + ), + FakeRadioController.ConfigWrite( + destination = 7, + config = Config.Builder().also { wb -> wb.device = Config.DeviceConfig.Builder().build() }.build(), + ), ), controller.configWrites, ) @@ -450,11 +484,21 @@ class RepositoryFakesTest { listOf( FakeRadioController.ModuleConfigWrite( destination = 8, - config = ModuleConfig(mqtt = ModuleConfig.MQTTConfig(enabled = true)), + config = + ModuleConfig.Builder() + .also { wb -> + wb.mqtt = ModuleConfig.MQTTConfig.Builder().also { wb -> wb.enabled = true }.build() + } + .build(), ), FakeRadioController.ModuleConfigWrite( destination = 7, - config = ModuleConfig(serial = ModuleConfig.SerialConfig(enabled = true)), + config = + ModuleConfig.Builder() + .also { wb -> + wb.serial = ModuleConfig.SerialConfig.Builder().also { wb -> wb.enabled = true }.build() + } + .build(), ), ), controller.moduleConfigWrites, @@ -468,8 +512,20 @@ class RepositoryFakesTest { @Test fun `FakeRadioController records owner destinations consistently`() = runTest { val controller = FakeRadioController() - val localOwner = User(id = "!00000001", long_name = "Local") - val remoteOwner = User(id = "!00000002", long_name = "Remote") + val localOwner = + User.Builder() + .also { wb -> + wb.id = "!00000001" + wb.long_name = "Local" + } + .build() + val remoteOwner = + User.Builder() + .also { wb -> + wb.id = "!00000002" + wb.long_name = "Remote" + } + .build() controller.setOwner(destNum = 0, user = localOwner, packetId = 1) controller.setOwner(destNum = 7, user = remoteOwner, packetId = 2) @@ -552,13 +608,20 @@ class RepositoryFakesTest { @Test fun `FakeRadioConfigRepository tracks channel set and module config`() = runTest { val repo = FakeRadioConfigRepository() - val a = ChannelSettings(name = "A") - val b = ChannelSettings(name = "B") + val a = ChannelSettings.Builder().also { wb -> wb.name = "A" }.build() + val b = ChannelSettings.Builder().also { wb -> wb.name = "B" }.build() repo.replaceAllSettings(listOf(a, b)) assertEquals(listOf(a, b), repo.currentChannelSet.settings) - repo.updateChannelSettings(Channel(index = 1, settings = ChannelSettings(name = "B2"))) + repo.updateChannelSettings( + Channel.Builder() + .also { wb -> + wb.index = 1 + wb.settings = ChannelSettings.Builder().also { wb -> wb.name = "B2" }.build() + } + .build(), + ) assertEquals("B2", repo.currentChannelSet.settings[1].name) repo.clearChannelSet() diff --git a/core/ui/build.gradle.kts b/core/ui/build.gradle.kts index c60358c184..979eb3291d 100644 --- a/core/ui/build.gradle.kts +++ b/core/ui/build.gradle.kts @@ -59,6 +59,10 @@ kotlin { implementation(libs.jetbrains.compose.material3.adaptive.navigation) implementation(libs.jetbrains.compose.material3.adaptive.navigation.suite) api(libs.jetbrains.navigation3.ui) + // Pinned past navigation3-ui's transitive 1.1.1 — the JetBrains mirror never + // published 1.1.6's SaveableStateHolder fix. Consumers import + // androidx.navigation3.runtime.* (NavKey, NavBackStack) directly from commonMain. + api(libs.androidx.navigation3.runtime) // navigation3-ui's own POM marks this runtime-scope, so it never reaches any // compile classpath transitively — declare it directly. Consumers (e.g. // feature:docs) import androidx.navigationevent.compose.* directly. diff --git a/core/ui/src/androidMain/kotlin/org/meshtastic/core/ui/util/KeyEventUtils.kt b/core/ui/src/androidMain/kotlin/org/meshtastic/core/ui/util/KeyEventUtils.kt new file mode 100644 index 0000000000..2ff9e7dd18 --- /dev/null +++ b/core/ui/src/androidMain/kotlin/org/meshtastic/core/ui/util/KeyEventUtils.kt @@ -0,0 +1,28 @@ +/* + * 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 . + */ +package org.meshtastic.core.ui.util + +import android.view.KeyCharacterMap +import androidx.compose.ui.input.key.KeyEvent +import android.view.KeyEvent as AndroidKeyEvent + +/** + * IMEs synthesise their key events against the virtual device and flag them; AOSP's own soft keyboard sets both, so + * either alone identifies one. + */ +actual fun KeyEvent.isFromSoftKeyboard(): Boolean = nativeKeyEvent.deviceId == KeyCharacterMap.VIRTUAL_KEYBOARD || + nativeKeyEvent.flags and AndroidKeyEvent.FLAG_SOFT_KEYBOARD != 0 diff --git a/core/ui/src/androidMain/kotlin/org/meshtastic/core/ui/util/PlatformUtils.kt b/core/ui/src/androidMain/kotlin/org/meshtastic/core/ui/util/PlatformUtils.kt index 5452a65ebe..d013ad19bc 100644 --- a/core/ui/src/androidMain/kotlin/org/meshtastic/core/ui/util/PlatformUtils.kt +++ b/core/ui/src/androidMain/kotlin/org/meshtastic/core/ui/util/PlatformUtils.kt @@ -60,6 +60,28 @@ import org.meshtastic.core.common.util.CommonUri import org.meshtastic.core.common.util.ioDispatcher import java.net.URLEncoder +@Composable +actual fun rememberShareText(): (text: String, subject: String) -> Unit { + val context = LocalContext.current + return remember(context) { + { text, subject -> + try { + val send = + Intent(Intent.ACTION_SEND).apply { + type = "text/plain" + putExtra(Intent.EXTRA_TEXT, text) + putExtra(Intent.EXTRA_SUBJECT, subject) + } + context.startActivity( + Intent.createChooser(send, subject).apply { addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) }, + ) + } catch (ex: ActivityNotFoundException) { + Logger.d { "No app available to share to: $ex" } + } + } + } +} + @Composable actual fun rememberOpenNfcSettings(): () -> Unit { val context = LocalContext.current diff --git a/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/AlertDialogs.kt b/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/AlertDialogs.kt index f94e134e4f..e59230ec3a 100644 --- a/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/AlertDialogs.kt +++ b/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/AlertDialogs.kt @@ -25,7 +25,6 @@ import androidx.compose.foundation.verticalScroll import androidx.compose.material3.AlertDialog import androidx.compose.material3.Button import androidx.compose.material3.ButtonDefaults -import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text @@ -69,7 +68,6 @@ import org.meshtastic.core.ui.util.annotatedStringFromHtml * @param dismissable Whether the dialog can be dismissed by clicking outside or pressing back. */ @Composable -@OptIn(ExperimentalMaterial3ExpressiveApi::class) @Suppress("LongMethod", "CyclomaticComplexMethod") fun MeshtasticDialog( modifier: Modifier = Modifier, diff --git a/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/BadgedIcon.kt b/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/BadgedIcon.kt new file mode 100644 index 0000000000..9e4c004d6f --- /dev/null +++ b/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/BadgedIcon.kt @@ -0,0 +1,75 @@ +/* + * 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 . + */ +package org.meshtastic.core.ui.component + +import androidx.compose.material3.Icon +import androidx.compose.material3.LocalContentColor +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.drawWithContent +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.geometry.Rect +import androidx.compose.ui.geometry.Size +import androidx.compose.ui.graphics.ClipOp +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.ColorFilter +import androidx.compose.ui.graphics.Path +import androidx.compose.ui.graphics.drawscope.clipPath +import androidx.compose.ui.graphics.drawscope.translate +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.graphics.vector.rememberVectorPainter + +/** Badge size as a fraction of the icon's shorter side. Sized so the person glyph keeps its torso at 18dp. */ +private const val BADGE_FRACTION = 0.48f + +/** Radius of the knockout behind the badge, as a fraction of the badge box. Slightly wider, for a visible gap. */ +private const val KNOCKOUT_FRACTION = 0.58f + +/** + * One glyph made of two: [icon] with [badge] set into its lower-trailing corner. + * + * Drawn, not laid out, so it measures as a single [Icon]. The base is knocked out behind the badge or the two + * silhouettes merge at list sizes. The gap is cut rather than filled, so any background shows through. + */ +@Composable +fun BadgedIcon( + icon: ImageVector, + badge: ImageVector, + contentDescription: String?, + modifier: Modifier = Modifier, + tint: Color = LocalContentColor.current, + badgeTint: Color = tint, +) { + val badgePainter = rememberVectorPainter(badge) + Icon( + imageVector = icon, + contentDescription = contentDescription, + tint = tint, + modifier = + modifier.drawWithContent { + val badgeSize = size.minDimension * BADGE_FRACTION + val corner = Offset(size.width - badgeSize, size.height - badgeSize) + val center = Offset(corner.x + badgeSize / 2f, corner.y + badgeSize / 2f) + val knockout = Path().apply { addOval(Rect(center, badgeSize * KNOCKOUT_FRACTION)) } + + clipPath(knockout, ClipOp.Difference) { this@drawWithContent.drawContent() } + translate(corner.x, corner.y) { + with(badgePainter) { draw(Size(badgeSize, badgeSize), colorFilter = ColorFilter.tint(badgeTint)) } + } + }, + ) +} diff --git a/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/ChannelItem.kt b/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/ChannelItem.kt index 7eeaea4448..934b82ccc7 100644 --- a/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/ChannelItem.kt +++ b/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/ChannelItem.kt @@ -14,8 +14,6 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -@file:OptIn(ExperimentalMaterial3ExpressiveApi::class) - package org.meshtastic.core.ui.component import androidx.compose.foundation.clickable @@ -25,7 +23,6 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.material3.AssistChip import androidx.compose.material3.Card -import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable diff --git a/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/ContactSharing.kt b/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/ContactSharing.kt index f8ee70ff6b..1a8e8ca2bf 100644 --- a/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/ContactSharing.kt +++ b/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/ContactSharing.kt @@ -22,22 +22,34 @@ import androidx.compose.runtime.Composable import org.jetbrains.compose.resources.stringResource import org.meshtastic.core.model.Node import org.meshtastic.core.model.util.getSharedContactUrl +import org.meshtastic.core.model.util.toSharedContact import org.meshtastic.core.resources.Res import org.meshtastic.core.resources.share_contact +import org.meshtastic.core.resources.share_contact_subject import org.meshtastic.proto.SharedContact /** * Displays a dialog with the contact's information as a QR code and URI. * + * Sharing your own contact marks it manually verified (design#149 point 2): you hold your own radio's key, and a QR + * shown in person is the in-person exchange. Relaying someone else's contact asserts nothing on their behalf. + * * @param contact The node representing the contact to share. Null if no contact is selected. + * @param isOwnContact True when [contact] is the connected radio. * @param onDismiss Callback invoked when the dialog is dismissed. */ @Composable -fun SharedContactDialog(contact: Node?, onDismiss: () -> Unit) { +fun SharedContactDialog(contact: Node?, onDismiss: () -> Unit, isOwnContact: Boolean = false) { if (contact == null) return - val contactToShare = SharedContact(user = contact.user, node_num = contact.num) + val contactToShare = contact.toSharedContact(isOwnContact) val uriString = contactToShare.getSharedContactUrl().toString() - QrDialog(title = stringResource(Res.string.share_contact), uriString = uriString, onDismiss = onDismiss) + QrDialog( + title = stringResource(Res.string.share_contact), + uriString = uriString, + onDismiss = onDismiss, + subtitle = contact.user?.long_name, + shareSubject = stringResource(Res.string.share_contact_subject), + ) } /** diff --git a/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/EditListPreference.kt b/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/EditListPreference.kt index c82b54c924..3f96e74db8 100644 --- a/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/EditListPreference.kt +++ b/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/EditListPreference.kt @@ -130,7 +130,7 @@ inline fun EditListPreference( keyboardActions = keyboardActions, onValueChanged = { newValue -> if (newValue in 0..255) { - listState[index] = value.copy(gpio_pin = newValue) as T + listState[index] = value.newBuilder().also { wb -> wb.gpio_pin = newValue }.build() as T onValuesChanged(listState) } }, @@ -145,7 +145,7 @@ inline fun EditListPreference( KeyboardOptions.Default.copy(keyboardType = KeyboardType.Text, imeAction = ImeAction.Done), keyboardActions = keyboardActions, onValueChanged = { newValue -> - listState[index] = value.copy(name = newValue) as T + listState[index] = value.newBuilder().also { wb -> wb.name = newValue }.build() as T onValuesChanged(listState) }, trailingIcon = trailingIcon, @@ -159,7 +159,7 @@ inline fun EditListPreference( .map { it to it.name }, selectedItem = value.type, onItemSelected = { - listState[index] = value.copy(type = it) as T + listState[index] = value.newBuilder().also { wb -> wb.type = it }.build() as T onValuesChanged(listState) }, ) @@ -174,7 +174,7 @@ inline fun EditListPreference( when (T::class) { Int::class -> 0 as T okio.ByteString::class -> okio.ByteString.EMPTY as T - RemoteHardwarePin::class -> RemoteHardwarePin() as T + RemoteHardwarePin::class -> RemoteHardwarePin.Builder().build() as T else -> throw IllegalArgumentException("Unsupported type: ${T::class}") } listState.add(listState.size, newElement) @@ -204,7 +204,13 @@ fun EditListPreferencePreview() { title = "Available pins", list = listOf( - RemoteHardwarePin(gpio_pin = 12, name = "Front door", type = RemoteHardwarePinType.DIGITAL_READ), + RemoteHardwarePin.Builder() + .also { wb -> + wb.gpio_pin = 12 + wb.name = "Front door" + wb.type = RemoteHardwarePinType.DIGITAL_READ + } + .build(), ), maxCount = 4, enabled = true, diff --git a/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/ImportFab.kt b/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/ImportFab.kt index 1711add2c8..e1d0b0f6c9 100644 --- a/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/ImportFab.kt +++ b/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/ImportFab.kt @@ -46,7 +46,7 @@ import org.meshtastic.core.resources.scan_nfc import org.meshtastic.core.resources.scan_nfc_text import org.meshtastic.core.resources.scan_shared_contact_nfc import org.meshtastic.core.resources.scan_shared_contact_qr -import org.meshtastic.core.resources.share_channels_qr +import org.meshtastic.core.resources.share_channels import org.meshtastic.core.resources.share_connected_node import org.meshtastic.core.resources.url import org.meshtastic.core.ui.icon.LinkIcon @@ -183,7 +183,7 @@ fun MeshtasticImportFAB( onShareChannels?.let { items.add( MenuFABItem( - label = stringResource(Res.string.share_channels_qr), + label = stringResource(Res.string.share_channels), icon = MeshtasticIcons.QrCode2, onClick = it, testTag = "share_channels", diff --git a/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/LoraSignalIndicator.kt b/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/LoraSignalIndicator.kt index d6eac2b8b4..7b4e8b159a 100644 --- a/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/LoraSignalIndicator.kt +++ b/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/LoraSignalIndicator.kt @@ -60,13 +60,10 @@ import org.meshtastic.core.ui.theme.StatusColors.StatusYellow import org.meshtastic.core.ui.util.LocalModemPreset import org.meshtastic.proto.Config.LoRaConfig.ModemPreset -// Fixed-threshold SNR colors retained for contexts without an active preset (e.g. traceroute hop coloring in -// AnnotatedStrings). Per-node signal quality uses preset-relative thresholds instead — see [determineSignalQuality]. -const val SNR_GOOD_THRESHOLD = -7f -const val SNR_FAIR_THRESHOLD = -15f - +// RSSI display bands match Apple's getRssiColor; quality needs SNR and a preset instead. const val RSSI_GOOD_THRESHOLD = -115 -const val RSSI_FAIR_THRESHOLD = -126 +const val RSSI_FAIR_THRESHOLD = -120 +const val RSSI_BAD_THRESHOLD = -126 // SNR offsets (dB) below a preset's demodulation floor that delimit the quality bands, matching Meshtastic-Apple's // getSnrColor(): within 5.5 dB below the limit is FAIR, within 7.5 dB is BAD, further down is NONE. @@ -139,12 +136,11 @@ fun Snr(snr: Float?, modifier: Modifier = Modifier, modemPreset: ModemPreset? = fun Rssi(rssi: Int?, modifier: Modifier = Modifier, label: String = stringResource(Res.string.rssi)) { if (rssi == null) return val color: Color = - if (rssi > RSSI_GOOD_THRESHOLD) { - Quality.GOOD.color.invoke() - } else if (rssi > RSSI_FAIR_THRESHOLD) { - Quality.FAIR.color.invoke() - } else { - Quality.BAD.color.invoke() + when { + rssi > RSSI_GOOD_THRESHOLD -> Quality.GOOD.color.invoke() + rssi > RSSI_FAIR_THRESHOLD -> Quality.FAIR.color.invoke() + rssi > RSSI_BAD_THRESHOLD -> Quality.BAD.color.invoke() + else -> Quality.NONE.color.invoke() } Text( modifier = modifier, @@ -165,6 +161,8 @@ fun Rssi(rssi: Int?, modifier: Modifier = Modifier, label: String = stringResour * a noisy channel is still a bad link. With either missing, the rating is SNR-only, unchanged from #5446. * * A null/unknown [modemPreset] falls back to the LongFast default limit. + * + * Without a noise floor, fixed RSSI thresholds cannot account for the preset's bandwidth, so rating stays SNR-only. */ fun determineSignalQuality(snr: Float, modemPreset: ModemPreset?, rssi: Int? = null, noiseFloor: Int? = null): Quality { val limit = modemPreset.snrLimit diff --git a/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/MenuFAB.kt b/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/MenuFAB.kt index 0a3028ba24..a016ed679c 100644 --- a/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/MenuFAB.kt +++ b/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/MenuFAB.kt @@ -17,7 +17,6 @@ package org.meshtastic.core.ui.component import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi import androidx.compose.material3.FloatingActionButtonMenu import androidx.compose.material3.FloatingActionButtonMenuItem import androidx.compose.material3.Icon @@ -41,7 +40,7 @@ import org.meshtastic.core.ui.icon.Close import org.meshtastic.core.ui.icon.MeshtasticIcons import org.meshtastic.core.ui.icon.OfflineShare -@OptIn(ExperimentalMaterial3ExpressiveApi::class, ExperimentalMaterial3Api::class) +@OptIn(ExperimentalMaterial3Api::class) @Composable fun MenuFAB( expanded: Boolean, diff --git a/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/MeshtasticNavDisplay.kt b/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/MeshtasticNavDisplay.kt index 3067b963ab..07bc14575f 100644 --- a/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/MeshtasticNavDisplay.kt +++ b/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/MeshtasticNavDisplay.kt @@ -20,12 +20,15 @@ import androidx.compose.animation.AnimatedContentTransitionScope import androidx.compose.animation.ContentTransform import androidx.compose.animation.ExperimentalSharedTransitionApi import androidx.compose.animation.SharedTransitionLayout -import androidx.compose.animation.core.tween +import androidx.compose.animation.core.FiniteAnimationSpec import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeOut import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.material3.MaterialTheme import androidx.compose.material3.VerticalDragHandle import androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi +import androidx.compose.material3.adaptive.layout.PaneExpansionState +import androidx.compose.material3.adaptive.layout.ThreePaneScaffoldScope import androidx.compose.material3.adaptive.layout.rememberPaneExpansionState import androidx.compose.material3.adaptive.navigation3.rememberListDetailSceneStrategy import androidx.compose.material3.adaptive.navigation3.rememberSupportingPaneSceneStrategy @@ -48,9 +51,6 @@ import org.meshtastic.core.navigation.MultiBackstack import org.meshtastic.core.navigation.rumViewName import org.meshtastic.core.repository.PlatformAnalytics -/** Duration in milliseconds for the shared crossfade transition between navigation scenes. */ -private const val TRANSITION_DURATION_MS = 350 - /** * Shared [NavDisplay] wrapper that configures the standard Meshtastic entry decorators, scene strategies, and * transition animations for all platform hosts. @@ -106,34 +106,12 @@ fun MeshtasticNavDisplay( val listDetailSceneStrategy = rememberListDetailSceneStrategy( paneExpansionState = rememberPaneExpansionState(), - paneExpansionDragHandle = { state -> - val interactionSource = remember { MutableInteractionSource() } - VerticalDragHandle( - modifier = - Modifier.paneExpansionDraggable( - state = state, - minTouchTargetSize = 48.dp, - interactionSource = interactionSource, - ), - interactionSource = interactionSource, - ) - }, + paneExpansionDragHandle = { state -> PaneExpansionDragHandle(state) }, ) val supportingPaneSceneStrategy = rememberSupportingPaneSceneStrategy( paneExpansionState = rememberPaneExpansionState(), - paneExpansionDragHandle = { state -> - val interactionSource = remember { MutableInteractionSource() } - VerticalDragHandle( - modifier = - Modifier.paneExpansionDraggable( - state = state, - minTouchTargetSize = 48.dp, - interactionSource = interactionSource, - ), - interactionSource = interactionSource, - ) - }, + paneExpansionDragHandle = { state -> PaneExpansionDragHandle(state) }, ) val saveableDecorator = rememberSaveableStateHolderNavEntryDecorator() @@ -142,6 +120,9 @@ fun MeshtasticNavDisplay( val activeDecorators = remember(backStack, saveableDecorator, vmStoreDecorator) { listOf(saveableDecorator, vmStoreDecorator) } + // Fades are alpha, not movement, so they follow the theme's effects spec rather than a spatial one. + val fadeSpec = MaterialTheme.motionScheme.defaultEffectsSpec() + SharedTransitionLayout { NavDisplay( backStack = backStack, @@ -157,14 +138,30 @@ fun MeshtasticNavDisplay( // NavDisplay falls back to SinglePaneSceneStrategy automatically when none of these compute a Scene. sceneStrategies = listOf(DialogSceneStrategy(), listDetailSceneStrategy, supportingPaneSceneStrategy), sharedTransitionScope = this@SharedTransitionLayout, - transitionSpec = meshtasticTransitionSpec(), - popTransitionSpec = meshtasticTransitionSpec(), - predictivePopTransitionSpec = meshtasticPredictivePopTransitionSpec(), + transitionSpec = meshtasticTransitionSpec(fadeSpec), + popTransitionSpec = meshtasticTransitionSpec(fadeSpec), + predictivePopTransitionSpec = meshtasticPredictivePopTransitionSpec(fadeSpec), modifier = modifier, ) } } +/** Drag handle shared by the list-detail and supporting-pane scene strategies, with a 48.dp touch-target floor. */ +@OptIn(ExperimentalMaterial3AdaptiveApi::class) +@Composable +private fun ThreePaneScaffoldScope.PaneExpansionDragHandle(state: PaneExpansionState) { + val interactionSource = remember { MutableInteractionSource() } + VerticalDragHandle( + modifier = + Modifier.paneExpansionDraggable( + state = state, + minTouchTargetSize = 48.dp, + interactionSource = interactionSource, + ), + interactionSource = interactionSource, + ) +} + internal class ScreenViewTracker(private val analytics: PlatformAnalytics) { private var activeName: String? = null @@ -183,19 +180,15 @@ internal class ScreenViewTracker(private val analytics: PlatformAnalytics) { } /** Shared crossfade [ContentTransform] used for both forward and pop navigation. */ -private fun meshtasticTransitionSpec(): AnimatedContentTransitionScope>.() -> ContentTransform = { - ContentTransform( - fadeIn(animationSpec = tween(TRANSITION_DURATION_MS)), - fadeOut(animationSpec = tween(TRANSITION_DURATION_MS)), - ) +private fun meshtasticTransitionSpec( + fadeSpec: FiniteAnimationSpec, +): AnimatedContentTransitionScope>.() -> ContentTransform = { + ContentTransform(fadeIn(animationSpec = fadeSpec), fadeOut(animationSpec = fadeSpec)) } /** Crossfade transition for predictive back gestures (Android 14+). */ -private fun meshtasticPredictivePopTransitionSpec(): - AnimatedContentTransitionScope>.(Int) -> ContentTransform = - { - ContentTransform( - fadeIn(animationSpec = tween(TRANSITION_DURATION_MS)), - fadeOut(animationSpec = tween(TRANSITION_DURATION_MS)), - ) - } +private fun meshtasticPredictivePopTransitionSpec( + fadeSpec: FiniteAnimationSpec, +): AnimatedContentTransitionScope>.(Int) -> ContentTransform = { + ContentTransform(fadeIn(animationSpec = fadeSpec), fadeOut(animationSpec = fadeSpec)) +} diff --git a/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/MeshtasticNavigationSuite.kt b/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/MeshtasticNavigationSuite.kt index d3c5a843c3..a48cc79a07 100644 --- a/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/MeshtasticNavigationSuite.kt +++ b/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/MeshtasticNavigationSuite.kt @@ -26,7 +26,6 @@ import androidx.compose.foundation.layout.Row import androidx.compose.material3.Badge import androidx.compose.material3.BadgedBox import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi import androidx.compose.material3.Icon import androidx.compose.material3.LocalContentColor import androidx.compose.material3.MaterialTheme.colorScheme @@ -73,7 +72,7 @@ import org.meshtastic.core.ui.viewmodel.UIViewModel * This implementation uses the [MultiBackstack] state holder to manage independent histories for each tab, aligning * with Navigation 3 best practices for state preservation during tab switching. */ -@OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class) +@OptIn(ExperimentalMaterial3Api::class) @Composable fun MeshtasticNavigationSuite( multiBackstack: MultiBackstack, diff --git a/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/NodeChip.kt b/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/NodeChip.kt index 2362de3764..3f3f630938 100644 --- a/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/NodeChip.kt +++ b/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/NodeChip.kt @@ -80,14 +80,32 @@ fun NodeChip(modifier: Modifier = Modifier, node: Node, onClick: ((Node) -> Unit @Preview @Composable fun NodeChipPreview() { - val user = User(short_name = "JD", long_name = "John Doe") + val user = + User.Builder() + .also { wb -> + wb.short_name = "JD" + wb.long_name = "John Doe" + } + .build() val node = Node( num = 13444, user = user, isIgnored = false, - paxcounter = Paxcount(ble = 10, wifi = 5), - environmentMetrics = EnvironmentMetrics(temperature = 25f, relative_humidity = 60f), + paxcounter = + Paxcount.Builder() + .also { wb -> + wb.ble = 10 + wb.wifi = 5 + } + .build(), + environmentMetrics = + EnvironmentMetrics.Builder() + .also { wb -> + wb.temperature = 25f + wb.relative_humidity = 60f + } + .build(), ) AppTheme { NodeChip(node = node) } } diff --git a/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/NodeItem.kt b/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/NodeItem.kt index 0ffd5af595..4ceefd78fb 100644 --- a/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/NodeItem.kt +++ b/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/NodeItem.kt @@ -461,9 +461,10 @@ private fun MetricsGrid(items: List<@Composable () -> Unit>) { * [LastHeardInfo] tinted StatusGreen when [online] — the "online" affordance — rendered plain otherwise. Shared by the * complete and compact node rows. * - * When [heardOnCurrentLora] is false the radio has not heard this node since its LoRa settings changed, so it cannot be - * reached from here. That is a different claim from "offline" and takes precedence: an online node can still be - * unreachable, and the online tint would say the opposite. + * When [heardOnCurrentLora] is false the radio has not heard this node on the LoRa configuration it is using now, so it + * cannot be reached from here. That is a different claim from "offline" and takes precedence: an online node can still + * be unreachable, and the online tint would say the opposite. Callers pass Node.isUnheardOnCurrentLora inverted, not + * the raw flag, so MQTT-only nodes are not presented as unreachable. */ @Composable internal fun StatusAwareLastHeard( @@ -498,24 +499,19 @@ internal fun StatusAwareLastHeard( ) } -/** Key status (always status-colored) + the signed-node shield. */ +/** + * The node's one security indicator (design#149) — verification state on 2.8, the PKI locks below it, always + * status-colored. [isThisNode] marks the connected radio, which reads as verified. + */ @Composable -fun NodeSecurityIcons(thatNode: Node, modifier: Modifier = Modifier, iconSize: Dp = 20.dp) { - Row( - modifier = modifier, - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(4.dp), - ) { - if (thatNode.signsPackets) { - NodeSignedStatusIcon(modifier = Modifier.size(iconSize)) - } - NodeKeyStatusIcon( - hasPKC = thatNode.hasPKC, - mismatchKey = thatNode.mismatchKey, - publicKey = thatNode.user.public_key, - modifier = Modifier.size(iconSize), - ) - } +fun NodeSecurityIcons( + thatNode: Node, + modifier: Modifier = Modifier, + iconSize: Dp = 20.dp, + isThisNode: Boolean = false, +) { + // iconSize sizes the glyph, not the button: sizing the IconButton would shrink the touch target below 44dp. + NodeSecurityIcon(node = thatNode, modifier = modifier, iconSize = iconSize, isOwnNode = isThisNode) } @Composable @@ -538,7 +534,7 @@ private fun NodeItemHeader( horizontalArrangement = Arrangement.spacedBy(4.dp), ) { NodeChip(node = thatNode) - NodeSecurityIcons(thatNode) + NodeSecurityIcons(thatNode, isThisNode = isThisNode) Column(modifier = Modifier.weight(1f), verticalArrangement = Arrangement.spacedBy(2.dp)) { Row( @@ -565,7 +561,7 @@ private fun NodeItemHeader( lastHeard = thatNode.lastHeard, online = !isThisNode && thatNode.isOnline, contentColor = contentColor, - heardOnCurrentLora = isThisNode || thatNode.heardOnCurrentLora, + heardOnCurrentLora = isThisNode || !thatNode.isUnheardOnCurrentLora, ) } } diff --git a/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/NodeItemCompact.kt b/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/NodeItemCompact.kt index 17529d0892..dae2125624 100644 --- a/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/NodeItemCompact.kt +++ b/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/NodeItemCompact.kt @@ -211,6 +211,7 @@ fun NodeItemCompact( // Row 1: Identity — name + PKC + favorite CompactNameRow( thatNode = thatNode, + isThisNode = isThisNode, longName = longName, style = style, isIgnored = isIgnored, @@ -257,6 +258,7 @@ fun NodeItemCompact( @Composable private fun CompactNameRow( thatNode: Node, + isThisNode: Boolean, longName: String, style: FontStyle, isIgnored: Boolean, @@ -268,7 +270,7 @@ private fun CompactNameRow( verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(6.dp), ) { - NodeSecurityIcons(thatNode, iconSize = 18.dp) + NodeSecurityIcons(thatNode, iconSize = 18.dp, isThisNode = isThisNode) Text( text = longName, style = MaterialTheme.typography.titleMediumEmphasized.copy(fontStyle = style), @@ -327,7 +329,7 @@ private fun CompactHealthRow( online = thatNode.isOnline, contentColor = contentColor, relative = lastHeardIsRelative, - heardOnCurrentLora = isThisNode || thatNode.heardOnCurrentLora, + heardOnCurrentLora = isThisNode || !thatNode.isUnheardOnCurrentLora, ) }, ) diff --git a/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/NodeKeyStatusIcon.kt b/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/NodeKeyStatusIcon.kt deleted file mode 100644 index 9ee118de56..0000000000 --- a/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/NodeKeyStatusIcon.kt +++ /dev/null @@ -1,311 +0,0 @@ -/* - * Copyright (c) 2026 Meshtastic LLC - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ -package org.meshtastic.core.ui.component - -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.rememberScrollState -import androidx.compose.foundation.text.selection.SelectionContainer -import androidx.compose.foundation.verticalScroll -import androidx.compose.material3.AlertDialog -import androidx.compose.material3.HorizontalDivider -import androidx.compose.material3.Icon -import androidx.compose.material3.IconButton -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.MaterialTheme.colorScheme -import androidx.compose.material3.Text -import androidx.compose.material3.TextButton -import androidx.compose.runtime.Composable -import androidx.compose.runtime.Immutable -import androidx.compose.runtime.Stable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.saveable.rememberSaveable -import androidx.compose.runtime.setValue -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.tooling.preview.PreviewLightDark -import androidx.compose.ui.unit.dp -import okio.ByteString -import org.jetbrains.compose.resources.DrawableResource -import org.jetbrains.compose.resources.StringResource -import org.jetbrains.compose.resources.stringResource -import org.jetbrains.compose.resources.vectorResource -import org.meshtastic.core.model.Channel -import org.meshtastic.core.resources.Res -import org.meshtastic.core.resources.config_security_public_key -import org.meshtastic.core.resources.encryption_error -import org.meshtastic.core.resources.encryption_error_text -import org.meshtastic.core.resources.encryption_pkc -import org.meshtastic.core.resources.encryption_pkc_text -import org.meshtastic.core.resources.error -import org.meshtastic.core.resources.ic_key_off -import org.meshtastic.core.resources.ic_lock -import org.meshtastic.core.resources.ic_lock_open -import org.meshtastic.core.resources.no_public_key -import org.meshtastic.core.resources.no_public_key_text -import org.meshtastic.core.resources.security_icon_help_dismiss -import org.meshtastic.core.resources.security_icon_help_show_all -import org.meshtastic.core.resources.security_icon_help_show_less -import org.meshtastic.core.resources.show_all_key_title -import org.meshtastic.core.ui.icon.KeyOff -import org.meshtastic.core.ui.icon.Lock -import org.meshtastic.core.ui.icon.LockOpen -import org.meshtastic.core.ui.icon.MeshtasticIcons -import org.meshtastic.core.ui.theme.AppTheme -import org.meshtastic.core.ui.theme.StatusColors.StatusGreen -import org.meshtastic.core.ui.theme.StatusColors.StatusRed -import org.meshtastic.core.ui.theme.StatusColors.StatusYellow - -/** - * function to display information about the current node's encryption key. - * - * @property hasPKC boolean if the node has public key encryption - * @property mismatchKey boolean if the public key does not match the recorded key. - * @property publicKey boolean if the node has a shared public key. - */ -@Composable -fun NodeKeyStatusIcon( - modifier: Modifier = Modifier, - hasPKC: Boolean, - mismatchKey: Boolean, - publicKey: ByteString? = null, -) { - var showEncryptionDialog by remember { mutableStateOf(false) } - if (showEncryptionDialog) { - val (title, text) = - when { - mismatchKey -> Res.string.encryption_error to Res.string.encryption_error_text - hasPKC -> Res.string.encryption_pkc to Res.string.encryption_pkc_text - else -> Res.string.no_public_key to Res.string.no_public_key_text - } - KeyStatusDialog(title, text, publicKey) { showEncryptionDialog = false } - } - - val (icon, tint) = - when { - mismatchKey -> MeshtasticIcons.KeyOff to colorScheme.StatusRed - hasPKC -> MeshtasticIcons.Lock to colorScheme.StatusGreen - else -> MeshtasticIcons.LockOpen to colorScheme.StatusYellow - } - - IconButton(onClick = { showEncryptionDialog = true }, modifier = modifier) { - Icon( - imageVector = icon, - contentDescription = - stringResource( - when { - mismatchKey -> Res.string.encryption_error - hasPKC -> Res.string.encryption_pkc - else -> Res.string.no_public_key - }, - ), - tint = tint, - ) - } -} - -/** - * Represents the various visual states of the node key as an enum. Each enum constant encapsulates the icon, color, - * descriptive text, and optional badge details. - * - * @property icon The primary vector graphic for the icon. - * @property color The tint color for the primary icon. - * @property descriptionResId The string resource ID for the accessibility description of the icon's state. - * @property helpTextResId The string resource ID for the detailed help text associated with this state. - * @property title The string resource ID for the title associated with this state. - */ -@Immutable -enum class NodeKeySecurityState( - @Stable val icon: DrawableResource, - @Stable val color: @Composable () -> Color, - val descriptionResId: StringResource, - val helpTextResId: StringResource, - @Stable val title: StringResource, -) { - // State for public key mismatch - PKM( - icon = Res.drawable.ic_key_off, - color = { colorScheme.StatusRed }, - descriptionResId = Res.string.encryption_error, - helpTextResId = Res.string.encryption_error_text, - title = Res.string.encryption_error, - ), - - // State for public key encryption - PKC( - icon = Res.drawable.ic_lock, - color = { colorScheme.StatusGreen }, - title = Res.string.encryption_pkc, - helpTextResId = Res.string.encryption_pkc_text, - descriptionResId = Res.string.encryption_pkc, - ), - - // State for a node we hold no public key for: the radio refuses a direct message rather than - // falling back to the channel key. - NO_KEY( - icon = Res.drawable.ic_lock_open, - color = { colorScheme.StatusYellow }, - title = Res.string.no_public_key, - helpTextResId = Res.string.no_public_key_text, - descriptionResId = Res.string.no_public_key, - ), -} - -@Suppress("LongMethod", "MagicNumber") -@Composable -private fun KeyStatusDialog(title: StringResource, text: StringResource, key: ByteString?, onDismiss: () -> Unit = {}) { - var showAll by rememberSaveable { mutableStateOf(false) } - AlertDialog( - modifier = Modifier, - onDismissRequest = onDismiss, - title = { - if (showAll) { - Text(stringResource(Res.string.show_all_key_title)) - } else { - Text(stringResource(title)) - } - }, - text = { - if (showAll) { - AllKeyStates() - } else { - Column(horizontalAlignment = Alignment.CenterHorizontally) { - Text(text = stringResource(text), textAlign = TextAlign.Center) - Spacer(Modifier.height(16.dp)) - if (key != null && (title == Res.string.encryption_pkc || title == Res.string.encryption_error)) { - val isMismatch = key.size == 32 && key.toByteArray().all { it == 0.toByte() } - val keyString = - if (isMismatch) { - stringResource(Res.string.error) - } else { - key.base64() - } - Text( - text = stringResource(Res.string.config_security_public_key) + ":", - textAlign = TextAlign.Center, - ) - Spacer(Modifier.height(8.dp)) - SelectionContainer { - Text( - text = keyString, - textAlign = TextAlign.Center, - color = if (isMismatch) MaterialTheme.colorScheme.error else Color.Unspecified, - ) - } - if (!isMismatch) { - Spacer(Modifier.height(8.dp)) - CopyIconButton(valueToCopy = keyString, modifier = Modifier.padding(start = 8.dp)) - } - Spacer(Modifier.height(16.dp)) - } - } - } - }, - confirmButton = { - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically, - ) { - TextButton(onClick = { showAll = !showAll }) { - Text( - if (showAll) { - stringResource(Res.string.security_icon_help_show_less) - } else { - stringResource(Res.string.security_icon_help_show_all) - }, - ) - } - TextButton(onClick = onDismiss) { Text(stringResource(Res.string.security_icon_help_dismiss)) } - } - }, - ) -} - -/** - * Displays a list of all possible node key states with their icons and descriptions within the help dialog. Iterates - * over `NodeKeySecurityState.entries` which is provided by the enum class. - */ -@Composable -private fun AllKeyStates() { - Column( - verticalArrangement = Arrangement.spacedBy(16.dp), - modifier = Modifier.verticalScroll(rememberScrollState()), - ) { - NodeKeySecurityState.entries.forEach { state -> - Row(verticalAlignment = Alignment.CenterVertically) { - IconButton(onClick = {}, modifier = Modifier) { - Icon( - imageVector = vectorResource(state.icon), - contentDescription = stringResource(state.descriptionResId), - tint = state.color(), - ) - } - - Column(modifier = Modifier.padding(start = 16.dp)) { - Text(text = stringResource(state.descriptionResId), style = MaterialTheme.typography.titleMedium) - Text(text = stringResource(state.helpTextResId), style = MaterialTheme.typography.bodyMedium) - } - } - if (state != NodeKeySecurityState.entries.lastOrNull()) { - HorizontalDivider(modifier = Modifier.padding(top = 8.dp)) - } - } - } -} - -@PreviewLightDark -@Composable -private fun KeyStatusDialogErrorPreview() { - AppTheme { - KeyStatusDialog(title = Res.string.encryption_error, text = Res.string.encryption_error_text, key = null) - } -} - -@PreviewLightDark -@Composable -private fun KeyStatusDialogPkcPreview() { - AppTheme { - KeyStatusDialog( - title = Res.string.encryption_pkc, - text = Res.string.encryption_pkc_text, - key = Channel.getRandomKey(), - ) - } -} - -@PreviewLightDark -@Composable -private fun KeyStatusDialogPskPreview() { - AppTheme { KeyStatusDialog(title = Res.string.no_public_key, text = Res.string.no_public_key_text, key = null) } -} - -@Preview -@Composable -private fun AllKeyStatusDialogPreview() { - AppTheme { AllKeyStates() } -} diff --git a/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/NodeSecurityIcon.kt b/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/NodeSecurityIcon.kt new file mode 100644 index 0000000000..4db7794a03 --- /dev/null +++ b/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/NodeSecurityIcon.kt @@ -0,0 +1,401 @@ +/* + * 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 . + */ +package org.meshtastic.core.ui.component + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.text.selection.SelectionContainer +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.AlertDialog +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.MaterialTheme.colorScheme +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.PreviewLightDark +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import okio.ByteString +import org.jetbrains.compose.resources.StringResource +import org.jetbrains.compose.resources.stringResource +import org.meshtastic.core.model.Channel +import org.meshtastic.core.model.Node +import org.meshtastic.core.model.NodeSecurityIndicator +import org.meshtastic.core.resources.Res +import org.meshtastic.core.resources.config_security_public_key +import org.meshtastic.core.resources.encryption_error +import org.meshtastic.core.resources.encryption_error_text +import org.meshtastic.core.resources.encryption_pkc +import org.meshtastic.core.resources.encryption_pkc_text +import org.meshtastic.core.resources.error +import org.meshtastic.core.resources.no_public_key +import org.meshtastic.core.resources.no_public_key_text +import org.meshtastic.core.resources.security +import org.meshtastic.core.resources.security_icon_help_dismiss +import org.meshtastic.core.resources.security_icon_help_show_all +import org.meshtastic.core.resources.security_icon_help_show_less +import org.meshtastic.core.resources.security_legend_any_version +import org.meshtastic.core.resources.security_legend_legacy_firmware +import org.meshtastic.core.resources.security_legend_signing_firmware +import org.meshtastic.core.resources.security_signed_node +import org.meshtastic.core.resources.security_signed_node_help +import org.meshtastic.core.resources.security_verified_contact +import org.meshtastic.core.resources.security_verified_contact_help +import org.meshtastic.core.ui.icon.KeyOff +import org.meshtastic.core.ui.icon.Lock +import org.meshtastic.core.ui.icon.LockOpen +import org.meshtastic.core.ui.icon.MeshtasticIcons +import org.meshtastic.core.ui.icon.Nodes +import org.meshtastic.core.ui.icon.Person +import org.meshtastic.core.ui.icon.ShieldCheck +import org.meshtastic.core.ui.theme.AppTheme +import org.meshtastic.core.ui.theme.StatusColors.StatusGreen +import org.meshtastic.core.ui.theme.StatusColors.StatusRed +import org.meshtastic.core.ui.theme.StatusColors.StatusYellow + +/** Key verified in person, by exchanging contact QR codes. Stronger than [SignedNodeIcon], the over-the-mesh one. */ +@Composable +fun VerifiedContactIcon(modifier: Modifier = Modifier, tint: Color = colorScheme.StatusGreen) { + BadgedIcon( + icon = MeshtasticIcons.Person, + badge = MeshtasticIcons.ShieldCheck, + contentDescription = stringResource(Res.string.security_verified_contact), + modifier = modifier, + tint = tint, + ) +} + +/** Signature verified by the radio over the mesh. Our mesh glyph, since what it vouched for is the node itself. */ +@Composable +fun SignedNodeIcon(modifier: Modifier = Modifier, tint: Color = colorScheme.StatusGreen) { + BadgedIcon( + icon = MeshtasticIcons.Nodes, + badge = MeshtasticIcons.ShieldCheck, + contentDescription = stringResource(Res.string.security_signed_node), + modifier = modifier, + tint = tint, + ) +} + +/** The name of this state, used as both the dialog title and the icon's accessibility label. */ +val NodeSecurityIndicator.title: StringResource + get() = + when (this) { + NodeSecurityIndicator.KEY_MISMATCH -> Res.string.encryption_error + NodeSecurityIndicator.VERIFIED_CONTACT -> Res.string.security_verified_contact + NodeSecurityIndicator.SIGNED_NODE -> Res.string.security_signed_node + NodeSecurityIndicator.PUBLIC_KEY -> Res.string.encryption_pkc + NodeSecurityIndicator.NO_PUBLIC_KEY -> Res.string.no_public_key + } + +/** The plain-language explanation of this state, shown in the dialog and the legend. */ +val NodeSecurityIndicator.helpText: StringResource + get() = + when (this) { + NodeSecurityIndicator.KEY_MISMATCH -> Res.string.encryption_error_text + NodeSecurityIndicator.VERIFIED_CONTACT -> Res.string.security_verified_contact_help + NodeSecurityIndicator.SIGNED_NODE -> Res.string.security_signed_node_help + NodeSecurityIndicator.PUBLIC_KEY -> Res.string.encryption_pkc_text + NodeSecurityIndicator.NO_PUBLIC_KEY -> Res.string.no_public_key_text + } + +/** + * Which firmware versions show this state (design#149, point 11). + * + * Legend only. In a per-state dialog, opened from a row that already is that state, a version caveat contradicts it. + */ +val NodeSecurityIndicator.legendFirmwareNote: StringResource + get() = + when (this) { + NodeSecurityIndicator.KEY_MISMATCH, + NodeSecurityIndicator.VERIFIED_CONTACT, + -> Res.string.security_legend_any_version + + NodeSecurityIndicator.SIGNED_NODE -> Res.string.security_legend_signing_firmware + + NodeSecurityIndicator.PUBLIC_KEY, + NodeSecurityIndicator.NO_PUBLIC_KEY, + -> Res.string.security_legend_legacy_firmware + } + +/** + * This state's glyph, drawn at whatever size [modifier] gives it. + * + * The accessibility label comes from [title], so a row drawing a lock can never be announced as a signed node. + */ +@Composable +fun NodeSecurityIndicator.Glyph(modifier: Modifier = Modifier) { + when (this) { + NodeSecurityIndicator.VERIFIED_CONTACT -> VerifiedContactIcon(modifier) + + NodeSecurityIndicator.SIGNED_NODE -> SignedNodeIcon(modifier) + + else -> { + val (icon, tint) = + when (this) { + NodeSecurityIndicator.KEY_MISMATCH -> MeshtasticIcons.KeyOff to colorScheme.StatusRed + NodeSecurityIndicator.PUBLIC_KEY -> MeshtasticIcons.Lock to colorScheme.StatusGreen + else -> MeshtasticIcons.LockOpen to colorScheme.StatusYellow + } + Icon(imageVector = icon, contentDescription = stringResource(title), tint = tint, modifier = modifier) + } + } +} + +/** + * The one security indicator a node row shows beside the name (design#149), tappable for the explanation. + * + * Decided once in [NodeSecurityIndicator], so no surface can contradict another. [isOwnNode] reads as verified. + */ +@Composable +fun NodeSecurityIcon( + node: Node, + modifier: Modifier = Modifier, + iconSize: Dp = DEFAULT_GLYPH_SIZE, + isOwnNode: Boolean = false, +) { + NodeSecurityIcon( + indicator = NodeSecurityIndicator.of(node, isOwnNode), + modifier = modifier, + iconSize = iconSize, + // The stored key, same source mismatchKey reads, so a mismatch dialog shows the sentinel not the advertised + // key. + publicKey = node.publicKey ?: node.user.public_key, + ) +} + +/** [NodeSecurityIcon] for a state resolved elsewhere — the DM thread's own header, say. */ +@Composable +fun NodeSecurityIcon( + indicator: NodeSecurityIndicator, + modifier: Modifier = Modifier, + iconSize: Dp = DEFAULT_GLYPH_SIZE, + publicKey: ByteString? = null, +) { + var showDialog by remember { mutableStateOf(false) } + if (showDialog) { + NodeSecurityDialog(indicator = indicator, key = publicKey, onDismiss = { showDialog = false }) + } + // The button keeps its own minimum touch target; only the glyph inside it takes iconSize. + IconButton(onClick = { showDialog = true }, modifier = modifier) { indicator.Glyph(Modifier.size(iconSize)) } +} + +/** + * The DM thread header's encryption lock. + * + * About this thread's encryption, not the node's identity, so it keeps the locks at every firmware version. + */ +@Composable +fun NodeKeyStatusIcon( + hasPKC: Boolean, + mismatchKey: Boolean, + modifier: Modifier = Modifier, + publicKey: ByteString? = null, +) { + val indicator = + NodeSecurityIndicator.resolve(firmwareVersion = null, hasPublicKey = hasPKC, mismatchKey = mismatchKey) + NodeSecurityIcon(indicator = indicator, modifier = modifier, publicKey = publicKey) +} + +/** The glyph size a row uses when it does not ask for one. */ +private val DEFAULT_GLYPH_SIZE = 20.dp + +/** The legend order, weakest claim last — the same reading order as the help sheet on the other clients. */ +private val LEGEND_ORDER = + listOf( + NodeSecurityIndicator.VERIFIED_CONTACT, + NodeSecurityIndicator.SIGNED_NODE, + NodeSecurityIndicator.PUBLIC_KEY, + NodeSecurityIndicator.NO_PUBLIC_KEY, + NodeSecurityIndicator.KEY_MISMATCH, + ) + +/** The explanation for one state, with a toggle to the full legend. Shared by every surface that shows a glyph. */ +@Suppress("LongMethod") +@Composable +fun NodeSecurityDialog(indicator: NodeSecurityIndicator, key: ByteString?, onDismiss: () -> Unit = {}) { + var showAll by rememberSaveable { mutableStateOf(false) } + AlertDialog( + onDismissRequest = onDismiss, + title = { Text(stringResource(if (showAll) Res.string.security else indicator.title)) }, + text = { + if (showAll) { + SecurityLegend() + } else { + Column(horizontalAlignment = Alignment.CenterHorizontally) { + Text(text = stringResource(indicator.helpText), textAlign = TextAlign.Center) + Spacer(Modifier.height(16.dp)) + PublicKeyBlock(indicator, key) + } + } + }, + confirmButton = { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically, + ) { + TextButton(onClick = { showAll = !showAll }) { + Text( + if (showAll) { + stringResource(Res.string.security_icon_help_show_less) + } else { + stringResource(Res.string.security_icon_help_show_all) + }, + ) + } + TextButton(onClick = onDismiss) { Text(stringResource(Res.string.security_icon_help_dismiss)) } + } + }, + ) +} + +/** The node's public key, shown only where it is the subject: a key on file, or one that stopped matching. */ +@Suppress("MagicNumber") +@Composable +private fun PublicKeyBlock(indicator: NodeSecurityIndicator, key: ByteString?) { + if (key == null) return + if (indicator != NodeSecurityIndicator.PUBLIC_KEY && indicator != NodeSecurityIndicator.KEY_MISMATCH) return + + PublicKeyContent(key) +} + +/** The key itself, with a copy affordance — or the error word when the stored key is the mismatch sentinel. */ +@Suppress("MagicNumber") +@Composable +private fun PublicKeyContent(key: ByteString) = Column(horizontalAlignment = Alignment.CenterHorizontally) { + val isMismatch = key.size == 32 && key.toByteArray().all { it == 0.toByte() } + val keyString = if (isMismatch) stringResource(Res.string.error) else key.base64() + Text(text = stringResource(Res.string.config_security_public_key) + ":", textAlign = TextAlign.Center) + Spacer(Modifier.height(8.dp)) + SelectionContainer { + Text( + text = keyString, + textAlign = TextAlign.Center, + color = if (isMismatch) MaterialTheme.colorScheme.error else Color.Unspecified, + ) + } + if (!isMismatch) { + Spacer(Modifier.height(8.dp)) + CopyIconButton(valueToCopy = keyString, modifier = Modifier.padding(start = 8.dp)) + } + Spacer(Modifier.height(16.dp)) +} + +/** Every state the node list can show, with the firmware each one applies to (design#149, point 11). */ +@Composable +private fun SecurityLegend() { + Column( + verticalArrangement = Arrangement.spacedBy(16.dp), + modifier = Modifier.verticalScroll(rememberScrollState()), + ) { + LEGEND_ORDER.forEach { state -> + Row(verticalAlignment = Alignment.CenterVertically) { + // Content, not a control: an IconButton here is focusable and activatable but does nothing. + Box(modifier = Modifier.size(48.dp), contentAlignment = Alignment.Center) { state.Glyph() } + Column(modifier = Modifier.padding(start = 16.dp)) { + Text(text = stringResource(state.title), style = MaterialTheme.typography.titleMedium) + Text(text = stringResource(state.helpText), style = MaterialTheme.typography.bodyMedium) + Text( + text = stringResource(state.legendFirmwareNote), + style = MaterialTheme.typography.bodySmall, + color = colorScheme.onSurfaceVariant, + ) + } + } + if (state != LEGEND_ORDER.last()) { + HorizontalDivider(modifier = Modifier.padding(top = 8.dp)) + } + } + } +} + +@PreviewLightDark +@Composable +private fun NodeSecurityDialogMismatchPreview() { + AppTheme { NodeSecurityDialog(indicator = NodeSecurityIndicator.KEY_MISMATCH, key = Node.ERROR_BYTE_STRING) } +} + +@PreviewLightDark +@Composable +private fun NodeSecurityDialogPkcPreview() { + AppTheme { NodeSecurityDialog(indicator = NodeSecurityIndicator.PUBLIC_KEY, key = Channel.getRandomKey()) } +} + +@PreviewLightDark +@Composable +private fun NodeSecurityDialogVerifiedPreview() { + AppTheme { NodeSecurityDialog(indicator = NodeSecurityIndicator.VERIFIED_CONTACT, key = null) } +} + +@PreviewLightDark +@Composable +private fun NodeSecurityDialogSignedPreview() { + AppTheme { NodeSecurityDialog(indicator = NodeSecurityIndicator.SIGNED_NODE, key = null) } +} + +@Suppress("PreviewPublic") +@Preview +@Composable +fun SecurityLegendPreview() { + AppTheme { SecurityLegend() } +} + +/** + * Every glyph at the two sizes the node rows actually use — 20dp complete, 18dp compact. + * + * The composites are drawn rather than laid out, so the badge only reads if it survives the smaller size; this is the + * preview that shows whether it does. + */ +@Suppress("PreviewPublic") +@PreviewLightDark +@Composable +fun NodeSecurityGlyphsPreview() { + AppTheme { + Column(modifier = Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(12.dp)) { + listOf(20.dp, 18.dp).forEach { size -> + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(12.dp), + ) { + NodeSecurityIndicator.entries.forEach { it.Glyph(Modifier.size(size)) } + } + } + } + } +} diff --git a/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/NodeSignedStatusIcon.kt b/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/NodeSignedStatusIcon.kt deleted file mode 100644 index 143137c0eb..0000000000 --- a/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/NodeSignedStatusIcon.kt +++ /dev/null @@ -1,65 +0,0 @@ -/* - * Copyright (c) 2026 Meshtastic LLC - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ -package org.meshtastic.core.ui.component - -import androidx.compose.material3.AlertDialog -import androidx.compose.material3.Icon -import androidx.compose.material3.IconButton -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Text -import androidx.compose.material3.TextButton -import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue -import androidx.compose.ui.Modifier -import org.jetbrains.compose.resources.stringResource -import org.meshtastic.core.resources.Res -import org.meshtastic.core.resources.security_icon_help_dismiss -import org.meshtastic.core.resources.security_signed_node -import org.meshtastic.core.resources.security_signed_node_help -import org.meshtastic.core.ui.icon.MeshtasticIcons -import org.meshtastic.core.ui.icon.ShieldCheck -import org.meshtastic.core.ui.theme.StatusColors.StatusGreen - -/** The plain-language "signed node" explanation, shared by [NodeSignedStatusIcon] and the node-details signed row. */ -@Composable -fun SignedNodeDialog(onDismiss: () -> Unit) { - AlertDialog( - onDismissRequest = onDismiss, - title = { Text(stringResource(Res.string.security_signed_node)) }, - text = { Text(stringResource(Res.string.security_signed_node_help)) }, - confirmButton = { - TextButton(onClick = onDismiss) { Text(stringResource(Res.string.security_icon_help_dismiss)) } - }, - ) -} - -/** The "signed node" shield (XEdDSA), tappable like [NodeKeyStatusIcon] — tap opens [SignedNodeDialog]. */ -@Composable -fun NodeSignedStatusIcon(modifier: Modifier = Modifier) { - var showDialog by remember { mutableStateOf(false) } - if (showDialog) SignedNodeDialog(onDismiss = { showDialog = false }) - IconButton(onClick = { showDialog = true }, modifier = modifier) { - Icon( - imageVector = MeshtasticIcons.ShieldCheck, - contentDescription = stringResource(Res.string.security_signed_node), - tint = MaterialTheme.colorScheme.StatusGreen, - ) - } -} diff --git a/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/PreferenceFooter.kt b/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/PreferenceFooter.kt index a52a4d7ae2..a722dd5643 100644 --- a/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/PreferenceFooter.kt +++ b/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/PreferenceFooter.kt @@ -23,7 +23,6 @@ import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.ElevatedButton -import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment @@ -45,10 +44,8 @@ fun PreferenceFooter( horizontalArrangement = Arrangement.spacedBy(8.dp), verticalAlignment = Alignment.CenterVertically, ) { - @OptIn(ExperimentalMaterial3ExpressiveApi::class) val mediumHeight = ButtonDefaults.MediumContainerHeight if (negativeText != null) { - @OptIn(ExperimentalMaterial3ExpressiveApi::class) ElevatedButton( shapes = ButtonDefaults.shapesFor(mediumHeight), modifier = Modifier.height(mediumHeight).weight(1f), @@ -60,7 +57,6 @@ fun PreferenceFooter( } } if (positiveText != null) { - @OptIn(ExperimentalMaterial3ExpressiveApi::class) ElevatedButton( shapes = ButtonDefaults.shapesFor(mediumHeight), modifier = Modifier.height(mediumHeight).weight(1f), diff --git a/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/QrDialog.kt b/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/QrDialog.kt index 155f41a5a6..77967b720e 100644 --- a/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/QrDialog.kt +++ b/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/QrDialog.kt @@ -24,10 +24,16 @@ import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size +import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedButton +import androidx.compose.material3.PlainTooltip import androidx.compose.material3.Text +import androidx.compose.material3.TooltipBox +import androidx.compose.material3.TooltipDefaults +import androidx.compose.material3.rememberTooltipState import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf @@ -41,6 +47,7 @@ import androidx.compose.ui.platform.LocalClipboard import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.semantics.isSensitiveData import androidx.compose.ui.semantics.semantics +import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import kotlinx.coroutines.launch @@ -50,6 +57,9 @@ import org.meshtastic.core.resources.cancel import org.meshtastic.core.resources.copy import org.meshtastic.core.resources.okay import org.meshtastic.core.resources.qr_code +import org.meshtastic.core.resources.share_action +import org.meshtastic.core.resources.share_by_tap_subtext +import org.meshtastic.core.resources.share_qr_subtext import org.meshtastic.core.resources.write_nfc import org.meshtastic.core.resources.write_nfc_failed import org.meshtastic.core.resources.write_nfc_subtext @@ -58,17 +68,28 @@ import org.meshtastic.core.resources.write_nfc_text import org.meshtastic.core.ui.icon.Copy import org.meshtastic.core.ui.icon.MeshtasticIcons import org.meshtastic.core.ui.icon.Nfc +import org.meshtastic.core.ui.icon.Share import org.meshtastic.core.ui.theme.SemanticColors +import org.meshtastic.core.ui.util.LocalNfcEmulationSupported +import org.meshtastic.core.ui.util.LocalNfcEmulatorProvider import org.meshtastic.core.ui.util.LocalNfcScannerSupported import org.meshtastic.core.ui.util.LocalNfcWriterProvider import org.meshtastic.core.ui.util.SetScreenBrightness import org.meshtastic.core.ui.util.createClipEntry import org.meshtastic.core.ui.util.rememberQrCodePainter +import org.meshtastic.core.ui.util.rememberShareText private const val QR_DISPLAY_SIZE = 320 +@OptIn(ExperimentalMaterial3Api::class) @Composable -fun QrDialog(title: String, uriString: String, onDismiss: () -> Unit) { +fun QrDialog( + title: String, + uriString: String, + onDismiss: () -> Unit, + subtitle: String? = null, + shareSubject: String = title, +) { val clipboardManager = LocalClipboard.current val coroutineScope = rememberCoroutineScope() // Render at the actual on-screen pixel size so low-density devices don't over-allocate bitmap memory. @@ -77,12 +98,22 @@ fun QrDialog(title: String, uriString: String, onDismiss: () -> Unit) { val nfcSupported = LocalNfcScannerSupported.current val nfcWriter = LocalNfcWriterProvider.current + val shareText = rememberShareText() + val hceSupported = LocalNfcEmulationSupported.current + val nfcEmulator = LocalNfcEmulatorProvider.current var isWritingNfc by rememberSaveable { mutableStateOf(false) } var showNfcDisabled by rememberSaveable { mutableStateOf(false) } var writeSucceeded by rememberSaveable { mutableStateOf(null) } SetScreenBrightness(1f) + // Offer the same URL to readers for as long as this dialog is up. The exposure matches the + // QR already on screen, and arming ends with the dialog. An armed write takes the radio into + // reader mode, which suspends card emulation, so the two never contend. + if (hceSupported) { + nfcEmulator(uriString) + } + if (isWritingNfc) { nfcWriter( uriString, @@ -125,6 +156,15 @@ fun QrDialog(title: String, uriString: String, onDismiss: () -> Unit) { onConfirm = onDismiss, text = { Column(modifier = Modifier.fillMaxWidth(), horizontalAlignment = Alignment.CenterHorizontally) { + subtitle?.let { + Text( + text = it, + modifier = Modifier.fillMaxWidth().padding(bottom = 12.dp), + style = MaterialTheme.typography.titleSmall, + textAlign = TextAlign.Center, + ) + } + Image( painter = qrPainter, contentDescription = stringResource(Res.string.qr_code), @@ -132,8 +172,15 @@ fun QrDialog(title: String, uriString: String, onDismiss: () -> Unit) { contentScale = ContentScale.Fit, ) + Text( + text = stringResource(Res.string.share_qr_subtext), + modifier = Modifier.fillMaxWidth().padding(top = 12.dp), + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + Row( - modifier = Modifier.fillMaxWidth().padding(top = 16.dp), + modifier = Modifier.fillMaxWidth().padding(top = 8.dp), verticalAlignment = Alignment.CenterVertically, ) { Text( @@ -143,29 +190,55 @@ fun QrDialog(title: String, uriString: String, onDismiss: () -> Unit) { overflow = TextOverflow.Visible, softWrap = true, ) - if (nfcSupported) { - IconButton(onClick = { isWritingNfc = true }) { + // Copy stays an icon: it is the secondary of the three, and a tooltip carries the + // label on desktop, where standards section 4 asks for one on an icon-only control. + TooltipBox( + positionProvider = TooltipDefaults.rememberPlainTooltipPositionProvider(), + tooltip = { PlainTooltip { Text(stringResource(Res.string.copy)) } }, + state = rememberTooltipState(), + ) { + IconButton( + onClick = { + // The channel URL embeds channel PSKs, so mark the clip as sensitive. + coroutineScope.launch { + clipboardManager.setClipEntry(createClipEntry(uriString, sensitive = true)) + } + }, + ) { Icon( - imageVector = MeshtasticIcons.Nfc, - contentDescription = stringResource(Res.string.write_nfc), + imageVector = MeshtasticIcons.Copy, + contentDescription = stringResource(Res.string.copy), ) } } - IconButton( - onClick = { - // The channel URL embeds channel PSKs, so mark the clip as sensitive. - coroutineScope.launch { - clipboardManager.setClipEntry(createClipEntry(uriString, sensitive = true)) - } - }, - ) { - Icon(imageVector = MeshtasticIcons.Copy, contentDescription = stringResource(Res.string.copy)) - } + } + + OutlinedButton( + onClick = { shareText(uriString, shareSubject) }, + modifier = Modifier.fillMaxWidth().padding(top = 8.dp), + ) { + Icon(imageVector = MeshtasticIcons.Share, contentDescription = null) + Text(text = stringResource(Res.string.share_action), modifier = Modifier.padding(start = 8.dp)) } if (nfcSupported) { + OutlinedButton( + onClick = { isWritingNfc = true }, + modifier = Modifier.fillMaxWidth().padding(top = 8.dp), + ) { + Icon(imageVector = MeshtasticIcons.Nfc, contentDescription = null) + Text(text = stringResource(Res.string.write_nfc), modifier = Modifier.padding(start = 8.dp)) + } + } + + if (nfcSupported || hceSupported) { Text( - text = stringResource(Res.string.write_nfc_subtext), + text = + listOfNotNull( + stringResource(Res.string.write_nfc_subtext).takeIf { nfcSupported }, + stringResource(Res.string.share_by_tap_subtext).takeIf { hceSupported }, + ) + .joinToString(" "), modifier = Modifier.fillMaxWidth().padding(top = 8.dp), style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSurfaceVariant, diff --git a/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/TracerouteAlertHandler.kt b/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/TracerouteAlertHandler.kt index 382883c46b..cfb1022bac 100644 --- a/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/TracerouteAlertHandler.kt +++ b/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/TracerouteAlertHandler.kt @@ -38,7 +38,9 @@ import org.meshtastic.core.resources.traceroute import org.meshtastic.core.resources.view_on_map import org.meshtastic.core.ui.theme.StatusColors.StatusGreen import org.meshtastic.core.ui.theme.StatusColors.StatusOrange +import org.meshtastic.core.ui.theme.StatusColors.StatusRed import org.meshtastic.core.ui.theme.StatusColors.StatusYellow +import org.meshtastic.core.ui.util.LocalModemPreset import org.meshtastic.core.ui.util.annotateTraceroute import org.meshtastic.core.ui.util.toMessageRes import org.meshtastic.core.ui.viewmodel.UIViewModel @@ -71,6 +73,8 @@ fun TracerouteAlertHandler( statusGreen = colorScheme.StatusGreen, statusYellow = colorScheme.StatusYellow, statusOrange = colorScheme.StatusOrange, + statusRed = colorScheme.StatusRed, + modemPreset = LocalModemPreset.current, ), ) } diff --git a/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/preview/NodePreviewParameterProvider.kt b/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/preview/NodePreviewParameterProvider.kt index 8c69cfc4fd..d3e9e94fa6 100644 --- a/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/preview/NodePreviewParameterProvider.kt +++ b/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/preview/NodePreviewParameterProvider.kt @@ -32,26 +32,38 @@ class NodePreviewParameterProvider : PreviewParameterProvider { Node( num = 1955, user = - User( - id = "mickeyMouseId", - long_name = "Mickey Mouse", - short_name = "MM", - hw_model = HardwareModel.TBEAM, - role = Config.DeviceConfig.Role.ROUTER, - ), - position = Position(latitude_i = 338125110, longitude_i = -1179189760, altitude = 138, sats_in_view = 4), + User.Builder() + .also { wb -> + wb.id = "mickeyMouseId" + wb.long_name = "Mickey Mouse" + wb.short_name = "MM" + wb.hw_model = HardwareModel.TBEAM + wb.role = Config.DeviceConfig.Role.ROUTER + } + .build(), + position = + Position.Builder() + .also { wb -> + wb.latitude_i = 338125110 + wb.longitude_i = -1179189760 + wb.altitude = 138 + wb.sats_in_view = 4 + } + .build(), lastHeard = 1700000000, channel = 0, snr = 12.5F, rssi = -42, deviceMetrics = - DeviceMetrics( - channel_utilization = 2.4F, - air_util_tx = 3.5F, - battery_level = 85, - voltage = 3.7F, - uptime_seconds = 3600, - ), + DeviceMetrics.Builder() + .also { wb -> + wb.channel_utilization = 2.4F + wb.air_util_tx = 3.5F + wb.battery_level = 85 + wb.voltage = 3.7F + wb.uptime_seconds = 3600 + } + .build(), isFavorite = true, hopsAway = 0, ) @@ -60,55 +72,78 @@ class NodePreviewParameterProvider : PreviewParameterProvider { mickeyMouse.copy( num = 1928, user = - User( - long_name = "Minnie Mouse", - short_name = "MiMo", - id = "minnieMouseId", - hw_model = HardwareModel.HELTEC_V3, - ), + User.Builder() + .also { wb -> + wb.long_name = "Minnie Mouse" + wb.short_name = "MiMo" + wb.id = "minnieMouseId" + wb.hw_model = HardwareModel.HELTEC_V3 + } + .build(), snr = 12.5F, rssi = -42, - position = Position(), + position = Position.Builder().build(), hopsAway = 1, ) private val donaldDuck = Node( num = 1934, - position = Position(latitude_i = 338052347, longitude_i = -1179208460, altitude = 121, sats_in_view = 66), + position = + Position.Builder() + .also { wb -> + wb.latitude_i = 338052347 + wb.longitude_i = -1179208460 + wb.altitude = 121 + wb.sats_in_view = 66 + } + .build(), lastHeard = 1699999700, channel = 0, snr = 12.5F, rssi = -42, deviceMetrics = - DeviceMetrics( - channel_utilization = 2.4F, - air_util_tx = 3.5F, - battery_level = 85, - voltage = 3.7F, - uptime_seconds = 3600, - ), + DeviceMetrics.Builder() + .also { wb -> + wb.channel_utilization = 2.4F + wb.air_util_tx = 3.5F + wb.battery_level = 85 + wb.voltage = 3.7F + wb.uptime_seconds = 3600 + } + .build(), user = - User( - id = "donaldDuckId", - long_name = "Donald Duck, the Grand Duck of the Ducks", - short_name = "DoDu", - hw_model = HardwareModel.HELTEC_V3, - public_key = ByteArray(32) { 1 }.toByteString(), - ), + User.Builder() + .also { wb -> + wb.id = "donaldDuckId" + wb.long_name = "Donald Duck, the Grand Duck of the Ducks" + wb.short_name = "DoDu" + wb.hw_model = HardwareModel.HELTEC_V3 + wb.public_key = ByteArray(32) { 1 }.toByteString() + } + .build(), environmentMetrics = - EnvironmentMetrics( - temperature = 28.0F, - relative_humidity = 50.0F, - barometric_pressure = 1013.25F, - gas_resistance = 0.0F, - voltage = 3.7F, - current = 0.0F, - iaq = 100, - soil_temperature = 28.0F, - soil_moisture = 50, - ), - paxcounter = Paxcount(wifi = 30, ble = 39, uptime = 420), + EnvironmentMetrics.Builder() + .also { wb -> + wb.temperature = 28.0F + wb.relative_humidity = 50.0F + wb.barometric_pressure = 1013.25F + wb.gas_resistance = 0.0F + wb.voltage = 3.7F + wb.current = 0.0F + wb.iaq = 100 + wb.soil_temperature = 28.0F + wb.soil_moisture = 50 + } + .build(), + paxcounter = + Paxcount.Builder() + .also { wb -> + wb.wifi = 30 + wb.ble = 39 + wb.uptime = 420 + } + .build(), isFavorite = true, hopsAway = 2, ) @@ -116,9 +151,16 @@ class NodePreviewParameterProvider : PreviewParameterProvider { val unknown = donaldDuck.copy( user = - User(id = "myId", long_name = "Meshtastic myId", short_name = "myId", hw_model = HardwareModel.UNSET), - environmentMetrics = EnvironmentMetrics(), - paxcounter = Paxcount(), + User.Builder() + .also { wb -> + wb.id = "myId" + wb.long_name = "Meshtastic myId" + wb.short_name = "myId" + wb.hw_model = HardwareModel.UNSET + } + .build(), + environmentMetrics = EnvironmentMetrics.Builder().build(), + paxcounter = Paxcount.Builder().build(), ) private val almostNothing = Node(num = 9999) diff --git a/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/preview/PreviewUtils.kt b/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/preview/PreviewUtils.kt index 5c6db69251..17bec03ae3 100644 --- a/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/preview/PreviewUtils.kt +++ b/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/preview/PreviewUtils.kt @@ -29,12 +29,30 @@ class BooleanProvider : PreviewParameterProvider { override val values: Sequence = sequenceOf(false, true) } -private val user = User(short_name = "\uD83E\uDEE0", long_name = "John Doe") +private val user = + User.Builder() + .also { wb -> + wb.short_name = "\uD83E\uDEE0" + wb.long_name = "John Doe" + } + .build() val previewNode = Node( num = 13444, user = user, isIgnored = false, - paxcounter = Paxcount(ble = 10, wifi = 5), - environmentMetrics = EnvironmentMetrics(temperature = 25f, relative_humidity = 60f), + paxcounter = + Paxcount.Builder() + .also { wb -> + wb.ble = 10 + wb.wifi = 5 + } + .build(), + environmentMetrics = + EnvironmentMetrics.Builder() + .also { wb -> + wb.temperature = 25f + wb.relative_humidity = 60f + } + .build(), ) diff --git a/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/qr/ScannedQrCodeDialog.kt b/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/qr/ScannedQrCodeDialog.kt index 29346e7c29..15d27b1a5c 100644 --- a/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/qr/ScannedQrCodeDialog.kt +++ b/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/qr/ScannedQrCodeDialog.kt @@ -29,7 +29,6 @@ import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.ButtonDefaults -import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi import androidx.compose.material3.MaterialTheme import androidx.compose.material3.OutlinedButton import androidx.compose.material3.Surface @@ -120,15 +119,21 @@ fun ScannedQrCodeDialog( // When replacing, apply the incoming LoRa configuration but preserve certain // locally safe fields such as MQTT flags and TX power. This prevents QR codes // from unintentionally overriding device-specific power limits (e.g. E22 caps). - incoming.copy( - lora_config = - incoming.lora_config?.copy( - config_ok_to_mqtt = channels.lora_config?.config_ok_to_mqtt ?: false, - tx_power = channels.lora_config?.tx_power ?: 0, - ), - ) + incoming + .newBuilder() + .also { wb -> + wb.lora_config = + incoming.lora_config + ?.newBuilder() + ?.also { wb -> + wb.config_ok_to_mqtt = channels.lora_config?.config_ok_to_mqtt ?: false + wb.tx_power = channels.lora_config?.tx_power ?: 0 + } + ?.build() + } + .build() } else { - channels.copy(settings = addPreview.settings) + channels.newBuilder().also { wb -> wb.settings = addPreview.settings }.build() } } @@ -148,19 +153,26 @@ fun ScannedQrCodeDialog( val selectedChannelSet = if (shouldReplace) { - channelSet.copy( - settings = channelSet.settings.filterIndexed { i, _ -> channelSelections.getOrNull(i) == true }, - ) + channelSet + .newBuilder() + .also { wb -> + wb.settings = channelSet.settings.filterIndexed { i, _ -> channelSelections.getOrNull(i) == true } + } + .build() } else { - channelSet.copy( - settings = - channelSet.settings.filterIndexed { i, _ -> - // Primary (index 0) is always kept; existing secondaries can be dropped to free a slot for the - // incoming channel when the radio is full (Apple FR-017 "replace a secondary, never the - // primary"). - i == 0 || channelSelections.getOrNull(i) == true - }, - ) + channelSet + .newBuilder() + .also { wb -> + wb.settings = + channelSet.settings.filterIndexed { i, _ -> + // Primary (index 0) is always kept; existing secondaries can be dropped to free a slot for + // the + // incoming channel when the radio is full (Apple FR-017 "replace a secondary, never the + // primary"). + i == 0 || channelSelections.getOrNull(i) == true + } + } + .build() } // Compute LoRa configuration changes when in replace mode @@ -277,9 +289,7 @@ fun ScannedQrCodeDialog( val unselectedColors = ButtonDefaults.outlinedButtonColors(contentColor = MaterialTheme.colorScheme.onSurface) - @OptIn(ExperimentalMaterial3ExpressiveApi::class) val mediumHeight = ButtonDefaults.MediumContainerHeight - @OptIn(ExperimentalMaterial3ExpressiveApi::class) OutlinedButton( onClick = { shouldReplace = false }, shapes = ButtonDefaults.shapesFor(mediumHeight), @@ -292,7 +302,6 @@ fun ScannedQrCodeDialog( ) } - @OptIn(ExperimentalMaterial3ExpressiveApi::class) OutlinedButton( onClick = { shouldReplace = true }, shapes = ButtonDefaults.shapesFor(mediumHeight), @@ -352,9 +361,19 @@ private fun ScannedQrCodeDialogPreview() { AppTheme { ScannedQrCodeDialog( channels = - ChannelSet(settings = listOf(Channel.default.settings), lora_config = Channel.default.loraConfig), + ChannelSet.Builder() + .also { wb -> + wb.settings = listOf(Channel.default.settings) + wb.lora_config = Channel.default.loraConfig + } + .build(), incoming = - ChannelSet(settings = listOf(Channel.default.settings), lora_config = Channel.default.loraConfig), + ChannelSet.Builder() + .also { wb -> + wb.settings = listOf(Channel.default.settings) + wb.lora_config = Channel.default.loraConfig + } + .build(), onDismiss = {}, onConfirm = {}, ) diff --git a/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/qr/ScannedQrCodeViewModel.kt b/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/qr/ScannedQrCodeViewModel.kt index 7c4cb89f32..9842eb855e 100644 --- a/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/qr/ScannedQrCodeViewModel.kt +++ b/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/qr/ScannedQrCodeViewModel.kt @@ -36,7 +36,8 @@ class ScannedQrCodeViewModel( nodeRepository: NodeRepository, ) : ViewModel() { - val channels = radioConfigRepository.channelSetFlow.stateInWhileSubscribed(initialValue = ChannelSet()) + val channels = + radioConfigRepository.channelSetFlow.stateInWhileSubscribed(initialValue = ChannelSet.Builder().build()) val maxChannels = nodeRepository.myNodeInfo diff --git a/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/share/SharedContactDialog.kt b/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/share/SharedContactDialog.kt index 7e1e13d1cd..c015ebdf8a 100644 --- a/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/share/SharedContactDialog.kt +++ b/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/share/SharedContactDialog.kt @@ -95,7 +95,8 @@ fun SharedContactDialogContent( text = { Column { // Node identity badge: the same node identity chip used across the app (Design Standards §1). - val chipNode = node ?: Node(num = sharedContact.node_num, user = sharedContact.user ?: User()) + val chipNode = + node ?: Node(num = sharedContact.node_num, user = sharedContact.user ?: User.Builder().build()) Box(modifier = Modifier.fillMaxWidth().padding(bottom = 12.dp), contentAlignment = Alignment.Center) { NodeChip(node = chipNode) } @@ -115,9 +116,9 @@ fun SharedContactDialogContent( ) } HorizontalDivider() - Text(text = compareUsers(node.user, sharedContact.user ?: User())) + Text(text = compareUsers(node.user, sharedContact.user ?: User.Builder().build())) } else { - Text(text = userFieldsToString(sharedContact.user ?: User())) + Text(text = userFieldsToString(sharedContact.user ?: User.Builder().build())) } } }, @@ -141,16 +142,20 @@ fun PreviewSharedContactImportAlert() { Box(modifier = Modifier.fillMaxSize()) { SharedContactDialogContent( sharedContact = - SharedContact( - node_num = 13444, - user = - User( - id = "!00003484", - long_name = "John Doe", - short_name = "JD", - public_key = PREVIEW_PUBLIC_KEY, - ), - ), + SharedContact.Builder() + .also { wb -> + wb.node_num = 13444 + wb.user = + User.Builder() + .also { wb -> + wb.id = "!00003484" + wb.long_name = "John Doe" + wb.short_name = "JD" + wb.public_key = PREVIEW_PUBLIC_KEY + } + .build() + } + .build(), node = null, onDismiss = {}, onImport = {}, diff --git a/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/theme/Theme.kt b/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/theme/Theme.kt index a856728bea..40e3d44d12 100644 --- a/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/theme/Theme.kt +++ b/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/theme/Theme.kt @@ -15,12 +15,10 @@ * along with this program. If not, see . */ @file:Suppress("MatchingDeclarationName") -@file:OptIn(ExperimentalMaterial3ExpressiveApi::class) package org.meshtastic.core.ui.theme import androidx.compose.foundation.isSystemInDarkTheme -import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi import androidx.compose.material3.MaterialExpressiveTheme import androidx.compose.material3.MotionScheme.Companion.expressive import androidx.compose.material3.darkColorScheme @@ -117,7 +115,6 @@ data class ColorFamily(val color: Color, val onColor: Color, val colorContainer: val unspecified_scheme = ColorFamily(Color.Unspecified, Color.Unspecified, Color.Unspecified, Color.Unspecified) -@OptIn(ExperimentalMaterial3ExpressiveApi::class) @Composable fun AppTheme( darkTheme: Boolean = isSystemInDarkTheme(), diff --git a/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/theme/Type.kt b/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/theme/Type.kt index b7b28717a9..649752543b 100644 --- a/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/theme/Type.kt +++ b/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/theme/Type.kt @@ -14,11 +14,8 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -@file:OptIn(ExperimentalMaterial3ExpressiveApi::class) - package org.meshtastic.core.ui.theme -import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi import androidx.compose.material3.Typography import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.font.FontFamily diff --git a/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/util/AlertPreviews.kt b/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/util/AlertPreviews.kt index dc4363f872..2c7c9e51f9 100644 --- a/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/util/AlertPreviews.kt +++ b/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/util/AlertPreviews.kt @@ -23,6 +23,7 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.ui.Modifier import androidx.compose.ui.tooling.preview.Preview import org.jetbrains.compose.resources.stringResource @@ -34,6 +35,7 @@ import org.meshtastic.core.resources.write_nfc import org.meshtastic.core.resources.write_nfc_failed import org.meshtastic.core.resources.write_nfc_success import org.meshtastic.core.ui.component.MeshtasticDialog +import org.meshtastic.core.ui.component.QrDialog import org.meshtastic.core.ui.icon.MeshtasticIcons import org.meshtastic.core.ui.icon.Warning import org.meshtastic.core.ui.theme.AppTheme @@ -177,3 +179,41 @@ fun PreviewComposableAlert() { } } } + +private const val PREVIEW_SHARE_URL = + "https://meshtastic.org/v/#CLGGyZIEEiQKCSE0MjUyNDMzMRIPUmVwbGF5IE9ic2VydmVyGgRSUExZKCs=" + +@Suppress("PreviewPublic") +@Preview(showBackground = true, name = "Share Dialog") +@Composable +fun PreviewShareDialog() { + AppTheme { + Box(modifier = Modifier.fillMaxSize()) { + QrDialog( + title = "Share Contact", + uriString = PREVIEW_SHARE_URL, + onDismiss = {}, + subtitle = "Replay Observer", + ) + } + } +} + +/** The same dialog on a phone that can both write a tag and act as one, which adds two rows and a line of subtext. */ +@Suppress("PreviewPublic") +@Preview(showBackground = true, name = "Share Dialog with NFC") +@Composable +fun PreviewShareDialogWithNfc() { + AppTheme { + CompositionLocalProvider(LocalNfcScannerSupported provides true, LocalNfcEmulationSupported provides true) { + Box(modifier = Modifier.fillMaxSize()) { + QrDialog( + title = "Share Contact", + uriString = PREVIEW_SHARE_URL, + onDismiss = {}, + subtitle = "Replay Observer", + ) + } + } + } +} diff --git a/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/util/AnnotatedStrings.kt b/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/util/AnnotatedStrings.kt index 3e037340ec..b5a880e0f1 100644 --- a/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/util/AnnotatedStrings.kt +++ b/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/util/AnnotatedStrings.kt @@ -22,8 +22,9 @@ import androidx.compose.ui.text.SpanStyle import androidx.compose.ui.text.buildAnnotatedString import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.withStyle -import org.meshtastic.core.ui.component.SNR_FAIR_THRESHOLD -import org.meshtastic.core.ui.component.SNR_GOOD_THRESHOLD +import org.meshtastic.core.ui.component.Quality +import org.meshtastic.core.ui.component.determineSignalQuality +import org.meshtastic.proto.Config.LoRaConfig.ModemPreset /** * Converts a raw traceroute string into an [AnnotatedString] with SNR values highlighted according to their quality. @@ -33,6 +34,8 @@ fun annotateTraceroute( statusGreen: Color, statusYellow: Color, statusOrange: Color, + statusRed: Color, + modemPreset: ModemPreset?, ): AnnotatedString { if (inString == null) return buildAnnotatedString { append("") } @@ -47,11 +50,7 @@ fun annotateTraceroute( if (snrValue != null) { val snrColor = - when { - snrValue >= SNR_GOOD_THRESHOLD -> statusGreen - snrValue >= SNR_FAIR_THRESHOLD -> statusYellow - else -> statusOrange - } + snrTierColor(snrValue, modemPreset, statusGreen, statusYellow, statusOrange, statusRed) withStyle(style = SpanStyle(color = snrColor, fontWeight = FontWeight.Bold)) { append(line) } } else { append(line) @@ -71,6 +70,8 @@ fun annotateNeighborInfo( statusGreen: Color, statusYellow: Color, statusOrange: Color, + statusRed: Color, + modemPreset: ModemPreset?, ): AnnotatedString { if (inString == null) return buildAnnotatedString { append("") } @@ -85,11 +86,7 @@ fun annotateNeighborInfo( if (snrValue != null) { val snrColor = - when { - snrValue >= SNR_GOOD_THRESHOLD -> statusGreen - snrValue >= SNR_FAIR_THRESHOLD -> statusYellow - else -> statusOrange - } + snrTierColor(snrValue, modemPreset, statusGreen, statusYellow, statusOrange, statusRed) val snrPrefix = "(SNR: " append(line.substring(0, line.indexOf(snrPrefix) + snrPrefix.length)) withStyle(style = SpanStyle(color = snrColor, fontWeight = FontWeight.Bold)) { append("$snrValue") } @@ -103,3 +100,17 @@ fun annotateNeighborInfo( } } } + +private fun snrTierColor( + snr: Float, + modemPreset: ModemPreset?, + statusGreen: Color, + statusYellow: Color, + statusOrange: Color, + statusRed: Color, +): Color = when (determineSignalQuality(snr, modemPreset)) { + Quality.GOOD -> statusGreen + Quality.FAIR -> statusYellow + Quality.BAD -> statusOrange + Quality.NONE -> statusRed +} diff --git a/feature/map-maplibre/src/commonMain/kotlin/org/meshtastic/feature/map/maplibre/layers/SoftwareCanvasDecoding.kt b/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/util/KeyEventUtils.kt similarity index 54% rename from feature/map-maplibre/src/commonMain/kotlin/org/meshtastic/feature/map/maplibre/layers/SoftwareCanvasDecoding.kt rename to core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/util/KeyEventUtils.kt index 16b22531b8..3915c6d013 100644 --- a/feature/map-maplibre/src/commonMain/kotlin/org/meshtastic/feature/map/maplibre/layers/SoftwareCanvasDecoding.kt +++ b/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/util/KeyEventUtils.kt @@ -14,15 +14,14 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -package org.meshtastic.feature.map.maplibre.layers +package org.meshtastic.core.ui.util -import coil3.request.ImageRequest +import androidx.compose.ui.input.key.KeyEvent /** - * Asks Coil for an image a software canvas can draw. + * True when this event came from an on-screen keyboard rather than a physical one. * - * MapLibre rasterizes a style image outside the composition, into a software canvas. Android's Coil decodes to a - * hardware bitmap by default, and drawing one there throws `Software rendering doesn't support hardware bitmaps` — so - * an imported layer's first icon killed the app. Nowhere else has the notion, hence expect/actual rather than a flag. + * An on-screen keyboard has no Shift to reach a newline with, so a shortcut that consumes Enter must exempt it or that + * field can never hold more than one line. */ -internal expect fun ImageRequest.Builder.decodeForSoftwareCanvas(): ImageRequest.Builder +expect fun KeyEvent.isFromSoftKeyboard(): Boolean diff --git a/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/util/LocalNfcScannerProvider.kt b/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/util/LocalNfcScannerProvider.kt index c80e59fdd5..72c5589bcb 100644 --- a/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/util/LocalNfcScannerProvider.kt +++ b/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/util/LocalNfcScannerProvider.kt @@ -28,3 +28,11 @@ val LocalNfcWriterProvider = compositionLocalOf<@Composable (url: String, onResult: (Boolean) -> Unit, onNfcDisabled: () -> Unit) -> Unit> { { _, _, _ -> } } + +/** Offers a share URL to NFC readers for as long as the composable stays composed. */ +@Suppress("CompositionLocalAllowlist") +val LocalNfcEmulatorProvider = compositionLocalOf<@Composable (url: String) -> Unit> { {} } + +/** True where the platform can emulate an NFC tag (Android host card emulation). */ +@Suppress("CompositionLocalAllowlist") +val LocalNfcEmulationSupported = compositionLocalOf { false } diff --git a/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/util/PlatformUtils.kt b/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/util/PlatformUtils.kt index ed29cc2dae..7277545907 100644 --- a/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/util/PlatformUtils.kt +++ b/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/util/PlatformUtils.kt @@ -34,6 +34,15 @@ import org.meshtastic.core.common.util.CommonUri /** Returns a function to open the platform's map application at the given coordinates. */ @Composable expect fun rememberOpenMap(): (latitude: Double, longitude: Double, label: String) -> Unit +/** + * Returns a function that hands [text] to the platform's share mechanism, so the user can send it through any app they + * have. [subject] titles the share where the platform shows one. + * + * Distinct from copying to the clipboard: the clipboard makes the user find the destination themselves, which is the + * difference between sharing with someone in the room and sharing with someone who is not. + */ +@Composable expect fun rememberShareText(): (text: String, subject: String) -> Unit + /** Returns a function to open the platform's browser with the given URL. */ @Composable expect fun rememberOpenUrl(): (url: String) -> Unit diff --git a/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/util/ProtoExtensions.kt b/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/util/ProtoExtensions.kt index 997bfb9e7c..11125aa92c 100644 --- a/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/util/ProtoExtensions.kt +++ b/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/util/ProtoExtensions.kt @@ -76,16 +76,18 @@ fun getChannelList(new: List, old: List): List for (i in 0..maxOf(old.lastIndex, new.lastIndex)) { if (old.getOrNull(i) != new.getOrNull(i)) { add( - Channel( - role = - when (i) { - 0 -> Channel.Role.PRIMARY - in 1..new.lastIndex -> Channel.Role.SECONDARY - else -> Channel.Role.DISABLED - }, - index = i, - settings = new.getOrNull(i) ?: ChannelSettings(), - ), + Channel.Builder() + .also { wb -> + wb.role = + when (i) { + 0 -> Channel.Role.PRIMARY + in 1..new.lastIndex -> Channel.Role.SECONDARY + else -> Channel.Role.DISABLED + } + wb.index = i + wb.settings = new.getOrNull(i) ?: ChannelSettings.Builder().build() + } + .build(), ) } } @@ -148,7 +150,7 @@ suspend fun importChannelSet( } setChannel(channel) } - importedLoraConfig?.let { setConfig(Config(lora = it)) } + importedLoraConfig?.let { setConfig(Config.Builder().also { wb -> wb.lora = it }.build()) } } withContext(NonCancellable) { radioConfigRepository.updateChannelSet( diff --git a/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/viewmodel/ConnectionsViewModel.kt b/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/viewmodel/ConnectionsViewModel.kt index f63288d9c1..8ebf7e34a0 100644 --- a/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/viewmodel/ConnectionsViewModel.kt +++ b/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/viewmodel/ConnectionsViewModel.kt @@ -111,7 +111,7 @@ class ConnectionsViewModel( private val scheduledFirmwareUpdateNotificationKeys = mutableSetOf() val localConfig: StateFlow = - radioConfigRepository.localConfigFlow.stateInWhileSubscribed(initialValue = LocalConfig()) + radioConfigRepository.localConfigFlow.stateInWhileSubscribed(initialValue = LocalConfig.Builder().build()) val connectionState = serviceRepository.connectionState val lockdownState = serviceRepository.lockdownState diff --git a/core/ui/src/commonTest/kotlin/org/meshtastic/core/ui/component/ImportFabUiTest.kt b/core/ui/src/commonTest/kotlin/org/meshtastic/core/ui/component/ImportFabUiTest.kt index e97c690dd9..9ae6cfe718 100644 --- a/core/ui/src/commonTest/kotlin/org/meshtastic/core/ui/component/ImportFabUiTest.kt +++ b/core/ui/src/commonTest/kotlin/org/meshtastic/core/ui/component/ImportFabUiTest.kt @@ -139,7 +139,13 @@ class ImportFabUiTest { @Test fun importFab_showsSharedContactDialog_whenProvided() = runComposeUiTest { - val contact = SharedContact(user = User(long_name = "Suzume Goddess"), node_num = 1) + val contact = + SharedContact.Builder() + .also { wb -> + wb.user = User.Builder().also { wb -> wb.long_name = "Suzume Goddess" }.build() + wb.node_num = 1 + } + .build() setContent { MeshtasticImportFAB( onImport = {}, diff --git a/core/ui/src/commonTest/kotlin/org/meshtastic/core/ui/component/LoraSignalIndicatorTest.kt b/core/ui/src/commonTest/kotlin/org/meshtastic/core/ui/component/LoraSignalIndicatorTest.kt index 55065d144a..53b3b7e6b1 100644 --- a/core/ui/src/commonTest/kotlin/org/meshtastic/core/ui/component/LoraSignalIndicatorTest.kt +++ b/core/ui/src/commonTest/kotlin/org/meshtastic/core/ui/component/LoraSignalIndicatorTest.kt @@ -28,21 +28,64 @@ import kotlin.test.assertEquals */ class LoraSignalIndicatorTest { + // Firmware spreading factors (MeshRadio.h) paired with literal Semtech demodulation floors. + // Keep expected values independent of the production formula. + private val expectedFloors = + mapOf( + ModemPreset.SHORT_TURBO to (7 to -7.5f), + ModemPreset.SHORT_FAST to (7 to -7.5f), + ModemPreset.SHORT_SLOW to (8 to -10f), + ModemPreset.MEDIUM_FAST to (9 to -12.5f), + ModemPreset.MEDIUM_SLOW to (10 to -15f), + ModemPreset.MEDIUM_TURBO to (9 to -12.5f), + ModemPreset.LONG_TURBO to (11 to -17.5f), + ModemPreset.LONG_FAST to (11 to -17.5f), + ModemPreset.LONG_MODERATE to (11 to -17.5f), + ModemPreset.LONG_SLOW to (12 to -20f), + ModemPreset.VERY_LONG_SLOW to (12 to -20f), + ModemPreset.LITE_FAST to (9 to -12.5f), + ModemPreset.LITE_SLOW to (10 to -15f), + ModemPreset.NARROW_FAST to (7 to -7.5f), + ModemPreset.NARROW_SLOW to (8 to -10f), + ModemPreset.TINY_FAST to (7 to -7.5f), + ModemPreset.TINY_SLOW to (8 to -10f), + ) + @Test - fun `snrLimit follows spreading-factor demod floor per preset`() { - assertEquals(-7.5f, ModemPreset.SHORT_FAST.snrLimit) // SF7 - assertEquals(-7.5f, ModemPreset.SHORT_TURBO.snrLimit) // SF7 - assertEquals(-10f, ModemPreset.SHORT_SLOW.snrLimit) // SF8 - assertEquals(-12.5f, ModemPreset.MEDIUM_FAST.snrLimit) // SF9 - assertEquals(-15f, ModemPreset.MEDIUM_SLOW.snrLimit) // SF10 - assertEquals(-17.5f, ModemPreset.LONG_FAST.snrLimit) // SF11 - assertEquals(-17.5f, ModemPreset.LONG_MODERATE.snrLimit) // SF11 + fun `every preset's snrLimit is its spreading factor's demod floor`() { + expectedFloors.forEach { (preset, expected) -> + val (sf, floor) = expected + assertEquals(floor, preset.snrLimit, "$preset (SF$sf)") + } + } + + @Test + fun `presets sharing a spreading factor share a floor regardless of bandwidth`() { + // Bandwidth changes sensitivity in dBm, not the demodulation SNR floor. + assertEquals(ModemPreset.LONG_FAST.snrLimit, ModemPreset.LONG_TURBO.snrLimit) + assertEquals(ModemPreset.MEDIUM_FAST.snrLimit, ModemPreset.MEDIUM_TURBO.snrLimit) + assertEquals(ModemPreset.SHORT_FAST.snrLimit, ModemPreset.SHORT_TURBO.snrLimit) + assertEquals(ModemPreset.SHORT_FAST.snrLimit, ModemPreset.TINY_FAST.snrLimit) + } + + @Test + fun `every ModemPreset is covered by the firmware spreading-factor table`() { + // A preset added to the proto without a row above would otherwise be rated against LongFast's floor silently. + val uncovered = + ModemPreset.entries + .filter { it.name != "UNSET" && it.name != "UNRECOGNIZED" } + .filter { it !in expectedFloors } + assertEquals(emptyList(), uncovered, "presets missing from expectedFloors") + } + + @Test + fun `LongTurbo is rated at its SF11 floor rather than SF9`() { + assertEquals(-17.5f, ModemPreset.LONG_TURBO.snrLimit) + assertEquals(Quality.GOOD, determineSignalQuality(snr = -15f, modemPreset = ModemPreset.LONG_TURBO)) } @Test fun `LONG_SLOW uses physically-correct SF12 floor`() { - // Meshtastic-Apple's snrLimit() returns -7.5 here (the SF7 value, an apparent bug). The correct SF12 - // demodulation floor is -20 dB — see #5446. assertEquals(-20f, ModemPreset.LONG_SLOW.snrLimit) assertEquals(-20f, ModemPreset.VERY_LONG_SLOW.snrLimit) } diff --git a/core/ui/src/commonTest/kotlin/org/meshtastic/core/ui/component/NodeItemDistanceUnitsTest.kt b/core/ui/src/commonTest/kotlin/org/meshtastic/core/ui/component/NodeItemDistanceUnitsTest.kt index a2297cae01..6ae5f02304 100644 --- a/core/ui/src/commonTest/kotlin/org/meshtastic/core/ui/component/NodeItemDistanceUnitsTest.kt +++ b/core/ui/src/commonTest/kotlin/org/meshtastic/core/ui/component/NodeItemDistanceUnitsTest.kt @@ -81,7 +81,19 @@ class NodeItemDistanceUnitsTest { private fun node(num: Int, latitudeI: Int, longitudeI: Int): Node = Node( num = num, - user = User(id = "!$num", long_name = "Node $num"), - position = Position(latitude_i = latitudeI, longitude_i = longitudeI), + user = + User.Builder() + .also { wb -> + wb.id = "!$num" + wb.long_name = "Node $num" + } + .build(), + position = + Position.Builder() + .also { wb -> + wb.latitude_i = latitudeI + wb.longitude_i = longitudeI + } + .build(), ) } diff --git a/core/ui/src/commonTest/kotlin/org/meshtastic/core/ui/component/NodeItemUnheardMarkerTest.kt b/core/ui/src/commonTest/kotlin/org/meshtastic/core/ui/component/NodeItemUnheardMarkerTest.kt new file mode 100644 index 0000000000..841cb3f8b1 --- /dev/null +++ b/core/ui/src/commonTest/kotlin/org/meshtastic/core/ui/component/NodeItemUnheardMarkerTest.kt @@ -0,0 +1,105 @@ +/* + * 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 . + */ +package org.meshtastic.core.ui.component + +import androidx.compose.material3.MaterialTheme +import androidx.compose.ui.test.ComposeUiTest +import androidx.compose.ui.test.ExperimentalTestApi +import androidx.compose.ui.test.assertCountEquals +import androidx.compose.ui.test.onAllNodesWithContentDescription +import androidx.compose.ui.test.v2.runComposeUiTest +import org.meshtastic.core.common.util.MeasurementSystem +import org.meshtastic.core.common.util.nowSeconds +import org.meshtastic.core.model.ConnectionState +import org.meshtastic.core.model.Node +import org.meshtastic.core.resources.Res +import org.meshtastic.core.resources.getString +import org.meshtastic.core.resources.node_not_heard_on_current_lora +import org.meshtastic.proto.User +import kotlin.test.Test + +/** + * The unreachable marker on both node rows. Each feeds [StatusAwareLastHeard] from `Node.isUnheardOnCurrentLora`, not + * the raw `heardOnCurrentLora` flag, so an MQTT-only node must not wear it: the radio sets the mark only on an RF hear, + * so such a node reports false for the life of the entry and would otherwise be badged forever and offered for removal. + * + * Driven through the real rows rather than [StatusAwareLastHeard] directly - the wiring is what can regress, so the + * via_mqtt cases fail if either row goes back to reading the raw flag. + */ +@OptIn(ExperimentalTestApi::class) +class NodeItemUnheardMarkerTest { + + private fun node(viaMqtt: Boolean) = Node( + num = 1928, + user = + User.Builder() + .also { wb -> + wb.long_name = "Minnie Mouse" + wb.short_name = "MiMo" + wb.id = "!minnie" + } + .build(), + lastHeard = (nowSeconds - 300).toInt(), + heardOnCurrentLora = false, + viaMqtt = viaMqtt, + ) + + private fun ComposeUiTest.markerCount() = + onAllNodesWithContentDescription(getString(Res.string.node_not_heard_on_current_lora)) + + private fun ComposeUiTest.setNodeItem(viaMqtt: Boolean) = setContent { + MaterialTheme { + NodeItem( + thisNode = null, + thatNode = node(viaMqtt), + distanceUnits = MeasurementSystem.METRIC, + tempInFahrenheit = false, + connectionState = ConnectionState.Connected, + ) + } + } + + private fun ComposeUiTest.setNodeItemCompact(viaMqtt: Boolean) = setContent { + MaterialTheme { + NodeItemCompact(thisNode = null, thatNode = node(viaMqtt), distanceUnits = MeasurementSystem.METRIC) + } + } + + @Test + fun nodeItem_marksANodeNotHeardOnTheConfigInForceNow() = runComposeUiTest { + setNodeItem(viaMqtt = false) + markerCount().assertCountEquals(1) + } + + @Test + fun nodeItem_withholdsTheMarkerFromAnMqttNode() = runComposeUiTest { + setNodeItem(viaMqtt = true) + markerCount().assertCountEquals(0) + } + + @Test + fun nodeItemCompact_marksANodeNotHeardOnTheConfigInForceNow() = runComposeUiTest { + setNodeItemCompact(viaMqtt = false) + markerCount().assertCountEquals(1) + } + + @Test + fun nodeItemCompact_withholdsTheMarkerFromAnMqttNode() = runComposeUiTest { + setNodeItemCompact(viaMqtt = true) + markerCount().assertCountEquals(0) + } +} diff --git a/core/ui/src/commonTest/kotlin/org/meshtastic/core/ui/component/NodeItemZeroMetricsTest.kt b/core/ui/src/commonTest/kotlin/org/meshtastic/core/ui/component/NodeItemZeroMetricsTest.kt index bab11eed25..eb9fb26b2a 100644 --- a/core/ui/src/commonTest/kotlin/org/meshtastic/core/ui/component/NodeItemZeroMetricsTest.kt +++ b/core/ui/src/commonTest/kotlin/org/meshtastic/core/ui/component/NodeItemZeroMetricsTest.kt @@ -39,40 +39,54 @@ class NodeItemZeroMetricsTest { @Test fun nodeItem_showsZeroTemperature() = runComposeUiTest { - setNodeItem(EnvironmentMetrics(temperature = 0f)) + setNodeItem(EnvironmentMetrics.Builder().also { wb -> wb.temperature = 0f }.build()) onNodeWithText("0.0°C").assertIsDisplayed() } @Test fun nodeItem_hidesAbsentTemperature() = runComposeUiTest { - setNodeItem(EnvironmentMetrics()) + setNodeItem(EnvironmentMetrics.Builder().build()) onNodeWithText("0.0°C").assertDoesNotExist() } @Test fun nodeItem_showsZeroVoltageAndCurrent() = runComposeUiTest { - setNodeItem(EnvironmentMetrics(voltage = 0f, current = 0f)) + setNodeItem( + EnvironmentMetrics.Builder() + .also { wb -> + wb.voltage = 0f + wb.current = 0f + } + .build(), + ) onNodeWithText("0.00 V").assertIsDisplayed() onNodeWithText("0.0 mA").assertIsDisplayed() } @Test fun nodeItem_hidesAbsentVoltageAndCurrent() = runComposeUiTest { - setNodeItem(EnvironmentMetrics()) + setNodeItem(EnvironmentMetrics.Builder().build()) onNodeWithText("0.00 V").assertDoesNotExist() onNodeWithText("0.0 mA").assertDoesNotExist() } @Test fun nodeItem_showsZeroSoilReadings() = runComposeUiTest { - setNodeItem(EnvironmentMetrics(soil_temperature = 0f, soil_moisture = 0)) + setNodeItem( + EnvironmentMetrics.Builder() + .also { wb -> + wb.soil_temperature = 0f + wb.soil_moisture = 0 + } + .build(), + ) onNodeWithText("0.0°C").assertIsDisplayed() onNodeWithText("0%").assertIsDisplayed() } @Test fun nodeItem_hidesAbsentSoilReadings() = runComposeUiTest { - setNodeItem(EnvironmentMetrics()) + setNodeItem(EnvironmentMetrics.Builder().build()) onNodeWithText("0.0°C").assertDoesNotExist() onNodeWithText("0%").assertDoesNotExist() } @@ -80,26 +94,26 @@ class NodeItemZeroMetricsTest { @Test fun nodeItem_showsSoilMoistureWithoutSoilTemperature() = runComposeUiTest { // The old guard required a non-zero soil temperature before moisture was drawn at all. - setNodeItem(EnvironmentMetrics(soil_moisture = 42)) + setNodeItem(EnvironmentMetrics.Builder().also { wb -> wb.soil_moisture = 42 }.build()) onNodeWithText("42%").assertIsDisplayed() } @Test fun nodeItem_hidesOutOfRangeSoilMoisture() = runComposeUiTest { // A sensor fault reporting 101% is not a reading — matches Node.getTelemetryStrings. - setNodeItem(EnvironmentMetrics(soil_moisture = 101)) + setNodeItem(EnvironmentMetrics.Builder().also { wb -> wb.soil_moisture = 101 }.build()) onNodeWithText("101%").assertDoesNotExist() } @Test fun nodeItemCompact_showsZeroTemperature() = runComposeUiTest { - setNodeItemCompact(EnvironmentMetrics(temperature = 0f)) + setNodeItemCompact(EnvironmentMetrics.Builder().also { wb -> wb.temperature = 0f }.build()) onNodeWithText("0.0°C").assertIsDisplayed() } @Test fun nodeItemCompact_hidesAbsentTemperature() = runComposeUiTest { - setNodeItemCompact(EnvironmentMetrics()) + setNodeItemCompact(EnvironmentMetrics.Builder().build()) onNodeWithText("0.0°C").assertDoesNotExist() } @@ -121,6 +135,15 @@ class NodeItemZeroMetricsTest { } } - private fun node(metrics: EnvironmentMetrics) = - Node(num = 2, user = User(id = "!2", long_name = "Sensor"), environmentMetrics = metrics) + private fun node(metrics: EnvironmentMetrics) = Node( + num = 2, + user = + User.Builder() + .also { wb -> + wb.id = "!2" + wb.long_name = "Sensor" + } + .build(), + environmentMetrics = metrics, + ) } diff --git a/core/ui/src/commonTest/kotlin/org/meshtastic/core/ui/share/SharedContactViewModelTest.kt b/core/ui/src/commonTest/kotlin/org/meshtastic/core/ui/share/SharedContactViewModelTest.kt index 77dbe36a7f..68282287f6 100644 --- a/core/ui/src/commonTest/kotlin/org/meshtastic/core/ui/share/SharedContactViewModelTest.kt +++ b/core/ui/src/commonTest/kotlin/org/meshtastic/core/ui/share/SharedContactViewModelTest.kt @@ -72,7 +72,7 @@ class SharedContactViewModelTest { @Test fun `addSharedContact delegates to serviceRepository`() = runTest(testDispatcher) { - val contact = SharedContact(node_num = 123) + val contact = SharedContact.Builder().also { wb -> wb.node_num = 123 }.build() val job = viewModel.addSharedContact(contact) job.join() diff --git a/core/ui/src/commonTest/kotlin/org/meshtastic/core/ui/util/AnnotatedStringsTest.kt b/core/ui/src/commonTest/kotlin/org/meshtastic/core/ui/util/AnnotatedStringsTest.kt new file mode 100644 index 0000000000..d367230be9 --- /dev/null +++ b/core/ui/src/commonTest/kotlin/org/meshtastic/core/ui/util/AnnotatedStringsTest.kt @@ -0,0 +1,71 @@ +/* + * 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 . + */ +package org.meshtastic.core.ui.util + +import androidx.compose.ui.graphics.Color +import org.meshtastic.proto.Config.LoRaConfig.ModemPreset +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertTrue + +class AnnotatedStringsTest { + @Test + fun `traceroute colors the same SNR differently for different presets`() { + val text = "Node A\n⇊ -15.0 dB\nNode B" + val longTurbo = + annotateTraceroute(text, Color.Green, Color.Yellow, Color.Magenta, Color.Red, ModemPreset.LONG_TURBO) + val shortFast = + annotateTraceroute(text, Color.Green, Color.Yellow, Color.Magenta, Color.Red, ModemPreset.SHORT_FAST) + + assertEquals(text, longTurbo.text) + assertEquals(Color.Green, longTurbo.spanStyles.single().item.color) + assertEquals(Color.Magenta, shortFast.spanStyles.single().item.color) + } + + @Test + fun `neighbor info uses the preset and the lowest quality band`() { + val text = "• Node B (SNR: -15.0)" + val longTurbo = + annotateNeighborInfo(text, Color.Green, Color.Yellow, Color.Magenta, Color.Red, ModemPreset.LONG_TURBO) + val shortFast = + annotateNeighborInfo(text, Color.Green, Color.Yellow, Color.Magenta, Color.Red, ModemPreset.SHORT_FAST) + val belowFloor = + annotateNeighborInfo( + "• Node B (SNR: -30.0)", + Color.Green, + Color.Yellow, + Color.Magenta, + Color.Red, + ModemPreset.LONG_FAST, + ) + + assertEquals(text, longTurbo.text) + assertEquals(Color.Green, longTurbo.spanStyles.single().item.color) + assertEquals(Color.Magenta, shortFast.spanStyles.single().item.color) + assertEquals(Color.Red, belowFloor.spanStyles.single().item.color) + } + + @Test + fun `unknown traceroute SNR remains unstyled while zero is a measurement`() { + val unknown = annotateTraceroute("⇊ ? dB", Color.Green, Color.Yellow, Color.Magenta, Color.Red, null) + val zero = annotateTraceroute("⇊ 0.0 dB", Color.Green, Color.Yellow, Color.Magenta, Color.Red, null) + + assertEquals("⇊ ? dB", unknown.text) + assertTrue(unknown.spanStyles.isEmpty()) + assertEquals(Color.Green, zero.spanStyles.single().item.color) + } +} diff --git a/core/ui/src/commonTest/kotlin/org/meshtastic/core/ui/util/ProtoExtensionsTest.kt b/core/ui/src/commonTest/kotlin/org/meshtastic/core/ui/util/ProtoExtensionsTest.kt index af8b7f0ec5..193be4c3d2 100644 --- a/core/ui/src/commonTest/kotlin/org/meshtastic/core/ui/util/ProtoExtensionsTest.kt +++ b/core/ui/src/commonTest/kotlin/org/meshtastic/core/ui/util/ProtoExtensionsTest.kt @@ -43,7 +43,13 @@ import org.meshtastic.core.model.Channel as ModelChannel class ProtoExtensionsTest { @Test fun index_zero_emits_primary_with_new_settings_even_when_unchanged_from_old() { - val same = ChannelSettings(name = "Main", psk = byteArrayOf(1, 2, 3).toByteString()) + val same = + ChannelSettings.Builder() + .also { wb -> + wb.name = "Main" + wb.psk = byteArrayOf(1, 2, 3).toByteString() + } + .build() val result = getChannelReplacementList(new = listOf(same), currentSettings = listOf(same)) @@ -55,8 +61,8 @@ class ProtoExtensionsTest { @Test fun secondary_indices_emit_secondary_with_new_settings_even_when_unchanged_from_old() { - val primary = ChannelSettings(name = "Main") - val secondary = ChannelSettings(name = "Chat") + val primary = ChannelSettings.Builder().also { wb -> wb.name = "Main" }.build() + val secondary = ChannelSettings.Builder().also { wb -> wb.name = "Chat" }.build() val result = getChannelReplacementList(new = listOf(primary, secondary), currentSettings = listOf(primary, secondary)) @@ -71,12 +77,12 @@ class ProtoExtensionsTest { @Test fun old_trailing_indices_beyond_new_are_emitted_as_disabled_with_empty_settings() { - val primary = ChannelSettings(name = "Main") + val primary = ChannelSettings.Builder().also { wb -> wb.name = "Main" }.build() val result = getChannelReplacementList( new = listOf(primary), - currentSettings = listOf(primary, ChannelSettings(name = "Old")), + currentSettings = listOf(primary, ChannelSettings.Builder().also { wb -> wb.name = "Old" }.build()), ) // index 0 PRIMARY (new), index 1 DISABLED (trailing old slot) @@ -85,7 +91,7 @@ class ProtoExtensionsTest { assertEquals(primary, result[0].settings) assertEquals(Channel.Role.DISABLED, result[1].role) assertEquals(1, result[1].index) - assertEquals(ChannelSettings(), result[1].settings) + assertEquals(ChannelSettings.Builder().build(), result[1].settings) } @Test @@ -98,7 +104,11 @@ class ProtoExtensionsTest { @Test fun empty_new_with_non_empty_current_emits_disabled_for_every_current_index() { val currentSettings = - listOf(ChannelSettings(name = "A"), ChannelSettings(name = "B"), ChannelSettings(name = "C")) + listOf( + ChannelSettings.Builder().also { wb -> wb.name = "A" }.build(), + ChannelSettings.Builder().also { wb -> wb.name = "B" }.build(), + ChannelSettings.Builder().also { wb -> wb.name = "C" }.build(), + ) val result = getChannelReplacementList(new = emptyList(), currentSettings = currentSettings) @@ -106,18 +116,18 @@ class ProtoExtensionsTest { result.forEachIndexed { i, channel -> assertEquals(Channel.Role.DISABLED, channel.role, "index $i should be DISABLED") assertEquals(i, channel.index) - assertEquals(ChannelSettings(), channel.settings, "index $i should carry empty settings") + assertEquals(ChannelSettings.Builder().build(), channel.settings, "index $i should carry empty settings") } } @Test fun single_entry_new_with_multi_entry_current_emits_primary_then_disabled_trailing() { - val newPrimary = ChannelSettings(name = "Imported") + val newPrimary = ChannelSettings.Builder().also { wb -> wb.name = "Imported" }.build() val currentSettings = listOf( - ChannelSettings(name = "CurrentPrimary"), - ChannelSettings(name = "CurrentSecondary"), - ChannelSettings(name = "CurrentTertiary"), + ChannelSettings.Builder().also { wb -> wb.name = "CurrentPrimary" }.build(), + ChannelSettings.Builder().also { wb -> wb.name = "CurrentSecondary" }.build(), + ChannelSettings.Builder().also { wb -> wb.name = "CurrentTertiary" }.build(), ) val result = getChannelReplacementList(new = listOf(newPrimary), currentSettings = currentSettings) @@ -128,15 +138,15 @@ class ProtoExtensionsTest { assertEquals(newPrimary, result[0].settings) assertEquals(Channel.Role.DISABLED, result[1].role) assertEquals(Channel.Role.DISABLED, result[2].role) - assertEquals(ChannelSettings(), result[1].settings) - assertEquals(ChannelSettings(), result[2].settings) + assertEquals(ChannelSettings.Builder().build(), result[1].settings) + assertEquals(ChannelSettings.Builder().build(), result[2].settings) } @Test fun new_larger_than_old_emits_primary_plus_secondaries_for_every_new_index() { - val primary = ChannelSettings(name = "Main") - val secondaryA = ChannelSettings(name = "Chat") - val secondaryB = ChannelSettings(name = "Data") + val primary = ChannelSettings.Builder().also { wb -> wb.name = "Main" }.build() + val secondaryA = ChannelSettings.Builder().also { wb -> wb.name = "Chat" }.build() + val secondaryB = ChannelSettings.Builder().also { wb -> wb.name = "Data" }.build() val result = getChannelReplacementList(new = listOf(primary, secondaryA, secondaryB), currentSettings = listOf(primary)) @@ -157,7 +167,7 @@ class ProtoExtensionsTest { fun replacement_list_rejects_minimum_slot_count_above_maximum_slot_count() { assertFailsWith { getChannelReplacementList( - new = listOf(ChannelSettings(name = "Main")), + new = listOf(ChannelSettings.Builder().also { wb -> wb.name = "Main" }.build()), currentSettings = emptyList(), minimumSlotCount = 2, maximumSlotCount = 1, @@ -171,15 +181,19 @@ class ProtoExtensionsTest { val radioConfigRepository = FakeRadioConfigRepository() val oldSettings = listOf( - ChannelSettings(name = "Old Primary"), - ChannelSettings(name = "Old Secondary"), - ChannelSettings(name = "Old Tertiary"), + ChannelSettings.Builder().also { wb -> wb.name = "Old Primary" }.build(), + ChannelSettings.Builder().also { wb -> wb.name = "Old Secondary" }.build(), + ChannelSettings.Builder().also { wb -> wb.name = "Old Tertiary" }.build(), ) - val importedSettings = listOf(ChannelSettings(name = "Imported"), ChannelSettings(name = "Private")) - radioConfigRepository.setChannelSet(ChannelSet(settings = oldSettings)) + val importedSettings = + listOf( + ChannelSettings.Builder().also { wb -> wb.name = "Imported" }.build(), + ChannelSettings.Builder().also { wb -> wb.name = "Private" }.build(), + ) + radioConfigRepository.setChannelSet(ChannelSet.Builder().also { wb -> wb.settings = oldSettings }.build()) importChannelSet( - channelSet = ChannelSet(settings = importedSettings), + channelSet = ChannelSet.Builder().also { wb -> wb.settings = importedSettings }.build(), radioController = radioController, radioConfigRepository = radioConfigRepository, ) @@ -199,7 +213,7 @@ class ProtoExtensionsTest { radioController.localChannels.map { it.role }, ) assertEquals( - importedSettings + List(6) { ChannelSettings() }, + importedSettings + List(6) { ChannelSettings.Builder().build() }, radioController.localChannels.map { it.settings ?: error("Channel write omitted settings") }, ) assertEquals(importedSettings, radioConfigRepository.currentChannelSet.settings) @@ -211,18 +225,22 @@ class ProtoExtensionsTest { val radioConfigRepository = FakeRadioConfigRepository() val oldSettings = listOf( - ChannelSettings(name = "Old Primary"), - ChannelSettings(name = "Old Secondary"), - ChannelSettings(name = "Old Tertiary"), + ChannelSettings.Builder().also { wb -> wb.name = "Old Primary" }.build(), + ChannelSettings.Builder().also { wb -> wb.name = "Old Secondary" }.build(), + ChannelSettings.Builder().also { wb -> wb.name = "Old Tertiary" }.build(), ) - val importedSettings = listOf(ChannelSettings(name = "Imported"), ChannelSettings(name = "Private")) - radioConfigRepository.setChannelSet(ChannelSet(settings = oldSettings)) + val importedSettings = + listOf( + ChannelSettings.Builder().also { wb -> wb.name = "Imported" }.build(), + ChannelSettings.Builder().also { wb -> wb.name = "Private" }.build(), + ) + radioConfigRepository.setChannelSet(ChannelSet.Builder().also { wb -> wb.settings = oldSettings }.build()) // A write failing inside the editLocalSettings session propagates out before the post-session cache // replace, so the local cache stays exactly as it was — nothing partially applied. assertFailsWith { importChannelSet( - channelSet = ChannelSet(settings = importedSettings), + channelSet = ChannelSet.Builder().also { wb -> wb.settings = importedSettings }.build(), radioController = radioController, radioConfigRepository = radioConfigRepository, ) @@ -235,13 +253,14 @@ class ProtoExtensionsTest { fun import_rejects_imported_settings_beyond_slot_count_before_writing() = runTest { val radioController = FakeRadioController() val radioConfigRepository = FakeRadioConfigRepository() - val oldSettings = listOf(ChannelSettings(name = "Old")) - val oversizedSettings = (0..8).map { index -> ChannelSettings(name = "Imported $index") } - radioConfigRepository.setChannelSet(ChannelSet(settings = oldSettings)) + val oldSettings = listOf(ChannelSettings.Builder().also { wb -> wb.name = "Old" }.build()) + val oversizedSettings = + (0..8).map { index -> ChannelSettings.Builder().also { wb -> wb.name = "Imported $index" }.build() } + radioConfigRepository.setChannelSet(ChannelSet.Builder().also { wb -> wb.settings = oldSettings }.build()) assertFailsWith { importChannelSet( - channelSet = ChannelSet(settings = oversizedSettings), + channelSet = ChannelSet.Builder().also { wb -> wb.settings = oversizedSettings }.build(), radioController = radioController, radioConfigRepository = radioConfigRepository, ) @@ -255,12 +274,13 @@ class ProtoExtensionsTest { fun replacement_apply_ignores_cached_settings_beyond_slot_count() = runTest { val radioController = FakeRadioController() val radioConfigRepository = FakeRadioConfigRepository() - val oldSettings = (0..9).map { index -> ChannelSettings(name = "Old $index") } - val importedSettings = listOf(ChannelSettings(name = "Imported")) - radioConfigRepository.setChannelSet(ChannelSet(settings = oldSettings)) + val oldSettings = + (0..9).map { index -> ChannelSettings.Builder().also { wb -> wb.name = "Old $index" }.build() } + val importedSettings = listOf(ChannelSettings.Builder().also { wb -> wb.name = "Imported" }.build()) + radioConfigRepository.setChannelSet(ChannelSet.Builder().also { wb -> wb.settings = oldSettings }.build()) importChannelSet( - channelSet = ChannelSet(settings = importedSettings), + channelSet = ChannelSet.Builder().also { wb -> wb.settings = importedSettings }.build(), radioController = radioController, radioConfigRepository = radioConfigRepository, ) @@ -273,19 +293,76 @@ class ProtoExtensionsTest { fun replacement_apply_normalizes_oversized_raw_import_under_limit_before_writing() = runTest { val radioController = FakeRadioController() val radioConfigRepository = FakeRadioConfigRepository() - radioConfigRepository.setChannelSet(ChannelSet(settings = listOf(ChannelSettings(name = "Old")))) + radioConfigRepository.setChannelSet( + ChannelSet.Builder() + .also { wb -> wb.settings = listOf(ChannelSettings.Builder().also { wb -> wb.name = "Old" }.build()) } + .build(), + ) // 9 raw entries: 7 unique valid secondaries + 2 blank placeholders -> normalizes to 7 (under the 8-slot limit). - val ch0 = ChannelSettings(name = "Ch0", psk = byteArrayOf(1).toByteString()) - val ch1 = ChannelSettings(name = "Ch1", psk = byteArrayOf(2).toByteString()) - val ch2 = ChannelSettings(name = "Ch2", psk = byteArrayOf(3).toByteString()) - val ch3 = ChannelSettings(name = "Ch3", psk = byteArrayOf(4).toByteString()) - val ch4 = ChannelSettings(name = "Ch4", psk = byteArrayOf(5).toByteString()) - val ch5 = ChannelSettings(name = "Ch5", psk = byteArrayOf(6).toByteString()) - val ch6 = ChannelSettings(name = "Ch6", psk = byteArrayOf(7).toByteString()) - val raw = listOf(ch0, ch1, ChannelSettings(), ch2, ch3, ChannelSettings(), ch4, ch5, ch6) + val ch0 = + ChannelSettings.Builder() + .also { wb -> + wb.name = "Ch0" + wb.psk = byteArrayOf(1).toByteString() + } + .build() + val ch1 = + ChannelSettings.Builder() + .also { wb -> + wb.name = "Ch1" + wb.psk = byteArrayOf(2).toByteString() + } + .build() + val ch2 = + ChannelSettings.Builder() + .also { wb -> + wb.name = "Ch2" + wb.psk = byteArrayOf(3).toByteString() + } + .build() + val ch3 = + ChannelSettings.Builder() + .also { wb -> + wb.name = "Ch3" + wb.psk = byteArrayOf(4).toByteString() + } + .build() + val ch4 = + ChannelSettings.Builder() + .also { wb -> + wb.name = "Ch4" + wb.psk = byteArrayOf(5).toByteString() + } + .build() + val ch5 = + ChannelSettings.Builder() + .also { wb -> + wb.name = "Ch5" + wb.psk = byteArrayOf(6).toByteString() + } + .build() + val ch6 = + ChannelSettings.Builder() + .also { wb -> + wb.name = "Ch6" + wb.psk = byteArrayOf(7).toByteString() + } + .build() + val raw = + listOf( + ch0, + ch1, + ChannelSettings.Builder().build(), + ch2, + ch3, + ChannelSettings.Builder().build(), + ch4, + ch5, + ch6, + ) importChannelSet( - channelSet = ChannelSet(settings = raw), + channelSet = ChannelSet.Builder().also { wb -> wb.settings = raw }.build(), radioController = radioController, radioConfigRepository = radioConfigRepository, ) @@ -298,15 +375,30 @@ class ProtoExtensionsTest { fun replacement_apply_rejects_settings_still_oversized_after_normalization_drops_placeholders() = runTest { val radioController = FakeRadioController() val radioConfigRepository = FakeRadioConfigRepository() - radioConfigRepository.setChannelSet(ChannelSet(settings = listOf(ChannelSettings(name = "Old")))) + radioConfigRepository.setChannelSet( + ChannelSet.Builder() + .also { wb -> wb.settings = listOf(ChannelSettings.Builder().also { wb -> wb.name = "Old" }.build()) } + .build(), + ) // 10 raw entries: 9 genuinely unique + 1 blank placeholder. Normalization drops the blank // (-> 9) but the result still exceeds the 8-slot limit, so the post-normalize bounds check // must reject before any write or cache mutation. - val unique = (1..9).map { ChannelSettings(name = "Ch$it", psk = byteArrayOf(it.toByte(), 0).toByteString()) } + val unique = + (1..9).map { + ChannelSettings.Builder() + .also { wb -> + wb.name = "Ch$it" + wb.psk = byteArrayOf(it.toByte(), 0).toByteString() + } + .build() + } assertFailsWith { importChannelSet( - channelSet = ChannelSet(settings = unique + ChannelSettings()), + channelSet = + ChannelSet.Builder() + .also { wb -> wb.settings = unique + ChannelSettings.Builder().build() } + .build(), radioController = radioController, radioConfigRepository = radioConfigRepository, ) @@ -322,9 +414,17 @@ class ProtoExtensionsTest { // Device is on MEDIUM_FAST. The import omits lora_config, so identity resolution must fall // back to the device's current preset to detect this duplicate. radioConfigRepository.setLocalConfigDirect( - LocalConfig( - lora = Config.LoRaConfig(use_preset = true, modem_preset = Config.LoRaConfig.ModemPreset.MEDIUM_FAST), - ), + LocalConfig.Builder() + .also { wb -> + wb.lora = + Config.LoRaConfig.Builder() + .also { wb -> + wb.use_preset = true + wb.modem_preset = Config.LoRaConfig.ModemPreset.MEDIUM_FAST + } + .build() + } + .build(), ) // Primary carries an explicit preset name; the secondary has an empty name that resolves to // the preset display name. Under MEDIUM_FAST the secondary resolves to "MediumFast" and @@ -332,11 +432,20 @@ class ProtoExtensionsTest { // and survive — so asserting the secondary is dropped proves the current-local preset was // used for identity (a regression to Config.LoRaConfig() would fail this test). val psk = byteArrayOf(1, 2, 3).toByteString() - val primary = ChannelSettings(name = "MediumFast", psk = psk) - val unnamedSecondary = ChannelSettings(psk = psk) + val primary = + ChannelSettings.Builder() + .also { wb -> + wb.name = "MediumFast" + wb.psk = psk + } + .build() + val unnamedSecondary = ChannelSettings.Builder().also { wb -> wb.psk = psk }.build() importChannelSet( - channelSet = ChannelSet(settings = listOf(primary, unnamedSecondary)), // no lora_config + channelSet = + ChannelSet.Builder() + .also { wb -> wb.settings = listOf(primary, unnamedSecondary) } + .build(), // no lora_config radioController = radioController, radioConfigRepository = radioConfigRepository, ) @@ -349,43 +458,79 @@ class ProtoExtensionsTest { fun imported_lora_config_is_written_inside_the_same_transaction_when_it_differs() = runTest { val radioController = FakeRadioController() val radioConfigRepository = FakeRadioConfigRepository() - val imported = Config.LoRaConfig(region = Config.LoRaConfig.RegionCode.US) + val imported = Config.LoRaConfig.Builder().also { wb -> wb.region = Config.LoRaConfig.RegionCode.US }.build() radioConfigRepository.setLocalConfigDirect( - LocalConfig(lora = Config.LoRaConfig(region = Config.LoRaConfig.RegionCode.EU_868)), + LocalConfig.Builder() + .also { wb -> + wb.lora = + Config.LoRaConfig.Builder() + .also { wb -> wb.region = Config.LoRaConfig.RegionCode.EU_868 } + .build() + } + .build(), + ) + radioConfigRepository.setChannelSet( + ChannelSet.Builder() + .also { wb -> wb.settings = listOf(ChannelSettings.Builder().also { wb -> wb.name = "Old" }.build()) } + .build(), ) - radioConfigRepository.setChannelSet(ChannelSet(settings = listOf(ChannelSettings(name = "Old")))) importChannelSet( - channelSet = ChannelSet(settings = listOf(ChannelSettings(name = "Imported")), lora_config = imported), + channelSet = + ChannelSet.Builder() + .also { wb -> + wb.settings = listOf(ChannelSettings.Builder().also { wb -> wb.name = "Imported" }.build()) + wb.lora_config = imported + } + .build(), radioController = radioController, radioConfigRepository = radioConfigRepository, ) // LoRa write is the last op in the edit session, with no settle delays around it. - assertEquals(listOf(Config(lora = imported)), radioController.localConfigs) + assertEquals(listOf(Config.Builder().also { wb -> wb.lora = imported }.build()), radioController.localConfigs) assertEquals(imported, radioConfigRepository.currentChannelSet.lora_config) } @Test fun imported_lora_config_is_not_written_when_absent_or_unchanged() = runTest { - val current = Config.LoRaConfig(region = Config.LoRaConfig.RegionCode.US) + val current = Config.LoRaConfig.Builder().also { wb -> wb.region = Config.LoRaConfig.RegionCode.US }.build() val absent = FakeRadioController() val absentRepo = FakeRadioConfigRepository() - absentRepo.setLocalConfigDirect(LocalConfig(lora = current)) - absentRepo.setChannelSet(ChannelSet(settings = listOf(ChannelSettings(name = "Old")))) + absentRepo.setLocalConfigDirect(LocalConfig.Builder().also { wb -> wb.lora = current }.build()) + absentRepo.setChannelSet( + ChannelSet.Builder() + .also { wb -> wb.settings = listOf(ChannelSettings.Builder().also { wb -> wb.name = "Old" }.build()) } + .build(), + ) importChannelSet( - channelSet = ChannelSet(settings = listOf(ChannelSettings(name = "Imported"))), // no lora_config + channelSet = + ChannelSet.Builder() + .also { wb -> + wb.settings = listOf(ChannelSettings.Builder().also { wb -> wb.name = "Imported" }.build()) + } + .build(), // no lora_config radioController = absent, radioConfigRepository = absentRepo, ) val unchanged = FakeRadioController() val unchangedRepo = FakeRadioConfigRepository() - unchangedRepo.setLocalConfigDirect(LocalConfig(lora = current)) - unchangedRepo.setChannelSet(ChannelSet(settings = listOf(ChannelSettings(name = "Old")))) + unchangedRepo.setLocalConfigDirect(LocalConfig.Builder().also { wb -> wb.lora = current }.build()) + unchangedRepo.setChannelSet( + ChannelSet.Builder() + .also { wb -> wb.settings = listOf(ChannelSettings.Builder().also { wb -> wb.name = "Old" }.build()) } + .build(), + ) importChannelSet( - channelSet = ChannelSet(settings = listOf(ChannelSettings(name = "Imported")), lora_config = current), + channelSet = + ChannelSet.Builder() + .also { wb -> + wb.settings = listOf(ChannelSettings.Builder().also { wb -> wb.name = "Imported" }.build()) + wb.lora_config = current + } + .build(), radioController = unchanged, radioConfigRepository = unchangedRepo, ) @@ -398,7 +543,11 @@ class ProtoExtensionsTest { @Test fun preview_existing_channels_are_always_shown_and_selected() { - val existing = listOf(ChannelSettings(name = "A"), ChannelSettings(name = "B")) + val existing = + listOf( + ChannelSettings.Builder().also { wb -> wb.name = "A" }.build(), + ChannelSettings.Builder().also { wb -> wb.name = "B" }.build(), + ) val preview = getChannelPreviewForAdd(existing, emptyList(), ModelChannel.default.loraConfig, maxChannels = 8) @@ -408,7 +557,11 @@ class ProtoExtensionsTest { @Test fun preview_unique_incoming_channels_are_shown_and_selected() { - val incoming = listOf(ChannelSettings(name = "C"), ChannelSettings(name = "D")) + val incoming = + listOf( + ChannelSettings.Builder().also { wb -> wb.name = "C" }.build(), + ChannelSettings.Builder().also { wb -> wb.name = "D" }.build(), + ) val preview = getChannelPreviewForAdd(emptyList(), incoming, ModelChannel.default.loraConfig, maxChannels = 8) @@ -418,7 +571,13 @@ class ProtoExtensionsTest { @Test fun preview_incoming_duplicate_of_existing_is_omitted() { - val channel = ChannelSettings(name = "Test", psk = byteArrayOf(1).toByteString()) + val channel = + ChannelSettings.Builder() + .also { wb -> + wb.name = "Test" + wb.psk = byteArrayOf(1).toByteString() + } + .build() val preview = getChannelPreviewForAdd(listOf(channel), listOf(channel), ModelChannel.default.loraConfig, maxChannels = 8) @@ -428,8 +587,20 @@ class ProtoExtensionsTest { @Test fun preview_duplicate_inside_incoming_keeps_first_and_omits_later() { - val a = ChannelSettings(name = "A", psk = byteArrayOf(1).toByteString()) - val b = ChannelSettings(name = "B", psk = byteArrayOf(2).toByteString()) + val a = + ChannelSettings.Builder() + .also { wb -> + wb.name = "A" + wb.psk = byteArrayOf(1).toByteString() + } + .build() + val b = + ChannelSettings.Builder() + .also { wb -> + wb.name = "B" + wb.psk = byteArrayOf(2).toByteString() + } + .build() val preview = getChannelPreviewForAdd(emptyList(), listOf(a, a, b), ModelChannel.default.loraConfig, maxChannels = 8) @@ -441,8 +612,20 @@ class ProtoExtensionsTest { @Test fun preview_same_name_different_psk_remains_visible() { - val existingChan = ChannelSettings(name = "A", psk = byteArrayOf(1).toByteString()) - val incomingChan = ChannelSettings(name = "A", psk = byteArrayOf(2).toByteString()) + val existingChan = + ChannelSettings.Builder() + .also { wb -> + wb.name = "A" + wb.psk = byteArrayOf(1).toByteString() + } + .build() + val incomingChan = + ChannelSettings.Builder() + .also { wb -> + wb.name = "A" + wb.psk = byteArrayOf(2).toByteString() + } + .build() val preview = getChannelPreviewForAdd( @@ -459,8 +642,20 @@ class ProtoExtensionsTest { @Test fun preview_same_psk_different_name_remains_visible() { val psk = byteArrayOf(1, 2).toByteString() - val existingChan = ChannelSettings(name = "A", psk = psk) - val incomingChan = ChannelSettings(name = "B", psk = psk) + val existingChan = + ChannelSettings.Builder() + .also { wb -> + wb.name = "A" + wb.psk = psk + } + .build() + val incomingChan = + ChannelSettings.Builder() + .also { wb -> + wb.name = "B" + wb.psk = psk + } + .build() val preview = getChannelPreviewForAdd( @@ -477,8 +672,17 @@ class ProtoExtensionsTest { @Test fun preview_empty_name_default_matches_explicit_preset_name_and_is_omitted() { val loraConfig = ModelChannel.default.loraConfig - val existingChan = ChannelSettings(psk = byteArrayOf(1).toByteString()) // resolves to "LongFast" - val incomingChan = ChannelSettings(name = "LongFast", psk = byteArrayOf(1).toByteString()) + val existingChan = + ChannelSettings.Builder() + .also { wb -> wb.psk = byteArrayOf(1).toByteString() } + .build() // resolves to "LongFast" + val incomingChan = + ChannelSettings.Builder() + .also { wb -> + wb.name = "LongFast" + wb.psk = byteArrayOf(1).toByteString() + } + .build() val preview = getChannelPreviewForAdd(listOf(existingChan), listOf(incomingChan), loraConfig, maxChannels = 8) @@ -488,8 +692,17 @@ class ProtoExtensionsTest { @Test fun preview_explicit_preset_name_matches_empty_name_default_and_is_omitted() { val loraConfig = ModelChannel.default.loraConfig - val existingChan = ChannelSettings(name = "LongFast", psk = byteArrayOf(1).toByteString()) - val incomingChan = ChannelSettings(psk = byteArrayOf(1).toByteString()) // resolves to "LongFast" + val existingChan = + ChannelSettings.Builder() + .also { wb -> + wb.name = "LongFast" + wb.psk = byteArrayOf(1).toByteString() + } + .build() + val incomingChan = + ChannelSettings.Builder() + .also { wb -> wb.psk = byteArrayOf(1).toByteString() } + .build() // resolves to "LongFast" val preview = getChannelPreviewForAdd(listOf(existingChan), listOf(incomingChan), loraConfig, maxChannels = 8) @@ -500,9 +713,25 @@ class ProtoExtensionsTest { fun preview_psk_marker_matches_expanded_default_key_and_is_omitted() { val loraConfig = ModelChannel.default.loraConfig val expandedPsk = - ModelChannel(settings = ChannelSettings(psk = byteArrayOf(1).toByteString()), loraConfig = loraConfig).psk - val markerChan = ChannelSettings(name = "Test", psk = byteArrayOf(1).toByteString()) - val expandedChan = ChannelSettings(name = "Test", psk = expandedPsk) + ModelChannel( + settings = ChannelSettings.Builder().also { wb -> wb.psk = byteArrayOf(1).toByteString() }.build(), + loraConfig = loraConfig, + ) + .psk + val markerChan = + ChannelSettings.Builder() + .also { wb -> + wb.name = "Test" + wb.psk = byteArrayOf(1).toByteString() + } + .build() + val expandedChan = + ChannelSettings.Builder() + .also { wb -> + wb.name = "Test" + wb.psk = expandedPsk + } + .build() val preview = getChannelPreviewForAdd(listOf(markerChan), listOf(expandedChan), loraConfig, maxChannels = 8) @@ -511,9 +740,24 @@ class ProtoExtensionsTest { @Test fun preview_non_long_fast_preset_default_duplicate_is_omitted() { - val loraConfig = Config.LoRaConfig(use_preset = true, modem_preset = Config.LoRaConfig.ModemPreset.MEDIUM_FAST) - val existingChan = ChannelSettings(psk = byteArrayOf(1).toByteString()) // resolves to "MediumFast" - val incomingChan = ChannelSettings(name = "MediumFast", psk = byteArrayOf(1).toByteString()) + val loraConfig = + Config.LoRaConfig.Builder() + .also { wb -> + wb.use_preset = true + wb.modem_preset = Config.LoRaConfig.ModemPreset.MEDIUM_FAST + } + .build() + val existingChan = + ChannelSettings.Builder() + .also { wb -> wb.psk = byteArrayOf(1).toByteString() } + .build() // resolves to "MediumFast" + val incomingChan = + ChannelSettings.Builder() + .also { wb -> + wb.name = "MediumFast" + wb.psk = byteArrayOf(1).toByteString() + } + .build() val preview = getChannelPreviewForAdd(listOf(existingChan), listOf(incomingChan), loraConfig, maxChannels = 8) @@ -522,9 +766,18 @@ class ProtoExtensionsTest { @Test fun preview_omitted_duplicates_do_not_consume_capacity() { - val existing = listOf(ChannelSettings(name = "A"), ChannelSettings(name = "B")) - val dup = ChannelSettings(name = "A") - val unique = listOf(ChannelSettings(name = "C"), ChannelSettings(name = "D"), ChannelSettings(name = "E")) + val existing = + listOf( + ChannelSettings.Builder().also { wb -> wb.name = "A" }.build(), + ChannelSettings.Builder().also { wb -> wb.name = "B" }.build(), + ) + val dup = ChannelSettings.Builder().also { wb -> wb.name = "A" }.build() + val unique = + listOf( + ChannelSettings.Builder().also { wb -> wb.name = "C" }.build(), + ChannelSettings.Builder().also { wb -> wb.name = "D" }.build(), + ChannelSettings.Builder().also { wb -> wb.name = "E" }.build(), + ) val preview = getChannelPreviewForAdd(existing, listOf(dup) + unique, ModelChannel.default.loraConfig, maxChannels = 5) @@ -539,13 +792,17 @@ class ProtoExtensionsTest { @Test fun preview_over_capacity_unique_incoming_remains_visible_but_unchecked() { - val existing = listOf(ChannelSettings(name = "A"), ChannelSettings(name = "B")) + val existing = + listOf( + ChannelSettings.Builder().also { wb -> wb.name = "A" }.build(), + ChannelSettings.Builder().also { wb -> wb.name = "B" }.build(), + ) val incoming = listOf( - ChannelSettings(name = "C"), - ChannelSettings(name = "D"), - ChannelSettings(name = "E"), - ChannelSettings(name = "F"), + ChannelSettings.Builder().also { wb -> wb.name = "C" }.build(), + ChannelSettings.Builder().also { wb -> wb.name = "D" }.build(), + ChannelSettings.Builder().also { wb -> wb.name = "E" }.build(), + ChannelSettings.Builder().also { wb -> wb.name = "F" }.build(), ) val preview = getChannelPreviewForAdd(existing, incoming, ModelChannel.default.loraConfig, maxChannels = 4) @@ -560,8 +817,8 @@ class ProtoExtensionsTest { @Test fun preview_existing_at_max_omits_nothing_but_all_incoming_unchecked() { - val existing = (1..8).map { ChannelSettings(name = "Ch$it") } - val incoming = listOf(ChannelSettings(name = "New")) + val existing = (1..8).map { ChannelSettings.Builder().also { wb -> wb.name = "Ch$it" }.build() } + val incoming = listOf(ChannelSettings.Builder().also { wb -> wb.name = "New" }.build()) val preview = getChannelPreviewForAdd(existing, incoming, ModelChannel.default.loraConfig, maxChannels = 8) @@ -572,12 +829,21 @@ class ProtoExtensionsTest { @Test fun preview_settings_and_selections_are_always_size_matched() { - val existing = listOf(ChannelSettings(name = "A"), ChannelSettings(name = "B")) + val existing = + listOf( + ChannelSettings.Builder().also { wb -> wb.name = "A" }.build(), + ChannelSettings.Builder().also { wb -> wb.name = "B" }.build(), + ) val incoming = listOf( - ChannelSettings(name = "A", psk = byteArrayOf(1).toByteString()), // duplicate of existing - ChannelSettings(name = "C"), - ChannelSettings(name = "D"), + ChannelSettings.Builder() + .also { wb -> + wb.name = "A" + wb.psk = byteArrayOf(1).toByteString() + } + .build(), // duplicate of existing + ChannelSettings.Builder().also { wb -> wb.name = "C" }.build(), + ChannelSettings.Builder().also { wb -> wb.name = "D" }.build(), ) val preview = getChannelPreviewForAdd(existing, incoming, ModelChannel.default.loraConfig, maxChannels = 8) @@ -603,26 +869,53 @@ class ProtoExtensionsTest { @Test fun normalize_single_element_passes_through() { - val primary = ChannelSettings(name = "Solo", psk = byteArrayOf(1).toByteString()) + val primary = + ChannelSettings.Builder() + .also { wb -> + wb.name = "Solo" + wb.psk = byteArrayOf(1).toByteString() + } + .build() assertEquals(listOf(primary), normalizeReplacementSettings(listOf(primary), ModelChannel.default.loraConfig)) } @Test fun normalize_drops_blank_placeholder_secondary() { - val primary = ChannelSettings(name = "Main", psk = byteArrayOf(1, 2).toByteString()) - val real = ChannelSettings(name = "Chat", psk = byteArrayOf(3).toByteString()) + val primary = + ChannelSettings.Builder() + .also { wb -> + wb.name = "Main" + wb.psk = byteArrayOf(1, 2).toByteString() + } + .build() + val real = + ChannelSettings.Builder() + .also { wb -> + wb.name = "Chat" + wb.psk = byteArrayOf(3).toByteString() + } + .build() val result = - normalizeReplacementSettings(listOf(primary, ChannelSettings(), real), ModelChannel.default.loraConfig) + normalizeReplacementSettings( + listOf(primary, ChannelSettings.Builder().build(), real), + ModelChannel.default.loraConfig, + ) assertEquals(listOf(primary, real), result) } @Test fun normalize_preserves_blank_primary() { - val blankPrimary = ChannelSettings() - val real = ChannelSettings(name = "Chat", psk = byteArrayOf(3).toByteString()) + val blankPrimary = ChannelSettings.Builder().build() + val real = + ChannelSettings.Builder() + .also { wb -> + wb.name = "Chat" + wb.psk = byteArrayOf(3).toByteString() + } + .build() // Slot 0 is always preserved, even when blank (deliberate disable signal). val result = normalizeReplacementSettings(listOf(blankPrimary, real), ModelChannel.default.loraConfig) @@ -634,8 +927,8 @@ class ProtoExtensionsTest { @Test fun normalize_blank_primary_does_not_seed_duplicate_tracking() { - val blankPrimary = ChannelSettings() - val publicSecondary = ChannelSettings(psk = byteArrayOf(1).toByteString()) + val blankPrimary = ChannelSettings.Builder().build() + val publicSecondary = ChannelSettings.Builder().also { wb -> wb.psk = byteArrayOf(1).toByteString() }.build() val result = normalizeReplacementSettings(listOf(blankPrimary, publicSecondary), ModelChannel.default.loraConfig) @@ -645,8 +938,20 @@ class ProtoExtensionsTest { @Test fun normalize_drops_semantic_duplicate_secondary() { - val primary = ChannelSettings(name = "Main", psk = byteArrayOf(1).toByteString()) - val dup = ChannelSettings(name = "Main", psk = byteArrayOf(1).toByteString()) + val primary = + ChannelSettings.Builder() + .also { wb -> + wb.name = "Main" + wb.psk = byteArrayOf(1).toByteString() + } + .build() + val dup = + ChannelSettings.Builder() + .also { wb -> + wb.name = "Main" + wb.psk = byteArrayOf(1).toByteString() + } + .build() val result = normalizeReplacementSettings(listOf(primary, dup), ModelChannel.default.loraConfig) @@ -655,8 +960,20 @@ class ProtoExtensionsTest { @Test fun normalize_keeps_same_name_different_psk() { - val primary = ChannelSettings(name = "A", psk = byteArrayOf(1).toByteString()) - val other = ChannelSettings(name = "A", psk = byteArrayOf(2).toByteString()) + val primary = + ChannelSettings.Builder() + .also { wb -> + wb.name = "A" + wb.psk = byteArrayOf(1).toByteString() + } + .build() + val other = + ChannelSettings.Builder() + .also { wb -> + wb.name = "A" + wb.psk = byteArrayOf(2).toByteString() + } + .build() val result = normalizeReplacementSettings(listOf(primary, other), ModelChannel.default.loraConfig) @@ -666,8 +983,20 @@ class ProtoExtensionsTest { @Test fun normalize_keeps_same_psk_different_name() { val psk = byteArrayOf(1, 2).toByteString() - val primary = ChannelSettings(name = "A", psk = psk) - val other = ChannelSettings(name = "B", psk = psk) + val primary = + ChannelSettings.Builder() + .also { wb -> + wb.name = "A" + wb.psk = psk + } + .build() + val other = + ChannelSettings.Builder() + .also { wb -> + wb.name = "B" + wb.psk = psk + } + .build() val result = normalizeReplacementSettings(listOf(primary, other), ModelChannel.default.loraConfig) @@ -676,18 +1005,41 @@ class ProtoExtensionsTest { @Test fun normalize_compacts_valid_secondaries_into_sequential_slots() { - val primary = ChannelSettings(name = "Main", psk = byteArrayOf(1).toByteString()) - val b = ChannelSettings(name = "B", psk = byteArrayOf(2).toByteString()) - val c = ChannelSettings(name = "C", psk = byteArrayOf(3).toByteString()) + val primary = + ChannelSettings.Builder() + .also { wb -> + wb.name = "Main" + wb.psk = byteArrayOf(1).toByteString() + } + .build() + val b = + ChannelSettings.Builder() + .also { wb -> + wb.name = "B" + wb.psk = byteArrayOf(2).toByteString() + } + .build() + val c = + ChannelSettings.Builder() + .also { wb -> + wb.name = "C" + wb.psk = byteArrayOf(3).toByteString() + } + .build() // blank + duplicate mixed in; valid B and C must compact to slots 1 and 2 with no gap val result = normalizeReplacementSettings( listOf( primary, - ChannelSettings(), + ChannelSettings.Builder().build(), b, - ChannelSettings(name = "Main", psk = byteArrayOf(1).toByteString()), + ChannelSettings.Builder() + .also { wb -> + wb.name = "Main" + wb.psk = byteArrayOf(1).toByteString() + } + .build(), c, ), ModelChannel.default.loraConfig, @@ -698,9 +1050,15 @@ class ProtoExtensionsTest { @Test fun normalize_null_lora_falls_back_to_defaults_without_crashing() { - val primary = ChannelSettings(name = "Main", psk = byteArrayOf(1).toByteString()) + val primary = + ChannelSettings.Builder() + .also { wb -> + wb.name = "Main" + wb.psk = byteArrayOf(1).toByteString() + } + .build() - val result = normalizeReplacementSettings(listOf(primary, ChannelSettings()), loraConfig = null) + val result = normalizeReplacementSettings(listOf(primary, ChannelSettings.Builder().build()), loraConfig = null) assertEquals(listOf(primary), result) } @@ -710,7 +1068,11 @@ class ProtoExtensionsTest { // Primary is always preserved (even blank); both blank placeholder secondaries are dropped. val result = normalizeReplacementSettings( - listOf(ChannelSettings(), ChannelSettings(), ChannelSettings()), + listOf( + ChannelSettings.Builder().build(), + ChannelSettings.Builder().build(), + ChannelSettings.Builder().build(), + ), ModelChannel.default.loraConfig, ) diff --git a/core/ui/src/commonTest/kotlin/org/meshtastic/core/ui/viewmodel/ConnectionsViewModelTest.kt b/core/ui/src/commonTest/kotlin/org/meshtastic/core/ui/viewmodel/ConnectionsViewModelTest.kt index 17204b14fe..dee52c9a89 100644 --- a/core/ui/src/commonTest/kotlin/org/meshtastic/core/ui/viewmodel/ConnectionsViewModelTest.kt +++ b/core/ui/src/commonTest/kotlin/org/meshtastic/core/ui/viewmodel/ConnectionsViewModelTest.kt @@ -101,7 +101,7 @@ class ConnectionsViewModelTest { dispatchedNotifications.clear() notificationsCanBeScheduled = true - every { radioConfigRepository.localConfigFlow } returns MutableStateFlow(LocalConfig()) + every { radioConfigRepository.localConfigFlow } returns MutableStateFlow(LocalConfig.Builder().build()) every { nodeManager.connectionIdentity } returns connectionIdentity connectionIdentity.value = null uiPrefs.hasShownNotPairedWarning.value = false @@ -136,7 +136,7 @@ class ConnectionsViewModelTest { @Test fun `txDisabled follows the LoRa tx_enabled flag and stays false before config arrives`() = runTest { - val configFlow = MutableStateFlow(LocalConfig()) + val configFlow = MutableStateFlow(LocalConfig.Builder().build()) every { radioConfigRepository.localConfigFlow } returns configFlow val vm = newViewModel() @@ -144,10 +144,16 @@ class ConnectionsViewModelTest { // No lora config yet: absence of the flag is not a disabled transmitter. assertEquals(false, awaitItem()) - configFlow.value = LocalConfig(lora = Config.LoRaConfig(tx_enabled = false)) + configFlow.value = + LocalConfig.Builder() + .also { wb -> wb.lora = Config.LoRaConfig.Builder().also { wb -> wb.tx_enabled = false }.build() } + .build() assertEquals(true, awaitItem()) - configFlow.value = LocalConfig(lora = Config.LoRaConfig(tx_enabled = true)) + configFlow.value = + LocalConfig.Builder() + .also { wb -> wb.lora = Config.LoRaConfig.Builder().also { wb -> wb.tx_enabled = true }.build() } + .build() assertEquals(false, awaitItem()) cancelAndIgnoreRemainingEvents() @@ -165,7 +171,16 @@ class ConnectionsViewModelTest { @Test fun `connection status stays lifecycle-only when region is unset`() = runTest { val configFlow = - MutableStateFlow(LocalConfig(lora = Config.LoRaConfig(region = Config.LoRaConfig.RegionCode.UNSET))) + MutableStateFlow( + LocalConfig.Builder() + .also { wb -> + wb.lora = + Config.LoRaConfig.Builder() + .also { wb -> wb.region = Config.LoRaConfig.RegionCode.UNSET } + .build() + } + .build(), + ) every { radioConfigRepository.localConfigFlow } returns configFlow val vm = newViewModel() @@ -179,7 +194,10 @@ class ConnectionsViewModelTest { assertEquals(false, awaitItem()) nodeRepository.setOurNode( - org.meshtastic.core.model.Node(num = 2, user = User(hw_model = HardwareModel.TBEAM)), + org.meshtastic.core.model.Node( + num = 2, + user = User.Builder().also { wb -> wb.hw_model = HardwareModel.TBEAM }.build(), + ), ) connectionIdentity.value = ConnectionIdentity(sessionGeneration = 2, address = "test", nodeNum = 2, deviceId = null) @@ -204,7 +222,10 @@ class ConnectionsViewModelTest { // A cached node whose num matches the fresh session identity can become warning-ready before Connected. nodeRepository.setOurNode( - org.meshtastic.core.model.Node(num = 7, user = User(hw_model = HardwareModel.TBEAM)), + org.meshtastic.core.model.Node( + num = 7, + user = User.Builder().also { wb -> wb.hw_model = HardwareModel.TBEAM }.build(), + ), ) connectionIdentity.value = ConnectionIdentity(sessionGeneration = 3, address = "test", nodeNum = 7, deviceId = null) @@ -235,7 +256,10 @@ class ConnectionsViewModelTest { assertEquals(false, awaitItem()) nodeRepository.setOurNode( - org.meshtastic.core.model.Node(num = 7, user = User(hw_model = HardwareModel.TBEAM)), + org.meshtastic.core.model.Node( + num = 7, + user = User.Builder().also { wb -> wb.hw_model = HardwareModel.TBEAM }.build(), + ), ) connectionIdentity.value = ConnectionIdentity(sessionGeneration = 3, address = "first", nodeNum = 7, deviceId = null) @@ -250,7 +274,10 @@ class ConnectionsViewModelTest { assertEquals(false, vm.activeNodeInfoReady.value) nodeRepository.setOurNode( - org.meshtastic.core.model.Node(num = 8, user = User(hw_model = HardwareModel.TBEAM)), + org.meshtastic.core.model.Node( + num = 8, + user = User.Builder().also { wb -> wb.hw_model = HardwareModel.TBEAM }.build(), + ), ) assertEquals(true, awaitItem()) @@ -319,7 +346,12 @@ class ConnectionsViewModelTest { ) nodeRepository.setMyId("!local") nodeRepository.setMyNodeInfo(TestDataFactory.createMyNodeInfo(firmwareVersion = "2.7.0", pioEnv = target)) - nodeRepository.setOurNode(org.meshtastic.core.model.Node(num = 1, user = User(hw_model = HardwareModel.TBEAM))) + nodeRepository.setOurNode( + org.meshtastic.core.model.Node( + num = 1, + user = User.Builder().also { wb -> wb.hw_model = HardwareModel.TBEAM }.build(), + ), + ) radioPrefs.setDevAddr("x:connected") firmwareReleaseRepository.setManifestTargets("v2.8.0", setOf(target)) firmwareReleaseRepository.setStableRelease(FirmwareRelease(id = "v2.8.0")) @@ -350,7 +382,12 @@ class ConnectionsViewModelTest { ) nodeRepository.setMyId("!local") nodeRepository.setMyNodeInfo(TestDataFactory.createMyNodeInfo(firmwareVersion = "2.7.0", pioEnv = target)) - nodeRepository.setOurNode(org.meshtastic.core.model.Node(num = 1, user = User(hw_model = HardwareModel.TBEAM))) + nodeRepository.setOurNode( + org.meshtastic.core.model.Node( + num = 1, + user = User.Builder().also { wb -> wb.hw_model = HardwareModel.TBEAM }.build(), + ), + ) radioPrefs.setDevAddr("x:connected") firmwareReleaseRepository.setManifestTargets("v2.8.0", setOf(target)) firmwareReleaseRepository.setStableRelease(FirmwareRelease(id = "v2.8.0")) @@ -374,7 +411,12 @@ class ConnectionsViewModelTest { ) nodeRepository.setMyId("!local") nodeRepository.setMyNodeInfo(TestDataFactory.createMyNodeInfo(firmwareVersion = "2.7.0", pioEnv = target)) - nodeRepository.setOurNode(org.meshtastic.core.model.Node(num = 1, user = User(hw_model = HardwareModel.TBEAM))) + nodeRepository.setOurNode( + org.meshtastic.core.model.Node( + num = 1, + user = User.Builder().also { wb -> wb.hw_model = HardwareModel.TBEAM }.build(), + ), + ) radioPrefs.setDevAddr("x:connected") firmwareReleaseRepository.setStableRelease(FirmwareRelease(id = "v2.8.0", lastUpdated = 0)) serviceRepository.setConnectionState(ConnectionState.Connected) @@ -395,7 +437,12 @@ class ConnectionsViewModelTest { ) nodeRepository.setMyId("!local") nodeRepository.setMyNodeInfo(TestDataFactory.createMyNodeInfo(firmwareVersion = "2.7.0", pioEnv = target)) - nodeRepository.setOurNode(org.meshtastic.core.model.Node(num = 1, user = User(hw_model = HardwareModel.TBEAM))) + nodeRepository.setOurNode( + org.meshtastic.core.model.Node( + num = 1, + user = User.Builder().also { wb -> wb.hw_model = HardwareModel.TBEAM }.build(), + ), + ) radioPrefs.setDevAddr("x:connected") firmwareReleaseRepository.setManifestTargets("v2.8.0", setOf("t-echo")) firmwareReleaseRepository.setStableRelease(FirmwareRelease(id = "v2.8.0")) diff --git a/core/ui/src/commonTest/kotlin/org/meshtastic/core/ui/viewmodel/UIViewModelImportSummaryTest.kt b/core/ui/src/commonTest/kotlin/org/meshtastic/core/ui/viewmodel/UIViewModelImportSummaryTest.kt index f2d90e78e9..b0d986428b 100644 --- a/core/ui/src/commonTest/kotlin/org/meshtastic/core/ui/viewmodel/UIViewModelImportSummaryTest.kt +++ b/core/ui/src/commonTest/kotlin/org/meshtastic/core/ui/viewmodel/UIViewModelImportSummaryTest.kt @@ -51,20 +51,57 @@ class UIViewModelImportSummaryTest { @Test fun ota_status_notifications_are_suppressed() { - assertTrue(ClientNotification(message = "Rebooting to WiFi OTA").isOtaStatusNotification()) - assertTrue(ClientNotification(message = "Rebooting to BLE OTA").isOtaStatusNotification()) assertTrue( - ClientNotification(message = "Cannot start OTA: OTA Loader partition not found.").isOtaStatusNotification(), + ClientNotification.Builder() + .also { wb -> wb.message = "Rebooting to WiFi OTA" } + .build() + .isOtaStatusNotification(), + ) + assertTrue( + ClientNotification.Builder() + .also { wb -> wb.message = "Rebooting to BLE OTA" } + .build() + .isOtaStatusNotification(), + ) + assertTrue( + ClientNotification.Builder() + .also { wb -> wb.message = "Cannot start OTA: OTA Loader partition not found." } + .build() + .isOtaStatusNotification(), + ) + assertTrue( + ClientNotification.Builder() + .also { wb -> wb.message = "OTA Loader does not support WiFi" } + .build() + .isOtaStatusNotification(), + ) + assertTrue( + ClientNotification.Builder() + .also { wb -> wb.message = "Unable to switch to the OTA partition." } + .build() + .isOtaStatusNotification(), ) - assertTrue(ClientNotification(message = "OTA Loader does not support WiFi").isOtaStatusNotification()) - assertTrue(ClientNotification(message = "Unable to switch to the OTA partition.").isOtaStatusNotification()) } @Test fun non_ota_status_notifications_are_not_suppressed() { - assertFalse(ClientNotification(message = "Low battery").isOtaStatusNotification()) - assertFalse(ClientNotification(message = "Key verification requested").isOtaStatusNotification()) - assertFalse(ClientNotification(message = "ROTATE credentials").isOtaStatusNotification()) - assertFalse(ClientNotification(message = "Quota exceeded").isOtaStatusNotification()) + assertFalse( + ClientNotification.Builder().also { wb -> wb.message = "Low battery" }.build().isOtaStatusNotification(), + ) + assertFalse( + ClientNotification.Builder() + .also { wb -> wb.message = "Key verification requested" } + .build() + .isOtaStatusNotification(), + ) + assertFalse( + ClientNotification.Builder() + .also { wb -> wb.message = "ROTATE credentials" } + .build() + .isOtaStatusNotification(), + ) + assertFalse( + ClientNotification.Builder().also { wb -> wb.message = "Quota exceeded" }.build().isOtaStatusNotification(), + ) } } diff --git a/core/ui/src/iosMain/kotlin/org/meshtastic/core/ui/util/NoopStubs.kt b/core/ui/src/iosMain/kotlin/org/meshtastic/core/ui/util/NoopStubs.kt index f45b20d3a4..e9d775da6f 100644 --- a/core/ui/src/iosMain/kotlin/org/meshtastic/core/ui/util/NoopStubs.kt +++ b/core/ui/src/iosMain/kotlin/org/meshtastic/core/ui/util/NoopStubs.kt @@ -17,6 +17,7 @@ package org.meshtastic.core.ui.util import androidx.compose.runtime.Composable +import androidx.compose.ui.input.key.KeyEvent import androidx.compose.ui.platform.ClipEntry import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.text.TextLinkStyles @@ -38,6 +39,8 @@ actual fun annotatedStringFromHtml(html: String, linkStyles: TextLinkStyles?): A @Composable actual fun rememberOpenUrl(): (url: String) -> Unit = { _ -> } +@Composable actual fun rememberShareText(): (text: String, subject: String) -> Unit = { _, _ -> } + @Composable actual fun rememberSaveFileLauncher( onUriReceived: (CommonUri) -> Unit, @@ -85,3 +88,5 @@ actual fun SetScreenBrightness(brightness: Float) { @Composable actual fun rememberLocalNetworkPermissionState(): PermissionUiState = grantedPermissionUiState() @Composable actual fun rememberCameraPermissionState(): PermissionUiState = grantedPermissionUiState() + +actual fun KeyEvent.isFromSoftKeyboard(): Boolean = false diff --git a/feature/map-maplibre/src/androidMain/kotlin/org/meshtastic/feature/map/maplibre/layers/SoftwareCanvasDecoding.android.kt b/core/ui/src/jvmMain/kotlin/org/meshtastic/core/ui/util/KeyEventUtils.kt similarity index 75% rename from feature/map-maplibre/src/androidMain/kotlin/org/meshtastic/feature/map/maplibre/layers/SoftwareCanvasDecoding.android.kt rename to core/ui/src/jvmMain/kotlin/org/meshtastic/core/ui/util/KeyEventUtils.kt index e936504e2c..b758d385fc 100644 --- a/feature/map-maplibre/src/androidMain/kotlin/org/meshtastic/feature/map/maplibre/layers/SoftwareCanvasDecoding.android.kt +++ b/core/ui/src/jvmMain/kotlin/org/meshtastic/core/ui/util/KeyEventUtils.kt @@ -14,9 +14,9 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -package org.meshtastic.feature.map.maplibre.layers +package org.meshtastic.core.ui.util -import coil3.request.ImageRequest -import coil3.request.allowHardware +import androidx.compose.ui.input.key.KeyEvent -internal actual fun ImageRequest.Builder.decodeForSoftwareCanvas(): ImageRequest.Builder = allowHardware(false) +/** Desktop has no on-screen keyboard; every key event is physical. */ +actual fun KeyEvent.isFromSoftKeyboard(): Boolean = false diff --git a/core/ui/src/jvmMain/kotlin/org/meshtastic/core/ui/util/PlatformUtils.kt b/core/ui/src/jvmMain/kotlin/org/meshtastic/core/ui/util/PlatformUtils.kt index 3efe4e9b3f..7b7781361c 100644 --- a/core/ui/src/jvmMain/kotlin/org/meshtastic/core/ui/util/PlatformUtils.kt +++ b/core/ui/src/jvmMain/kotlin/org/meshtastic/core/ui/util/PlatformUtils.kt @@ -19,7 +19,10 @@ package org.meshtastic.core.ui.util import androidx.compose.runtime.Composable +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.platform.LocalClipboard import co.touchlab.kermit.Logger +import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import org.jetbrains.compose.resources.StringResource import org.meshtastic.core.common.util.CommonUri @@ -32,6 +35,14 @@ import java.net.URI import javax.swing.JFileChooser /** JVM stub — NFC settings are not available on Desktop. */ +@Composable +actual fun rememberShareText(): (text: String, subject: String) -> Unit { + // Desktop has no share sheet. The clipboard is the honest equivalent, and the caller's UI says so. + val clipboard = LocalClipboard.current + val scope = rememberCoroutineScope() + return { text, _ -> scope.launch { clipboard.setClipEntry(createClipEntry(text, sensitive = true)) } } +} + @Composable actual fun rememberOpenNfcSettings(): () -> Unit = { Logger.w { "NFC settings not available on JVM/Desktop" } } diff --git a/crowdin.yml b/crowdin.yml index 974a9c4ca3..21d692ee8c 100644 --- a/crowdin.yml +++ b/crowdin.yml @@ -10,10 +10,46 @@ files: escape_quotes: 0 escape_special_characters: 0 type: android + # Crowdin's %locale% is not Google Play's language set: it renders regions Play rejects + # (ar-SA), `srp` for Serbian, and the modern Hebrew code where Play wants the legacy one. + # Nothing validates this - supply uploads every directory it finds and fastlane documents + # no behaviour for unknown ones - so a bad code is rejected by the Play API mid-upload, + # after earlier locales have already been written. - source: /fastlane/metadata/android/en-US/*.txt translation: /fastlane/metadata/android/%locale%/%original_file_name% + languages_mapping: &play_locales + locale: + # Keys are Crowdin language ids, not the locale %locale% renders. Play takes the + # bare language for these; %locale% would render a region it rejects. + ar: ar + be: be + bg: bg + ca: ca + et: et + hr: hr + lt: lt + ro: ro + sk: sk + sl: sl + sq: sq + uk: uk + # Play still uses the pre-1989 ISO code for Hebrew; %locale% renders he-IL. + he: iw-IL + # Crowdin renders Serbian as `srp`, which Play does not accept in any form. + sr: sr + # Play publishes no slot for these. Irish and Haitian Creole are absent from its + # supported-languages list entirely, and it lists Serbian only as `sr` with no script + # variant - mapping sr-Latn there too would point two languages at one directory and + # silently lose whichever wrote first. They keep translating the app's own strings; + # only the store listing is out of reach. + excluded_target_languages: &play_unsupported + - ga + - ht + - sr-Latn - source: /fastlane/metadata/android/en-US/changelogs/default.txt translation: /fastlane/metadata/android/%locale%/changelogs/%original_file_name% + languages_mapping: *play_locales + excluded_target_languages: *play_unsupported # In-app docs — user guide only (developer guide is English-only) # English sources live under docs/en/; translations land at docs/{android_code}/ # Uses %android_code% to output Android/CMP qualifier format directly (pt-rBR, zh-rCN, fr) diff --git a/desktopApp/build.gradle.kts b/desktopApp/build.gradle.kts index 5e2847c7c4..78470dc1be 100644 --- a/desktopApp/build.gradle.kts +++ b/desktopApp/build.gradle.kts @@ -19,6 +19,7 @@ import dev.detekt.gradle.Detekt import org.jetbrains.compose.desktop.application.dsl.TargetFormat import org.jetbrains.kotlin.gradle.dsl.JvmTarget import org.meshtastic.buildlogic.configureGraphTasks +import org.meshtastic.buildlogic.maplibreDesktopRuntime import org.meshtastic.buildlogic.resolveVersionInfo plugins { @@ -321,31 +322,8 @@ dependencies { // The MapLibre map surfaces, shared with the F-Droid Android flavor. implementation(projects.feature.mapMaplibre) - // Exactly one native runtime — the one matching this build host. Each carries that platform's - // maplibre-native blob, so adding them all would bloat every distribution with four unusable - // copies. Upstream publishes no macos-x64 artifact, which matches our release matrix. - run { - val osName = providers.systemProperty("os.name").get().lowercase() - val osArch = providers.systemProperty("os.arch").get().lowercase() - val isArm = osArch.contains("aarch64") || osArch.contains("arm64") - when { - osName.contains("mac") -> runtimeOnly(libs.maplibre.compose.runtime.metal.macos.arm64) - - osName.contains("win") -> - if (isArm) { - runtimeOnly(libs.maplibre.compose.runtime.vulkan.windows.arm64) - } else { - runtimeOnly(libs.maplibre.compose.runtime.vulkan.windows.x64) - } - - else -> - if (isArm) { - runtimeOnly(libs.maplibre.compose.runtime.vulkan.linux.arm64) - } else { - runtimeOnly(libs.maplibre.compose.runtime.vulkan.linux.x64) - } - } - } + // Exactly one native runtime, the one matching this build host (see maplibreDesktopRuntime()). + maplibreDesktopRuntime() implementation(libs.compose.multiplatform.animation) implementation(libs.compose.multiplatform.material3) implementation(libs.compose.multiplatform.runtime) diff --git a/desktopApp/packaging/linux/org.meshtastic.MeshtasticDesktop.metainfo.xml b/desktopApp/packaging/linux/org.meshtastic.MeshtasticDesktop.metainfo.xml index dfab0a674e..06cda696ec 100644 --- a/desktopApp/packaging/linux/org.meshtastic.MeshtasticDesktop.metainfo.xml +++ b/desktopApp/packaging/linux/org.meshtastic.MeshtasticDesktop.metainfo.xml @@ -75,19 +75,23 @@ Track every node on the mesh at a glance - https://raw.githubusercontent.com/meshtastic/Meshtastic-Android/fce37d60fbb54878549de8bd03cf102e4042808e/desktopApp/packaging/linux/screenshots/meshtastic-desktop-02-nodes.png + https://raw.githubusercontent.com/meshtastic/Meshtastic-Android/d6e4b82b2725c3798a5bf58ce3a4ff965b48fbf8/desktopApp/packaging/linux/screenshots/meshtastic-desktop-01-nodes.png Chat with your mesh, channel by channel - https://raw.githubusercontent.com/meshtastic/Meshtastic-Android/fce37d60fbb54878549de8bd03cf102e4042808e/desktopApp/packaging/linux/screenshots/meshtastic-desktop-03-messages.png + https://raw.githubusercontent.com/meshtastic/Meshtastic-Android/d6e4b82b2725c3798a5bf58ce3a4ff965b48fbf8/desktopApp/packaging/linux/screenshots/meshtastic-desktop-02-messages.png + + + See the whole mesh on a map + https://raw.githubusercontent.com/meshtastic/Meshtastic-Android/d6e4b82b2725c3798a5bf58ce3a4ff965b48fbf8/desktopApp/packaging/linux/screenshots/meshtastic-desktop-03-map.png Connect over Bluetooth, USB, or Wi-Fi - https://raw.githubusercontent.com/meshtastic/Meshtastic-Android/fce37d60fbb54878549de8bd03cf102e4042808e/desktopApp/packaging/linux/screenshots/meshtastic-desktop-01-connections.png + https://raw.githubusercontent.com/meshtastic/Meshtastic-Android/d6e4b82b2725c3798a5bf58ce3a4ff965b48fbf8/desktopApp/packaging/linux/screenshots/meshtastic-desktop-04-connections.png Full radio and module configuration - https://raw.githubusercontent.com/meshtastic/Meshtastic-Android/fce37d60fbb54878549de8bd03cf102e4042808e/desktopApp/packaging/linux/screenshots/meshtastic-desktop-04-settings.png + https://raw.githubusercontent.com/meshtastic/Meshtastic-Android/d6e4b82b2725c3798a5bf58ce3a4ff965b48fbf8/desktopApp/packaging/linux/screenshots/meshtastic-desktop-05-settings.png diff --git a/desktopApp/packaging/linux/screenshots/meshtastic-desktop-01-connections.png b/desktopApp/packaging/linux/screenshots/meshtastic-desktop-01-connections.png deleted file mode 100644 index 4297575af7..0000000000 Binary files a/desktopApp/packaging/linux/screenshots/meshtastic-desktop-01-connections.png and /dev/null differ diff --git a/desktopApp/packaging/linux/screenshots/meshtastic-desktop-01-nodes.png b/desktopApp/packaging/linux/screenshots/meshtastic-desktop-01-nodes.png new file mode 100644 index 0000000000..69c7bdf5ba Binary files /dev/null and b/desktopApp/packaging/linux/screenshots/meshtastic-desktop-01-nodes.png differ diff --git a/desktopApp/packaging/linux/screenshots/meshtastic-desktop-02-messages.png b/desktopApp/packaging/linux/screenshots/meshtastic-desktop-02-messages.png new file mode 100644 index 0000000000..7ce1027754 Binary files /dev/null and b/desktopApp/packaging/linux/screenshots/meshtastic-desktop-02-messages.png differ diff --git a/desktopApp/packaging/linux/screenshots/meshtastic-desktop-02-nodes.png b/desktopApp/packaging/linux/screenshots/meshtastic-desktop-02-nodes.png deleted file mode 100644 index fb4cb54abf..0000000000 Binary files a/desktopApp/packaging/linux/screenshots/meshtastic-desktop-02-nodes.png and /dev/null differ diff --git a/desktopApp/packaging/linux/screenshots/meshtastic-desktop-03-map.png b/desktopApp/packaging/linux/screenshots/meshtastic-desktop-03-map.png new file mode 100644 index 0000000000..d72654355e Binary files /dev/null and b/desktopApp/packaging/linux/screenshots/meshtastic-desktop-03-map.png differ diff --git a/desktopApp/packaging/linux/screenshots/meshtastic-desktop-03-messages.png b/desktopApp/packaging/linux/screenshots/meshtastic-desktop-03-messages.png deleted file mode 100644 index 4e8d3157b0..0000000000 Binary files a/desktopApp/packaging/linux/screenshots/meshtastic-desktop-03-messages.png and /dev/null differ diff --git a/desktopApp/packaging/linux/screenshots/meshtastic-desktop-04-connections.png b/desktopApp/packaging/linux/screenshots/meshtastic-desktop-04-connections.png new file mode 100644 index 0000000000..6b75454c48 Binary files /dev/null and b/desktopApp/packaging/linux/screenshots/meshtastic-desktop-04-connections.png differ diff --git a/desktopApp/packaging/linux/screenshots/meshtastic-desktop-04-settings.png b/desktopApp/packaging/linux/screenshots/meshtastic-desktop-04-settings.png deleted file mode 100644 index 4449243389..0000000000 Binary files a/desktopApp/packaging/linux/screenshots/meshtastic-desktop-04-settings.png and /dev/null differ diff --git a/desktopApp/packaging/linux/screenshots/meshtastic-desktop-05-settings.png b/desktopApp/packaging/linux/screenshots/meshtastic-desktop-05-settings.png new file mode 100644 index 0000000000..1ae722fff4 Binary files /dev/null and b/desktopApp/packaging/linux/screenshots/meshtastic-desktop-05-settings.png differ diff --git a/desktopApp/src/main/kotlin/org/meshtastic/desktop/DesktopNotificationManager.kt b/desktopApp/src/main/kotlin/org/meshtastic/desktop/DesktopNotificationManager.kt index 51d4e24f04..09e792a754 100644 --- a/desktopApp/src/main/kotlin/org/meshtastic/desktop/DesktopNotificationManager.kt +++ b/desktopApp/src/main/kotlin/org/meshtastic/desktop/DesktopNotificationManager.kt @@ -39,7 +39,7 @@ import androidx.compose.ui.window.Notification as ComposeNotification * Native sends run on [Dispatchers.IO] within the suspending [dispatch] call, so the returned Boolean reflects the * resolved outcome (native success, or acceptance by the tray fallback) rather than an optimistic guess. * - * Registered manually in `desktopPlatformStubsModule` -- do **not** add `@Single` to avoid double-registration with the + * Registered manually in `DesktopRuntimeModule` -- do **not** add `@Single` to avoid double-registration with the * `@ComponentScan("org.meshtastic.desktop")` in [DesktopDiModule][org.meshtastic.desktop.di.DesktopDiModule]. */ class DesktopNotificationManager( diff --git a/desktopApp/src/main/kotlin/org/meshtastic/desktop/Main.kt b/desktopApp/src/main/kotlin/org/meshtastic/desktop/Main.kt index 8955bee458..84681d7ace 100644 --- a/desktopApp/src/main/kotlin/org/meshtastic/desktop/Main.kt +++ b/desktopApp/src/main/kotlin/org/meshtastic/desktop/Main.kt @@ -71,10 +71,11 @@ import org.jetbrains.compose.resources.getString import org.jetbrains.compose.resources.stringResource import org.koin.compose.koinInject import org.koin.compose.viewmodel.koinViewModel -import org.koin.core.context.startKoin +import org.koin.core.context.GlobalContext import org.koin.core.context.stopKoin -import org.maplibre.compose.desktop.ProvideMapHost -import org.maplibre.compose.desktop.rememberAwtComposeMapHost +import org.koin.plugin.module.dsl.startKoin +import org.maplibre.compose.desktop.ProvideMapPresentationHost +import org.maplibre.compose.desktop.rememberAwtComposeMapPresentationHost import org.meshtastic.core.common.BuildConfigProvider import org.meshtastic.core.common.log.InMemoryLogBuffer import org.meshtastic.core.common.util.CommonUri @@ -105,11 +106,11 @@ import org.meshtastic.core.ui.util.LocalTracerouteMapProvider import org.meshtastic.core.ui.util.rememberOpenUrl import org.meshtastic.core.ui.viewmodel.UIViewModel import org.meshtastic.desktop.data.DesktopPreferencesDataSource -import org.meshtastic.desktop.di.desktopModule -import org.meshtastic.desktop.di.desktopPlatformModule +import org.meshtastic.desktop.di.DesktopKoinApp import org.meshtastic.desktop.map.DesktopTracerouteMap import org.meshtastic.desktop.map.desktopMapViewProvider import org.meshtastic.desktop.notification.DesktopOS +import org.meshtastic.desktop.notification.NativeNotificationSender import org.meshtastic.desktop.ui.DesktopMainScreen import org.meshtastic.feature.map.MapScreen import org.meshtastic.feature.map.SharedMapViewModel @@ -146,6 +147,7 @@ private fun svgPainterResource(path: String, density: Density): Painter = rememb @OptIn(ExperimentalCoilApi::class) fun main(args: Array) { + installQuitHandler() // exitProcessOnExit = false is what makes the shutdown block below reachable at all: with the default (true), // application() calls System.exit(0) itself as soon as the Compose loop ends, and control never returns here. // Do not "simplify" this back to a bare application {} — that silently disables every teardown that follows. @@ -156,8 +158,9 @@ fun main(args: Array) { // Keep console output and also capture into the in-memory buffer the Debug screen views/exports. Logger.setLogWriters(listOf(platformLogWriter(), InMemoryLogBuffer)) Logger.i { "Meshtastic Desktop — Starting" } - startKoin { modules(desktopPlatformModule(), desktopModule()) } + startKoin {} } + LaunchedEffect(Unit) { publishExitApplication(::exitApplication) } val systemLocale = remember { Locale.getDefault() } val uiViewModel = remember { koinApp.koin.get() } val httpClient = remember { koinApp.koin.get() } @@ -167,9 +170,11 @@ fun main(args: Array) { ThemeAndLocaleProvider(uiViewModel) } - // Runs on the main thread with the UI already gone. Closing the container fires the `onClose` callbacks that - // release native handles — currently libnotify's process-wide state in LinuxNotificationSender. Guarded because - // a teardown failure must not turn a clean quit into a non-zero exit. + // Runs on the main thread with the UI already gone. The native sender must be closed before the container goes, + // because it owns libnotify's process-wide handle on Linux. Both guarded: a teardown failure must not turn a + // clean quit into a non-zero exit. + runCatching { (GlobalContext.get().get() as? AutoCloseable)?.close() } + .onFailure { Logger.w(it) { "Closing the native notification sender failed during shutdown" } } runCatching { stopKoin() }.onFailure { Logger.w(it) { "stopKoin() failed during shutdown" } } Logger.i { "Meshtastic Desktop — Stopped" } @@ -410,7 +415,7 @@ private fun ApplicationScope.MeshtasticWindow( CoilImageLoaderSetup() // Each window hands MapLibre its own GPU context; the map composites into Compose from there. - ProvideMapHost(host = rememberAwtComposeMapHost(window)) { + ProvideMapPresentationHost(host = rememberAwtComposeMapPresentationHost(window)) { CompositionLocalProvider( LocalEventBranding provides eventEdition, LocalMapViewProvider provides desktopMapViewProvider(), diff --git a/desktopApp/src/main/kotlin/org/meshtastic/desktop/QuitHandler.kt b/desktopApp/src/main/kotlin/org/meshtastic/desktop/QuitHandler.kt new file mode 100644 index 0000000000..7c602e19dd --- /dev/null +++ b/desktopApp/src/main/kotlin/org/meshtastic/desktop/QuitHandler.kt @@ -0,0 +1,70 @@ +/* + * 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 . + */ +package org.meshtastic.desktop + +import java.awt.Desktop +import java.awt.desktop.QuitResponse +import java.util.concurrent.atomic.AtomicReference +import kotlin.concurrent.thread + +/** Published by the composition so [installQuitHandler] can end the Compose loop from AppKit's quit thread. */ +private val exitApplicationRef = AtomicReference<(() -> Unit)?>(null) + +/** A quit that arrived before the composition published its callback, held until it can be honoured. */ +private val pendingQuitRef = AtomicReference(null) + +/** Hands the running composition's `exitApplication` to [installQuitHandler], honouring a quit that beat it here. */ +internal fun publishExitApplication(exitApplication: () -> Unit) { + exitApplicationRef.set(exitApplication) + pendingQuitRef.getAndSet(null)?.let { response -> + exitApplication() + startQuitWatchdog() + response.cancelQuit() + } +} + +/** + * Routes macOS's AppKit quit (Cmd+Q and the app menu) into `exitApplication` so the shutdown in `main` runs at all. + * Without it AppKit kills the JVM directly and every teardown is skipped. CMP-6359, still open. + */ +internal fun installQuitHandler() { + if (!Desktop.isDesktopSupported()) return + val desktop = Desktop.getDesktop() + if (!desktop.isSupported(Desktop.Action.APP_QUIT_HANDLER)) return + desktop.setQuitHandler { _, response -> + val exitApplication = exitApplicationRef.get() + if (exitApplication == null) { + // Quitting before the composition published its callback must not skip the shutdown: hold the + // response until publishExitApplication can run it. + pendingQuitRef.set(response) + } else { + // Cancels the native quit because the shutdown this unblocks ends in exitProcess(). The watchdog is + // what stops a Compose loop that never returns from leaving the app un-quittable. + exitApplication() + startQuitWatchdog() + response.cancelQuit() + } + } +} + +/** Quitting must not depend on the Compose loop returning: force the exit if the shutdown has not run in time. */ +private fun startQuitWatchdog() = thread(isDaemon = true, name = "quit-watchdog") { + Thread.sleep(QUIT_TIMEOUT_MS) + Runtime.getRuntime().halt(0) +} + +private const val QUIT_TIMEOUT_MS = 5_000L diff --git a/desktopApp/src/main/kotlin/org/meshtastic/desktop/di/DesktopAiModule.kt b/desktopApp/src/main/kotlin/org/meshtastic/desktop/di/DesktopAiModule.kt new file mode 100644 index 0000000000..a3f512eb71 --- /dev/null +++ b/desktopApp/src/main/kotlin/org/meshtastic/desktop/di/DesktopAiModule.kt @@ -0,0 +1,41 @@ +/* + * Copyright (c) 2026 Meshtastic LLC + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package org.meshtastic.desktop.di + +import org.koin.core.annotation.Module +import org.koin.core.annotation.Single +import org.meshtastic.feature.discovery.ai.AlgorithmicSummaryProvider +import org.meshtastic.feature.discovery.ai.DiscoverySummaryAiProvider +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 +import org.meshtastic.feature.messaging.translation.MessageTranslationService +import org.meshtastic.feature.messaging.translation.NoOpMessageTranslator + +/** Keyword-only fallback AI assistant and no-op translators for Desktop (no on-device model). */ +@Module +class DesktopAiModule { + + @Single fun aiDocAssistant(fallback: KeywordFallbackAssistant): AIDocAssistant = fallback + + @Single fun discoverySummaryAiProvider(fallback: AlgorithmicSummaryProvider): DiscoverySummaryAiProvider = fallback + + @Single fun docTranslationService(): DocTranslationService = NoOpDocTranslator() + + @Single fun messageTranslationService(): MessageTranslationService = NoOpMessageTranslator() +} diff --git a/desktopApp/src/main/kotlin/org/meshtastic/desktop/di/DesktopKoinApp.kt b/desktopApp/src/main/kotlin/org/meshtastic/desktop/di/DesktopKoinApp.kt new file mode 100644 index 0000000000..4d4b37963c --- /dev/null +++ b/desktopApp/src/main/kotlin/org/meshtastic/desktop/di/DesktopKoinApp.kt @@ -0,0 +1,26 @@ +/* + * Copyright (c) 2026 Meshtastic LLC + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package org.meshtastic.desktop.di + +import org.koin.core.annotation.KoinApplication + +/** + * Root Koin bootstrap for Desktop. The K2 compiler plugin uses this to discover the full module graph when + * [org.koin.plugin.module.dsl.startKoin] is called with this type parameter. + */ +@KoinApplication(modules = [DesktopKoinModule::class]) +object DesktopKoinApp diff --git a/desktopApp/src/main/kotlin/org/meshtastic/desktop/di/DesktopKoinModule.kt b/desktopApp/src/main/kotlin/org/meshtastic/desktop/di/DesktopKoinModule.kt index ac5dc2eec2..f7d6d77505 100644 --- a/desktopApp/src/main/kotlin/org/meshtastic/desktop/di/DesktopKoinModule.kt +++ b/desktopApp/src/main/kotlin/org/meshtastic/desktop/di/DesktopKoinModule.kt @@ -14,258 +14,70 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -@file:Suppress( - "ktlint:standard:no-unused-imports", -) // Koin K2 compiler plugin generates aliased module extensions referenced in desktopModule() - package org.meshtastic.desktop.di -// Generated Koin module extensions from core KMP modules -import io.ktor.client.HttpClient -import io.ktor.client.engine.java.Java -import io.ktor.client.plugins.DefaultRequest -import io.ktor.client.plugins.HttpRequestRetry -import io.ktor.client.plugins.HttpTimeout -import io.ktor.client.plugins.contentnegotiation.ContentNegotiation -import io.ktor.client.plugins.logging.LogLevel -import io.ktor.client.plugins.logging.Logging -import io.ktor.client.request.header -import io.ktor.http.HttpHeaders -import io.ktor.serialization.kotlinx.json.json -import kotlinx.serialization.json.Json -import org.koin.dsl.module -import org.koin.dsl.onClose -import org.meshtastic.core.common.di.ServiceScope -import org.meshtastic.core.data.datasource.BundledAssetReader -import org.meshtastic.core.network.HttpClientDefaults -import org.meshtastic.core.network.KermitHttpLogger -import org.meshtastic.core.network.configureDefaultRetry -import org.meshtastic.core.network.repository.MQTTRepository -import org.meshtastic.core.network.service.ApiService -import org.meshtastic.core.network.service.ApiServiceImpl -import org.meshtastic.core.repository.AdminController -import org.meshtastic.core.repository.AppWidgetUpdater -import org.meshtastic.core.repository.ConnectionStateProvider -import org.meshtastic.core.repository.LocationRepository -import org.meshtastic.core.repository.MeshLocationManager -import org.meshtastic.core.repository.MeshNotificationManager -import org.meshtastic.core.repository.MeshWorkerManager -import org.meshtastic.core.repository.MessageQueue -import org.meshtastic.core.repository.MessagingController -import org.meshtastic.core.repository.NeighborInfoResponseProvider -import org.meshtastic.core.repository.NodeController -import org.meshtastic.core.repository.NotificationManager -import org.meshtastic.core.repository.PlatformAnalytics -import org.meshtastic.core.repository.QueryController -import org.meshtastic.core.repository.RadioController -import org.meshtastic.core.repository.RadioTransportFactory -import org.meshtastic.core.repository.ServiceRepository -import org.meshtastic.core.repository.ServiceStateWriter -import org.meshtastic.core.repository.TracerouteResponseProvider -import org.meshtastic.core.service.RadioControllerImpl -import org.meshtastic.core.service.ServiceRepositoryImpl -import org.meshtastic.desktop.DesktopBuildConfig -import org.meshtastic.desktop.DesktopNotificationManager -import org.meshtastic.desktop.notification.DesktopMeshNotificationManager -import org.meshtastic.desktop.notification.DesktopOS -import org.meshtastic.desktop.notification.LinuxNotificationSender -import org.meshtastic.desktop.notification.MacOSNotificationSender -import org.meshtastic.desktop.notification.NativeNotificationSender -import org.meshtastic.desktop.notification.WindowsNotificationSender -import org.meshtastic.desktop.radio.DesktopMessageQueue -import org.meshtastic.desktop.radio.DesktopRadioTransportFactory -import org.meshtastic.desktop.stub.NoopAppWidgetUpdater -import org.meshtastic.desktop.stub.NoopCompassHeadingProvider -import org.meshtastic.desktop.stub.NoopLocationRepository -import org.meshtastic.desktop.stub.NoopMQTTRepository -import org.meshtastic.desktop.stub.NoopMagneticFieldProvider -import org.meshtastic.desktop.stub.NoopMeshLocationManager -import org.meshtastic.desktop.stub.NoopMeshWorkerManager -import org.meshtastic.desktop.stub.NoopPhoneLocationProvider -import org.meshtastic.desktop.stub.NoopPlatformAnalytics -import org.meshtastic.feature.discovery.ai.AlgorithmicSummaryProvider -import org.meshtastic.feature.discovery.ai.DiscoverySummaryAiProvider -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 -import org.meshtastic.feature.messaging.translation.MessageTranslationService -import org.meshtastic.feature.messaging.translation.NoOpMessageTranslator -import org.meshtastic.feature.node.compass.CompassHeadingProvider -import org.meshtastic.feature.node.compass.MagneticFieldProvider -import org.meshtastic.feature.node.compass.PhoneLocationProvider -import org.meshtastic.core.ble.di.module as coreBleModule -import org.meshtastic.core.common.di.module as coreCommonModule -import org.meshtastic.core.data.di.module as coreDataModule -import org.meshtastic.core.database.di.module as coreDatabaseModule -import org.meshtastic.core.datastore.di.module as coreDatastoreModule -import org.meshtastic.core.di.di.module as coreDiModule -import org.meshtastic.core.domain.di.module as coreDomainModule -import org.meshtastic.core.network.di.module as coreNetworkModule -import org.meshtastic.core.prefs.di.module as corePrefsModule -import org.meshtastic.core.repository.di.module as coreRepositoryModule -import org.meshtastic.core.service.di.module as coreServiceModule -import org.meshtastic.core.takserver.di.module as coreTakServerModule -import org.meshtastic.core.ui.di.module as coreUiModule -import org.meshtastic.desktop.di.module as desktopDiModule -import org.meshtastic.feature.connections.di.module as featureConnectionsModule -import org.meshtastic.feature.discovery.di.module as featureDiscoveryModule -import org.meshtastic.feature.docs.di.module as featureDocsModule -import org.meshtastic.feature.firmware.di.module as featureFirmwareModule -import org.meshtastic.feature.intro.di.module as featureIntroModule -import org.meshtastic.feature.map.di.module as featureMapModule -import org.meshtastic.feature.messaging.di.module as featureMessagingModule -import org.meshtastic.feature.node.di.module as featureNodeModule -import org.meshtastic.feature.settings.di.module as featureSettingsModule -import org.meshtastic.feature.wifiprovision.di.module as featureWifiProvisionModule +import org.koin.core.annotation.Module +import org.meshtastic.core.ble.di.CoreBleModule +import org.meshtastic.core.common.di.CoreCommonModule +import org.meshtastic.core.data.di.CoreDataModule +import org.meshtastic.core.database.di.CoreDatabaseModule +import org.meshtastic.core.datastore.di.CoreDatastoreModule +import org.meshtastic.core.network.di.CoreNetworkModule +import org.meshtastic.core.prefs.di.CorePrefsModule +import org.meshtastic.core.service.di.CoreServiceModule +import org.meshtastic.core.takserver.di.CoreTakServerModule +import org.meshtastic.core.ui.di.CoreUiModule +import org.meshtastic.feature.connections.di.FeatureConnectionsModule +import org.meshtastic.feature.discovery.di.FeatureDiscoveryModule +import org.meshtastic.feature.docs.di.FeatureDocsModule +import org.meshtastic.feature.firmware.di.FeatureFirmwareModule +import org.meshtastic.feature.intro.di.FeatureIntroModule +import org.meshtastic.feature.map.di.FeatureMapModule +import org.meshtastic.feature.messaging.di.FeatureMessagingModule +import org.meshtastic.feature.node.di.FeatureNodeModule +import org.meshtastic.feature.settings.di.FeatureSettingsModule +import org.meshtastic.feature.wifiprovision.di.FeatureWifiProvisionModule /** - * Koin module for the Desktop target. + * Aggregate Koin module for the Desktop target — the single entry [DesktopKoinApp] points at. * - * Includes the generated Koin K2 modules from core KMP libraries (which provide real implementations of prefs, data - * repositories, managers, datastore data sources, use cases, and ViewModels from `commonMain`). - * - * Only truly platform-specific interfaces are stubbed here — things that require Android APIs (BLE/USB transport, - * notifications, WorkManager, location services, broadcasts, widgets). - * - * Platform infrastructure (DataStores, Room database, Lifecycle) is provided by [desktopPlatformModule]. + * Includes the `commonMain` module classes from the core KMP libraries (prefs, data repositories, managers, datastore + * data sources, use cases, and ViewModels), then the desktop-specific modules. The desktop modules come last so their + * bindings win over any `commonMain` default they deliberately replace. */ -fun desktopModule() = module { - // Include generated Koin K2 modules from core KMP libraries (commonMain implementations) - includes( - org.meshtastic.core.di.di.CoreDiModule().coreDiModule(), - org.meshtastic.core.common.di.CoreCommonModule().coreCommonModule(), - org.meshtastic.core.datastore.di.CoreDatastoreModule().coreDatastoreModule(), - org.meshtastic.core.prefs.di.CorePrefsModule().corePrefsModule(), - org.meshtastic.core.database.di.CoreDatabaseModule().coreDatabaseModule(), - org.meshtastic.core.data.di.CoreDataModule().coreDataModule(), - org.meshtastic.core.domain.di.CoreDomainModule().coreDomainModule(), - org.meshtastic.core.repository.di.CoreRepositoryModule().coreRepositoryModule(), - org.meshtastic.core.network.di.CoreNetworkModule().coreNetworkModule(), - org.meshtastic.core.ble.di.CoreBleModule().coreBleModule(), - org.meshtastic.core.ui.di.CoreUiModule().coreUiModule(), - org.meshtastic.core.service.di.CoreServiceModule().coreServiceModule(), - org.meshtastic.core.takserver.di.CoreTakServerModule().coreTakServerModule(), - org.meshtastic.feature.settings.di.FeatureSettingsModule().featureSettingsModule(), - org.meshtastic.feature.node.di.FeatureNodeModule().featureNodeModule(), - org.meshtastic.feature.messaging.di.FeatureMessagingModule().featureMessagingModule(), - org.meshtastic.feature.connections.di.FeatureConnectionsModule().featureConnectionsModule(), - org.meshtastic.feature.map.di.FeatureMapModule().featureMapModule(), - org.meshtastic.feature.discovery.di.FeatureDiscoveryModule().featureDiscoveryModule(), - org.meshtastic.feature.firmware.di.FeatureFirmwareModule().featureFirmwareModule(), - org.meshtastic.feature.docs.di.FeatureDocsModule().featureDocsModule(), - org.meshtastic.feature.intro.di.FeatureIntroModule().featureIntroModule(), - org.meshtastic.feature.wifiprovision.di.FeatureWifiProvisionModule().featureWifiProvisionModule(), - org.meshtastic.desktop.di.DesktopDiModule().desktopDiModule(), - desktopPlatformStubsModule(), - ) -} - -/** - * Stubs for truly platform-specific interfaces that have no `commonMain` implementation. These require Android APIs - * (BLE/USB transport, notifications, WorkManager, location, broadcasts, widgets). - */ -@Suppress("LongMethod") -private fun desktopPlatformStubsModule() = module { - single { ServiceRepositoryImpl() } - single { get() } - single { get() } - single { get() } - single { get() } - single { - DesktopRadioTransportFactory( - dispatchers = get(), - scanner = get(), - bluetoothRepository = get(), - connectionFactory = get(), - ) - } - single { - RadioControllerImpl( - serviceRepository = get(), - nodeRepository = get(), - commandSender = get(), - nodeManager = get(), - radioInterfaceService = get(), - locationManager = get(), - packetRepository = lazy { get() }, - dataHandler = lazy { get() }, - analytics = get(), - meshPrefs = get(), - uiPrefs = get(), - databaseManager = get(), - notificationManager = get(), - messageProcessor = lazy { get() }, - radioConfigRepository = get(), - scope = get(), - ) - } - single { get() } - single { get() } - single { get() } - single { get() } - single { - when (DesktopOS.current()) { - DesktopOS.Linux -> LinuxNotificationSender() - DesktopOS.MacOS -> MacOSNotificationSender() - DesktopOS.Windows -> WindowsNotificationSender() - } - } - .onClose { sender -> - // Only the Linux sender holds a native handle; the others are stateless. `stopKoin()` in Main.kt is what - // drives this, after the Compose application loop has returned. - (sender as? AutoCloseable)?.close() - } - single { DesktopNotificationManager(prefs = get(), nativeSender = get()) } - single { get() } - single { DesktopMeshNotificationManager(notificationManager = get()) } - single { NoopPlatformAnalytics() } - single { NoopAppWidgetUpdater() } - single { NoopMeshWorkerManager() } - single { DesktopMessageQueue(packetRepository = get(), radioController = get(), dispatchers = get()) } - single { NoopMeshLocationManager() } - single { NoopLocationRepository() } - single { NoopMQTTRepository() } - single { NoopCompassHeadingProvider() } - single { NoopPhoneLocationProvider() } - single { NoopMagneticFieldProvider() } - - // AI assistant: keyword-only fallback on desktop (no on-device model) - single { get() } - single { get() } - single { NoOpDocTranslator() } - single { NoOpMessageTranslator() } - - // Desktop uses the real ApiService implementation (no flavor stub needed) - single { ApiServiceImpl(client = get()) } - - // Ktor HttpClient for JVM/Desktop (equivalent of CoreNetworkAndroidModule on Android) - single { - HttpClient(Java) { - engine { - protocolVersion = java.net.http.HttpClient.Version.HTTP_2 - config { followRedirects(java.net.http.HttpClient.Redirect.NORMAL) } - } - install(ContentNegotiation) { json(get()) } - install(DefaultRequest) { - url(HttpClientDefaults.API_BASE_URL) - header(HttpHeaders.UserAgent, "Meshtastic-Desktop/${DesktopBuildConfig.VERSION_NAME}") - } - install(HttpTimeout) { - requestTimeoutMillis = HttpClientDefaults.REQUEST_TIMEOUT_MS - connectTimeoutMillis = HttpClientDefaults.TIMEOUT_MS - socketTimeoutMillis = HttpClientDefaults.TIMEOUT_MS - } - install(HttpRequestRetry) { configureDefaultRetry() } - if (DesktopBuildConfig.IS_DEBUG) { - install(Logging) { - logger = KermitHttpLogger - level = LogLevel.INFO - } - } - } - } - - // Desktop has no bundled Android assets; repositories seed from the network instead. - single { BundledAssetReader { null } } -} +@Module( + includes = + [ + org.meshtastic.core.di.di.CoreDiModule::class, + CoreCommonModule::class, + CoreBleModule::class, + CoreDataModule::class, + org.meshtastic.core.domain.di.CoreDomainModule::class, + CoreDatabaseModule::class, + org.meshtastic.core.repository.di.CoreRepositoryModule::class, + CoreDatastoreModule::class, + CorePrefsModule::class, + CoreServiceModule::class, + CoreNetworkModule::class, + CoreTakServerModule::class, + CoreUiModule::class, + FeatureNodeModule::class, + FeatureMessagingModule::class, + FeatureConnectionsModule::class, + FeatureMapModule::class, + FeatureSettingsModule::class, + FeatureDiscoveryModule::class, + FeatureDocsModule::class, + FeatureFirmwareModule::class, + FeatureIntroModule::class, + FeatureWifiProvisionModule::class, + DesktopDiModule::class, + DesktopPlatformModule::class, + DesktopPreferencesDataStoreModule::class, + DesktopProtoDataStoreModule::class, + DesktopRuntimeModule::class, + DesktopStubsModule::class, + DesktopAiModule::class, + ], +) +class DesktopKoinModule diff --git a/desktopApp/src/main/kotlin/org/meshtastic/desktop/di/DesktopPlatformModule.kt b/desktopApp/src/main/kotlin/org/meshtastic/desktop/di/DesktopPlatformModule.kt index ea79c13596..985a418c4a 100644 --- a/desktopApp/src/main/kotlin/org/meshtastic/desktop/di/DesktopPlatformModule.kt +++ b/desktopApp/src/main/kotlin/org/meshtastic/desktop/di/DesktopPlatformModule.kt @@ -16,89 +16,21 @@ */ package org.meshtastic.desktop.di -import androidx.datastore.core.DataStore -import androidx.datastore.core.DataStoreFactory -import androidx.datastore.core.handlers.ReplaceFileCorruptionHandler -import androidx.datastore.core.okio.OkioSerializer -import androidx.datastore.core.okio.OkioStorage -import androidx.datastore.preferences.core.PreferenceDataStoreFactory -import androidx.datastore.preferences.core.Preferences -import androidx.datastore.preferences.core.emptyPreferences import androidx.lifecycle.Lifecycle import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.LifecycleRegistry -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.SupervisorJob -import okio.FileSystem -import okio.Path.Companion.toPath -import org.koin.core.qualifier.named -import org.koin.dsl.module +import org.koin.core.annotation.Module +import org.koin.core.annotation.Named +import org.koin.core.annotation.Single import org.meshtastic.core.common.BuildConfigProvider import org.meshtastic.core.common.di.PROCESS_LIFECYCLE -import org.meshtastic.core.database.desktopDataDir -import org.meshtastic.core.datastore.di.CoreChannelSetDataStore -import org.meshtastic.core.datastore.di.CoreLocalConfigDataStore -import org.meshtastic.core.datastore.di.CoreLocalStatsDataStore -import org.meshtastic.core.datastore.di.CoreModuleConfigDataStore -import org.meshtastic.core.datastore.di.CorePreferencesDataStore -import org.meshtastic.core.datastore.di.DataStoreScope -import org.meshtastic.core.datastore.di.asCoreChannelSetDataStore -import org.meshtastic.core.datastore.di.asCoreLocalConfigDataStore -import org.meshtastic.core.datastore.di.asCoreLocalStatsDataStore -import org.meshtastic.core.datastore.di.asCoreModuleConfigDataStore -import org.meshtastic.core.datastore.di.asCorePreferencesDataStore -import org.meshtastic.core.datastore.di.asDataStoreScope -import org.meshtastic.core.datastore.serializer.ChannelSetSerializer -import org.meshtastic.core.datastore.serializer.LocalConfigSerializer -import org.meshtastic.core.datastore.serializer.LocalStatsSerializer -import org.meshtastic.core.datastore.serializer.ModuleConfigSerializer -import org.meshtastic.core.di.CoroutineDispatchers -import org.meshtastic.core.prefs.di.AnalyticsDataStore -import org.meshtastic.core.prefs.di.AppDataStore -import org.meshtastic.core.prefs.di.CustomEmojiDataStore -import org.meshtastic.core.prefs.di.FilterDataStore -import org.meshtastic.core.prefs.di.HomoglyphEncodingDataStore -import org.meshtastic.core.prefs.di.MapConsentDataStore -import org.meshtastic.core.prefs.di.MapDataStore -import org.meshtastic.core.prefs.di.MapTileProviderDataStore -import org.meshtastic.core.prefs.di.MeshDataStore -import org.meshtastic.core.prefs.di.MeshLogDataStore -import org.meshtastic.core.prefs.di.RadioDataStore -import org.meshtastic.core.prefs.di.UiDataStore -import org.meshtastic.core.prefs.di.asAnalyticsDataStore -import org.meshtastic.core.prefs.di.asAppDataStore -import org.meshtastic.core.prefs.di.asCustomEmojiDataStore -import org.meshtastic.core.prefs.di.asFilterDataStore -import org.meshtastic.core.prefs.di.asHomoglyphEncodingDataStore -import org.meshtastic.core.prefs.di.asMapConsentDataStore -import org.meshtastic.core.prefs.di.asMapDataStore -import org.meshtastic.core.prefs.di.asMapTileProviderDataStore -import org.meshtastic.core.prefs.di.asMeshDataStore -import org.meshtastic.core.prefs.di.asMeshLogDataStore -import org.meshtastic.core.prefs.di.asRadioDataStore -import org.meshtastic.core.prefs.di.asUiDataStore import org.meshtastic.desktop.DesktopBuildConfig -import org.meshtastic.proto.ChannelSet -import org.meshtastic.proto.LocalConfig -import org.meshtastic.proto.LocalModuleConfig -import org.meshtastic.proto.LocalStats - -/** Creates a file-backed [DataStore]<[Preferences]> at the given path under the data directory. */ -private fun prefsStore(name: String, scope: DataStoreScope): DataStore { - val dir = desktopDataDir() + "/datastore" - FileSystem.SYSTEM.createDirectories(dir.toPath()) - return PreferenceDataStoreFactory.createWithPath( - corruptionHandler = ReplaceFileCorruptionHandler(produceNewData = { emptyPreferences() }), - scope = scope, - produceFile = { "$dir/$name.preferences_pb".toPath() }, - ) -} /** * Synthetic [LifecycleOwner] that stays permanently in [Lifecycle.State.RESUMED]. Replaces Android's * `ProcessLifecycleOwner` for desktop. */ -private class DesktopProcessLifecycleOwner : LifecycleOwner { +internal class DesktopProcessLifecycleOwner : LifecycleOwner { private val registry = LifecycleRegistry(this) init { @@ -110,91 +42,30 @@ private class DesktopProcessLifecycleOwner : LifecycleOwner { } /** - * Desktop platform infrastructure module. + * Desktop platform infrastructure module: [BuildConfigProvider] and the process [Lifecycle]. * - * Provides all platform-specific bindings that the real KMP `commonMain` implementations need: - * - Named [DataStore]<[Preferences]> instances (12 preference stores + 1 core preferences store) - * - Proto [DataStore] instances (LocalConfig, ModuleConfig, ChannelSet, LocalStats) - * - [Lifecycle] (`ProcessLifecycle`) - * - [BuildConfigProvider] + * The DataStore instances the `commonMain` implementations need live in [DesktopPreferencesDataStoreModule] and + * [DesktopProtoDataStoreModule]. */ -fun desktopPlatformModule() = module { - // Application-lifetime scope shared by all DataStore instances. Per the DataStore docs: - // "The Job within this context dictates the lifecycle of the DataStore's internal operations. - // Ensure it is an application-scoped context that is not canceled by UI lifecycle events." - // DataStore has no close() API — the in-memory cache is released only when this Job is cancelled - // (at process exit). Using SupervisorJob so a single store's failure doesn't cascade. - single { CoroutineScope(get().io + SupervisorJob()).asDataStoreScope() } +@Module +class DesktopPlatformModule { - includes(desktopPreferencesDataStoreModule(), desktopProtoDataStoreModule()) - - // -- Build config (values generated at build time by generateDesktopBuildConfig) -- - single { - object : BuildConfigProvider { - override val isDebug: Boolean = DesktopBuildConfig.IS_DEBUG - override val applicationId: String = DesktopBuildConfig.APPLICATION_ID - override val versionCode: Int = DesktopBuildConfig.VERSION_CODE - override val versionName: String = DesktopBuildConfig.VERSION_NAME - override val absoluteMinFwVersion: String = DesktopBuildConfig.ABS_MIN_FW_VERSION - override val minFwVersion: String = DesktopBuildConfig.MIN_FW_VERSION - } + /** Values generated at build time by `generateDesktopBuildConfig`. */ + @Single + fun buildConfigProvider(): BuildConfigProvider = object : BuildConfigProvider { + override val isDebug: Boolean = DesktopBuildConfig.IS_DEBUG + override val applicationId: String = DesktopBuildConfig.APPLICATION_ID + override val versionCode: Int = DesktopBuildConfig.VERSION_CODE + override val versionName: String = DesktopBuildConfig.VERSION_NAME + override val absoluteMinFwVersion: String = DesktopBuildConfig.ABS_MIN_FW_VERSION + override val minFwVersion: String = DesktopBuildConfig.MIN_FW_VERSION } - // -- Process Lifecycle (stays RESUMED forever on desktop) -- - single(named(PROCESS_LIFECYCLE)) { DesktopProcessLifecycleOwner().lifecycle } -} - -/** Typed preference-datastore singletons for each preference domain. */ -private fun desktopPreferencesDataStoreModule() = module { - single { prefsStore("analytics", get()).asAnalyticsDataStore() } - single { prefsStore("homoglyph_encoding", get()).asHomoglyphEncodingDataStore() } - single { prefsStore("app", get()).asAppDataStore() } - single { prefsStore("custom_emoji", get()).asCustomEmojiDataStore() } - single { prefsStore("map", get()).asMapDataStore() } - single { prefsStore("map_consent", get()).asMapConsentDataStore() } - single { prefsStore("map_tile_provider", get()).asMapTileProviderDataStore() } - single { prefsStore("mesh", get()).asMeshDataStore() } - single { prefsStore("radio", get()).asRadioDataStore() } - single { prefsStore("ui", get()).asUiDataStore() } - single { prefsStore("meshlog", get()).asMeshLogDataStore() } - single { prefsStore("filter", get()).asFilterDataStore() } - single { prefsStore("core_preferences", get()).asCorePreferencesDataStore() } -} - -/** The path is an on-disk identity — changing it orphans existing user data. */ -private fun protoStore( - serializer: OkioSerializer, - path: String, - produceNewData: () -> T, - scope: DataStoreScope, -): DataStore = DataStoreFactory.create( - storage = OkioStorage(fileSystem = FileSystem.SYSTEM, serializer = serializer, producePath = { path.toPath() }), - corruptionHandler = ReplaceFileCorruptionHandler(produceNewData = { produceNewData() }), - scope = scope, -) - -/** Proto [DataStore] instances (OkioStorage-backed). */ -private fun desktopProtoDataStoreModule() = module { - val protoDir = desktopDataDir() + "/datastore" - FileSystem.SYSTEM.createDirectories(protoDir.toPath()) - - single { - protoStore(LocalConfigSerializer, "$protoDir/local_config.pb", { LocalConfig() }, get()) - .asCoreLocalConfigDataStore() - } - - single { - protoStore(ModuleConfigSerializer, "$protoDir/module_config.pb", { LocalModuleConfig() }, get()) - .asCoreModuleConfigDataStore() - } - - single { - protoStore(ChannelSetSerializer, "$protoDir/channel_set.pb", { ChannelSet() }, get()) - .asCoreChannelSetDataStore() - } - - single { - protoStore(LocalStatsSerializer, "$protoDir/local_stats.pb", { LocalStats() }, get()) - .asCoreLocalStatsDataStore() - } + // LifecycleRegistry holds its owner weakly, so the owner is a definition too: + // once it is collected, addObserver silently stops registering. + @Single internal fun processLifecycleOwner(): DesktopProcessLifecycleOwner = DesktopProcessLifecycleOwner() + + @Single + @Named(PROCESS_LIFECYCLE) + internal fun processLifecycle(owner: DesktopProcessLifecycleOwner): Lifecycle = owner.lifecycle } diff --git a/desktopApp/src/main/kotlin/org/meshtastic/desktop/di/DesktopPreferencesDataStoreModule.kt b/desktopApp/src/main/kotlin/org/meshtastic/desktop/di/DesktopPreferencesDataStoreModule.kt new file mode 100644 index 0000000000..8851e98363 --- /dev/null +++ b/desktopApp/src/main/kotlin/org/meshtastic/desktop/di/DesktopPreferencesDataStoreModule.kt @@ -0,0 +1,112 @@ +/* + * 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 . + */ +package org.meshtastic.desktop.di + +import androidx.datastore.core.DataStore +import androidx.datastore.core.handlers.ReplaceFileCorruptionHandler +import androidx.datastore.preferences.core.PreferenceDataStoreFactory +import androidx.datastore.preferences.core.Preferences +import androidx.datastore.preferences.core.emptyPreferences +import okio.FileSystem +import okio.Path.Companion.toPath +import org.koin.core.annotation.Module +import org.koin.core.annotation.Single +import org.meshtastic.core.database.desktopDataDir +import org.meshtastic.core.datastore.di.CorePreferencesDataStore +import org.meshtastic.core.datastore.di.DataStoreScope +import org.meshtastic.core.datastore.di.asCorePreferencesDataStore +import org.meshtastic.core.prefs.di.AnalyticsDataStore +import org.meshtastic.core.prefs.di.AppDataStore +import org.meshtastic.core.prefs.di.CustomEmojiDataStore +import org.meshtastic.core.prefs.di.FilterDataStore +import org.meshtastic.core.prefs.di.HomoglyphEncodingDataStore +import org.meshtastic.core.prefs.di.MapConsentDataStore +import org.meshtastic.core.prefs.di.MapDataStore +import org.meshtastic.core.prefs.di.MapTileProviderDataStore +import org.meshtastic.core.prefs.di.MeshDataStore +import org.meshtastic.core.prefs.di.MeshLogDataStore +import org.meshtastic.core.prefs.di.RadioDataStore +import org.meshtastic.core.prefs.di.UiDataStore +import org.meshtastic.core.prefs.di.asAnalyticsDataStore +import org.meshtastic.core.prefs.di.asAppDataStore +import org.meshtastic.core.prefs.di.asCustomEmojiDataStore +import org.meshtastic.core.prefs.di.asFilterDataStore +import org.meshtastic.core.prefs.di.asHomoglyphEncodingDataStore +import org.meshtastic.core.prefs.di.asMapConsentDataStore +import org.meshtastic.core.prefs.di.asMapDataStore +import org.meshtastic.core.prefs.di.asMapTileProviderDataStore +import org.meshtastic.core.prefs.di.asMeshDataStore +import org.meshtastic.core.prefs.di.asMeshLogDataStore +import org.meshtastic.core.prefs.di.asRadioDataStore +import org.meshtastic.core.prefs.di.asUiDataStore + +/** Typed preference-datastore singletons for each preference domain, one file each under the data directory. */ +@Suppress("TooManyFunctions") +@Module +class DesktopPreferencesDataStoreModule { + + @Single + fun analyticsDataStore(scope: DataStoreScope): AnalyticsDataStore = + prefsStore("analytics", scope).asAnalyticsDataStore() + + @Single + fun homoglyphEncodingDataStore(scope: DataStoreScope): HomoglyphEncodingDataStore = + prefsStore("homoglyph_encoding", scope).asHomoglyphEncodingDataStore() + + @Single fun appDataStore(scope: DataStoreScope): AppDataStore = prefsStore("app", scope).asAppDataStore() + + @Single + fun customEmojiDataStore(scope: DataStoreScope): CustomEmojiDataStore = + prefsStore("custom_emoji", scope).asCustomEmojiDataStore() + + @Single fun mapDataStore(scope: DataStoreScope): MapDataStore = prefsStore("map", scope).asMapDataStore() + + @Single + fun mapConsentDataStore(scope: DataStoreScope): MapConsentDataStore = + prefsStore("map_consent", scope).asMapConsentDataStore() + + @Single + fun mapTileProviderDataStore(scope: DataStoreScope): MapTileProviderDataStore = + prefsStore("map_tile_provider", scope).asMapTileProviderDataStore() + + @Single fun meshDataStore(scope: DataStoreScope): MeshDataStore = prefsStore("mesh", scope).asMeshDataStore() + + @Single fun radioDataStore(scope: DataStoreScope): RadioDataStore = prefsStore("radio", scope).asRadioDataStore() + + @Single fun uiDataStore(scope: DataStoreScope): UiDataStore = prefsStore("ui", scope).asUiDataStore() + + @Single + fun meshLogDataStore(scope: DataStoreScope): MeshLogDataStore = prefsStore("meshlog", scope).asMeshLogDataStore() + + @Single + fun filterDataStore(scope: DataStoreScope): FilterDataStore = prefsStore("filter", scope).asFilterDataStore() + + @Single + fun corePreferencesDataStore(scope: DataStoreScope): CorePreferencesDataStore = + prefsStore("core_preferences", scope).asCorePreferencesDataStore() +} + +/** [name] is an on-disk identity — changing it orphans existing user data. */ +private fun prefsStore(name: String, scope: DataStoreScope): DataStore { + val dir = desktopDataDir() + "/datastore" + FileSystem.SYSTEM.createDirectories(dir.toPath()) + return PreferenceDataStoreFactory.createWithPath( + corruptionHandler = ReplaceFileCorruptionHandler(produceNewData = { emptyPreferences() }), + scope = scope, + produceFile = { "$dir/$name.preferences_pb".toPath() }, + ) +} diff --git a/desktopApp/src/main/kotlin/org/meshtastic/desktop/di/DesktopProtoDataStoreModule.kt b/desktopApp/src/main/kotlin/org/meshtastic/desktop/di/DesktopProtoDataStoreModule.kt new file mode 100644 index 0000000000..4d5e85f990 --- /dev/null +++ b/desktopApp/src/main/kotlin/org/meshtastic/desktop/di/DesktopProtoDataStoreModule.kt @@ -0,0 +1,91 @@ +/* + * 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 . + */ +package org.meshtastic.desktop.di + +import androidx.datastore.core.DataStore +import androidx.datastore.core.DataStoreFactory +import androidx.datastore.core.handlers.ReplaceFileCorruptionHandler +import androidx.datastore.core.okio.OkioSerializer +import androidx.datastore.core.okio.OkioStorage +import okio.FileSystem +import okio.Path.Companion.toPath +import org.koin.core.annotation.Module +import org.koin.core.annotation.Single +import org.meshtastic.core.database.desktopDataDir +import org.meshtastic.core.datastore.di.CoreChannelSetDataStore +import org.meshtastic.core.datastore.di.CoreLocalConfigDataStore +import org.meshtastic.core.datastore.di.CoreLocalStatsDataStore +import org.meshtastic.core.datastore.di.CoreModuleConfigDataStore +import org.meshtastic.core.datastore.di.DataStoreScope +import org.meshtastic.core.datastore.di.asCoreChannelSetDataStore +import org.meshtastic.core.datastore.di.asCoreLocalConfigDataStore +import org.meshtastic.core.datastore.di.asCoreLocalStatsDataStore +import org.meshtastic.core.datastore.di.asCoreModuleConfigDataStore +import org.meshtastic.core.datastore.serializer.ChannelSetSerializer +import org.meshtastic.core.datastore.serializer.LocalConfigSerializer +import org.meshtastic.core.datastore.serializer.LocalStatsSerializer +import org.meshtastic.core.datastore.serializer.ModuleConfigSerializer +import org.meshtastic.proto.ChannelSet +import org.meshtastic.proto.LocalConfig +import org.meshtastic.proto.LocalModuleConfig +import org.meshtastic.proto.LocalStats + +/** Proto [DataStore] instances (OkioStorage-backed). */ +@Module +class DesktopProtoDataStoreModule { + + @Single + fun localConfigDataStore(scope: DataStoreScope): CoreLocalConfigDataStore = + protoStore(LocalConfigSerializer, "local_config.pb", { LocalConfig.Builder().build() }, scope) + .asCoreLocalConfigDataStore() + + @Single + fun moduleConfigDataStore(scope: DataStoreScope): CoreModuleConfigDataStore = + protoStore(ModuleConfigSerializer, "module_config.pb", { LocalModuleConfig.Builder().build() }, scope) + .asCoreModuleConfigDataStore() + + @Single + fun channelSetDataStore(scope: DataStoreScope): CoreChannelSetDataStore = + protoStore(ChannelSetSerializer, "channel_set.pb", { ChannelSet.Builder().build() }, scope) + .asCoreChannelSetDataStore() + + @Single + fun localStatsDataStore(scope: DataStoreScope): CoreLocalStatsDataStore = + protoStore(LocalStatsSerializer, "local_stats.pb", { LocalStats.Builder().build() }, scope) + .asCoreLocalStatsDataStore() +} + +/** [fileName] is an on-disk identity — changing it orphans existing user data. */ +private fun protoStore( + serializer: OkioSerializer, + fileName: String, + produceNewData: () -> T, + scope: DataStoreScope, +): DataStore { + val dir = desktopDataDir() + "/datastore" + FileSystem.SYSTEM.createDirectories(dir.toPath()) + return DataStoreFactory.create( + storage = + OkioStorage( + fileSystem = FileSystem.SYSTEM, + serializer = serializer, + producePath = { "$dir/$fileName".toPath() }, + ), + corruptionHandler = ReplaceFileCorruptionHandler(produceNewData = { produceNewData() }), + scope = scope, + ) +} diff --git a/desktopApp/src/main/kotlin/org/meshtastic/desktop/di/DesktopRuntimeModule.kt b/desktopApp/src/main/kotlin/org/meshtastic/desktop/di/DesktopRuntimeModule.kt new file mode 100644 index 0000000000..265f7e4d96 --- /dev/null +++ b/desktopApp/src/main/kotlin/org/meshtastic/desktop/di/DesktopRuntimeModule.kt @@ -0,0 +1,228 @@ +/* + * 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 . + */ +package org.meshtastic.desktop.di + +import io.ktor.client.HttpClient +import io.ktor.client.engine.java.Java +import io.ktor.client.plugins.DefaultRequest +import io.ktor.client.plugins.HttpRequestRetry +import io.ktor.client.plugins.HttpTimeout +import io.ktor.client.plugins.contentnegotiation.ContentNegotiation +import io.ktor.client.plugins.logging.LogLevel +import io.ktor.client.plugins.logging.Logging +import io.ktor.client.request.header +import io.ktor.http.HttpHeaders +import io.ktor.serialization.kotlinx.json.json +import kotlinx.serialization.json.Json +import org.koin.core.annotation.Module +import org.koin.core.annotation.Single +import org.meshtastic.core.ble.BleConnectionFactory +import org.meshtastic.core.ble.BleScanner +import org.meshtastic.core.ble.BluetoothRepository +import org.meshtastic.core.common.database.DatabaseManager +import org.meshtastic.core.common.di.ServiceScope +import org.meshtastic.core.data.datasource.BundledAssetReader +import org.meshtastic.core.di.CoroutineDispatchers +import org.meshtastic.core.network.HttpClientDefaults +import org.meshtastic.core.network.KermitHttpLogger +import org.meshtastic.core.network.configureDefaultRetry +import org.meshtastic.core.network.service.ApiService +import org.meshtastic.core.network.service.ApiServiceImpl +import org.meshtastic.core.repository.AdminController +import org.meshtastic.core.repository.CommandSender +import org.meshtastic.core.repository.ConnectionStateProvider +import org.meshtastic.core.repository.MeshDataHandler +import org.meshtastic.core.repository.MeshLocationManager +import org.meshtastic.core.repository.MeshMessageProcessor +import org.meshtastic.core.repository.MeshNotificationManager +import org.meshtastic.core.repository.MeshPrefs +import org.meshtastic.core.repository.MessageQueue +import org.meshtastic.core.repository.MessagingController +import org.meshtastic.core.repository.NeighborInfoResponseProvider +import org.meshtastic.core.repository.NodeController +import org.meshtastic.core.repository.NodeManager +import org.meshtastic.core.repository.NodeRepository +import org.meshtastic.core.repository.NotificationManager +import org.meshtastic.core.repository.NotificationPrefs +import org.meshtastic.core.repository.PacketRepository +import org.meshtastic.core.repository.PlatformAnalytics +import org.meshtastic.core.repository.QueryController +import org.meshtastic.core.repository.RadioConfigRepository +import org.meshtastic.core.repository.RadioController +import org.meshtastic.core.repository.RadioInterfaceService +import org.meshtastic.core.repository.RadioTransportFactory +import org.meshtastic.core.repository.ServiceRepository +import org.meshtastic.core.repository.ServiceStateWriter +import org.meshtastic.core.repository.TracerouteResponseProvider +import org.meshtastic.core.repository.UiPrefs +import org.meshtastic.core.service.RadioControllerImpl +import org.meshtastic.core.service.ServiceRepositoryImpl +import org.meshtastic.desktop.DesktopBuildConfig +import org.meshtastic.desktop.DesktopNotificationManager +import org.meshtastic.desktop.notification.DesktopMeshNotificationManager +import org.meshtastic.desktop.notification.DesktopOS +import org.meshtastic.desktop.notification.LinuxNotificationSender +import org.meshtastic.desktop.notification.MacOSNotificationSender +import org.meshtastic.desktop.notification.NativeNotificationSender +import org.meshtastic.desktop.notification.WindowsNotificationSender +import org.meshtastic.desktop.radio.DesktopMessageQueue +import org.meshtastic.desktop.radio.DesktopRadioTransportFactory + +/** + * Desktop runtime wiring: the radio stack, notifications and networking the JVM host owns. + * + * These replace bindings that exist only in Android source sets, so nothing here duplicates a `commonMain` default. + */ +@Module +class DesktopRuntimeModule { + + @Single( + binds = + [ + ServiceRepository::class, + ConnectionStateProvider::class, + TracerouteResponseProvider::class, + NeighborInfoResponseProvider::class, + ServiceStateWriter::class, + ], + ) + fun serviceRepository(): ServiceRepository = ServiceRepositoryImpl() + + @Single + fun radioTransportFactory( + dispatchers: CoroutineDispatchers, + scanner: BleScanner, + bluetoothRepository: BluetoothRepository, + connectionFactory: BleConnectionFactory, + ): RadioTransportFactory = DesktopRadioTransportFactory( + dispatchers = dispatchers, + scanner = scanner, + bluetoothRepository = bluetoothRepository, + connectionFactory = connectionFactory, + ) + + @Suppress("LongParameterList") + @Single( + binds = + [ + RadioController::class, + AdminController::class, + MessagingController::class, + NodeController::class, + QueryController::class, + ], + ) + fun radioController( + serviceRepository: ServiceRepository, + nodeRepository: NodeRepository, + commandSender: CommandSender, + nodeManager: NodeManager, + radioInterfaceService: RadioInterfaceService, + locationManager: MeshLocationManager, + packetRepository: Lazy, + dataHandler: Lazy, + analytics: PlatformAnalytics, + meshPrefs: MeshPrefs, + uiPrefs: UiPrefs, + databaseManager: DatabaseManager, + notificationManager: NotificationManager, + messageProcessor: Lazy, + radioConfigRepository: RadioConfigRepository, + scope: ServiceScope, + ): RadioController = RadioControllerImpl( + serviceRepository = serviceRepository, + nodeRepository = nodeRepository, + commandSender = commandSender, + nodeManager = nodeManager, + radioInterfaceService = radioInterfaceService, + locationManager = locationManager, + packetRepository = packetRepository, + dataHandler = dataHandler, + analytics = analytics, + meshPrefs = meshPrefs, + uiPrefs = uiPrefs, + databaseManager = databaseManager, + notificationManager = notificationManager, + messageProcessor = messageProcessor, + radioConfigRepository = radioConfigRepository, + scope = scope, + ) + + /** + * Only the Linux sender holds a native handle; the others are stateless. `Main.kt` closes it explicitly during + * shutdown, because annotations have no `onClose` equivalent. + */ + @Single + fun nativeNotificationSender(): NativeNotificationSender = when (DesktopOS.current()) { + DesktopOS.Linux -> LinuxNotificationSender() + DesktopOS.MacOS -> MacOSNotificationSender() + DesktopOS.Windows -> WindowsNotificationSender() + } + + @Single(binds = [DesktopNotificationManager::class, NotificationManager::class]) + fun desktopNotificationManager( + prefs: NotificationPrefs, + nativeSender: NativeNotificationSender, + ): DesktopNotificationManager = DesktopNotificationManager(prefs = prefs, nativeSender = nativeSender) + + @Single + fun meshNotificationManager(notificationManager: NotificationManager): MeshNotificationManager = + DesktopMeshNotificationManager(notificationManager = notificationManager) + + @Single + fun messageQueue( + packetRepository: PacketRepository, + radioController: RadioController, + dispatchers: CoroutineDispatchers, + ): MessageQueue = DesktopMessageQueue( + packetRepository = packetRepository, + radioController = radioController, + dispatchers = dispatchers, + ) + + /** Desktop uses the real `ApiService` implementation over the JVM `HttpClient` below — no flavor stub needed. */ + @Single fun apiService(apiServiceImpl: ApiServiceImpl): ApiService = apiServiceImpl + + /** Ktor [HttpClient] for JVM/Desktop — the equivalent of `CoreNetworkAndroidModule`'s OkHttp-backed client. */ + @Single + fun httpClient(json: Json): HttpClient = HttpClient(Java) { + engine { + protocolVersion = java.net.http.HttpClient.Version.HTTP_2 + config { followRedirects(java.net.http.HttpClient.Redirect.NORMAL) } + } + install(ContentNegotiation) { json(json) } + install(DefaultRequest) { + url(HttpClientDefaults.API_BASE_URL) + header(HttpHeaders.UserAgent, "Meshtastic-Desktop/${DesktopBuildConfig.VERSION_NAME}") + } + install(HttpTimeout) { + requestTimeoutMillis = HttpClientDefaults.REQUEST_TIMEOUT_MS + connectTimeoutMillis = HttpClientDefaults.TIMEOUT_MS + socketTimeoutMillis = HttpClientDefaults.TIMEOUT_MS + } + install(HttpRequestRetry) { configureDefaultRetry() } + if (DesktopBuildConfig.IS_DEBUG) { + install(Logging) { + logger = KermitHttpLogger + level = LogLevel.INFO + } + } + } + + /** Desktop has no bundled Android assets; repositories seed from the network instead. */ + @Single fun bundledAssetReader(): BundledAssetReader = BundledAssetReader { null } +} diff --git a/desktopApp/src/main/kotlin/org/meshtastic/desktop/di/DesktopStubsModule.kt b/desktopApp/src/main/kotlin/org/meshtastic/desktop/di/DesktopStubsModule.kt new file mode 100644 index 0000000000..c15033acc6 --- /dev/null +++ b/desktopApp/src/main/kotlin/org/meshtastic/desktop/di/DesktopStubsModule.kt @@ -0,0 +1,65 @@ +/* + * 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 . + */ +package org.meshtastic.desktop.di + +import org.koin.core.annotation.Module +import org.koin.core.annotation.Single +import org.meshtastic.core.network.repository.MQTTRepository +import org.meshtastic.core.repository.AppWidgetUpdater +import org.meshtastic.core.repository.LocationRepository +import org.meshtastic.core.repository.MeshLocationManager +import org.meshtastic.core.repository.MeshWorkerManager +import org.meshtastic.core.repository.PlatformAnalytics +import org.meshtastic.desktop.stub.NoopAppWidgetUpdater +import org.meshtastic.desktop.stub.NoopCompassHeadingProvider +import org.meshtastic.desktop.stub.NoopLocationRepository +import org.meshtastic.desktop.stub.NoopMQTTRepository +import org.meshtastic.desktop.stub.NoopMagneticFieldProvider +import org.meshtastic.desktop.stub.NoopMeshLocationManager +import org.meshtastic.desktop.stub.NoopMeshWorkerManager +import org.meshtastic.desktop.stub.NoopPhoneLocationProvider +import org.meshtastic.desktop.stub.NoopPlatformAnalytics +import org.meshtastic.feature.node.compass.CompassHeadingProvider +import org.meshtastic.feature.node.compass.MagneticFieldProvider +import org.meshtastic.feature.node.compass.PhoneLocationProvider + +/** + * Stubs for interfaces whose only real implementation needs Android APIs — WorkManager, widgets, location, sensors and + * analytics. [MQTTRepository] is the exception: it has a working `commonMain` implementation, and this binding + * deliberately shadows it because desktop does not run the MQTT bridge. + */ +@Module +class DesktopStubsModule { + + @Single fun platformAnalytics(): PlatformAnalytics = NoopPlatformAnalytics() + + @Single fun appWidgetUpdater(): AppWidgetUpdater = NoopAppWidgetUpdater() + + @Single fun meshWorkerManager(): MeshWorkerManager = NoopMeshWorkerManager() + + @Single fun meshLocationManager(): MeshLocationManager = NoopMeshLocationManager() + + @Single fun locationRepository(): LocationRepository = NoopLocationRepository() + + @Single fun mqttRepository(): MQTTRepository = NoopMQTTRepository() + + @Single fun compassHeadingProvider(): CompassHeadingProvider = NoopCompassHeadingProvider() + + @Single fun phoneLocationProvider(): PhoneLocationProvider = NoopPhoneLocationProvider() + + @Single fun magneticFieldProvider(): MagneticFieldProvider = NoopMagneticFieldProvider() +} diff --git a/desktopApp/src/main/kotlin/org/meshtastic/desktop/notification/DesktopMeshNotificationManager.kt b/desktopApp/src/main/kotlin/org/meshtastic/desktop/notification/DesktopMeshNotificationManager.kt index 50a98858da..a111c43a8f 100644 --- a/desktopApp/src/main/kotlin/org/meshtastic/desktop/notification/DesktopMeshNotificationManager.kt +++ b/desktopApp/src/main/kotlin/org/meshtastic/desktop/notification/DesktopMeshNotificationManager.kt @@ -42,7 +42,7 @@ import org.meshtastic.proto.Telemetry * * Android-only concepts (notification channels, foreground-service state updates) are intentionally no-ops. * - * Registered manually in `desktopPlatformStubsModule` -- do **not** add `@Single` to avoid double-registration with the + * Registered manually in `DesktopRuntimeModule` -- do **not** add `@Single` to avoid double-registration with the * `@ComponentScan("org.meshtastic.desktop")` in [DesktopDiModule][org.meshtastic.desktop.di.DesktopDiModule]. */ @Suppress("TooManyFunctions") diff --git a/desktopApp/src/main/kotlin/org/meshtastic/desktop/radio/DesktopRadioTransportFactory.kt b/desktopApp/src/main/kotlin/org/meshtastic/desktop/radio/DesktopRadioTransportFactory.kt index d7f7d13b28..efa79d1d9c 100644 --- a/desktopApp/src/main/kotlin/org/meshtastic/desktop/radio/DesktopRadioTransportFactory.kt +++ b/desktopApp/src/main/kotlin/org/meshtastic/desktop/radio/DesktopRadioTransportFactory.kt @@ -35,7 +35,7 @@ import org.meshtastic.core.repository.RadioTransportFactory * Desktop implementation of [RadioTransportFactory] delegating multiplatform transports (BLE, TCP) and providing * platform-specific transports (USB/Serial) via jSerialComm. * - * Registered manually in [desktopPlatformStubsModule] — do NOT add @Single to avoid double-registration with + * Registered manually in [DesktopRuntimeModule] — do NOT add @Single to avoid double-registration with * the @ComponentScan("org.meshtastic.desktop") in DesktopDiModule. */ class DesktopRadioTransportFactory( diff --git a/desktopApp/src/main/kotlin/org/meshtastic/desktop/stub/NoopStubs.kt b/desktopApp/src/main/kotlin/org/meshtastic/desktop/stub/NoopStubs.kt index e7a0627551..cd69ea5c38 100644 --- a/desktopApp/src/main/kotlin/org/meshtastic/desktop/stub/NoopStubs.kt +++ b/desktopApp/src/main/kotlin/org/meshtastic/desktop/stub/NoopStubs.kt @@ -57,7 +57,7 @@ import org.meshtastic.proto.Position as ProtoPosition * real `commonMain` implementations wired through the generated Koin K2 modules. * * As real desktop implementations become available (e.g., serial transport, TCP transport), they replace individual - * stubs in [desktopModule]. + * stubs in [org.meshtastic.desktop.di.DesktopStubsModule]. */ private const val TAG = "NoopStub" diff --git a/desktopApp/src/test/kotlin/org/meshtastic/desktop/di/DesktopKoinTest.kt b/desktopApp/src/test/kotlin/org/meshtastic/desktop/di/DesktopKoinTest.kt index 364aead5ba..9ab331e0aa 100644 --- a/desktopApp/src/test/kotlin/org/meshtastic/desktop/di/DesktopKoinTest.kt +++ b/desktopApp/src/test/kotlin/org/meshtastic/desktop/di/DesktopKoinTest.kt @@ -22,24 +22,28 @@ import io.ktor.client.engine.HttpClientEngine import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineScope import org.koin.core.annotation.KoinExperimentalAPI -import org.koin.dsl.koinApplication -import org.koin.dsl.module -import org.koin.dsl.onClose +import org.koin.plugin.module.dsl.koinApplication import org.koin.test.verify.verify import org.meshtastic.core.ble.BleLogFormat import org.meshtastic.core.ble.BleLogLevel +import org.meshtastic.core.network.repository.MQTTRepository +import org.meshtastic.desktop.stub.NoopMQTTRepository +import org.meshtastic.feature.docs.translation.DocTranslationService +import org.meshtastic.feature.docs.translation.NoOpDocTranslator +import org.meshtastic.feature.messaging.translation.MessageTranslationService +import org.meshtastic.feature.messaging.translation.NoOpMessageTranslator import kotlin.test.Test -import kotlin.test.assertTrue +import kotlin.test.assertIs -@OptIn(KoinExperimentalAPI::class) class DesktopKoinTest { + @OptIn(KoinExperimentalAPI::class) @Test fun `verify desktop koin modules`() { - // This test validates the full Koin DI graph for the Desktop target. - // It includes the main desktopModule (repositories, use cases, ViewModels, stubs) - // and the desktopPlatformModule (DataStores, Room database, lifecycle). - module { includes(desktopModule(), desktopPlatformModule()) } + // Validates the full Koin DI graph for the Desktop target: the core KMP modules (repositories, use cases, + // ViewModels) plus the desktop-specific platform, datastore, runtime, stub and AI modules. + DesktopKoinModule() + .module() .verify( extraTypes = listOf( @@ -61,18 +65,26 @@ class DesktopKoinTest { } @Test - fun `closing a koin container fires onClose for instantiated singles`() { - // Pins the mechanism desktopModule() relies on: LinuxNotificationSender's native teardown - // (notify_uninit) runs only because Koin invokes onClose when the container closes, which - // Main.kt triggers via stopKoin() once the Compose application loop returns. A Koin upgrade - // that changed this would silently reinstate the leak, so it is asserted rather than assumed. - var closed = false - val app = koinApplication { - modules(module { single { AutoCloseable { closed = true } }.onClose { it?.close() } }) + fun `desktop bindings win over the shared graph`() { + // @Configuration modules load before the ones listed in @KoinApplication, and Koin is last-wins, so which + // binding survives is ordering-dependent. MQTTRepository is the live case: core:network commonMain declares + // MQTTRepositoryImpl, and desktop must shadow it. verify() only checks definitions exist, never who won. + val app = koinApplication() + try { + val koin = app.koin + assertIs(koin.get()) + assertIs(koin.get()) + assertIs(koin.get()) + } finally { + app.close() } - app.koin.get() // onClose only fires for singles that were actually instantiated - app.close() + } - assertTrue(closed, "Expected Koin to invoke onClose when the container is closed") + @Test + fun `typed bootstrap loads the module graph`() { + // koinApplication() is a K2 compiler plugin stub. If the plugin fails to transform it, the stub throws + // NotImplementedError at runtime. This is the production bootstrap path Main.kt takes via startKoin. + val app = koinApplication() + app.close() } } diff --git a/docs/README.md b/docs/README.md index 5433a585bd..9c5d65cb44 100644 --- a/docs/README.md +++ b/docs/README.md @@ -27,6 +27,13 @@ docs/ ## Editing Guidelines +- **How to write**: Section 11 of the + [Meshtastic Client Design Standards](https://github.com/meshtastic/design/tree/master/standards) + is the style guide for everything under `docs/en/` — voice, terminology, page + structure, code examples, media and accessibility. Read it first. + [Documentation Style](en/developer/documentation-style.md) carries what is + specific to this repository, including the admonition form, and defers to + Section 11 everywhere else. The rules below are mechanics, not style. - **English source**: Edit files under `docs/en/`. These are the authoritative source. - **Translations**: Do **not** edit files in locale folders directly. They are auto-generated by [Crowdin](https://crowdin.com/project/meshtastic-android) and will be overwritten on sync. diff --git a/docs/ar-rSA/user/desktop.md b/docs/ar-rSA/user/desktop.md index d997fe66ef..b33c76b28a 100644 --- a/docs/ar-rSA/user/desktop.md +++ b/docs/ar-rSA/user/desktop.md @@ -2,7 +2,7 @@ title: Desktop App parent: User Guide nav_order: 14 -last_updated: 2026-08-30 +last_updated: 2026-09-11 description: Install and use the Meshtastic Desktop app on Linux, macOS, and Windows — connections, feature parity, and keyboard shortcuts. aliases: - desktop @@ -58,20 +58,20 @@ Bluetooth Low Energy is supported on desktop via the [Kable](https://github.com/ ## Feature Parity -| Feature | Android | Desktop | Notes | -| ----------------------------------------------------- | ------- | ------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Messaging | ✓ | ✓ | Full parity | -| Node List | ✓ | ✓ | Full parity | -| الخريطة | ✓ | ✓ | Interactive MapLibre map, with base map and overlay pickers and custom tile sources. No offline downloads or local `.mbtiles` archives | -| Map layers (`.kml`/`.kmz`/GeoJSON) | ✓ | ✓ | Same layer store and sheet as Android; imported files draw on the desktop map | -| Site Planner | ✓ | ✓\* | \*Opens in your browser on desktop; the estimate is not drawn on the desktop map | -| الإعدادات | ✓ | ✓ | Full parity | -| Bluetooth (BLE) | ✓ | ✓ | Via Kable on desktop | -| Firmware Update | ✓ | ✓ | In-app USB, BLE, and Wi-Fi (ESP32) update work the same as Android. The USB maintenance flow — nRF52/RP2040 factory erase and bootloader upgrade — is Android-only | -| Notifications | ✓ | ✓ | Native OS notifications | -| Widgets | ✓ | ✗ | Android-only | -| AI Assistant (Chirpy) | ✓\* | ✗ | Google flavor Android only | -| App Functions (system AI) | ✓† | ✗ | Google flavor Android only | +| Feature | Android | Desktop | Notes | +| ----------------------------------------------------- | ------- | ------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Messaging | ✓ | ✓ | Full parity | +| Node List | ✓ | ✓ | Full parity | +| الخريطة | ✓ | ✓ | Interactive MapLibre map, with base map and overlay pickers and custom tile sources. Offline terrain (hillshade and contours) can be downloaded; offline base-map packs and local `.mbtiles` archives cannot | +| Map layers (`.kml`/`.kmz`/GeoJSON) | ✓ | ✓ | Same layer store and sheet as Android; imported files draw on the desktop map | +| Site Planner | ✓ | ✓\* | \*Opens in your browser on desktop; the estimate is not drawn on the desktop map | +| الإعدادات | ✓ | ✓ | Full parity | +| Bluetooth (BLE) | ✓ | ✓ | Via Kable on desktop | +| Firmware Update | ✓ | ✓ | In-app USB, BLE, and Wi-Fi (ESP32) update work the same as Android. The USB maintenance flow — nRF52/RP2040 factory erase and bootloader upgrade — is Android-only | +| Notifications | ✓ | ✓ | Native OS notifications | +| Widgets | ✓ | ✗ | Android-only | +| AI Assistant (Chirpy) | ✓\* | ✗ | Google flavor Android only | +| App Functions (system AI) | ✓† | ✗ | Google flavor Android only | \*Chirpy AI requires Android 14+ on Google flavor builds with supported hardware. @@ -122,7 +122,7 @@ Individual doc pages render with full formatting: ## Building from Source -```bash +```shell git clone https://github.com/meshtastic/Meshtastic-Android.git cd Meshtastic-Android ./gradlew :desktopApp:run @@ -135,7 +135,7 @@ Requirements: ## Known Limitations -- Offline tile downloads and local `.mbtiles` archives are not available on desktop. +- Offline base-map downloads and local `.mbtiles` archives are not available on desktop. Offline terrain is — see [Map & Waypoints](map-and-waypoints). - `.kml`/`.kmz`/GeoJSON layer import works — see [Map & Waypoints](map-and-waypoints#map-layers). Site Planner opens in your browser rather than in the app; to bring its coverage estimate onto the map, click the transmitter pin diff --git a/docs/ar-rSA/user/discovery.md b/docs/ar-rSA/user/discovery.md index 3ff0aec410..2f0fc47232 100644 --- a/docs/ar-rSA/user/discovery.md +++ b/docs/ar-rSA/user/discovery.md @@ -2,7 +2,7 @@ title: Local Mesh Discovery parent: User Guide nav_order: 12 -last_updated: 2026-08-30 +last_updated: 2026-09-11 description: Explore your mesh network — the Local Mesh Discovery scanner, traceroute paths, neighbor maps, and node discovery tools. aliases: - discovery @@ -97,7 +97,7 @@ Mesh Beacon lets nodes invite others to join their mesh. A beaconing node period Configure it under **Settings → Module configuration → Mesh Beacon**. The entry appears only on radios running firmware 2.8.0 or newer. A read-only **Region** row at the top of the screen shows the region the beacon advertises: that region, and the preset, are always the ones the radio itself uses, so a beacon cannot invite anyone onto settings your radio is not running. - **Listen for beacons** — receive invitations broadcast by other nodes. -- **Broadcast a beacon** — periodically advertise this mesh to nearby nodes, with an optional **Beacon message** of up to 100 bytes, a **Broadcast interval** picked from fixed intervals between 1 hour and 72 hours, and an **Offered channel** chosen from your radio's own channels. The offered channel is required, and defaults to your primary channel. +- **Broadcast a beacon** — periodically advertise this mesh to nearby nodes, with an optional **Beacon message** of up to 100 bytes, a **Broadcast interval** picked from fixed intervals between 1 hour and 72 hours, and an **Offered channel** chosen from your radio's own channels. The offered channel is required, and defaults to your primary channel. Over remote admin the picker offers the primary channel only. - **Broadcast targets** — optional extra destinations beyond the offered channel. **Add target** appends a row; each row picks a **Channel** and a **Transmit preset**, and **Remove target** deletes it. With no targets, the beacon goes out on the offered channel alone. Two conditions block beacon setup: diff --git a/docs/ar-rSA/user/help-and-docs.md b/docs/ar-rSA/user/help-and-docs.md index f306cde7f7..c83898cea3 100644 --- a/docs/ar-rSA/user/help-and-docs.md +++ b/docs/ar-rSA/user/help-and-docs.md @@ -2,7 +2,7 @@ title: Help & In-App Docs parent: User Guide nav_order: 21 -last_updated: 2026-08-30 +last_updated: 2026-09-11 description: Browse this documentation inside the app, search it, and ask Chirpy — the on-device AI assistant — questions about Meshtastic. aliases: - help @@ -23,7 +23,7 @@ The docs browser lists every user-guide page. Tap a page to read it; images and ### Search -The search box sits at the top of the docs browser. Type in it to filter pages by title and keywords — results update as you type, and the **Clear search** button (✕) empties the box. +Tap the search field at the top of the docs browser to open a full-screen search view. Type to filter pages by title and keywords — results update as you type, under the same field. The back arrow, or the keyboard's search action, closes the view. The field hides while you scroll down the page list and comes back when you scroll up. ![Searching the in-app documentation](../../assets/screenshots/docs-browser_search.png) diff --git a/docs/ar-rSA/user/map-and-waypoints.md b/docs/ar-rSA/user/map-and-waypoints.md index 8a0c10fe54..3ddafd3094 100644 --- a/docs/ar-rSA/user/map-and-waypoints.md +++ b/docs/ar-rSA/user/map-and-waypoints.md @@ -2,7 +2,7 @@ title: Map & Waypoints parent: User Guide nav_order: 6 -last_updated: 2026-09-01 +last_updated: 2026-09-11 description: View node positions on the map, create and share waypoints, manage map layers and Site Planner, and control position sharing and privacy. aliases: - map @@ -44,7 +44,7 @@ The floating toolbar provides quick access to the compass, the map type and laye ### Filtering the Map -Tap the filter button in the floating toolbar to open **Filter map**. **Display** controls what is drawn: **Only Favorites**, **Show Waypoints**, **Show Precision Circles**, and a slider that hides nodes not heard from recently. **Node roles** is a chip per device role, plus **All** to show every role; a selected chip means that role is shown. **Nodes** narrows the set further with **Hide offline nodes**, **Only show direct nodes**, **Exclude MQTT**, **Show ignored nodes**, and **Include unknown**. +Tap the filter button in the floating toolbar to open **Filter map**. **Display** controls what is drawn: **Only Favorites**, **Show Waypoints**, **Show Precision Circles**, and a slider that hides nodes not heard from recently. **Node roles** is a chip per device role, plus **All** to show every role; a selected chip means that role is shown. **Nodes** narrows the set further with **Hide offline nodes**, **Only show direct nodes**, **Signed only**, **Encrypted only**, **Exclude MQTT**, **Show ignored nodes**, and **Include unknown** — the same rules as the node list's [filter toggles](nodes). A dot on the filter button means at least one filter is hiding something — check it before concluding the mesh is quiet. Turning **Show Waypoints** off hides every waypoint, including your own. **Show ignored nodes** adds them to the map rather than showing only them — unlike the node list's **Only show ignored Nodes**. @@ -147,8 +147,9 @@ picker, all three offer the same raster base maps: Overlays can be toggled on top of any base map, from the layers sheet: - **Weather radar** — NOAA NEXRAD reflectivity (US coverage) -- **Hillshade** — terrain relief, on **F-Droid** and **Desktop** only. Useful for understanding why a - link fails, since LoRa range is limited by terrain +- **Hillshade** — terrain relief from an online tile source, on **F-Droid** and **Desktop** only; Google + Play draws hillshade from downloaded offline terrain instead. Useful for understanding why a link + fails, since LoRa range is limited by terrain ### Adding your own tile source @@ -156,7 +157,7 @@ Any XYZ tile endpoint can be added as a base map, on every flavor and on desktop Tile Sources** at the foot of the base map picker and paste a URL template using `{z}`, `{x}` and `{y}` — plus `{s}` if the provider uses rotating subdomains. A national mapping service, for example: -``` +```text https://wmts.geo.admin.ch/1.0.0/ch.swisstopo.pixelkarte-farbe/default/current/3857/{z}/{x}/{y}.jpeg ``` @@ -173,7 +174,18 @@ On **Google Play**, the layers sheet's **Offline Manager** section downloads its — water, roads and administrative boundaries extracted from the public Protomaps basemap dataset, drawn directly as map shapes rather than raster tiles. Frame the area you want, tap **Start Download**, and once it finishes flip **Show on map** to draw it (it's off by default so it never surprises you by covering up the -live map). You can still import a pre-made `.mbtiles` archive here too, as before. **Desktop** has neither. +live map). You can still import a pre-made `.mbtiles` archive here too, as before. **Desktop** has neither, though +the offline terrain described next does work there. + +### Offline terrain + +Hillshade and elevation contours can be downloaded for offline use as well, so a downloaded area still +shows why a link fails when there is no network. The terrain data comes from Mapterhorn and is credited on +the map. On **Google Play**, each downloaded region in the **Offline Manager** list has a **Download Terrain** +button and **Hillshade** and **Contours** switches; terrain attaches to that region and shares its storage limit, and an area +too large for terrain asks you to zoom in or pick a smaller region. On **F-Droid** and **Desktop**, the +layers sheet has an **Offline Terrain** section of its own: frame the area, tap **Start Download**, and the +hillshade and contours draw from the downloaded tiles from then on. ### Going offline @@ -181,8 +193,8 @@ An **Offline** pill appears over the map whenever the device has no network conn builds, if you've imported a local `.mbtiles` archive, the map switches to it automatically the moment the network drops — no toggle to remember — and switches back once you're reconnected, as long as you haven't picked a different base map yourself in between. On **F-Droid**, a downloaded offline area keeps rendering on -its own; the pill is purely informational there. **Desktop** has no offline downloads at all yet, so the pill -is informational there too. +its own; the pill is purely informational there. **Desktop** downloads offline terrain but no base-map +packs, so the pill is informational there too. ## Related Topics diff --git a/docs/ar-rSA/user/messages-and-channels.md b/docs/ar-rSA/user/messages-and-channels.md index 6e66faa8f1..33eb074c98 100644 --- a/docs/ar-rSA/user/messages-and-channels.md +++ b/docs/ar-rSA/user/messages-and-channels.md @@ -2,7 +2,7 @@ title: Messages & Channels parent: User Guide nav_order: 3 -last_updated: 2026-08-30 +last_updated: 2026-09-14 description: Send and receive messages, manage channels, configure encryption, search conversations, and use quick chat, reactions, and message actions. aliases: - channels @@ -50,6 +50,21 @@ Key length alone does not change the icon: a 128-bit key and a 256-bit key both Tapping an existing channel opens the same editor, where you can change the name, the PSK, MQTT uplink and downlink, and position precision. Every edit on this screen — adding, editing, deleting, or dragging a channel into a new order — waits on **Send** the same way. +### Archived Channels + +A conversation belongs to the channel it was held on, not to the slot that channel occupied. When a channel leaves your radio — you scan a different channel QR, replace a channel's key, or switch the modem preset — its conversation is **archived** rather than deleted or merged into whatever takes the slot. + +An archived conversation: + +- stays in the **Channels** list under the name the channel had, below the channels currently on your radio, marked with a history icon; +- keeps its full message history, searchable as usual; +- is read-only — you cannot send, reply, or react, because the channel is no longer on the radio to send on, and it is not offered as a share target; +- comes back automatically if you re-add that exact channel, taking its messages back to the live slot. + +Changing the modem preset archives the channel too: the preset is part of a default channel's identity, so **LongFast** and **MediumFast** are different channels even though neither the name nor the key changed. Changing only the region does not archive anything. + +To remove an archived conversation for good, long-press it in the conversation list and delete it. + ## Direct Messages Direct messages (DMs) go to one specific node. When both radios hold each other's public keys, your radio encrypts the message to that node's public key, so no one else on the mesh can read it — not even nodes that share your channel. diff --git a/docs/ar-rSA/user/mqtt.md b/docs/ar-rSA/user/mqtt.md index c57e17d9c2..bc22fad019 100644 --- a/docs/ar-rSA/user/mqtt.md +++ b/docs/ar-rSA/user/mqtt.md @@ -2,7 +2,7 @@ title: MQTT parent: User Guide nav_order: 11 -last_updated: 2026-08-30 +last_updated: 2026-09-11 description: Bridge your mesh to the internet — MQTT broker setup, encryption layers, and map reporting. aliases: - mqtt @@ -25,7 +25,7 @@ The MQTT module connects your node to an MQTT broker, allowing: ## How It Works -``` +```text [Your Node] → Radio → [Gateway Node with Wi-Fi] → MQTT Broker → [Remote Gateway] → Radio → [Remote Node] ``` diff --git a/docs/ar-rSA/user/node-metrics.md b/docs/ar-rSA/user/node-metrics.md index 7a17f44d1a..abe7cbfacf 100644 --- a/docs/ar-rSA/user/node-metrics.md +++ b/docs/ar-rSA/user/node-metrics.md @@ -2,7 +2,7 @@ title: Node Metrics parent: User Guide nav_order: 5 -last_updated: 2026-08-30 +last_updated: 2026-09-09 description: Telemetry dashboards for each mesh node — device health, environment sensors, air quality, signal quality, power, traceroute, and position history. aliases: - metrics @@ -114,7 +114,7 @@ Radio signal quality information: ### Signal Quality Reference -Signal quality is rated from **SNR relative to the active LoRa modem preset's demodulation floor**, not from fixed thresholds — a given SNR means different things on different presets (e.g. −15 dB is fine on LongSlow but unusable on ShortFast). RSSI is shown but is not part of the rating. In the table, _limit_ is the preset's SNR limit. +Signal quality is rated from **SNR relative to the active LoRa modem preset's demodulation floor**, not from fixed thresholds — a given SNR means different things on different presets (e.g. −15 dB is fine on LongSlow but unusable on ShortFast). When RSSI and a noise-floor reading are both available, the app also rates their difference against the preset limit and uses the worse rating. Otherwise, RSSI is display-only. In the table, _limit_ is the preset's SNR limit. | Quality | Criteria | | ------- | ---------------------------------------------------------------- | diff --git a/docs/ar-rSA/user/nodes.md b/docs/ar-rSA/user/nodes.md index f67a7bfc46..6c7565363c 100644 --- a/docs/ar-rSA/user/nodes.md +++ b/docs/ar-rSA/user/nodes.md @@ -2,7 +2,7 @@ title: Nodes parent: User Guide nav_order: 4 -last_updated: 2026-09-04 +last_updated: 2026-09-11 description: Browse, filter, and sort mesh nodes — view details, signal quality, roles, and quick actions. aliases: - node-list @@ -74,19 +74,23 @@ Most users should keep the default **Client** role. Consider a different role wh > 💡 **Tip:** The mesh works best when most nodes are **Client** or **Router**. Too many Client Mute nodes reduce mesh resilience; too many Routers in a dense area can cause congestion. A good rule of thumb: one Router per 5–10 Clients in your area. -### Encryption Indicators +### Security indicators -Nodes display encryption status icons next to their name: +Each node carries one security icon beside its name in the node list. Tap it to read what it means, and choose **Show All Meanings** in that dialog for the full legend. The detail screen shows the same state in words, as a **Security** row that opens the same dialog. -| Icon | Meaning | -| ----------- | ------------------------------------------------------------------------------------------------------------------------------------------------- | -| 🔒 Locked | Communication uses PKI (public key infrastructure) — end-to-end encrypted with verified identity | -| 🔓 Unlocked | No public key has been received for this node, so it cannot be direct messaged — use **Request User Info** on the node detail page to ask for one | -| ⚠️ Mismatch | Public key mismatch — the node's key has changed since last seen (investigate before trusting) | +| Icon | Meaning | Shown for | +| --------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------- | +| Person with a shield check (green) | **Verified contact** — you verified this node's key in person by exchanging contact QR codes, so its identity is confirmed. The strongest trust the list shows. Your own connected node carries it too | Any firmware version | +| Nodes icon with a shield check (green) | **Signed node** — this node signs its broadcasts with its identity key, so its identity is consistent over time, but you have not verified it in person. On firmware 2.8 the icon appears from the version alone, before any signed broadcast has been heard | Firmware 2.8 or newer, and any node whose signed broadcast your node has verified | +| 🔒 Closed lock | A public key is on file and matches, so direct messages to this node are encrypted | Firmware before 2.8, or no reported version | +| 🔓 Open lock | No public key has been received for this node, so it cannot be direct messaged — use **Request User Info** on the node detail page to ask for one | Firmware before 2.8, or no reported version | +| ⚠️ Mismatch | **Public key mismatch** — a different key arrived for this node after one was stored. Investigate before trusting | Any firmware version | -> 💡 **Tip:** Direct messages always use PKI, so the radio needs the other node's public key before it can send one. It refuses the send rather than falling back to channel encryption. Keys arrive inside node info, which is why an open lock usually clears itself once that node is heard from properly. If you see a key mismatch warning, the node may have been reset or compromised. +Every node on firmware 2.8 or newer signs its broadcasts, so on that firmware the signed state is the baseline and the locks are not shown. -To clear a mismatch, first confirm through another trusted channel that the key change was intentional — a factory reset causes one. Then touch & hold the node, choose **Remove**, and let the two radios exchange keys again the next time yours hears it. +Direct messages always use public-key encryption, so your node needs the other node's public key before it can send one. It refuses the send rather than falling back to channel encryption. Keys arrive inside node info, which is why an open lock usually clears itself once that node is heard from properly. + +A mismatch never replaces the key you already hold. The app keeps the first key it recorded and refuses the new one, as the firmware does, so a stray or hostile node info cannot silently break encrypted messaging to a contact. To clear a mismatch, first confirm through another trusted channel that the key change was intentional — a factory reset causes one. Then touch & hold the node, choose **Remove**, and let the two nodes exchange keys again the next time yours hears it. ## Quick Actions @@ -108,7 +112,9 @@ radio is connected and running firmware 2.8 or newer — see ## Sharing a Contact -On a node's detail screen, tap **Share Contact** to produce a link and a QR code for that node. From the same dialog, **Write to NFC tag** saves the link to a writable NFC tag that anyone can tap to open. +On a node's detail screen, tap **Share Contact** to produce a link and a QR code for that node. From the same dialog, **Share link** opens the Android share sheet (on desktop it copies the link instead), **Write to NFC tag** saves it to a writable NFC tag, and **Copy** puts it on the clipboard. While that dialog is open and in front of you, the phone also offers the same link to any NFC reader, so someone can take the contact by tapping their phone against yours with no tag involved. + +Sharing your own contact this way marks it as verified in person, so whoever imports it sees the **Verified contact** icon rather than the signed one. Relaying someone else's contact passes on only what your app had already recorded about them. To add someone else's contact, use the import button on the node list and choose **Scan Shared Contact QR Code**, **Scan Shared Contact NFC**, or **Input Shared Contact URL**. The app asks you to confirm with **Import Shared Contact?**, and warns you when the contact is one you already have. @@ -120,14 +126,16 @@ Type in the search field to filter nodes by name or short name. The filter updat ### Filter Toggles -| عربي | الوصف | -| --------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| **Hide offline nodes** | Show only nodes heard within the last 2 hours | -| **Only show direct nodes** | Show only nodes your radio heard directly, with no relay in between | -| **Include unknown** | Show nodes that haven't sent user info yet. **On by default**, so a node heard before its info arrives stays visible; these carry a badge marking them incomplete, and cannot be direct messaged until their user info brings a public key | -| **Exclude infrastructure** | Hide infrastructure-role nodes (Router, Router Late, Client Base, and legacy Repeater nodes) and any node that cannot be messaged, whatever its role | -| **Exclude MQTT** | Hide nodes heard only via MQTT internet bridge | -| **Only show ignored Nodes** | Replace the list with the nodes you have ignored. Every other node is hidden while this is on, and a banner appears at the top of the list to take you back | +| عربي | الوصف | +| --------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **Hide offline nodes** | Show only nodes heard within the last 2 hours | +| **Only show direct nodes** | Show only nodes your radio heard directly, with no relay in between | +| **Include unknown** | Show nodes that haven't sent user info yet. **On by default**, so a node heard before its info arrives stays visible; these carry a badge marking them incomplete, and cannot be direct messaged until their user info brings a public key | +| **Exclude infrastructure** | Hide infrastructure-role nodes (Router, Router Late, Client Base, and legacy Repeater nodes) and any node that cannot be messaged, whatever its role | +| **Exclude MQTT** | Hide nodes heard only via MQTT internet bridge | +| **Signed only** | Show only nodes whose signed broadcasts your node has actually heard and verified. Stricter than the icon: a node on 2.8 shows as signed by its firmware version before any signed broadcast arrives, and a contact verified in person on older firmware is not signed at all | +| **Encrypted only** | Show nodes with a matching public key on file, the key an encrypted direct message needs. A node with a key mismatch is excluded | +| **Only show ignored Nodes** | Replace the list with the nodes you have ignored. Every other node is hidden while this is on, and a banner appears at the top of the list to take you back | ### Sort Options @@ -149,6 +157,8 @@ Tap the hop-histogram icon in the node list's app bar to open a bar chart of how Tapping a node opens the detail view with comprehensive information. See [Node Metrics](node-metrics) for full details on metrics and telemetry. +Signal quality is rated against your modem preset. The same SNR can be good on a long-range preset and poor on a faster one. Traceroute and neighbor-info SNR colors use that preset too. RSSI text has its own strength colors; it affects the quality rating only when a noise-floor reading is also available. + The Details card carries the node's short name, role, IDs, last heard time, hops away, uptime, and its SNR and RSSI: ![Node detail section](../../assets/screenshots/nodes_detail_section.png) diff --git a/docs/ar-rSA/user/settings-module-admin.md b/docs/ar-rSA/user/settings-module-admin.md index 16d6fd4b79..94dd6d1980 100644 --- a/docs/ar-rSA/user/settings-module-admin.md +++ b/docs/ar-rSA/user/settings-module-admin.md @@ -2,7 +2,7 @@ title: Settings — Modules & Admin parent: User Guide nav_order: 8 -last_updated: 2026-08-30 +last_updated: 2026-09-16 description: Configure optional feature modules (MQTT, telemetry, canned messages, TAK, and more) and perform device administration. aliases: - modules @@ -333,7 +333,7 @@ appear only when your own node is selected: ### حول -**Settings → About** carries the app's own identity rather than the radio's: +**Settings → About** carries the app's own identity rather than the node's: Three sections: @@ -341,7 +341,11 @@ Three sections: - **Apps** — opens with **Need Hardware?**, a rotating carousel of popular devices that links out to where to buy one. It also lists the GitHub repository, the running app version, and **Acknowledgements** (see the next section). -- **Project information** — links to the website and to this documentation. +- **Project information** — links to the website, to this documentation, and to the GNU GPL v3 + license the app is released under. + +The footer states the copyright and that the app is free software with no warranty, which you +may redistribute under the same license. ### Acknowledgements diff --git a/docs/ar-rSA/user/settings-radio-user.md b/docs/ar-rSA/user/settings-radio-user.md index 3a8e595b41..30ae17c8ce 100644 --- a/docs/ar-rSA/user/settings-radio-user.md +++ b/docs/ar-rSA/user/settings-radio-user.md @@ -2,7 +2,7 @@ title: Settings — Radio & User parent: User Guide nav_order: 7 -last_updated: 2026-09-04 +last_updated: 2026-09-11 description: Configure your radio hardware, LoRa presets, user profile, position sharing, power management, and security. aliases: - الإعدادات @@ -51,7 +51,7 @@ The footer appears as soon as you change something. **Discard** throws the chang The status message is saved with the same **Save**, but it never reboots the node — and, like the rest of this screen, it can be edited on a remote node you administer. For your own radio there is a shortcut while it is connected: touch & hold your node in the [node list](nodes.md) and choose -**Update status**. Older firmware and a disconnected radio have no shortcut — the field above is +**Update status**. Older firmware and a disconnected node have no shortcut — the field itself is still the way in. ## Configuration @@ -77,13 +77,13 @@ On **Settings → LoRa**. | Setting | الوصف | Default | | ------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------- | -| الجهة | Regulatory region for frequency bands. You must set this before transmitting | Unset (must configure) | +| الجهة | Regulatory region for frequency bands. You must set this before transmitting. On firmware 2.8 or newer, the first region set also creates the node's identity key and gives it a new node number | Unset (must configure) | | Presets | Speed/range tradeoff | LongFast | | Number of Hops | Maximum retransmit hops | 3 | | Transmit Power | Transmission power (dBm); 0 = max allowed for region | 0 (region max) | | Frequency Override | Overrides the computed operating frequency outright (MHz). It does not offset the calculated value — leave at 0 unless you know you need a specific frequency | 0 (use calculated) | | Use Preset | On by default. Turn it off to set Spread Factor, Coding Rate and Bandwidth by hand instead of taking them from the modem preset | On | -| Spread Factor | Manual mode only: 7–12. Higher spreads further but slower | From preset | +| Spread Factor | Manual mode only: 5–12. Higher spreads further but slower. On SX127x (RF95) radios the firmware does not accept 5 or 6 and uses 11 instead | From preset | | Coding Rate | Manual mode only: 5–8. More redundancy costs airtime | From preset | | Bandwidth | Manual mode only: the channel bandwidth in kHz, typed in directly. On the 2.4 GHz region the app offers a list of the bandwidths your radio supports instead, and a stored value that is not on that list shows as _Unsupported_ and blocks saving until you pick a supported one | From preset | | Frequency Slot | Which slot within the region's band to use. 0 derives it from the primary channel name | 0 (automatic) | @@ -111,13 +111,13 @@ The Lite, Narrow, Medium Turbo, and Tiny presets need firmware 2.8 or newer — | Short Slow | ~5 km | 6.25 kbps | −10 dB | Suburban short-range; moderate building density | | Medium Fast | ~5 km | 3.52 kbps | −12.5 dB | Suburban areas; moderate building density | | Medium Slow | ~8 km | 1.95 kbps | −15 dB | Suburban/rural; moderate range with slower speed | -| Long Turbo | ~10 km | 1.34 kbps | −12.5 dB | Similar range to Long Fast but with 500 kHz bandwidth; faster throughput | +| Long Turbo | ~10 km | 1.34 kbps | −17.5 dB | Similar range to Long Fast but with 500 kHz bandwidth; faster throughput | | Long Fast | ~10 km | 1.1 kbps | −17.5 dB | **General use (default)** — balanced range and speed | | Long Moderate | ~20 km | 0.34 kbps | −17.5 dB | Rural with some terrain; occasional use | | Lite Fast | ~5 km | 1.76 kbps | −12.5 dB | EU 866 MHz SRD band (125 kHz BW); comparable to Medium Fast | | Lite Slow | ~10 km | 0.98 kbps | −15 dB | EU 866 MHz SRD band (125 kHz BW); comparable to Long Fast | -| Narrow Fast | ~5 km | 2.28 kbps | −10 dB | EU 868 MHz band (62.5 kHz BW); avoids interference with other devices | -| Narrow Slow | ~10 km | 1.30 kbps | −12.5 dB | EU 868 MHz band (62.5 kHz BW); comparable to Long Fast | +| Narrow Fast | ~5 km | 2.28 kbps | −7.5 dB | EU 868 MHz band (62.5 kHz BW); avoids interference with other devices | +| Narrow Slow | ~10 km | 1.30 kbps | −10 dB | EU 868 MHz band (62.5 kHz BW); comparable to Long Fast | | Medium Turbo | ~5 km | 7.0 kbps | −12.5 dB | Like Medium Fast but with 500 kHz bandwidth; not legal in every region. Needs firmware 2.8 or newer | | Tiny Fast | ~10 km | 0.68 kbps | −7.5 dB | Amateur bands that cap occupied bandwidth; these presets use 15.6 kHz. Needs firmware 2.8 or newer, an SX126x or SX127x radio, and a TCXO of ±5 ppm or better | | Tiny Slow | ~20 km | 0.33 kbps | −10 dB | Same band restrictions as Tiny Fast, longer range. Same firmware, radio, and TCXO requirements | diff --git a/docs/ar-rSA/user/signal-meter.md b/docs/ar-rSA/user/signal-meter.md index 6d727170a5..ac65bff2f7 100644 --- a/docs/ar-rSA/user/signal-meter.md +++ b/docs/ar-rSA/user/signal-meter.md @@ -2,7 +2,7 @@ title: How the Meshtastic Signal Meter Works parent: User Guide nav_order: 15 -last_updated: 2026-08-30 +last_updated: 2026-09-09 description: How the signal meter rates quality from SNR relative to the LoRa modem preset — spread spectrum, presets, and what the bars really mean. aliases: - signal @@ -32,7 +32,7 @@ Each modem preset has an SNR limit: the lowest SNR at which that preset can stil ## Rating Signal Quality -The app rates signal quality (None, Bad, Fair, or Good) from SNR alone, measured against the active preset's SNR limit. It does not factor in RSSI: without knowing the local noise floor, RSSI alone cannot say whether a signal is decodable. RSSI is still available — on the node detail screen and in the metrics charts. +The app rates signal quality (None, Bad, Fair, or Good) from SNR relative to the active preset's SNR limit. When RSSI and a noise-floor reading are both available, it also rates the difference between them against that limit and uses the worse rating. Otherwise, it uses SNR alone. Because the rating is relative to the preset, the same SNR rates differently on different presets. An SNR of −16 dB rates Good on Long Fast (SNR limit −17.5 dB) but None on Short Fast (SNR limit −7.5 dB). Let `limit` be the active preset's SNR limit: @@ -45,7 +45,7 @@ Because the rating is relative to the preset, the same SNR rates differently on The icon never goes blank, so count the bars carefully: a single bar means None, not a weak but usable link, and Good is a solid wedge rather than a set of bars. A gray three-bar icon labeled Unknown is a different state again — the packet carried no SNR measurement at all, which is not the same as measuring one and finding it too weak. -> ℹ️ **Note:** Traceroute hop colors use fixed thresholds (−7 dB / −15 dB); the per-node signal meter uses the preset-relative rating instead. +> ℹ️ **Note:** Traceroute and neighbor-info SNR colors use the active preset's limits too. RSSI text has separate strength colors: green above −115 dBm, yellow above −120 dBm, orange above −126 dBm, and red at or below −126 dBm. ## Diagnosing Local Interference diff --git a/docs/ar-rSA/user/tak.md b/docs/ar-rSA/user/tak.md index 4a89a266e2..4acacac5b2 100644 --- a/docs/ar-rSA/user/tak.md +++ b/docs/ar-rSA/user/tak.md @@ -2,7 +2,7 @@ title: TAK Integration parent: User Guide nav_order: 10 -last_updated: 2026-08-30 +last_updated: 2026-09-11 description: Interoperate with ATAK and WinTAK — CoT position sharing, TAK roles, and plugin setup. aliases: - tak @@ -36,7 +36,7 @@ The TAK module allows Meshtastic nodes to: > ⚠️ **Warning:** The old **Meshtastic ATAK Plugin** is no longer part of this path and cannot > work. It bridged through the cross-process AIDL API, which was removed in app 2.8.0; the mesh > service is now in-process only. Do not install it. Interop today runs over the app's own local -> TAK server plus the Mesh to CoT Converter, both described below, with stock ATAK/iTAK/WinTAK. +> TAK server plus the Mesh to CoT Converter, both described in the following sections, with stock ATAK/iTAK/WinTAK. ### Configuration diff --git a/docs/ar-rSA/user/translate.md b/docs/ar-rSA/user/translate.md index 67d69e2d96..cdc8b6d87f 100644 --- a/docs/ar-rSA/user/translate.md +++ b/docs/ar-rSA/user/translate.md @@ -2,7 +2,7 @@ title: Translate the App parent: User Guide nav_order: 17 -last_updated: 2026-08-30 +last_updated: 2026-09-11 description: How the app and its documentation are translated via Crowdin, and guidelines for contributing translations. aliases: - translate @@ -45,7 +45,7 @@ If your language is not yet listed on Crowdin: The Android app uses **Compose Multiplatform resources** for all user-visible strings: -``` +```text core/resources/src/commonMain/composeResources/ ├── values/ ← English (default) │ └── strings.xml @@ -58,7 +58,7 @@ core/resources/src/commonMain/composeResources/ In-app documentation follows a similar pattern under `docs/`: -``` +```text docs/ ├── en/user/ ← English source (default) │ ├── onboarding.md diff --git a/docs/be-rBY/user/desktop.md b/docs/be-rBY/user/desktop.md index 36fcc31d73..f8d415ffd5 100644 --- a/docs/be-rBY/user/desktop.md +++ b/docs/be-rBY/user/desktop.md @@ -2,7 +2,7 @@ title: Desktop App parent: User Guide nav_order: 14 -last_updated: 2026-08-30 +last_updated: 2026-09-11 description: Install and use the Meshtastic Desktop app on Linux, macOS, and Windows — connections, feature parity, and keyboard shortcuts. aliases: - desktop @@ -58,20 +58,20 @@ Bluetooth Low Energy is supported on desktop via the [Kable](https://github.com/ ## Feature Parity -| Feature | Android | Desktop | Notes | -| ----------------------------------------------------- | ------- | ------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Messaging | ✓ | ✓ | Full parity | -| Node List | ✓ | ✓ | Full parity | -| Map | ✓ | ✓ | Interactive MapLibre map, with base map and overlay pickers and custom tile sources. No offline downloads or local `.mbtiles` archives | -| Map layers (`.kml`/`.kmz`/GeoJSON) | ✓ | ✓ | Same layer store and sheet as Android; imported files draw on the desktop map | -| Site Planner | ✓ | ✓\* | \*Opens in your browser on desktop; the estimate is not drawn on the desktop map | -| Налады | ✓ | ✓ | Full parity | -| Bluetooth (BLE) | ✓ | ✓ | Via Kable on desktop | -| Firmware Update | ✓ | ✓ | In-app USB, BLE, and Wi-Fi (ESP32) update work the same as Android. The USB maintenance flow — nRF52/RP2040 factory erase and bootloader upgrade — is Android-only | -| Notifications | ✓ | ✓ | Native OS notifications | -| Widgets | ✓ | ✗ | Android-only | -| AI Assistant (Chirpy) | ✓\* | ✗ | Google flavor Android only | -| App Functions (system AI) | ✓† | ✗ | Google flavor Android only | +| Feature | Android | Desktop | Notes | +| ----------------------------------------------------- | ------- | ------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Messaging | ✓ | ✓ | Full parity | +| Node List | ✓ | ✓ | Full parity | +| Map | ✓ | ✓ | Interactive MapLibre map, with base map and overlay pickers and custom tile sources. Offline terrain (hillshade and contours) can be downloaded; offline base-map packs and local `.mbtiles` archives cannot | +| Map layers (`.kml`/`.kmz`/GeoJSON) | ✓ | ✓ | Same layer store and sheet as Android; imported files draw on the desktop map | +| Site Planner | ✓ | ✓\* | \*Opens in your browser on desktop; the estimate is not drawn on the desktop map | +| Налады | ✓ | ✓ | Full parity | +| Bluetooth (BLE) | ✓ | ✓ | Via Kable on desktop | +| Firmware Update | ✓ | ✓ | In-app USB, BLE, and Wi-Fi (ESP32) update work the same as Android. The USB maintenance flow — nRF52/RP2040 factory erase and bootloader upgrade — is Android-only | +| Notifications | ✓ | ✓ | Native OS notifications | +| Widgets | ✓ | ✗ | Android-only | +| AI Assistant (Chirpy) | ✓\* | ✗ | Google flavor Android only | +| App Functions (system AI) | ✓† | ✗ | Google flavor Android only | \*Chirpy AI requires Android 14+ on Google flavor builds with supported hardware. @@ -122,7 +122,7 @@ Individual doc pages render with full formatting: ## Building from Source -```bash +```shell git clone https://github.com/meshtastic/Meshtastic-Android.git cd Meshtastic-Android ./gradlew :desktopApp:run @@ -135,7 +135,7 @@ Requirements: ## Known Limitations -- Offline tile downloads and local `.mbtiles` archives are not available on desktop. +- Offline base-map downloads and local `.mbtiles` archives are not available on desktop. Offline terrain is — see [Map & Waypoints](map-and-waypoints). - `.kml`/`.kmz`/GeoJSON layer import works — see [Map & Waypoints](map-and-waypoints#map-layers). Site Planner opens in your browser rather than in the app; to bring its coverage estimate onto the map, click the transmitter pin diff --git a/docs/be-rBY/user/discovery.md b/docs/be-rBY/user/discovery.md index 2d15c25d5c..ba27fa0d1c 100644 --- a/docs/be-rBY/user/discovery.md +++ b/docs/be-rBY/user/discovery.md @@ -2,7 +2,7 @@ title: Local Mesh Discovery parent: User Guide nav_order: 12 -last_updated: 2026-08-30 +last_updated: 2026-09-11 description: Explore your mesh network — the Local Mesh Discovery scanner, traceroute paths, neighbor maps, and node discovery tools. aliases: - discovery @@ -97,7 +97,7 @@ Mesh Beacon lets nodes invite others to join their mesh. A beaconing node period Configure it under **Settings → Module configuration → Mesh Beacon**. The entry appears only on radios running firmware 2.8.0 or newer. A read-only **Region** row at the top of the screen shows the region the beacon advertises: that region, and the preset, are always the ones the radio itself uses, so a beacon cannot invite anyone onto settings your radio is not running. - **Listen for beacons** — receive invitations broadcast by other nodes. -- **Broadcast a beacon** — periodically advertise this mesh to nearby nodes, with an optional **Beacon message** of up to 100 bytes, a **Broadcast interval** picked from fixed intervals between 1 hour and 72 hours, and an **Offered channel** chosen from your radio's own channels. The offered channel is required, and defaults to your primary channel. +- **Broadcast a beacon** — periodically advertise this mesh to nearby nodes, with an optional **Beacon message** of up to 100 bytes, a **Broadcast interval** picked from fixed intervals between 1 hour and 72 hours, and an **Offered channel** chosen from your radio's own channels. The offered channel is required, and defaults to your primary channel. Over remote admin the picker offers the primary channel only. - **Broadcast targets** — optional extra destinations beyond the offered channel. **Add target** appends a row; each row picks a **Channel** and a **Transmit preset**, and **Remove target** deletes it. With no targets, the beacon goes out on the offered channel alone. Two conditions block beacon setup: diff --git a/docs/be-rBY/user/help-and-docs.md b/docs/be-rBY/user/help-and-docs.md index a8e4a80549..453e3068d8 100644 --- a/docs/be-rBY/user/help-and-docs.md +++ b/docs/be-rBY/user/help-and-docs.md @@ -2,7 +2,7 @@ title: Help & In-App Docs parent: User Guide nav_order: 21 -last_updated: 2026-08-30 +last_updated: 2026-09-11 description: Browse this documentation inside the app, search it, and ask Chirpy — the on-device AI assistant — questions about Meshtastic. aliases: - help @@ -23,7 +23,7 @@ The docs browser lists every user-guide page. Tap a page to read it; images and ### Поиск -The search box sits at the top of the docs browser. Type in it to filter pages by title and keywords — results update as you type, and the **Clear search** button (✕) empties the box. +Tap the search field at the top of the docs browser to open a full-screen search view. Type to filter pages by title and keywords — results update as you type, under the same field. The back arrow, or the keyboard's search action, closes the view. The field hides while you scroll down the page list and comes back when you scroll up. ![Searching the in-app documentation](../../assets/screenshots/docs-browser_search.png) diff --git a/docs/be-rBY/user/map-and-waypoints.md b/docs/be-rBY/user/map-and-waypoints.md index 992c2c22d3..63146a64df 100644 --- a/docs/be-rBY/user/map-and-waypoints.md +++ b/docs/be-rBY/user/map-and-waypoints.md @@ -2,7 +2,7 @@ title: Map & Waypoints parent: User Guide nav_order: 6 -last_updated: 2026-09-01 +last_updated: 2026-09-11 description: View node positions on the map, create and share waypoints, manage map layers and Site Planner, and control position sharing and privacy. aliases: - map @@ -44,7 +44,7 @@ The floating toolbar provides quick access to the compass, the map type and laye ### Filtering the Map -Tap the filter button in the floating toolbar to open **Filter map**. **Display** controls what is drawn: **Only Favorites**, **Show Waypoints**, **Show Precision Circles**, and a slider that hides nodes not heard from recently. **Node roles** is a chip per device role, plus **All** to show every role; a selected chip means that role is shown. **Nodes** narrows the set further with **Hide offline nodes**, **Only show direct nodes**, **Exclude MQTT**, **Show ignored nodes**, and **Include unknown**. +Tap the filter button in the floating toolbar to open **Filter map**. **Display** controls what is drawn: **Only Favorites**, **Show Waypoints**, **Show Precision Circles**, and a slider that hides nodes not heard from recently. **Node roles** is a chip per device role, plus **All** to show every role; a selected chip means that role is shown. **Nodes** narrows the set further with **Hide offline nodes**, **Only show direct nodes**, **Signed only**, **Encrypted only**, **Exclude MQTT**, **Show ignored nodes**, and **Include unknown** — the same rules as the node list's [filter toggles](nodes). A dot on the filter button means at least one filter is hiding something — check it before concluding the mesh is quiet. Turning **Show Waypoints** off hides every waypoint, including your own. **Show ignored nodes** adds them to the map rather than showing only them — unlike the node list's **Only show ignored Nodes**. @@ -147,8 +147,9 @@ picker, all three offer the same raster base maps: Overlays can be toggled on top of any base map, from the layers sheet: - **Weather radar** — NOAA NEXRAD reflectivity (US coverage) -- **Hillshade** — terrain relief, on **F-Droid** and **Desktop** only. Useful for understanding why a - link fails, since LoRa range is limited by terrain +- **Hillshade** — terrain relief from an online tile source, on **F-Droid** and **Desktop** only; Google + Play draws hillshade from downloaded offline terrain instead. Useful for understanding why a link + fails, since LoRa range is limited by terrain ### Adding your own tile source @@ -156,7 +157,7 @@ Any XYZ tile endpoint can be added as a base map, on every flavor and on desktop Tile Sources** at the foot of the base map picker and paste a URL template using `{z}`, `{x}` and `{y}` — plus `{s}` if the provider uses rotating subdomains. A national mapping service, for example: -``` +```text https://wmts.geo.admin.ch/1.0.0/ch.swisstopo.pixelkarte-farbe/default/current/3857/{z}/{x}/{y}.jpeg ``` @@ -173,7 +174,18 @@ On **Google Play**, the layers sheet's **Offline Manager** section downloads its — water, roads and administrative boundaries extracted from the public Protomaps basemap dataset, drawn directly as map shapes rather than raster tiles. Frame the area you want, tap **Start Download**, and once it finishes flip **Show on map** to draw it (it's off by default so it never surprises you by covering up the -live map). You can still import a pre-made `.mbtiles` archive here too, as before. **Desktop** has neither. +live map). You can still import a pre-made `.mbtiles` archive here too, as before. **Desktop** has neither, though +the offline terrain described next does work there. + +### Offline terrain + +Hillshade and elevation contours can be downloaded for offline use as well, so a downloaded area still +shows why a link fails when there is no network. The terrain data comes from Mapterhorn and is credited on +the map. On **Google Play**, each downloaded region in the **Offline Manager** list has a **Download Terrain** +button and **Hillshade** and **Contours** switches; terrain attaches to that region and shares its storage limit, and an area +too large for terrain asks you to zoom in or pick a smaller region. On **F-Droid** and **Desktop**, the +layers sheet has an **Offline Terrain** section of its own: frame the area, tap **Start Download**, and the +hillshade and contours draw from the downloaded tiles from then on. ### Going offline @@ -181,8 +193,8 @@ An **Offline** pill appears over the map whenever the device has no network conn builds, if you've imported a local `.mbtiles` archive, the map switches to it automatically the moment the network drops — no toggle to remember — and switches back once you're reconnected, as long as you haven't picked a different base map yourself in between. On **F-Droid**, a downloaded offline area keeps rendering on -its own; the pill is purely informational there. **Desktop** has no offline downloads at all yet, so the pill -is informational there too. +its own; the pill is purely informational there. **Desktop** downloads offline terrain but no base-map +packs, so the pill is informational there too. ## Related Topics diff --git a/docs/be-rBY/user/messages-and-channels.md b/docs/be-rBY/user/messages-and-channels.md index 3eaa57d8d1..4980030fec 100644 --- a/docs/be-rBY/user/messages-and-channels.md +++ b/docs/be-rBY/user/messages-and-channels.md @@ -2,7 +2,7 @@ title: Messages & Channels parent: User Guide nav_order: 3 -last_updated: 2026-08-30 +last_updated: 2026-09-14 description: Send and receive messages, manage channels, configure encryption, search conversations, and use quick chat, reactions, and message actions. aliases: - channels @@ -50,6 +50,21 @@ Key length alone does not change the icon: a 128-bit key and a 256-bit key both Tapping an existing channel opens the same editor, where you can change the name, the PSK, MQTT uplink and downlink, and position precision. Every edit on this screen — adding, editing, deleting, or dragging a channel into a new order — waits on **Send** the same way. +### Archived Channels + +A conversation belongs to the channel it was held on, not to the slot that channel occupied. When a channel leaves your radio — you scan a different channel QR, replace a channel's key, or switch the modem preset — its conversation is **archived** rather than deleted or merged into whatever takes the slot. + +An archived conversation: + +- stays in the **Channels** list under the name the channel had, below the channels currently on your radio, marked with a history icon; +- keeps its full message history, searchable as usual; +- is read-only — you cannot send, reply, or react, because the channel is no longer on the radio to send on, and it is not offered as a share target; +- comes back automatically if you re-add that exact channel, taking its messages back to the live slot. + +Changing the modem preset archives the channel too: the preset is part of a default channel's identity, so **LongFast** and **MediumFast** are different channels even though neither the name nor the key changed. Changing only the region does not archive anything. + +To remove an archived conversation for good, long-press it in the conversation list and delete it. + ## Direct Messages Direct messages (DMs) go to one specific node. When both radios hold each other's public keys, your radio encrypts the message to that node's public key, so no one else on the mesh can read it — not even nodes that share your channel. diff --git a/docs/be-rBY/user/mqtt.md b/docs/be-rBY/user/mqtt.md index 7cfe6e6c52..5c907f349f 100644 --- a/docs/be-rBY/user/mqtt.md +++ b/docs/be-rBY/user/mqtt.md @@ -2,7 +2,7 @@ title: MQTT parent: User Guide nav_order: 11 -last_updated: 2026-08-30 +last_updated: 2026-09-11 description: Bridge your mesh to the internet — MQTT broker setup, encryption layers, and map reporting. aliases: - mqtt @@ -25,7 +25,7 @@ The MQTT module connects your node to an MQTT broker, allowing: ## How It Works -``` +```text [Your Node] → Radio → [Gateway Node with Wi-Fi] → MQTT Broker → [Remote Gateway] → Radio → [Remote Node] ``` diff --git a/docs/be-rBY/user/node-metrics.md b/docs/be-rBY/user/node-metrics.md index f4b1b3ce50..9ede3163e1 100644 --- a/docs/be-rBY/user/node-metrics.md +++ b/docs/be-rBY/user/node-metrics.md @@ -2,7 +2,7 @@ title: Node Metrics parent: User Guide nav_order: 5 -last_updated: 2026-08-30 +last_updated: 2026-09-09 description: Telemetry dashboards for each mesh node — device health, environment sensors, air quality, signal quality, power, traceroute, and position history. aliases: - metrics @@ -114,7 +114,7 @@ Radio signal quality information: ### Signal Quality Reference -Signal quality is rated from **SNR relative to the active LoRa modem preset's demodulation floor**, not from fixed thresholds — a given SNR means different things on different presets (e.g. −15 dB is fine on LongSlow but unusable on ShortFast). RSSI is shown but is not part of the rating. In the table, _limit_ is the preset's SNR limit. +Signal quality is rated from **SNR relative to the active LoRa modem preset's demodulation floor**, not from fixed thresholds — a given SNR means different things on different presets (e.g. −15 dB is fine on LongSlow but unusable on ShortFast). When RSSI and a noise-floor reading are both available, the app also rates their difference against the preset limit and uses the worse rating. Otherwise, RSSI is display-only. In the table, _limit_ is the preset's SNR limit. | Quality | Criteria | | ------- | ---------------------------------------------------------------- | diff --git a/docs/be-rBY/user/nodes.md b/docs/be-rBY/user/nodes.md index e2245fed9e..218753f9f5 100644 --- a/docs/be-rBY/user/nodes.md +++ b/docs/be-rBY/user/nodes.md @@ -2,7 +2,7 @@ title: Nodes parent: User Guide nav_order: 4 -last_updated: 2026-09-04 +last_updated: 2026-09-11 description: Browse, filter, and sort mesh nodes — view details, signal quality, roles, and quick actions. aliases: - node-list @@ -74,19 +74,23 @@ Most users should keep the default **Client** role. Consider a different role wh > 💡 **Tip:** The mesh works best when most nodes are **Client** or **Router**. Too many Client Mute nodes reduce mesh resilience; too many Routers in a dense area can cause congestion. A good rule of thumb: one Router per 5–10 Clients in your area. -### Encryption Indicators +### Security indicators -Nodes display encryption status icons next to their name: +Each node carries one security icon beside its name in the node list. Tap it to read what it means, and choose **Show All Meanings** in that dialog for the full legend. The detail screen shows the same state in words, as a **Security** row that opens the same dialog. -| Icon | Meaning | -| ----------- | ------------------------------------------------------------------------------------------------------------------------------------------------- | -| 🔒 Locked | Communication uses PKI (public key infrastructure) — end-to-end encrypted with verified identity | -| 🔓 Unlocked | No public key has been received for this node, so it cannot be direct messaged — use **Request User Info** on the node detail page to ask for one | -| ⚠️ Mismatch | Public key mismatch — the node's key has changed since last seen (investigate before trusting) | +| Icon | Meaning | Shown for | +| --------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------- | +| Person with a shield check (green) | **Verified contact** — you verified this node's key in person by exchanging contact QR codes, so its identity is confirmed. The strongest trust the list shows. Your own connected node carries it too | Any firmware version | +| Nodes icon with a shield check (green) | **Signed node** — this node signs its broadcasts with its identity key, so its identity is consistent over time, but you have not verified it in person. On firmware 2.8 the icon appears from the version alone, before any signed broadcast has been heard | Firmware 2.8 or newer, and any node whose signed broadcast your node has verified | +| 🔒 Closed lock | A public key is on file and matches, so direct messages to this node are encrypted | Firmware before 2.8, or no reported version | +| 🔓 Open lock | No public key has been received for this node, so it cannot be direct messaged — use **Request User Info** on the node detail page to ask for one | Firmware before 2.8, or no reported version | +| ⚠️ Mismatch | **Public key mismatch** — a different key arrived for this node after one was stored. Investigate before trusting | Any firmware version | -> 💡 **Tip:** Direct messages always use PKI, so the radio needs the other node's public key before it can send one. It refuses the send rather than falling back to channel encryption. Keys arrive inside node info, which is why an open lock usually clears itself once that node is heard from properly. If you see a key mismatch warning, the node may have been reset or compromised. +Every node on firmware 2.8 or newer signs its broadcasts, so on that firmware the signed state is the baseline and the locks are not shown. -To clear a mismatch, first confirm through another trusted channel that the key change was intentional — a factory reset causes one. Then touch & hold the node, choose **Remove**, and let the two radios exchange keys again the next time yours hears it. +Direct messages always use public-key encryption, so your node needs the other node's public key before it can send one. It refuses the send rather than falling back to channel encryption. Keys arrive inside node info, which is why an open lock usually clears itself once that node is heard from properly. + +A mismatch never replaces the key you already hold. The app keeps the first key it recorded and refuses the new one, as the firmware does, so a stray or hostile node info cannot silently break encrypted messaging to a contact. To clear a mismatch, first confirm through another trusted channel that the key change was intentional — a factory reset causes one. Then touch & hold the node, choose **Remove**, and let the two nodes exchange keys again the next time yours hears it. ## Quick Actions @@ -108,7 +112,9 @@ radio is connected and running firmware 2.8 or newer — see ## Sharing a Contact -On a node's detail screen, tap **Share Contact** to produce a link and a QR code for that node. From the same dialog, **Write to NFC tag** saves the link to a writable NFC tag that anyone can tap to open. +On a node's detail screen, tap **Share Contact** to produce a link and a QR code for that node. From the same dialog, **Share link** opens the Android share sheet (on desktop it copies the link instead), **Write to NFC tag** saves it to a writable NFC tag, and **Copy** puts it on the clipboard. While that dialog is open and in front of you, the phone also offers the same link to any NFC reader, so someone can take the contact by tapping their phone against yours with no tag involved. + +Sharing your own contact this way marks it as verified in person, so whoever imports it sees the **Verified contact** icon rather than the signed one. Relaying someone else's contact passes on only what your app had already recorded about them. To add someone else's contact, use the import button on the node list and choose **Scan Shared Contact QR Code**, **Scan Shared Contact NFC**, or **Input Shared Contact URL**. The app asks you to confirm with **Import Shared Contact?**, and warns you when the contact is one you already have. @@ -120,14 +126,16 @@ Type in the search field to filter nodes by name or short name. The filter updat ### Filter Toggles -| Фільтраваць | Апісанне | -| --------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| **Hide offline nodes** | Show only nodes heard within the last 2 hours | -| **Only show direct nodes** | Show only nodes your radio heard directly, with no relay in between | -| **Include unknown** | Show nodes that haven't sent user info yet. **On by default**, so a node heard before its info arrives stays visible; these carry a badge marking them incomplete, and cannot be direct messaged until their user info brings a public key | -| **Exclude infrastructure** | Hide infrastructure-role nodes (Router, Router Late, Client Base, and legacy Repeater nodes) and any node that cannot be messaged, whatever its role | -| **Exclude MQTT** | Hide nodes heard only via MQTT internet bridge | -| **Only show ignored Nodes** | Replace the list with the nodes you have ignored. Every other node is hidden while this is on, and a banner appears at the top of the list to take you back | +| Фільтраваць | Апісанне | +| --------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **Hide offline nodes** | Show only nodes heard within the last 2 hours | +| **Only show direct nodes** | Show only nodes your radio heard directly, with no relay in between | +| **Include unknown** | Show nodes that haven't sent user info yet. **On by default**, so a node heard before its info arrives stays visible; these carry a badge marking them incomplete, and cannot be direct messaged until their user info brings a public key | +| **Exclude infrastructure** | Hide infrastructure-role nodes (Router, Router Late, Client Base, and legacy Repeater nodes) and any node that cannot be messaged, whatever its role | +| **Exclude MQTT** | Hide nodes heard only via MQTT internet bridge | +| **Signed only** | Show only nodes whose signed broadcasts your node has actually heard and verified. Stricter than the icon: a node on 2.8 shows as signed by its firmware version before any signed broadcast arrives, and a contact verified in person on older firmware is not signed at all | +| **Encrypted only** | Show nodes with a matching public key on file, the key an encrypted direct message needs. A node with a key mismatch is excluded | +| **Only show ignored Nodes** | Replace the list with the nodes you have ignored. Every other node is hidden while this is on, and a banner appears at the top of the list to take you back | ### Sort Options @@ -149,6 +157,8 @@ Tap the hop-histogram icon in the node list's app bar to open a bar chart of how Tapping a node opens the detail view with comprehensive information. See [Node Metrics](node-metrics) for full details on metrics and telemetry. +Signal quality is rated against your modem preset. The same SNR can be good on a long-range preset and poor on a faster one. Traceroute and neighbor-info SNR colors use that preset too. RSSI text has its own strength colors; it affects the quality rating only when a noise-floor reading is also available. + The Details card carries the node's short name, role, IDs, last heard time, hops away, uptime, and its SNR and RSSI: ![Node detail section](../../assets/screenshots/nodes_detail_section.png) diff --git a/docs/be-rBY/user/settings-module-admin.md b/docs/be-rBY/user/settings-module-admin.md index f2446237a2..7e53722a59 100644 --- a/docs/be-rBY/user/settings-module-admin.md +++ b/docs/be-rBY/user/settings-module-admin.md @@ -2,7 +2,7 @@ title: Settings — Modules & Admin parent: User Guide nav_order: 8 -last_updated: 2026-08-30 +last_updated: 2026-09-16 description: Configure optional feature modules (MQTT, telemetry, canned messages, TAK, and more) and perform device administration. aliases: - modules @@ -333,7 +333,7 @@ appear only when your own node is selected: ### О нас -**Settings → About** carries the app's own identity rather than the radio's: +**Settings → About** carries the app's own identity rather than the node's: Three sections: @@ -341,7 +341,11 @@ Three sections: - **Apps** — opens with **Need Hardware?**, a rotating carousel of popular devices that links out to where to buy one. It also lists the GitHub repository, the running app version, and **Acknowledgements** (see the next section). -- **Project information** — links to the website and to this documentation. +- **Project information** — links to the website, to this documentation, and to the GNU GPL v3 + license the app is released under. + +The footer states the copyright and that the app is free software with no warranty, which you +may redistribute under the same license. ### Acknowledgements diff --git a/docs/be-rBY/user/settings-radio-user.md b/docs/be-rBY/user/settings-radio-user.md index 336d0c62a8..90393181c7 100644 --- a/docs/be-rBY/user/settings-radio-user.md +++ b/docs/be-rBY/user/settings-radio-user.md @@ -2,7 +2,7 @@ title: Settings — Radio & User parent: User Guide nav_order: 7 -last_updated: 2026-09-04 +last_updated: 2026-09-11 description: Configure your radio hardware, LoRa presets, user profile, position sharing, power management, and security. aliases: - settings @@ -51,7 +51,7 @@ The footer appears as soon as you change something. **Discard** throws the chang The status message is saved with the same **Save**, but it never reboots the node — and, like the rest of this screen, it can be edited on a remote node you administer. For your own radio there is a shortcut while it is connected: touch & hold your node in the [node list](nodes.md) and choose -**Update status**. Older firmware and a disconnected radio have no shortcut — the field above is +**Update status**. Older firmware and a disconnected node have no shortcut — the field itself is still the way in. ## Конфигурация @@ -77,13 +77,13 @@ On **Settings → LoRa**. | Setting | Апісанне | Default | | ------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------- | -| Рэгіён | Regulatory region for frequency bands. You must set this before transmitting | Unset (must configure) | +| Рэгіён | Regulatory region for frequency bands. You must set this before transmitting. On firmware 2.8 or newer, the first region set also creates the node's identity key and gives it a new node number | Unset (must configure) | | Presets | Speed/range tradeoff | LongFast | | Number of Hops | Maximum retransmit hops | 3 | | Transmit Power | Transmission power (dBm); 0 = max allowed for region | 0 (region max) | | Frequency Override | Overrides the computed operating frequency outright (MHz). It does not offset the calculated value — leave at 0 unless you know you need a specific frequency | 0 (use calculated) | | Use Preset | On by default. Turn it off to set Spread Factor, Coding Rate and Bandwidth by hand instead of taking them from the modem preset | On | -| Spread Factor | Manual mode only: 7–12. Higher spreads further but slower | From preset | +| Spread Factor | Manual mode only: 5–12. Higher spreads further but slower. On SX127x (RF95) radios the firmware does not accept 5 or 6 and uses 11 instead | From preset | | Coding Rate | Manual mode only: 5–8. More redundancy costs airtime | From preset | | Bandwidth | Manual mode only: the channel bandwidth in kHz, typed in directly. On the 2.4 GHz region the app offers a list of the bandwidths your radio supports instead, and a stored value that is not on that list shows as _Unsupported_ and blocks saving until you pick a supported one | From preset | | Frequency Slot | Which slot within the region's band to use. 0 derives it from the primary channel name | 0 (automatic) | @@ -111,13 +111,13 @@ The Lite, Narrow, Medium Turbo, and Tiny presets need firmware 2.8 or newer — | Short Slow | ~5 km | 6.25 kbps | −10 dB | Suburban short-range; moderate building density | | Medium Fast | ~5 km | 3.52 kbps | −12.5 dB | Suburban areas; moderate building density | | Medium Slow | ~8 km | 1.95 kbps | −15 dB | Suburban/rural; moderate range with slower speed | -| Long Turbo | ~10 km | 1.34 kbps | −12.5 dB | Similar range to Long Fast but with 500 kHz bandwidth; faster throughput | +| Long Turbo | ~10 km | 1.34 kbps | −17.5 dB | Similar range to Long Fast but with 500 kHz bandwidth; faster throughput | | Long Fast | ~10 km | 1.1 kbps | −17.5 dB | **General use (default)** — balanced range and speed | | Long Moderate | ~20 km | 0.34 kbps | −17.5 dB | Rural with some terrain; occasional use | | Lite Fast | ~5 km | 1.76 kbps | −12.5 dB | EU 866 MHz SRD band (125 kHz BW); comparable to Medium Fast | | Lite Slow | ~10 km | 0.98 kbps | −15 dB | EU 866 MHz SRD band (125 kHz BW); comparable to Long Fast | -| Narrow Fast | ~5 km | 2.28 kbps | −10 dB | EU 868 MHz band (62.5 kHz BW); avoids interference with other devices | -| Narrow Slow | ~10 km | 1.30 kbps | −12.5 dB | EU 868 MHz band (62.5 kHz BW); comparable to Long Fast | +| Narrow Fast | ~5 km | 2.28 kbps | −7.5 dB | EU 868 MHz band (62.5 kHz BW); avoids interference with other devices | +| Narrow Slow | ~10 km | 1.30 kbps | −10 dB | EU 868 MHz band (62.5 kHz BW); comparable to Long Fast | | Medium Turbo | ~5 km | 7.0 kbps | −12.5 dB | Like Medium Fast but with 500 kHz bandwidth; not legal in every region. Needs firmware 2.8 or newer | | Tiny Fast | ~10 km | 0.68 kbps | −7.5 dB | Amateur bands that cap occupied bandwidth; these presets use 15.6 kHz. Needs firmware 2.8 or newer, an SX126x or SX127x radio, and a TCXO of ±5 ppm or better | | Tiny Slow | ~20 km | 0.33 kbps | −10 dB | Same band restrictions as Tiny Fast, longer range. Same firmware, radio, and TCXO requirements | diff --git a/docs/be-rBY/user/signal-meter.md b/docs/be-rBY/user/signal-meter.md index d7e21c1c20..7321a5aa49 100644 --- a/docs/be-rBY/user/signal-meter.md +++ b/docs/be-rBY/user/signal-meter.md @@ -2,7 +2,7 @@ title: How the Meshtastic Signal Meter Works parent: User Guide nav_order: 15 -last_updated: 2026-08-30 +last_updated: 2026-09-09 description: How the signal meter rates quality from SNR relative to the LoRa modem preset — spread spectrum, presets, and what the bars really mean. aliases: - signal @@ -32,7 +32,7 @@ Each modem preset has an SNR limit: the lowest SNR at which that preset can stil ## Rating Signal Quality -The app rates signal quality (None, Bad, Fair, or Good) from SNR alone, measured against the active preset's SNR limit. It does not factor in RSSI: without knowing the local noise floor, RSSI alone cannot say whether a signal is decodable. RSSI is still available — on the node detail screen and in the metrics charts. +The app rates signal quality (None, Bad, Fair, or Good) from SNR relative to the active preset's SNR limit. When RSSI and a noise-floor reading are both available, it also rates the difference between them against that limit and uses the worse rating. Otherwise, it uses SNR alone. Because the rating is relative to the preset, the same SNR rates differently on different presets. An SNR of −16 dB rates Good on Long Fast (SNR limit −17.5 dB) but None on Short Fast (SNR limit −7.5 dB). Let `limit` be the active preset's SNR limit: @@ -45,7 +45,7 @@ Because the rating is relative to the preset, the same SNR rates differently on The icon never goes blank, so count the bars carefully: a single bar means None, not a weak but usable link, and Good is a solid wedge rather than a set of bars. A gray three-bar icon labeled Unknown is a different state again — the packet carried no SNR measurement at all, which is not the same as measuring one and finding it too weak. -> ℹ️ **Note:** Traceroute hop colors use fixed thresholds (−7 dB / −15 dB); the per-node signal meter uses the preset-relative rating instead. +> ℹ️ **Note:** Traceroute and neighbor-info SNR colors use the active preset's limits too. RSSI text has separate strength colors: green above −115 dBm, yellow above −120 dBm, orange above −126 dBm, and red at or below −126 dBm. ## Diagnosing Local Interference diff --git a/docs/be-rBY/user/tak.md b/docs/be-rBY/user/tak.md index 90d700e40f..e70ca9bd38 100644 --- a/docs/be-rBY/user/tak.md +++ b/docs/be-rBY/user/tak.md @@ -2,7 +2,7 @@ title: TAK Integration parent: User Guide nav_order: 10 -last_updated: 2026-08-30 +last_updated: 2026-09-11 description: Interoperate with ATAK and WinTAK — CoT position sharing, TAK roles, and plugin setup. aliases: - tak @@ -36,7 +36,7 @@ The TAK module allows Meshtastic nodes to: > ⚠️ **Warning:** The old **Meshtastic ATAK Plugin** is no longer part of this path and cannot > work. It bridged through the cross-process AIDL API, which was removed in app 2.8.0; the mesh > service is now in-process only. Do not install it. Interop today runs over the app's own local -> TAK server plus the Mesh to CoT Converter, both described below, with stock ATAK/iTAK/WinTAK. +> TAK server plus the Mesh to CoT Converter, both described in the following sections, with stock ATAK/iTAK/WinTAK. ### Конфигурация diff --git a/docs/be-rBY/user/translate.md b/docs/be-rBY/user/translate.md index 67d69e2d96..cdc8b6d87f 100644 --- a/docs/be-rBY/user/translate.md +++ b/docs/be-rBY/user/translate.md @@ -2,7 +2,7 @@ title: Translate the App parent: User Guide nav_order: 17 -last_updated: 2026-08-30 +last_updated: 2026-09-11 description: How the app and its documentation are translated via Crowdin, and guidelines for contributing translations. aliases: - translate @@ -45,7 +45,7 @@ If your language is not yet listed on Crowdin: The Android app uses **Compose Multiplatform resources** for all user-visible strings: -``` +```text core/resources/src/commonMain/composeResources/ ├── values/ ← English (default) │ └── strings.xml @@ -58,7 +58,7 @@ core/resources/src/commonMain/composeResources/ In-app documentation follows a similar pattern under `docs/`: -``` +```text docs/ ├── en/user/ ← English source (default) │ ├── onboarding.md diff --git a/docs/bg-rBG/user/desktop.md b/docs/bg-rBG/user/desktop.md index 3f4bce8a9d..5b8adbb570 100644 --- a/docs/bg-rBG/user/desktop.md +++ b/docs/bg-rBG/user/desktop.md @@ -2,7 +2,7 @@ title: Настолно приложение parent: Ръководство за потребители nav_order: 14 -last_updated: 2026-08-30 +last_updated: 2026-09-11 description: Install and use the Meshtastic Desktop app on Linux, macOS, and Windows — connections, feature parity, and keyboard shortcuts. aliases: - desktop @@ -58,20 +58,20 @@ Bluetooth Low Energy is supported on desktop via the [Kable](https://github.com/ ## Feature Parity -| Feature | Android | Desktop | Бележки | -| ----------------------------------------------------- | ------- | ------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Messaging | ✓ | ✓ | Full parity | -| Node List | ✓ | ✓ | Full parity | -| Карта | ✓ | ✓ | Interactive MapLibre map, with base map and overlay pickers and custom tile sources. No offline downloads or local `.mbtiles` archives | -| Map layers (`.kml`/`.kmz`/GeoJSON) | ✓ | ✓ | Same layer store and sheet as Android; imported files draw on the desktop map | -| Site Planner | ✓ | ✓\* | \*Opens in your browser on desktop; the estimate is not drawn on the desktop map | -| Настройки | ✓ | ✓ | Full parity | -| Bluetooth (BLE) | ✓ | ✓ | Via Kable on desktop | -| Актуализация на фърмуера | ✓ | ✓ | In-app USB, BLE, and Wi-Fi (ESP32) update work the same as Android. The USB maintenance flow — nRF52/RP2040 factory erase and bootloader upgrade — is Android-only | -| Известия | ✓ | ✓ | Native OS notifications | -| Widgets | ✓ | ✗ | Android-only | -| AI Assistant (Chirpy) | ✓\* | ✗ | Google flavor Android only | -| App Functions (system AI) | ✓† | ✗ | Google flavor Android only | +| Feature | Android | Desktop | Бележки | +| ----------------------------------------------------- | ------- | ------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Messaging | ✓ | ✓ | Full parity | +| Node List | ✓ | ✓ | Full parity | +| Карта | ✓ | ✓ | Interactive MapLibre map, with base map and overlay pickers and custom tile sources. Offline terrain (hillshade and contours) can be downloaded; offline base-map packs and local `.mbtiles` archives cannot | +| Map layers (`.kml`/`.kmz`/GeoJSON) | ✓ | ✓ | Same layer store and sheet as Android; imported files draw on the desktop map | +| Site Planner | ✓ | ✓\* | \*Opens in your browser on desktop; the estimate is not drawn on the desktop map | +| Настройки | ✓ | ✓ | Full parity | +| Bluetooth (BLE) | ✓ | ✓ | Via Kable on desktop | +| Актуализация на фърмуера | ✓ | ✓ | In-app USB, BLE, and Wi-Fi (ESP32) update work the same as Android. The USB maintenance flow — nRF52/RP2040 factory erase and bootloader upgrade — is Android-only | +| Известия | ✓ | ✓ | Native OS notifications | +| Widgets | ✓ | ✗ | Android-only | +| AI Assistant (Chirpy) | ✓\* | ✗ | Google flavor Android only | +| App Functions (system AI) | ✓† | ✗ | Google flavor Android only | \*Chirpy AI requires Android 14+ on Google flavor builds with supported hardware. @@ -122,7 +122,7 @@ Individual doc pages render with full formatting: ## Building from Source -```bash +```shell git clone https://github.com/meshtastic/Meshtastic-Android.git cd Meshtastic-Android ./gradlew :desktopApp:run @@ -135,7 +135,7 @@ cd Meshtastic-Android ## Известни ограничения -- Offline tile downloads and local `.mbtiles` archives are not available on desktop. +- Offline base-map downloads and local `.mbtiles` archives are not available on desktop. Offline terrain is — see [Map & Waypoints](map-and-waypoints). - `.kml`/`.kmz`/GeoJSON layer import works — see [Map & Waypoints](map-and-waypoints#map-layers). Site Planner opens in your browser rather than in the app; to bring its coverage estimate onto the map, click the transmitter pin diff --git a/docs/bg-rBG/user/discovery.md b/docs/bg-rBG/user/discovery.md index d983168b2d..8388312f1a 100644 --- a/docs/bg-rBG/user/discovery.md +++ b/docs/bg-rBG/user/discovery.md @@ -2,7 +2,7 @@ title: Local Mesh Discovery parent: Ръководство за потребители nav_order: 12 -last_updated: 2026-08-30 +last_updated: 2026-09-11 description: Explore your mesh network — the Local Mesh Discovery scanner, traceroute paths, neighbor maps, and node discovery tools. aliases: - discovery @@ -97,7 +97,7 @@ Mesh Beacon lets nodes invite others to join their mesh. A beaconing node period Configure it under **Settings → Module configuration → Mesh Beacon**. The entry appears only on radios running firmware 2.8.0 or newer. A read-only **Region** row at the top of the screen shows the region the beacon advertises: that region, and the preset, are always the ones the radio itself uses, so a beacon cannot invite anyone onto settings your radio is not running. - **Listen for beacons** — receive invitations broadcast by other nodes. -- **Broadcast a beacon** — periodically advertise this mesh to nearby nodes, with an optional **Beacon message** of up to 100 bytes, a **Broadcast interval** picked from fixed intervals between 1 hour and 72 hours, and an **Offered channel** chosen from your radio's own channels. The offered channel is required, and defaults to your primary channel. +- **Broadcast a beacon** — periodically advertise this mesh to nearby nodes, with an optional **Beacon message** of up to 100 bytes, a **Broadcast interval** picked from fixed intervals between 1 hour and 72 hours, and an **Offered channel** chosen from your radio's own channels. The offered channel is required, and defaults to your primary channel. Over remote admin the picker offers the primary channel only. - **Broadcast targets** — optional extra destinations beyond the offered channel. **Add target** appends a row; each row picks a **Channel** and a **Transmit preset**, and **Remove target** deletes it. With no targets, the beacon goes out on the offered channel alone. Two conditions block beacon setup: diff --git a/docs/bg-rBG/user/help-and-docs.md b/docs/bg-rBG/user/help-and-docs.md index 5eb432276d..c490d3b6c9 100644 --- a/docs/bg-rBG/user/help-and-docs.md +++ b/docs/bg-rBG/user/help-and-docs.md @@ -2,7 +2,7 @@ title: Help & In-App Docs parent: Ръководство за потребители nav_order: 21 -last_updated: 2026-08-30 +last_updated: 2026-09-11 description: Browse this documentation inside the app, search it, and ask Chirpy — the on-device AI assistant — questions about Meshtastic. aliases: - help @@ -23,7 +23,7 @@ The docs browser lists every user-guide page. Tap a page to read it; images and ### Търсене -The search box sits at the top of the docs browser. Type in it to filter pages by title and keywords — results update as you type, and the **Clear search** button (✕) empties the box. +Tap the search field at the top of the docs browser to open a full-screen search view. Type to filter pages by title and keywords — results update as you type, under the same field. The back arrow, or the keyboard's search action, closes the view. The field hides while you scroll down the page list and comes back when you scroll up. ![Searching the in-app documentation](../../assets/screenshots/docs-browser_search.png) diff --git a/docs/bg-rBG/user/map-and-waypoints.md b/docs/bg-rBG/user/map-and-waypoints.md index fc06fbca74..b348260d5a 100644 --- a/docs/bg-rBG/user/map-and-waypoints.md +++ b/docs/bg-rBG/user/map-and-waypoints.md @@ -2,7 +2,7 @@ title: Map & Waypoints parent: Ръководство за потребители nav_order: 6 -last_updated: 2026-09-01 +last_updated: 2026-09-11 description: View node positions on the map, create and share waypoints, manage map layers and Site Planner, and control position sharing and privacy. aliases: - map @@ -44,7 +44,7 @@ The floating toolbar provides quick access to the compass, the map type and laye ### Filtering the Map -Tap the filter button in the floating toolbar to open **Filter map**. **Display** controls what is drawn: **Only Favorites**, **Show Waypoints**, **Show Precision Circles**, and a slider that hides nodes not heard from recently. **Node roles** is a chip per device role, plus **All** to show every role; a selected chip means that role is shown. **Nodes** narrows the set further with **Hide offline nodes**, **Only show direct nodes**, **Exclude MQTT**, **Show ignored nodes**, and **Include unknown**. +Tap the filter button in the floating toolbar to open **Filter map**. **Display** controls what is drawn: **Only Favorites**, **Show Waypoints**, **Show Precision Circles**, and a slider that hides nodes not heard from recently. **Node roles** is a chip per device role, plus **All** to show every role; a selected chip means that role is shown. **Nodes** narrows the set further with **Hide offline nodes**, **Only show direct nodes**, **Signed only**, **Encrypted only**, **Exclude MQTT**, **Show ignored nodes**, and **Include unknown** — the same rules as the node list's [filter toggles](nodes). A dot on the filter button means at least one filter is hiding something — check it before concluding the mesh is quiet. Turning **Show Waypoints** off hides every waypoint, including your own. **Show ignored nodes** adds them to the map rather than showing only them — unlike the node list's **Only show ignored Nodes**. @@ -147,8 +147,9 @@ picker, all three offer the same raster base maps: Overlays can be toggled on top of any base map, from the layers sheet: - **Weather radar** — NOAA NEXRAD reflectivity (US coverage) -- **Hillshade** — terrain relief, on **F-Droid** and **Desktop** only. Useful for understanding why a - link fails, since LoRa range is limited by terrain +- **Hillshade** — terrain relief from an online tile source, on **F-Droid** and **Desktop** only; Google + Play draws hillshade from downloaded offline terrain instead. Useful for understanding why a link + fails, since LoRa range is limited by terrain ### Adding your own tile source @@ -156,7 +157,7 @@ Any XYZ tile endpoint can be added as a base map, on every flavor and on desktop Tile Sources** at the foot of the base map picker and paste a URL template using `{z}`, `{x}` and `{y}` — plus `{s}` if the provider uses rotating subdomains. A national mapping service, for example: -``` +```text https://wmts.geo.admin.ch/1.0.0/ch.swisstopo.pixelkarte-farbe/default/current/3857/{z}/{x}/{y}.jpeg ``` @@ -173,7 +174,18 @@ On **Google Play**, the layers sheet's **Offline Manager** section downloads its — water, roads and administrative boundaries extracted from the public Protomaps basemap dataset, drawn directly as map shapes rather than raster tiles. Frame the area you want, tap **Start Download**, and once it finishes flip **Show on map** to draw it (it's off by default so it never surprises you by covering up the -live map). You can still import a pre-made `.mbtiles` archive here too, as before. **Desktop** has neither. +live map). You can still import a pre-made `.mbtiles` archive here too, as before. **Desktop** has neither, though +the offline terrain described next does work there. + +### Offline terrain + +Hillshade and elevation contours can be downloaded for offline use as well, so a downloaded area still +shows why a link fails when there is no network. The terrain data comes from Mapterhorn and is credited on +the map. On **Google Play**, each downloaded region in the **Offline Manager** list has a **Download Terrain** +button and **Hillshade** and **Contours** switches; terrain attaches to that region and shares its storage limit, and an area +too large for terrain asks you to zoom in or pick a smaller region. On **F-Droid** and **Desktop**, the +layers sheet has an **Offline Terrain** section of its own: frame the area, tap **Start Download**, and the +hillshade and contours draw from the downloaded tiles from then on. ### Going offline @@ -181,8 +193,8 @@ An **Offline** pill appears over the map whenever the device has no network conn builds, if you've imported a local `.mbtiles` archive, the map switches to it automatically the moment the network drops — no toggle to remember — and switches back once you're reconnected, as long as you haven't picked a different base map yourself in between. On **F-Droid**, a downloaded offline area keeps rendering on -its own; the pill is purely informational there. **Desktop** has no offline downloads at all yet, so the pill -is informational there too. +its own; the pill is purely informational there. **Desktop** downloads offline terrain but no base-map +packs, so the pill is informational there too. ## Свързани теми diff --git a/docs/bg-rBG/user/messages-and-channels.md b/docs/bg-rBG/user/messages-and-channels.md index 57405420ac..51f82f81a8 100644 --- a/docs/bg-rBG/user/messages-and-channels.md +++ b/docs/bg-rBG/user/messages-and-channels.md @@ -2,7 +2,7 @@ title: Съобщения & Канали parent: Ръководство за потребители nav_order: 3 -last_updated: 2026-08-30 +last_updated: 2026-09-14 description: Send and receive messages, manage channels, configure encryption, search conversations, and use quick chat, reactions, and message actions. aliases: - канали @@ -50,6 +50,21 @@ Key length alone does not change the icon: a 128-bit key and a 256-bit key both Tapping an existing channel opens the same editor, where you can change the name, the PSK, MQTT uplink and downlink, and position precision. Every edit on this screen — adding, editing, deleting, or dragging a channel into a new order — waits on **Send** the same way. +### Archived Channels + +A conversation belongs to the channel it was held on, not to the slot that channel occupied. When a channel leaves your radio — you scan a different channel QR, replace a channel's key, or switch the modem preset — its conversation is **archived** rather than deleted or merged into whatever takes the slot. + +An archived conversation: + +- stays in the **Channels** list under the name the channel had, below the channels currently on your radio, marked with a history icon; +- keeps its full message history, searchable as usual; +- is read-only — you cannot send, reply, or react, because the channel is no longer on the radio to send on, and it is not offered as a share target; +- comes back automatically if you re-add that exact channel, taking its messages back to the live slot. + +Changing the modem preset archives the channel too: the preset is part of a default channel's identity, so **LongFast** and **MediumFast** are different channels even though neither the name nor the key changed. Changing only the region does not archive anything. + +To remove an archived conversation for good, long-press it in the conversation list and delete it. + ## Директни съобщения Direct messages (DMs) go to one specific node. When both radios hold each other's public keys, your radio encrypts the message to that node's public key, so no one else on the mesh can read it — not even nodes that share your channel. diff --git a/docs/bg-rBG/user/mqtt.md b/docs/bg-rBG/user/mqtt.md index e59e00c00e..710e446a7f 100644 --- a/docs/bg-rBG/user/mqtt.md +++ b/docs/bg-rBG/user/mqtt.md @@ -2,7 +2,7 @@ title: MQTT parent: Ръководство за потребители nav_order: 11 -last_updated: 2026-08-30 +last_updated: 2026-09-11 description: Bridge your mesh to the internet — MQTT broker setup, encryption layers, and map reporting. aliases: - mqtt @@ -25,7 +25,7 @@ The MQTT module connects your node to an MQTT broker, allowing: ## Как работи -``` +```text [Your Node] → Radio → [Gateway Node with Wi-Fi] → MQTT Broker → [Remote Gateway] → Radio → [Remote Node] ``` diff --git a/docs/bg-rBG/user/node-metrics.md b/docs/bg-rBG/user/node-metrics.md index 65d962e1b7..132c719dbf 100644 --- a/docs/bg-rBG/user/node-metrics.md +++ b/docs/bg-rBG/user/node-metrics.md @@ -2,7 +2,7 @@ title: Node Metrics parent: Ръководство за потребители nav_order: 5 -last_updated: 2026-08-30 +last_updated: 2026-09-09 description: Telemetry dashboards for each mesh node — device health, environment sensors, air quality, signal quality, power, traceroute, and position history. aliases: - metrics @@ -114,7 +114,7 @@ Radio signal quality information: ### Signal Quality Reference -Signal quality is rated from **SNR relative to the active LoRa modem preset's demodulation floor**, not from fixed thresholds — a given SNR means different things on different presets (e.g. −15 dB is fine on LongSlow but unusable on ShortFast). RSSI is shown but is not part of the rating. In the table, _limit_ is the preset's SNR limit. +Signal quality is rated from **SNR relative to the active LoRa modem preset's demodulation floor**, not from fixed thresholds — a given SNR means different things on different presets (e.g. −15 dB is fine on LongSlow but unusable on ShortFast). When RSSI and a noise-floor reading are both available, the app also rates their difference against the preset limit and uses the worse rating. Otherwise, RSSI is display-only. In the table, _limit_ is the preset's SNR limit. | Quality | Criteria | | ------------- | ---------------------------------------------------------------- | diff --git a/docs/bg-rBG/user/nodes.md b/docs/bg-rBG/user/nodes.md index 5044997849..df241998f1 100644 --- a/docs/bg-rBG/user/nodes.md +++ b/docs/bg-rBG/user/nodes.md @@ -2,7 +2,7 @@ title: Възли parent: Ръководство за потребители nav_order: 4 -last_updated: 2026-09-04 +last_updated: 2026-09-11 description: Browse, filter, and sort mesh nodes — view details, signal quality, roles, and quick actions. aliases: - node-list @@ -19,12 +19,12 @@ The Nodes screen lists every node visible on your mesh. The node list shows every node your radio has heard, including: -- **Node name** — user-configured long name -- **Short name** — 4-character identifier +- **Име на възела** — зададено от потребителя дълго име +- **Кратко име** — идентификатор от 4 знака - **Signal quality** — SNR, RSSI, and a quality word, shown only for nodes your radio heard directly. In the Complete layout a node reached through a relay shows its hop count here instead; a node heard only over MQTT shows neither -- **Last heard** — time since last communication -- **Distance** — estimated distance (if positions are shared) -- **Battery** — remote node battery level (if telemetry is enabled) +- **Последно чут** — време от последната комуникация +- **Разстояние** — приблизително разстояние (ако се споделят местоположения) +- **Батерия** — ниво на заряда на батерията на отдалечения възел (ако телеметрията е активирана) ### Choosing What the List Shows @@ -74,19 +74,23 @@ Most users should keep the default **Client** role. Consider a different role wh > 💡 **Tip:** The mesh works best when most nodes are **Client** or **Router**. Too many Client Mute nodes reduce mesh resilience; too many Routers in a dense area can cause congestion. A good rule of thumb: one Router per 5–10 Clients in your area. -### Encryption Indicators +### Security indicators -Nodes display encryption status icons next to their name: +Each node carries one security icon beside its name in the node list. Tap it to read what it means, and choose **Show All Meanings** in that dialog for the full legend. The detail screen shows the same state in words, as a **Security** row that opens the same dialog. -| Icon | Meaning | -| ----------- | ------------------------------------------------------------------------------------------------------------------------------------------------- | -| 🔒 Locked | Communication uses PKI (public key infrastructure) — end-to-end encrypted with verified identity | -| 🔓 Unlocked | No public key has been received for this node, so it cannot be direct messaged — use **Request User Info** on the node detail page to ask for one | -| ⚠️ Mismatch | Public key mismatch — the node's key has changed since last seen (investigate before trusting) | +| Icon | Meaning | Shown for | +| --------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------- | +| Person with a shield check (green) | **Verified contact** — you verified this node's key in person by exchanging contact QR codes, so its identity is confirmed. The strongest trust the list shows. Your own connected node carries it too | Any firmware version | +| Nodes icon with a shield check (green) | **Signed node** — this node signs its broadcasts with its identity key, so its identity is consistent over time, but you have not verified it in person. On firmware 2.8 the icon appears from the version alone, before any signed broadcast has been heard | Firmware 2.8 or newer, and any node whose signed broadcast your node has verified | +| 🔒 Closed lock | A public key is on file and matches, so direct messages to this node are encrypted | Firmware before 2.8, or no reported version | +| 🔓 Open lock | No public key has been received for this node, so it cannot be direct messaged — use **Request User Info** on the node detail page to ask for one | Firmware before 2.8, or no reported version | +| ⚠️ Mismatch | **Public key mismatch** — a different key arrived for this node after one was stored. Investigate before trusting | Any firmware version | -> 💡 **Tip:** Direct messages always use PKI, so the radio needs the other node's public key before it can send one. It refuses the send rather than falling back to channel encryption. Keys arrive inside node info, which is why an open lock usually clears itself once that node is heard from properly. If you see a key mismatch warning, the node may have been reset or compromised. +Every node on firmware 2.8 or newer signs its broadcasts, so on that firmware the signed state is the baseline and the locks are not shown. -To clear a mismatch, first confirm through another trusted channel that the key change was intentional — a factory reset causes one. Then touch & hold the node, choose **Remove**, and let the two radios exchange keys again the next time yours hears it. +Direct messages always use public-key encryption, so your node needs the other node's public key before it can send one. It refuses the send rather than falling back to channel encryption. Keys arrive inside node info, which is why an open lock usually clears itself once that node is heard from properly. + +A mismatch never replaces the key you already hold. The app keeps the first key it recorded and refuses the new one, as the firmware does, so a stray or hostile node info cannot silently break encrypted messaging to a contact. To clear a mismatch, first confirm through another trusted channel that the key change was intentional — a factory reset causes one. Then touch & hold the node, choose **Remove**, and let the two nodes exchange keys again the next time yours hears it. ## Quick Actions @@ -108,7 +112,9 @@ radio is connected and running firmware 2.8 or newer — see ## Sharing a Contact -On a node's detail screen, tap **Share Contact** to produce a link and a QR code for that node. From the same dialog, **Write to NFC tag** saves the link to a writable NFC tag that anyone can tap to open. +On a node's detail screen, tap **Share Contact** to produce a link and a QR code for that node. From the same dialog, **Share link** opens the Android share sheet (on desktop it copies the link instead), **Write to NFC tag** saves it to a writable NFC tag, and **Copy** puts it on the clipboard. While that dialog is open and in front of you, the phone also offers the same link to any NFC reader, so someone can take the contact by tapping their phone against yours with no tag involved. + +Sharing your own contact this way marks it as verified in person, so whoever imports it sees the **Verified contact** icon rather than the signed one. Relaying someone else's contact passes on only what your app had already recorded about them. To add someone else's contact, use the import button on the node list and choose **Scan Shared Contact QR Code**, **Scan Shared Contact NFC**, or **Input Shared Contact URL**. The app asks you to confirm with **Import Shared Contact?**, and warns you when the contact is one you already have. @@ -120,14 +126,16 @@ Type in the search field to filter nodes by name or short name. The filter updat ### Filter Toggles -| Филтър | Описание | -| --------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| **Hide offline nodes** | Show only nodes heard within the last 2 hours | -| **Only show direct nodes** | Show only nodes your radio heard directly, with no relay in between | -| **Include unknown** | Show nodes that haven't sent user info yet. **On by default**, so a node heard before its info arrives stays visible; these carry a badge marking them incomplete, and cannot be direct messaged until their user info brings a public key | -| **Exclude infrastructure** | Hide infrastructure-role nodes (Router, Router Late, Client Base, and legacy Repeater nodes) and any node that cannot be messaged, whatever its role | -| **Exclude MQTT** | Hide nodes heard only via MQTT internet bridge | -| **Only show ignored Nodes** | Replace the list with the nodes you have ignored. Every other node is hidden while this is on, and a banner appears at the top of the list to take you back | +| Филтър | Описание | +| --------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **Hide offline nodes** | Show only nodes heard within the last 2 hours | +| **Only show direct nodes** | Show only nodes your radio heard directly, with no relay in between | +| **Include unknown** | Show nodes that haven't sent user info yet. **On by default**, so a node heard before its info arrives stays visible; these carry a badge marking them incomplete, and cannot be direct messaged until their user info brings a public key | +| **Exclude infrastructure** | Hide infrastructure-role nodes (Router, Router Late, Client Base, and legacy Repeater nodes) and any node that cannot be messaged, whatever its role | +| **Exclude MQTT** | Hide nodes heard only via MQTT internet bridge | +| **Signed only** | Show only nodes whose signed broadcasts your node has actually heard and verified. Stricter than the icon: a node on 2.8 shows as signed by its firmware version before any signed broadcast arrives, and a contact verified in person on older firmware is not signed at all | +| **Encrypted only** | Show nodes with a matching public key on file, the key an encrypted direct message needs. A node with a key mismatch is excluded | +| **Only show ignored Nodes** | Replace the list with the nodes you have ignored. Every other node is hidden while this is on, and a banner appears at the top of the list to take you back | ### Sort Options @@ -149,6 +157,8 @@ Tap the hop-histogram icon in the node list's app bar to open a bar chart of how Tapping a node opens the detail view with comprehensive information. See [Node Metrics](node-metrics) for full details on metrics and telemetry. +Signal quality is rated against your modem preset. The same SNR can be good on a long-range preset and poor on a faster one. Traceroute and neighbor-info SNR colors use that preset too. RSSI text has its own strength colors; it affects the quality rating only when a noise-floor reading is also available. + The Details card carries the node's short name, role, IDs, last heard time, hops away, uptime, and its SNR and RSSI: ![Node detail section](../../assets/screenshots/nodes_detail_section.png) diff --git a/docs/bg-rBG/user/settings-module-admin.md b/docs/bg-rBG/user/settings-module-admin.md index 46b0b34ba8..104c04f18c 100644 --- a/docs/bg-rBG/user/settings-module-admin.md +++ b/docs/bg-rBG/user/settings-module-admin.md @@ -2,7 +2,7 @@ title: Settings — Modules & Admin parent: Ръководство за потребители nav_order: 8 -last_updated: 2026-08-30 +last_updated: 2026-09-16 description: Configure optional feature modules (MQTT, telemetry, canned messages, TAK, and more) and perform device administration. aliases: - modules @@ -333,7 +333,7 @@ appear only when your own node is selected: ### Относно -**Settings → About** carries the app's own identity rather than the radio's: +**Settings → About** carries the app's own identity rather than the node's: Three sections: @@ -341,7 +341,11 @@ Three sections: - **Apps** — opens with **Need Hardware?**, a rotating carousel of popular devices that links out to where to buy one. It also lists the GitHub repository, the running app version, and **Acknowledgements** (see the next section). -- **Project information** — links to the website and to this documentation. +- **Project information** — links to the website, to this documentation, and to the GNU GPL v3 + license the app is released under. + +The footer states the copyright and that the app is free software with no warranty, which you +may redistribute under the same license. ### Благодарности diff --git a/docs/bg-rBG/user/settings-radio-user.md b/docs/bg-rBG/user/settings-radio-user.md index 9bc75216d8..6d43c41e4e 100644 --- a/docs/bg-rBG/user/settings-radio-user.md +++ b/docs/bg-rBG/user/settings-radio-user.md @@ -2,7 +2,7 @@ title: Settings — Radio & User parent: Ръководство за потребители nav_order: 7 -last_updated: 2026-09-04 +last_updated: 2026-09-11 description: Configure your radio hardware, LoRa presets, user profile, position sharing, power management, and security. aliases: - настройки @@ -51,7 +51,7 @@ The footer appears as soon as you change something. **Discard** throws the chang The status message is saved with the same **Save**, but it never reboots the node — and, like the rest of this screen, it can be edited on a remote node you administer. For your own radio there is a shortcut while it is connected: touch & hold your node in the [node list](nodes.md) and choose -**Update status**. Older firmware and a disconnected radio have no shortcut — the field above is +**Update status**. Older firmware and a disconnected node have no shortcut — the field itself is still the way in. ## Конфигурация @@ -77,13 +77,13 @@ On **Settings → LoRa**. | Настройка | Описание | По подразбиране | | ---------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------- | -| Регион | Regulatory region for frequency bands. You must set this before transmitting | Unset (must configure) | +| Регион | Regulatory region for frequency bands. You must set this before transmitting. On firmware 2.8 or newer, the first region set also creates the node's identity key and gives it a new node number | Unset (must configure) | | Предварително зададени | Speed/range tradeoff | LongFast | | Брой отскоци | Maximum retransmit hops | 3 | | Мощност на предаване | Transmission power (dBm); 0 = max allowed for region | 0 (region max) | | Frequency Override | Overrides the computed operating frequency outright (MHz). It does not offset the calculated value — leave at 0 unless you know you need a specific frequency | 0 (use calculated) | | Използване на предварително зададени настройки | On by default. Turn it off to set Spread Factor, Coding Rate and Bandwidth by hand instead of taking them from the modem preset | On | -| Spread Factor | Manual mode only: 7–12. Higher spreads further but slower | From preset | +| Spread Factor | Manual mode only: 5–12. Higher spreads further but slower. On SX127x (RF95) radios the firmware does not accept 5 or 6 and uses 11 instead | From preset | | Coding Rate | Manual mode only: 5–8. More redundancy costs airtime | From preset | | Широчина на честотната лента | Manual mode only: the channel bandwidth in kHz, typed in directly. On the 2.4 GHz region the app offers a list of the bandwidths your radio supports instead, and a stored value that is not on that list shows as _Unsupported_ and blocks saving until you pick a supported one | From preset | | Честотен слот | Which slot within the region's band to use. 0 derives it from the primary channel name | 0 (automatic) | @@ -111,13 +111,13 @@ The Lite, Narrow, Medium Turbo, and Tiny presets need firmware 2.8 or newer — | Short Slow | ~5 km | 6.25 kbps | −10 dB | Suburban short-range; moderate building density | | Medium Fast | ~5 km | 3.52 kbps | −12.5 dB | Suburban areas; moderate building density | | Medium Slow | ~8 km | 1.95 kbps | −15 dB | Suburban/rural; moderate range with slower speed | -| Long Turbo | ~10 km | 1.34 kbps | −12.5 dB | Similar range to Long Fast but with 500 kHz bandwidth; faster throughput | +| Long Turbo | ~10 km | 1.34 kbps | −17.5 dB | Similar range to Long Fast but with 500 kHz bandwidth; faster throughput | | Long Fast | ~10 km | 1.1 kbps | −17.5 dB | **General use (default)** — balanced range and speed | | Long Moderate | ~20 km | 0.34 kbps | −17.5 dB | Rural with some terrain; occasional use | | Lite Fast | ~5 km | 1.76 kbps | −12.5 dB | EU 866 MHz SRD band (125 kHz BW); comparable to Medium Fast | | Lite Slow | ~10 km | 0.98 kbps | −15 dB | EU 866 MHz SRD band (125 kHz BW); comparable to Long Fast | -| Narrow Fast | ~5 km | 2.28 kbps | −10 dB | EU 868 MHz band (62.5 kHz BW); avoids interference with other devices | -| Narrow Slow | ~10 km | 1.30 kbps | −12.5 dB | EU 868 MHz band (62.5 kHz BW); comparable to Long Fast | +| Narrow Fast | ~5 km | 2.28 kbps | −7.5 dB | EU 868 MHz band (62.5 kHz BW); avoids interference with other devices | +| Narrow Slow | ~10 km | 1.30 kbps | −10 dB | EU 868 MHz band (62.5 kHz BW); comparable to Long Fast | | Medium Turbo | ~5 km | 7.0 kbps | −12.5 dB | Like Medium Fast but with 500 kHz bandwidth; not legal in every region. Needs firmware 2.8 or newer | | Tiny Fast | ~10 km | 0.68 kbps | −7.5 dB | Amateur bands that cap occupied bandwidth; these presets use 15.6 kHz. Needs firmware 2.8 or newer, an SX126x or SX127x radio, and a TCXO of ±5 ppm or better | | Tiny Slow | ~20 km | 0.33 kbps | −10 dB | Same band restrictions as Tiny Fast, longer range. Same firmware, radio, and TCXO requirements | diff --git a/docs/bg-rBG/user/signal-meter.md b/docs/bg-rBG/user/signal-meter.md index 5431ad254a..5493e53a98 100644 --- a/docs/bg-rBG/user/signal-meter.md +++ b/docs/bg-rBG/user/signal-meter.md @@ -2,7 +2,7 @@ title: How the Meshtastic Signal Meter Works parent: Ръководство за потребители nav_order: 15 -last_updated: 2026-08-30 +last_updated: 2026-09-09 description: How the signal meter rates quality from SNR relative to the LoRa modem preset — spread spectrum, presets, and what the bars really mean. aliases: - signal @@ -32,7 +32,7 @@ Each modem preset has an SNR limit: the lowest SNR at which that preset can stil ## Rating Signal Quality -The app rates signal quality (None, Bad, Fair, or Good) from SNR alone, measured against the active preset's SNR limit. It does not factor in RSSI: without knowing the local noise floor, RSSI alone cannot say whether a signal is decodable. RSSI is still available — on the node detail screen and in the metrics charts. +The app rates signal quality (None, Bad, Fair, or Good) from SNR relative to the active preset's SNR limit. When RSSI and a noise-floor reading are both available, it also rates the difference between them against that limit and uses the worse rating. Otherwise, it uses SNR alone. Because the rating is relative to the preset, the same SNR rates differently on different presets. An SNR of −16 dB rates Good on Long Fast (SNR limit −17.5 dB) but None on Short Fast (SNR limit −7.5 dB). Let `limit` be the active preset's SNR limit: @@ -45,7 +45,7 @@ Because the rating is relative to the preset, the same SNR rates differently on The icon never goes blank, so count the bars carefully: a single bar means None, not a weak but usable link, and Good is a solid wedge rather than a set of bars. A gray three-bar icon labeled Unknown is a different state again — the packet carried no SNR measurement at all, which is not the same as measuring one and finding it too weak. -> ℹ️ **Note:** Traceroute hop colors use fixed thresholds (−7 dB / −15 dB); the per-node signal meter uses the preset-relative rating instead. +> ℹ️ **Note:** Traceroute and neighbor-info SNR colors use the active preset's limits too. RSSI text has separate strength colors: green above −115 dBm, yellow above −120 dBm, orange above −126 dBm, and red at or below −126 dBm. ## Diagnosing Local Interference diff --git a/docs/bg-rBG/user/tak.md b/docs/bg-rBG/user/tak.md index 6e98be7156..ad5b4c9cc7 100644 --- a/docs/bg-rBG/user/tak.md +++ b/docs/bg-rBG/user/tak.md @@ -2,7 +2,7 @@ title: TAK Integration parent: Ръководство за потребители nav_order: 10 -last_updated: 2026-08-30 +last_updated: 2026-09-11 description: Interoperate with ATAK and WinTAK — CoT position sharing, TAK roles, and plugin setup. aliases: - tak @@ -36,7 +36,7 @@ The TAK module allows Meshtastic nodes to: > ⚠️ **Warning:** The old **Meshtastic ATAK Plugin** is no longer part of this path and cannot > work. It bridged through the cross-process AIDL API, which was removed in app 2.8.0; the mesh > service is now in-process only. Do not install it. Interop today runs over the app's own local -> TAK server plus the Mesh to CoT Converter, both described below, with stock ATAK/iTAK/WinTAK. +> TAK server plus the Mesh to CoT Converter, both described in the following sections, with stock ATAK/iTAK/WinTAK. ### Конфигурация diff --git a/docs/bg-rBG/user/telemetry-and-sensors.md b/docs/bg-rBG/user/telemetry-and-sensors.md index 5645ebddf7..7f11915889 100644 --- a/docs/bg-rBG/user/telemetry-and-sensors.md +++ b/docs/bg-rBG/user/telemetry-and-sensors.md @@ -5,9 +5,9 @@ nav_order: 9 last_updated: 2026-08-30 description: Sensor data on the mesh — supported environment, air quality, and power sensors, plus configuration and viewing guides. aliases: - - сензори - - околна среда - - времето + - sensors + - environment + - weather - power-metrics --- @@ -49,12 +49,12 @@ All Meshtastic nodes report basic device telemetry: | PMSA003I | PM1.0, PM2.5, PM10 | See [Air Quality Metrics](#air-quality-metrics) | | SEN55 | PM, Temp, Humidity | Multi-sensor. Its NOx and VOC indices are recorded and included in a CSV export, but are not shown as cards or charts | -### Soil +### Почва -| Метрични | Unit | Бележки | -| ---------- | ------- | ----------------------------------------------- | -| Soil Temp | °C / °F | Reported alongside soil moisture by soil probes | -| Soil Moist | % | Volumetric water content | +| Метрични | Единица | Бележки | +| -------------------------------- | ------- | ----------------------------------------------- | +| Темп. на почвата | °C / °F | Reported alongside soil moisture by soil probes | +| Soil Moist | % | Volumetric water content | Both appear as info cards on the node detail screen, next to the other environment readings. diff --git a/docs/bg-rBG/user/translate.md b/docs/bg-rBG/user/translate.md index 7a8aa911f4..6e50dc4b6b 100644 --- a/docs/bg-rBG/user/translate.md +++ b/docs/bg-rBG/user/translate.md @@ -1,18 +1,18 @@ --- -title: Translate the App -parent: User Guide +title: Преведете приложението +parent: Ръководство за потребителя nav_order: 17 -last_updated: 2026-08-30 -description: How the app and its documentation are translated via Crowdin, and guidelines for contributing translations. +last_updated: 2026-09-11 +description: Как се превеждат приложението и документацията му чрез Crowdin, както и насоки за принос към преводите. aliases: - translate - crowdin - localization --- -# Translate the App +# Преведете приложението -The app and its in-app docs are translated on Crowdin — this page shows how to contribute. The app uses [Crowdin](https://crowdin.com/) to manage community translations for both the user interface and in-app documentation. +Приложението и придружаващата го документация се превеждат в Crowdin – на тази страница е обяснено как да се включите. Приложението използва [Crowdin](https://crowdin.com/) за управление на преводите, извършвани от общността, както за потребителския интерфейс, така и за документацията в самото приложение. ## What Gets Translated @@ -33,19 +33,19 @@ The app and its in-app docs are translated on Crowdin — this page shows how to > 💡 **Tip:** Keep translations short. UI strings often appear in buttons, chips, or narrow columns. If a translation is significantly longer than the English original, consider abbreviating where the meaning stays clear. -## Adding a New Language +## Добавяне на нов език -If your language is not yet listed on Crowdin: +Ако езикът ви все още не е включен в списъка в Crowdin: -1. Open an issue on [GitHub](https://github.com/meshtastic/Meshtastic-Android/issues/new) requesting the new locale. -2. A maintainer will add the language to Crowdin and configure `crowdin.yml`. -3. Once added, you can begin translating immediately. +1. Отворете проблем в [GitHub](https://github.com/meshtastic/Meshtastic-Android/issues/new), като поискате новия език. +2. Поддържащият ще добави езика в Crowdin и ще конфигурира `crowdin.yml`. +3. След като бъде добавен, можете да започнете да превеждате веднага. -## How Translations Are Organized +## Как са организирани преводите The Android app uses **Compose Multiplatform resources** for all user-visible strings: -``` +```text core/resources/src/commonMain/composeResources/ ├── values/ ← English (default) │ └── strings.xml @@ -58,7 +58,7 @@ core/resources/src/commonMain/composeResources/ In-app documentation follows a similar pattern under `docs/`: -``` +```text docs/ ├── en/user/ ← English source (default) │ ├── onboarding.md diff --git a/docs/ca-rES/user/desktop.md b/docs/ca-rES/user/desktop.md index efca3c03c9..2ec0d98037 100644 --- a/docs/ca-rES/user/desktop.md +++ b/docs/ca-rES/user/desktop.md @@ -2,7 +2,7 @@ title: Desktop App parent: User Guide nav_order: 14 -last_updated: 2026-08-30 +last_updated: 2026-09-11 description: Install and use the Meshtastic Desktop app on Linux, macOS, and Windows — connections, feature parity, and keyboard shortcuts. aliases: - desktop @@ -58,20 +58,20 @@ Bluetooth Low Energy is supported on desktop via the [Kable](https://github.com/ ## Feature Parity -| Feature | Android | Desktop | Notes | -| ----------------------------------------------------- | ------- | ------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Messaging | ✓ | ✓ | Full parity | -| Node List | ✓ | ✓ | Full parity | -| Map | ✓ | ✓ | Interactive MapLibre map, with base map and overlay pickers and custom tile sources. No offline downloads or local `.mbtiles` archives | -| Map layers (`.kml`/`.kmz`/GeoJSON) | ✓ | ✓ | Same layer store and sheet as Android; imported files draw on the desktop map | -| Site Planner | ✓ | ✓\* | \*Opens in your browser on desktop; the estimate is not drawn on the desktop map | -| Settings | ✓ | ✓ | Full parity | -| Bluetooth (BLE) | ✓ | ✓ | Via Kable on desktop | -| Firmware Update | ✓ | ✓ | In-app USB, BLE, and Wi-Fi (ESP32) update work the same as Android. The USB maintenance flow — nRF52/RP2040 factory erase and bootloader upgrade — is Android-only | -| Notifications | ✓ | ✓ | Native OS notifications | -| Widgets | ✓ | ✗ | Android-only | -| AI Assistant (Chirpy) | ✓\* | ✗ | Google flavor Android only | -| App Functions (system AI) | ✓† | ✗ | Google flavor Android only | +| Feature | Android | Desktop | Notes | +| ----------------------------------------------------- | ------- | ------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Messaging | ✓ | ✓ | Full parity | +| Node List | ✓ | ✓ | Full parity | +| Map | ✓ | ✓ | Interactive MapLibre map, with base map and overlay pickers and custom tile sources. Offline terrain (hillshade and contours) can be downloaded; offline base-map packs and local `.mbtiles` archives cannot | +| Map layers (`.kml`/`.kmz`/GeoJSON) | ✓ | ✓ | Same layer store and sheet as Android; imported files draw on the desktop map | +| Site Planner | ✓ | ✓\* | \*Opens in your browser on desktop; the estimate is not drawn on the desktop map | +| Settings | ✓ | ✓ | Full parity | +| Bluetooth (BLE) | ✓ | ✓ | Via Kable on desktop | +| Firmware Update | ✓ | ✓ | In-app USB, BLE, and Wi-Fi (ESP32) update work the same as Android. The USB maintenance flow — nRF52/RP2040 factory erase and bootloader upgrade — is Android-only | +| Notifications | ✓ | ✓ | Native OS notifications | +| Widgets | ✓ | ✗ | Android-only | +| AI Assistant (Chirpy) | ✓\* | ✗ | Google flavor Android only | +| App Functions (system AI) | ✓† | ✗ | Google flavor Android only | \*Chirpy AI requires Android 14+ on Google flavor builds with supported hardware. @@ -122,7 +122,7 @@ Individual doc pages render with full formatting: ## Building from Source -```bash +```shell git clone https://github.com/meshtastic/Meshtastic-Android.git cd Meshtastic-Android ./gradlew :desktopApp:run @@ -135,7 +135,7 @@ Requirements: ## Known Limitations -- Offline tile downloads and local `.mbtiles` archives are not available on desktop. +- Offline base-map downloads and local `.mbtiles` archives are not available on desktop. Offline terrain is — see [Map & Waypoints](map-and-waypoints). - `.kml`/`.kmz`/GeoJSON layer import works — see [Map & Waypoints](map-and-waypoints#map-layers). Site Planner opens in your browser rather than in the app; to bring its coverage estimate onto the map, click the transmitter pin diff --git a/docs/ca-rES/user/discovery.md b/docs/ca-rES/user/discovery.md index 3ad0b05742..8d9f02a670 100644 --- a/docs/ca-rES/user/discovery.md +++ b/docs/ca-rES/user/discovery.md @@ -2,7 +2,7 @@ title: Local Mesh Discovery parent: User Guide nav_order: 12 -last_updated: 2026-08-30 +last_updated: 2026-09-11 description: Explore your mesh network — the Local Mesh Discovery scanner, traceroute paths, neighbor maps, and node discovery tools. aliases: - discovery @@ -97,7 +97,7 @@ Mesh Beacon lets nodes invite others to join their mesh. A beaconing node period Configure it under **Settings → Module configuration → Mesh Beacon**. The entry appears only on radios running firmware 2.8.0 or newer. A read-only **Region** row at the top of the screen shows the region the beacon advertises: that region, and the preset, are always the ones the radio itself uses, so a beacon cannot invite anyone onto settings your radio is not running. - **Listen for beacons** — receive invitations broadcast by other nodes. -- **Broadcast a beacon** — periodically advertise this mesh to nearby nodes, with an optional **Beacon message** of up to 100 bytes, a **Broadcast interval** picked from fixed intervals between 1 hour and 72 hours, and an **Offered channel** chosen from your radio's own channels. The offered channel is required, and defaults to your primary channel. +- **Broadcast a beacon** — periodically advertise this mesh to nearby nodes, with an optional **Beacon message** of up to 100 bytes, a **Broadcast interval** picked from fixed intervals between 1 hour and 72 hours, and an **Offered channel** chosen from your radio's own channels. The offered channel is required, and defaults to your primary channel. Over remote admin the picker offers the primary channel only. - **Broadcast targets** — optional extra destinations beyond the offered channel. **Add target** appends a row; each row picks a **Channel** and a **Transmit preset**, and **Remove target** deletes it. With no targets, the beacon goes out on the offered channel alone. Two conditions block beacon setup: diff --git a/docs/ca-rES/user/help-and-docs.md b/docs/ca-rES/user/help-and-docs.md index f306cde7f7..c83898cea3 100644 --- a/docs/ca-rES/user/help-and-docs.md +++ b/docs/ca-rES/user/help-and-docs.md @@ -2,7 +2,7 @@ title: Help & In-App Docs parent: User Guide nav_order: 21 -last_updated: 2026-08-30 +last_updated: 2026-09-11 description: Browse this documentation inside the app, search it, and ask Chirpy — the on-device AI assistant — questions about Meshtastic. aliases: - help @@ -23,7 +23,7 @@ The docs browser lists every user-guide page. Tap a page to read it; images and ### Search -The search box sits at the top of the docs browser. Type in it to filter pages by title and keywords — results update as you type, and the **Clear search** button (✕) empties the box. +Tap the search field at the top of the docs browser to open a full-screen search view. Type to filter pages by title and keywords — results update as you type, under the same field. The back arrow, or the keyboard's search action, closes the view. The field hides while you scroll down the page list and comes back when you scroll up. ![Searching the in-app documentation](../../assets/screenshots/docs-browser_search.png) diff --git a/docs/ca-rES/user/map-and-waypoints.md b/docs/ca-rES/user/map-and-waypoints.md index 72bebd3f91..a9502d1950 100644 --- a/docs/ca-rES/user/map-and-waypoints.md +++ b/docs/ca-rES/user/map-and-waypoints.md @@ -2,7 +2,7 @@ title: Map & Waypoints parent: User Guide nav_order: 6 -last_updated: 2026-09-01 +last_updated: 2026-09-11 description: View node positions on the map, create and share waypoints, manage map layers and Site Planner, and control position sharing and privacy. aliases: - map @@ -44,7 +44,7 @@ The floating toolbar provides quick access to the compass, the map type and laye ### Filtering the Map -Tap the filter button in the floating toolbar to open **Filter map**. **Display** controls what is drawn: **Only Favorites**, **Show Waypoints**, **Show Precision Circles**, and a slider that hides nodes not heard from recently. **Node roles** is a chip per device role, plus **All** to show every role; a selected chip means that role is shown. **Nodes** narrows the set further with **Hide offline nodes**, **Only show direct nodes**, **Exclude MQTT**, **Show ignored nodes**, and **Include unknown**. +Tap the filter button in the floating toolbar to open **Filter map**. **Display** controls what is drawn: **Only Favorites**, **Show Waypoints**, **Show Precision Circles**, and a slider that hides nodes not heard from recently. **Node roles** is a chip per device role, plus **All** to show every role; a selected chip means that role is shown. **Nodes** narrows the set further with **Hide offline nodes**, **Only show direct nodes**, **Signed only**, **Encrypted only**, **Exclude MQTT**, **Show ignored nodes**, and **Include unknown** — the same rules as the node list's [filter toggles](nodes). A dot on the filter button means at least one filter is hiding something — check it before concluding the mesh is quiet. Turning **Show Waypoints** off hides every waypoint, including your own. **Show ignored nodes** adds them to the map rather than showing only them — unlike the node list's **Only show ignored Nodes**. @@ -147,8 +147,9 @@ picker, all three offer the same raster base maps: Overlays can be toggled on top of any base map, from the layers sheet: - **Weather radar** — NOAA NEXRAD reflectivity (US coverage) -- **Hillshade** — terrain relief, on **F-Droid** and **Desktop** only. Useful for understanding why a - link fails, since LoRa range is limited by terrain +- **Hillshade** — terrain relief from an online tile source, on **F-Droid** and **Desktop** only; Google + Play draws hillshade from downloaded offline terrain instead. Useful for understanding why a link + fails, since LoRa range is limited by terrain ### Adding your own tile source @@ -156,7 +157,7 @@ Any XYZ tile endpoint can be added as a base map, on every flavor and on desktop Tile Sources** at the foot of the base map picker and paste a URL template using `{z}`, `{x}` and `{y}` — plus `{s}` if the provider uses rotating subdomains. A national mapping service, for example: -``` +```text https://wmts.geo.admin.ch/1.0.0/ch.swisstopo.pixelkarte-farbe/default/current/3857/{z}/{x}/{y}.jpeg ``` @@ -173,7 +174,18 @@ On **Google Play**, the layers sheet's **Offline Manager** section downloads its — water, roads and administrative boundaries extracted from the public Protomaps basemap dataset, drawn directly as map shapes rather than raster tiles. Frame the area you want, tap **Start Download**, and once it finishes flip **Show on map** to draw it (it's off by default so it never surprises you by covering up the -live map). You can still import a pre-made `.mbtiles` archive here too, as before. **Desktop** has neither. +live map). You can still import a pre-made `.mbtiles` archive here too, as before. **Desktop** has neither, though +the offline terrain described next does work there. + +### Offline terrain + +Hillshade and elevation contours can be downloaded for offline use as well, so a downloaded area still +shows why a link fails when there is no network. The terrain data comes from Mapterhorn and is credited on +the map. On **Google Play**, each downloaded region in the **Offline Manager** list has a **Download Terrain** +button and **Hillshade** and **Contours** switches; terrain attaches to that region and shares its storage limit, and an area +too large for terrain asks you to zoom in or pick a smaller region. On **F-Droid** and **Desktop**, the +layers sheet has an **Offline Terrain** section of its own: frame the area, tap **Start Download**, and the +hillshade and contours draw from the downloaded tiles from then on. ### Going offline @@ -181,8 +193,8 @@ An **Offline** pill appears over the map whenever the device has no network conn builds, if you've imported a local `.mbtiles` archive, the map switches to it automatically the moment the network drops — no toggle to remember — and switches back once you're reconnected, as long as you haven't picked a different base map yourself in between. On **F-Droid**, a downloaded offline area keeps rendering on -its own; the pill is purely informational there. **Desktop** has no offline downloads at all yet, so the pill -is informational there too. +its own; the pill is purely informational there. **Desktop** downloads offline terrain but no base-map +packs, so the pill is informational there too. ## Related Topics diff --git a/docs/ca-rES/user/messages-and-channels.md b/docs/ca-rES/user/messages-and-channels.md index 0c773b5736..8a300c40d9 100644 --- a/docs/ca-rES/user/messages-and-channels.md +++ b/docs/ca-rES/user/messages-and-channels.md @@ -2,7 +2,7 @@ title: Messages & Channels parent: User Guide nav_order: 3 -last_updated: 2026-08-30 +last_updated: 2026-09-14 description: Send and receive messages, manage channels, configure encryption, search conversations, and use quick chat, reactions, and message actions. aliases: - channels @@ -50,6 +50,21 @@ Key length alone does not change the icon: a 128-bit key and a 256-bit key both Tapping an existing channel opens the same editor, where you can change the name, the PSK, MQTT uplink and downlink, and position precision. Every edit on this screen — adding, editing, deleting, or dragging a channel into a new order — waits on **Send** the same way. +### Archived Channels + +A conversation belongs to the channel it was held on, not to the slot that channel occupied. When a channel leaves your radio — you scan a different channel QR, replace a channel's key, or switch the modem preset — its conversation is **archived** rather than deleted or merged into whatever takes the slot. + +An archived conversation: + +- stays in the **Channels** list under the name the channel had, below the channels currently on your radio, marked with a history icon; +- keeps its full message history, searchable as usual; +- is read-only — you cannot send, reply, or react, because the channel is no longer on the radio to send on, and it is not offered as a share target; +- comes back automatically if you re-add that exact channel, taking its messages back to the live slot. + +Changing the modem preset archives the channel too: the preset is part of a default channel's identity, so **LongFast** and **MediumFast** are different channels even though neither the name nor the key changed. Changing only the region does not archive anything. + +To remove an archived conversation for good, long-press it in the conversation list and delete it. + ## Direct Messages Direct messages (DMs) go to one specific node. When both radios hold each other's public keys, your radio encrypts the message to that node's public key, so no one else on the mesh can read it — not even nodes that share your channel. diff --git a/docs/ca-rES/user/mqtt.md b/docs/ca-rES/user/mqtt.md index bf9cb1724d..80953fa2e4 100644 --- a/docs/ca-rES/user/mqtt.md +++ b/docs/ca-rES/user/mqtt.md @@ -2,7 +2,7 @@ title: MQTT parent: User Guide nav_order: 11 -last_updated: 2026-08-30 +last_updated: 2026-09-11 description: Bridge your mesh to the internet — MQTT broker setup, encryption layers, and map reporting. aliases: - mqtt @@ -25,7 +25,7 @@ The MQTT module connects your node to an MQTT broker, allowing: ## How It Works -``` +```text [Your Node] → Radio → [Gateway Node with Wi-Fi] → MQTT Broker → [Remote Gateway] → Radio → [Remote Node] ``` diff --git a/docs/ca-rES/user/node-metrics.md b/docs/ca-rES/user/node-metrics.md index 5cc1941bcc..9dd4fbee89 100644 --- a/docs/ca-rES/user/node-metrics.md +++ b/docs/ca-rES/user/node-metrics.md @@ -2,7 +2,7 @@ title: Node Metrics parent: User Guide nav_order: 5 -last_updated: 2026-08-30 +last_updated: 2026-09-09 description: Telemetry dashboards for each mesh node — device health, environment sensors, air quality, signal quality, power, traceroute, and position history. aliases: - metrics @@ -114,7 +114,7 @@ Radio signal quality information: ### Signal Quality Reference -Signal quality is rated from **SNR relative to the active LoRa modem preset's demodulation floor**, not from fixed thresholds — a given SNR means different things on different presets (e.g. −15 dB is fine on LongSlow but unusable on ShortFast). RSSI is shown but is not part of the rating. In the table, _limit_ is the preset's SNR limit. +Signal quality is rated from **SNR relative to the active LoRa modem preset's demodulation floor**, not from fixed thresholds — a given SNR means different things on different presets (e.g. −15 dB is fine on LongSlow but unusable on ShortFast). When RSSI and a noise-floor reading are both available, the app also rates their difference against the preset limit and uses the worse rating. Otherwise, RSSI is display-only. In the table, _limit_ is the preset's SNR limit. | Quality | Criteria | | ------- | ---------------------------------------------------------------- | diff --git a/docs/ca-rES/user/nodes.md b/docs/ca-rES/user/nodes.md index 9bf22fe38e..865f413ef8 100644 --- a/docs/ca-rES/user/nodes.md +++ b/docs/ca-rES/user/nodes.md @@ -2,7 +2,7 @@ title: Nodes parent: User Guide nav_order: 4 -last_updated: 2026-09-04 +last_updated: 2026-09-11 description: Browse, filter, and sort mesh nodes — view details, signal quality, roles, and quick actions. aliases: - node-list @@ -74,19 +74,23 @@ Most users should keep the default **Client** role. Consider a different role wh > 💡 **Tip:** The mesh works best when most nodes are **Client** or **Router**. Too many Client Mute nodes reduce mesh resilience; too many Routers in a dense area can cause congestion. A good rule of thumb: one Router per 5–10 Clients in your area. -### Encryption Indicators +### Security indicators -Nodes display encryption status icons next to their name: +Each node carries one security icon beside its name in the node list. Tap it to read what it means, and choose **Show All Meanings** in that dialog for the full legend. The detail screen shows the same state in words, as a **Security** row that opens the same dialog. -| Icon | Meaning | -| ----------- | ------------------------------------------------------------------------------------------------------------------------------------------------- | -| 🔒 Locked | Communication uses PKI (public key infrastructure) — end-to-end encrypted with verified identity | -| 🔓 Unlocked | No public key has been received for this node, so it cannot be direct messaged — use **Request User Info** on the node detail page to ask for one | -| ⚠️ Mismatch | Public key mismatch — the node's key has changed since last seen (investigate before trusting) | +| Icon | Meaning | Shown for | +| --------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------- | +| Person with a shield check (green) | **Verified contact** — you verified this node's key in person by exchanging contact QR codes, so its identity is confirmed. The strongest trust the list shows. Your own connected node carries it too | Any firmware version | +| Nodes icon with a shield check (green) | **Signed node** — this node signs its broadcasts with its identity key, so its identity is consistent over time, but you have not verified it in person. On firmware 2.8 the icon appears from the version alone, before any signed broadcast has been heard | Firmware 2.8 or newer, and any node whose signed broadcast your node has verified | +| 🔒 Closed lock | A public key is on file and matches, so direct messages to this node are encrypted | Firmware before 2.8, or no reported version | +| 🔓 Open lock | No public key has been received for this node, so it cannot be direct messaged — use **Request User Info** on the node detail page to ask for one | Firmware before 2.8, or no reported version | +| ⚠️ Mismatch | **Public key mismatch** — a different key arrived for this node after one was stored. Investigate before trusting | Any firmware version | -> 💡 **Tip:** Direct messages always use PKI, so the radio needs the other node's public key before it can send one. It refuses the send rather than falling back to channel encryption. Keys arrive inside node info, which is why an open lock usually clears itself once that node is heard from properly. If you see a key mismatch warning, the node may have been reset or compromised. +Every node on firmware 2.8 or newer signs its broadcasts, so on that firmware the signed state is the baseline and the locks are not shown. -To clear a mismatch, first confirm through another trusted channel that the key change was intentional — a factory reset causes one. Then touch & hold the node, choose **Remove**, and let the two radios exchange keys again the next time yours hears it. +Direct messages always use public-key encryption, so your node needs the other node's public key before it can send one. It refuses the send rather than falling back to channel encryption. Keys arrive inside node info, which is why an open lock usually clears itself once that node is heard from properly. + +A mismatch never replaces the key you already hold. The app keeps the first key it recorded and refuses the new one, as the firmware does, so a stray or hostile node info cannot silently break encrypted messaging to a contact. To clear a mismatch, first confirm through another trusted channel that the key change was intentional — a factory reset causes one. Then touch & hold the node, choose **Remove**, and let the two nodes exchange keys again the next time yours hears it. ## Quick Actions @@ -108,7 +112,9 @@ radio is connected and running firmware 2.8 or newer — see ## Sharing a Contact -On a node's detail screen, tap **Share Contact** to produce a link and a QR code for that node. From the same dialog, **Write to NFC tag** saves the link to a writable NFC tag that anyone can tap to open. +On a node's detail screen, tap **Share Contact** to produce a link and a QR code for that node. From the same dialog, **Share link** opens the Android share sheet (on desktop it copies the link instead), **Write to NFC tag** saves it to a writable NFC tag, and **Copy** puts it on the clipboard. While that dialog is open and in front of you, the phone also offers the same link to any NFC reader, so someone can take the contact by tapping their phone against yours with no tag involved. + +Sharing your own contact this way marks it as verified in person, so whoever imports it sees the **Verified contact** icon rather than the signed one. Relaying someone else's contact passes on only what your app had already recorded about them. To add someone else's contact, use the import button on the node list and choose **Scan Shared Contact QR Code**, **Scan Shared Contact NFC**, or **Input Shared Contact URL**. The app asks you to confirm with **Import Shared Contact?**, and warns you when the contact is one you already have. @@ -120,14 +126,16 @@ Type in the search field to filter nodes by name or short name. The filter updat ### Filter Toggles -| Filtre | Descripció | -| --------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| **Hide offline nodes** | Show only nodes heard within the last 2 hours | -| **Only show direct nodes** | Show only nodes your radio heard directly, with no relay in between | -| **Include unknown** | Show nodes that haven't sent user info yet. **On by default**, so a node heard before its info arrives stays visible; these carry a badge marking them incomplete, and cannot be direct messaged until their user info brings a public key | -| **Exclude infrastructure** | Hide infrastructure-role nodes (Router, Router Late, Client Base, and legacy Repeater nodes) and any node that cannot be messaged, whatever its role | -| **Exclude MQTT** | Hide nodes heard only via MQTT internet bridge | -| **Only show ignored Nodes** | Replace the list with the nodes you have ignored. Every other node is hidden while this is on, and a banner appears at the top of the list to take you back | +| Filtre | Descripció | +| --------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **Hide offline nodes** | Show only nodes heard within the last 2 hours | +| **Only show direct nodes** | Show only nodes your radio heard directly, with no relay in between | +| **Include unknown** | Show nodes that haven't sent user info yet. **On by default**, so a node heard before its info arrives stays visible; these carry a badge marking them incomplete, and cannot be direct messaged until their user info brings a public key | +| **Exclude infrastructure** | Hide infrastructure-role nodes (Router, Router Late, Client Base, and legacy Repeater nodes) and any node that cannot be messaged, whatever its role | +| **Exclude MQTT** | Hide nodes heard only via MQTT internet bridge | +| **Signed only** | Show only nodes whose signed broadcasts your node has actually heard and verified. Stricter than the icon: a node on 2.8 shows as signed by its firmware version before any signed broadcast arrives, and a contact verified in person on older firmware is not signed at all | +| **Encrypted only** | Show nodes with a matching public key on file, the key an encrypted direct message needs. A node with a key mismatch is excluded | +| **Only show ignored Nodes** | Replace the list with the nodes you have ignored. Every other node is hidden while this is on, and a banner appears at the top of the list to take you back | ### Sort Options @@ -149,6 +157,8 @@ Tap the hop-histogram icon in the node list's app bar to open a bar chart of how Tapping a node opens the detail view with comprehensive information. See [Node Metrics](node-metrics) for full details on metrics and telemetry. +Signal quality is rated against your modem preset. The same SNR can be good on a long-range preset and poor on a faster one. Traceroute and neighbor-info SNR colors use that preset too. RSSI text has its own strength colors; it affects the quality rating only when a noise-floor reading is also available. + The Details card carries the node's short name, role, IDs, last heard time, hops away, uptime, and its SNR and RSSI: ![Node detail section](../../assets/screenshots/nodes_detail_section.png) diff --git a/docs/ca-rES/user/settings-module-admin.md b/docs/ca-rES/user/settings-module-admin.md index 73da98637b..4675093bf1 100644 --- a/docs/ca-rES/user/settings-module-admin.md +++ b/docs/ca-rES/user/settings-module-admin.md @@ -2,7 +2,7 @@ title: Settings — Modules & Admin parent: User Guide nav_order: 8 -last_updated: 2026-08-30 +last_updated: 2026-09-16 description: Configure optional feature modules (MQTT, telemetry, canned messages, TAK, and more) and perform device administration. aliases: - modules @@ -333,7 +333,7 @@ appear only when your own node is selected: ### Sobre -**Settings → About** carries the app's own identity rather than the radio's: +**Settings → About** carries the app's own identity rather than the node's: Three sections: @@ -341,7 +341,11 @@ Three sections: - **Apps** — opens with **Need Hardware?**, a rotating carousel of popular devices that links out to where to buy one. It also lists the GitHub repository, the running app version, and **Acknowledgements** (see the next section). -- **Project information** — links to the website and to this documentation. +- **Project information** — links to the website, to this documentation, and to the GNU GPL v3 + license the app is released under. + +The footer states the copyright and that the app is free software with no warranty, which you +may redistribute under the same license. ### Acknowledgements diff --git a/docs/ca-rES/user/settings-radio-user.md b/docs/ca-rES/user/settings-radio-user.md index a6311a6f4c..0346a61695 100644 --- a/docs/ca-rES/user/settings-radio-user.md +++ b/docs/ca-rES/user/settings-radio-user.md @@ -2,7 +2,7 @@ title: Settings — Radio & User parent: User Guide nav_order: 7 -last_updated: 2026-09-04 +last_updated: 2026-09-11 description: Configure your radio hardware, LoRa presets, user profile, position sharing, power management, and security. aliases: - settings @@ -51,7 +51,7 @@ The footer appears as soon as you change something. **Discard** throws the chang The status message is saved with the same **Save**, but it never reboots the node — and, like the rest of this screen, it can be edited on a remote node you administer. For your own radio there is a shortcut while it is connected: touch & hold your node in the [node list](nodes.md) and choose -**Update status**. Older firmware and a disconnected radio have no shortcut — the field above is +**Update status**. Older firmware and a disconnected node have no shortcut — the field itself is still the way in. ## Configuration @@ -77,13 +77,13 @@ On **Settings → LoRa**. | Setting | Descripció | Default | | ------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------- | -| Regió | Regulatory region for frequency bands. You must set this before transmitting | Unset (must configure) | +| Regió | Regulatory region for frequency bands. You must set this before transmitting. On firmware 2.8 or newer, the first region set also creates the node's identity key and gives it a new node number | Unset (must configure) | | Presets | Speed/range tradeoff | LongFast | | Number of Hops | Maximum retransmit hops | 3 | | Transmit Power | Transmission power (dBm); 0 = max allowed for region | 0 (region max) | | Frequency Override | Overrides the computed operating frequency outright (MHz). It does not offset the calculated value — leave at 0 unless you know you need a specific frequency | 0 (use calculated) | | Use Preset | On by default. Turn it off to set Spread Factor, Coding Rate and Bandwidth by hand instead of taking them from the modem preset | On | -| Spread Factor | Manual mode only: 7–12. Higher spreads further but slower | From preset | +| Spread Factor | Manual mode only: 5–12. Higher spreads further but slower. On SX127x (RF95) radios the firmware does not accept 5 or 6 and uses 11 instead | From preset | | Coding Rate | Manual mode only: 5–8. More redundancy costs airtime | From preset | | Bandwidth | Manual mode only: the channel bandwidth in kHz, typed in directly. On the 2.4 GHz region the app offers a list of the bandwidths your radio supports instead, and a stored value that is not on that list shows as _Unsupported_ and blocks saving until you pick a supported one | From preset | | Frequency Slot | Which slot within the region's band to use. 0 derives it from the primary channel name | 0 (automatic) | @@ -111,13 +111,13 @@ The Lite, Narrow, Medium Turbo, and Tiny presets need firmware 2.8 or newer — | Short Slow | ~5 km | 6.25 kbps | −10 dB | Suburban short-range; moderate building density | | Medium Fast | ~5 km | 3.52 kbps | −12.5 dB | Suburban areas; moderate building density | | Medium Slow | ~8 km | 1.95 kbps | −15 dB | Suburban/rural; moderate range with slower speed | -| Long Turbo | ~10 km | 1.34 kbps | −12.5 dB | Similar range to Long Fast but with 500 kHz bandwidth; faster throughput | +| Long Turbo | ~10 km | 1.34 kbps | −17.5 dB | Similar range to Long Fast but with 500 kHz bandwidth; faster throughput | | Long Fast | ~10 km | 1.1 kbps | −17.5 dB | **General use (default)** — balanced range and speed | | Long Moderate | ~20 km | 0.34 kbps | −17.5 dB | Rural with some terrain; occasional use | | Lite Fast | ~5 km | 1.76 kbps | −12.5 dB | EU 866 MHz SRD band (125 kHz BW); comparable to Medium Fast | | Lite Slow | ~10 km | 0.98 kbps | −15 dB | EU 866 MHz SRD band (125 kHz BW); comparable to Long Fast | -| Narrow Fast | ~5 km | 2.28 kbps | −10 dB | EU 868 MHz band (62.5 kHz BW); avoids interference with other devices | -| Narrow Slow | ~10 km | 1.30 kbps | −12.5 dB | EU 868 MHz band (62.5 kHz BW); comparable to Long Fast | +| Narrow Fast | ~5 km | 2.28 kbps | −7.5 dB | EU 868 MHz band (62.5 kHz BW); avoids interference with other devices | +| Narrow Slow | ~10 km | 1.30 kbps | −10 dB | EU 868 MHz band (62.5 kHz BW); comparable to Long Fast | | Medium Turbo | ~5 km | 7.0 kbps | −12.5 dB | Like Medium Fast but with 500 kHz bandwidth; not legal in every region. Needs firmware 2.8 or newer | | Tiny Fast | ~10 km | 0.68 kbps | −7.5 dB | Amateur bands that cap occupied bandwidth; these presets use 15.6 kHz. Needs firmware 2.8 or newer, an SX126x or SX127x radio, and a TCXO of ±5 ppm or better | | Tiny Slow | ~20 km | 0.33 kbps | −10 dB | Same band restrictions as Tiny Fast, longer range. Same firmware, radio, and TCXO requirements | diff --git a/docs/ca-rES/user/signal-meter.md b/docs/ca-rES/user/signal-meter.md index c4e7c190bd..12d6aaade0 100644 --- a/docs/ca-rES/user/signal-meter.md +++ b/docs/ca-rES/user/signal-meter.md @@ -2,7 +2,7 @@ title: How the Meshtastic Signal Meter Works parent: User Guide nav_order: 15 -last_updated: 2026-08-30 +last_updated: 2026-09-09 description: How the signal meter rates quality from SNR relative to the LoRa modem preset — spread spectrum, presets, and what the bars really mean. aliases: - signal @@ -32,7 +32,7 @@ Each modem preset has an SNR limit: the lowest SNR at which that preset can stil ## Rating Signal Quality -The app rates signal quality (None, Bad, Fair, or Good) from SNR alone, measured against the active preset's SNR limit. It does not factor in RSSI: without knowing the local noise floor, RSSI alone cannot say whether a signal is decodable. RSSI is still available — on the node detail screen and in the metrics charts. +The app rates signal quality (None, Bad, Fair, or Good) from SNR relative to the active preset's SNR limit. When RSSI and a noise-floor reading are both available, it also rates the difference between them against that limit and uses the worse rating. Otherwise, it uses SNR alone. Because the rating is relative to the preset, the same SNR rates differently on different presets. An SNR of −16 dB rates Good on Long Fast (SNR limit −17.5 dB) but None on Short Fast (SNR limit −7.5 dB). Let `limit` be the active preset's SNR limit: @@ -45,7 +45,7 @@ Because the rating is relative to the preset, the same SNR rates differently on The icon never goes blank, so count the bars carefully: a single bar means None, not a weak but usable link, and Good is a solid wedge rather than a set of bars. A gray three-bar icon labeled Unknown is a different state again — the packet carried no SNR measurement at all, which is not the same as measuring one and finding it too weak. -> ℹ️ **Note:** Traceroute hop colors use fixed thresholds (−7 dB / −15 dB); the per-node signal meter uses the preset-relative rating instead. +> ℹ️ **Note:** Traceroute and neighbor-info SNR colors use the active preset's limits too. RSSI text has separate strength colors: green above −115 dBm, yellow above −120 dBm, orange above −126 dBm, and red at or below −126 dBm. ## Diagnosing Local Interference diff --git a/docs/ca-rES/user/tak.md b/docs/ca-rES/user/tak.md index cf31612132..25da3cd3b9 100644 --- a/docs/ca-rES/user/tak.md +++ b/docs/ca-rES/user/tak.md @@ -2,7 +2,7 @@ title: TAK Integration parent: User Guide nav_order: 10 -last_updated: 2026-08-30 +last_updated: 2026-09-11 description: Interoperate with ATAK and WinTAK — CoT position sharing, TAK roles, and plugin setup. aliases: - tak @@ -36,7 +36,7 @@ The TAK module allows Meshtastic nodes to: > ⚠️ **Warning:** The old **Meshtastic ATAK Plugin** is no longer part of this path and cannot > work. It bridged through the cross-process AIDL API, which was removed in app 2.8.0; the mesh > service is now in-process only. Do not install it. Interop today runs over the app's own local -> TAK server plus the Mesh to CoT Converter, both described below, with stock ATAK/iTAK/WinTAK. +> TAK server plus the Mesh to CoT Converter, both described in the following sections, with stock ATAK/iTAK/WinTAK. ### Configuration diff --git a/docs/ca-rES/user/translate.md b/docs/ca-rES/user/translate.md index 67d69e2d96..cdc8b6d87f 100644 --- a/docs/ca-rES/user/translate.md +++ b/docs/ca-rES/user/translate.md @@ -2,7 +2,7 @@ title: Translate the App parent: User Guide nav_order: 17 -last_updated: 2026-08-30 +last_updated: 2026-09-11 description: How the app and its documentation are translated via Crowdin, and guidelines for contributing translations. aliases: - translate @@ -45,7 +45,7 @@ If your language is not yet listed on Crowdin: The Android app uses **Compose Multiplatform resources** for all user-visible strings: -``` +```text core/resources/src/commonMain/composeResources/ ├── values/ ← English (default) │ └── strings.xml @@ -58,7 +58,7 @@ core/resources/src/commonMain/composeResources/ In-app documentation follows a similar pattern under `docs/`: -``` +```text docs/ ├── en/user/ ← English source (default) │ ├── onboarding.md diff --git a/docs/cs-rCZ/index.md b/docs/cs-rCZ/index.md index d62add6ba7..b09dd2b109 100644 --- a/docs/cs-rCZ/index.md +++ b/docs/cs-rCZ/index.md @@ -4,23 +4,23 @@ layout: default nav_order: 0 --- -# Meshtastic Android App Documentation +# Dokumentace aplikace Meshtastic Android -User and developer documentation for the Meshtastic Android and Desktop app, built with Kotlin Multiplatform. +Uživatelská a vývojářská dokumentace aplikace Meshtastic pro Android a Desktop, vytvořené pomocí Kotlin Multiplatform. -Use the sidebar navigation to browse the **User Guide** for app features and the **Developer Guide** for contributing to the project. +Pomocí navigace v postranním panelu si můžete procházet **Uživatelskou příručku** s funkcemi aplikace a **Vývojářskou příručku** s informacemi o přispívání do projektu. -## Quick Links +## Rychlé odkazy -| Guide | Popis | -| --------------------------------------------------------------------- | -------------------------------------------------------------- | -| [Getting Started](user/onboarding) | Connect your first radio and send a message | -| [Messages & Channels](user/messages-and-channels) | Channel broadcasts, direct messages, reactions, and encryption | -| [Nodes](user/nodes) | Understanding the mesh network node list | -| [Signal Meter](user/signal-meter) | How the LoRa signal quality meter works | -| [Units & Locale](user/units-and-locale) | How temperatures, distances, and times adapt to your region | -| [Desktop App](user/desktop) | Linux, macOS, and Windows desktop usage | -| [Architecture](developer/architecture) | App architecture overview for contributors | -| [Contributing](developer/contributing) | Branch naming, PR workflow, and verification commands | +| Příručka | Popis | +| ------------------------------------------------ | -------------------------------------------------------------- | +| [Začínáme](user/onboarding) | Připojte své první rádio a pošlete zprávu | +| [Zprávy a kanály](user/messages-and-channels) | Vysílání kanálů, přímé zprávy, reakce a šifrování | +| [Uzel](user/nodes) | Porozumění seznamu uzlů mesh sítě | +| [Měřič signálu](user/signal-meter) | Jak funguje měřič kvality signálu LoRa | +| [Jednotky a lokalizace](user/units-and-locale) | Jak se teploty, vzdálenosti a časy přizpůsobují vašemu regionu | +| [Desktopová aplikace](user/desktop) | Používání desktopové aplikace v Linuxu, macOS a Windows | +| [Architektura](developer/architecture) | Přehled architektury aplikace pro přispěvatele | +| [Přispívání do projektu](developer/contributing) | Pojmenování větví, postup práce s PR a příkazy pro ověření | -> This documentation is served from the same markdown source that powers the in-app **Help & Documentation** browser. +> Tato dokumentace je generována ze stejného zdroje Markdownu, který používá prohlížeč **Nápovědy a dokumentace** přímo v aplikaci. diff --git a/docs/cs-rCZ/user/app-functions.md b/docs/cs-rCZ/user/app-functions.md index 94e17c2614..aca38fe93d 100644 --- a/docs/cs-rCZ/user/app-functions.md +++ b/docs/cs-rCZ/user/app-functions.md @@ -1,5 +1,5 @@ --- -title: App Functions +title: Funkce aplikace parent: Uživatelská příručka nav_order: 19 last_updated: 2026-08-30 @@ -11,7 +11,7 @@ aliases: - assistant --- -# App Functions +# Funkce aplikace App Functions expose Meshtastic capabilities to the Android system and to on-device AI assistants (such as Gemini) through the Android App Functions API. With them enabled, an assistant can discover and trigger mesh workflows for you — for example sending a message or checking your mesh status — without you opening the app. App Functions are available on **Google-flavor Android builds only**. @@ -55,7 +55,7 @@ The screenshot has **Send message** and **Get recent messages** switched off to > 🔒 **Privacy:** The **Send message** function lets an assistant send messages to your mesh on your behalf, and the read functions expose node, message, and metric data to it. Because all of them start enabled, the choice you make here is what to turn off rather than what to turn on. Each function has its own toggle, and **Allow AI access** turns all of them off at once. -## Related Topics +## Související témata - [Messages & Channels](messages-and-channels) — sending messages directly in the app - [Nodes](nodes) — the node list the read functions draw from diff --git a/docs/cs-rCZ/user/desktop.md b/docs/cs-rCZ/user/desktop.md index b0bcaa7eb3..5a4fd2b5d7 100644 --- a/docs/cs-rCZ/user/desktop.md +++ b/docs/cs-rCZ/user/desktop.md @@ -2,7 +2,7 @@ title: Desktop App parent: User Guide nav_order: 14 -last_updated: 2026-08-30 +last_updated: 2026-09-11 description: Install and use the Meshtastic Desktop app on Linux, macOS, and Windows — connections, feature parity, and keyboard shortcuts. aliases: - desktop @@ -58,20 +58,20 @@ Bluetooth Low Energy is supported on desktop via the [Kable](https://github.com/ ## Feature Parity -| Feature | Android | Desktop | Poznámka | -| ----------------------------------------------------- | ------- | ------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Messaging | ✓ | ✓ | Full parity | -| Node List | ✓ | ✓ | Full parity | -| Mapa | ✓ | ✓ | Interactive MapLibre map, with base map and overlay pickers and custom tile sources. No offline downloads or local `.mbtiles` archives | -| Map layers (`.kml`/`.kmz`/GeoJSON) | ✓ | ✓ | Same layer store and sheet as Android; imported files draw on the desktop map | -| Plánovač mesh sítě | ✓ | ✓\* | \*Opens in your browser on desktop; the estimate is not drawn on the desktop map | -| Nastavení | ✓ | ✓ | Full parity | -| Bluetooth (BLE) | ✓ | ✓ | Via Kable on desktop | -| Aktualizace firmware | ✓ | ✓ | In-app USB, BLE, and Wi-Fi (ESP32) update work the same as Android. The USB maintenance flow — nRF52/RP2040 factory erase and bootloader upgrade — is Android-only | -| Notifications | ✓ | ✓ | Native OS notifications | -| Widgets | ✓ | ✗ | Android-only | -| AI Assistant (Chirpy) | ✓\* | ✗ | Google flavor Android only | -| App Functions (system AI) | ✓† | ✗ | Google flavor Android only | +| Feature | Android | Desktop | Poznámka | +| ----------------------------------------------------- | ------- | ------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Messaging | ✓ | ✓ | Full parity | +| Node List | ✓ | ✓ | Full parity | +| Mapa | ✓ | ✓ | Interactive MapLibre map, with base map and overlay pickers and custom tile sources. Offline terrain (hillshade and contours) can be downloaded; offline base-map packs and local `.mbtiles` archives cannot | +| Map layers (`.kml`/`.kmz`/GeoJSON) | ✓ | ✓ | Same layer store and sheet as Android; imported files draw on the desktop map | +| Plánovač mesh sítě | ✓ | ✓\* | \*Opens in your browser on desktop; the estimate is not drawn on the desktop map | +| Nastavení | ✓ | ✓ | Full parity | +| Bluetooth (BLE) | ✓ | ✓ | Via Kable on desktop | +| Aktualizace firmware | ✓ | ✓ | In-app USB, BLE, and Wi-Fi (ESP32) update work the same as Android. The USB maintenance flow — nRF52/RP2040 factory erase and bootloader upgrade — is Android-only | +| Notifications | ✓ | ✓ | Native OS notifications | +| Widgets | ✓ | ✗ | Android-only | +| AI Assistant (Chirpy) | ✓\* | ✗ | Google flavor Android only | +| App Functions (system AI) | ✓† | ✗ | Google flavor Android only | \*Chirpy AI requires Android 14+ on Google flavor builds with supported hardware. @@ -122,7 +122,7 @@ Individual doc pages render with full formatting: ## Building from Source -```bash +```shell git clone https://github.com/meshtastic/Meshtastic-Android.git cd Meshtastic-Android ./gradlew :desktopApp:run @@ -135,7 +135,7 @@ Požadavky: ## Known Limitations -- Offline tile downloads and local `.mbtiles` archives are not available on desktop. +- Offline base-map downloads and local `.mbtiles` archives are not available on desktop. Offline terrain is — see [Map & Waypoints](map-and-waypoints). - `.kml`/`.kmz`/GeoJSON layer import works — see [Map & Waypoints](map-and-waypoints#map-layers). Site Planner opens in your browser rather than in the app; to bring its coverage estimate onto the map, click the transmitter pin diff --git a/docs/cs-rCZ/user/discovery.md b/docs/cs-rCZ/user/discovery.md index 94205818f1..9f02302564 100644 --- a/docs/cs-rCZ/user/discovery.md +++ b/docs/cs-rCZ/user/discovery.md @@ -2,7 +2,7 @@ title: Vyhledávání zařízení v místní mesh síti parent: User Guide nav_order: 12 -last_updated: 2026-08-30 +last_updated: 2026-09-11 description: Explore your mesh network — the Local Mesh Discovery scanner, traceroute paths, neighbor maps, and node discovery tools. aliases: - discovery @@ -97,7 +97,7 @@ Mesh Beacon lets nodes invite others to join their mesh. A beaconing node period Configure it under **Settings → Module configuration → Mesh Beacon**. The entry appears only on radios running firmware 2.8.0 or newer. A read-only **Region** row at the top of the screen shows the region the beacon advertises: that region, and the preset, are always the ones the radio itself uses, so a beacon cannot invite anyone onto settings your radio is not running. - **Listen for beacons** — receive invitations broadcast by other nodes. -- **Broadcast a beacon** — periodically advertise this mesh to nearby nodes, with an optional **Beacon message** of up to 100 bytes, a **Broadcast interval** picked from fixed intervals between 1 hour and 72 hours, and an **Offered channel** chosen from your radio's own channels. The offered channel is required, and defaults to your primary channel. +- **Broadcast a beacon** — periodically advertise this mesh to nearby nodes, with an optional **Beacon message** of up to 100 bytes, a **Broadcast interval** picked from fixed intervals between 1 hour and 72 hours, and an **Offered channel** chosen from your radio's own channels. The offered channel is required, and defaults to your primary channel. Over remote admin the picker offers the primary channel only. - **Broadcast targets** — optional extra destinations beyond the offered channel. **Add target** appends a row; each row picks a **Channel** and a **Transmit preset**, and **Remove target** deletes it. With no targets, the beacon goes out on the offered channel alone. Two conditions block beacon setup: diff --git a/docs/cs-rCZ/user/firmware.md b/docs/cs-rCZ/user/firmware.md index c20448b64b..a463aa2d4a 100644 --- a/docs/cs-rCZ/user/firmware.md +++ b/docs/cs-rCZ/user/firmware.md @@ -1,6 +1,6 @@ --- -title: Firmware Updates -parent: User Guide +title: Aktualizace firmware +parent: Uživatelská příručka nav_order: 13 last_updated: 2026-09-06 description: Update your radio firmware over Bluetooth or USB — OTA process, version channels, pre-flight checks, and recovery. @@ -11,7 +11,7 @@ aliases: - flash --- -# Firmware Updates +# Aktualizace firmware Keep your Meshtastic radio up to date with the latest firmware for new features, bug fixes, and security improvements. @@ -162,7 +162,7 @@ On connecting, the app compares the radio's firmware against two thresholds and A version string the app cannot parse is ignored rather than treated as too old, so a transient read never disconnects a working radio. -## Related Topics +## Související témata - [Connections](connections) — reconnecting after a firmware update - [Flashing firmware guide](https://meshtastic.org/docs/getting-started/flashing-firmware) — full firmware flashing walkthrough on meshtastic.org diff --git a/docs/cs-rCZ/user/help-and-docs.md b/docs/cs-rCZ/user/help-and-docs.md index 16b88a6ed0..b24e88bff1 100644 --- a/docs/cs-rCZ/user/help-and-docs.md +++ b/docs/cs-rCZ/user/help-and-docs.md @@ -2,7 +2,7 @@ title: Help & In-App Docs parent: Uživatelská příručka nav_order: 21 -last_updated: 2026-08-30 +last_updated: 2026-09-11 description: Browse this documentation inside the app, search it, and ask Chirpy — the on-device AI assistant — questions about Meshtastic. aliases: - help @@ -23,7 +23,7 @@ The docs browser lists every user-guide page. Tap a page to read it; images and ### Search -The search box sits at the top of the docs browser. Type in it to filter pages by title and keywords — results update as you type, and the **Clear search** button (✕) empties the box. +Tap the search field at the top of the docs browser to open a full-screen search view. Type to filter pages by title and keywords — results update as you type, under the same field. The back arrow, or the keyboard's search action, closes the view. The field hides while you scroll down the page list and comes back when you scroll up. ![Searching the in-app documentation](../../assets/screenshots/docs-browser_search.png) diff --git a/docs/cs-rCZ/user/map-and-waypoints.md b/docs/cs-rCZ/user/map-and-waypoints.md index 34a6c103ad..94f751c710 100644 --- a/docs/cs-rCZ/user/map-and-waypoints.md +++ b/docs/cs-rCZ/user/map-and-waypoints.md @@ -2,7 +2,7 @@ title: Map & Waypoints parent: User Guide nav_order: 6 -last_updated: 2026-09-01 +last_updated: 2026-09-11 description: View node positions on the map, create and share waypoints, manage map layers and Site Planner, and control position sharing and privacy. aliases: - map @@ -44,7 +44,7 @@ The floating toolbar provides quick access to the compass, the map type and laye ### Filtering the Map -Tap the filter button in the floating toolbar to open **Filter map**. **Display** controls what is drawn: **Only Favorites**, **Show Waypoints**, **Show Precision Circles**, and a slider that hides nodes not heard from recently. **Node roles** is a chip per device role, plus **All** to show every role; a selected chip means that role is shown. **Nodes** narrows the set further with **Hide offline nodes**, **Only show direct nodes**, **Exclude MQTT**, **Show ignored nodes**, and **Include unknown**. +Tap the filter button in the floating toolbar to open **Filter map**. **Display** controls what is drawn: **Only Favorites**, **Show Waypoints**, **Show Precision Circles**, and a slider that hides nodes not heard from recently. **Node roles** is a chip per device role, plus **All** to show every role; a selected chip means that role is shown. **Nodes** narrows the set further with **Hide offline nodes**, **Only show direct nodes**, **Signed only**, **Encrypted only**, **Exclude MQTT**, **Show ignored nodes**, and **Include unknown** — the same rules as the node list's [filter toggles](nodes). A dot on the filter button means at least one filter is hiding something — check it before concluding the mesh is quiet. Turning **Show Waypoints** off hides every waypoint, including your own. **Show ignored nodes** adds them to the map rather than showing only them — unlike the node list's **Only show ignored Nodes**. @@ -147,8 +147,9 @@ picker, all three offer the same raster base maps: Overlays can be toggled on top of any base map, from the layers sheet: - **Weather radar** — NOAA NEXRAD reflectivity (US coverage) -- **Hillshade** — terrain relief, on **F-Droid** and **Desktop** only. Useful for understanding why a - link fails, since LoRa range is limited by terrain +- **Hillshade** — terrain relief from an online tile source, on **F-Droid** and **Desktop** only; Google + Play draws hillshade from downloaded offline terrain instead. Useful for understanding why a link + fails, since LoRa range is limited by terrain ### Adding your own tile source @@ -156,7 +157,7 @@ Any XYZ tile endpoint can be added as a base map, on every flavor and on desktop Tile Sources** at the foot of the base map picker and paste a URL template using `{z}`, `{x}` and `{y}` — plus `{s}` if the provider uses rotating subdomains. A national mapping service, for example: -``` +```text https://wmts.geo.admin.ch/1.0.0/ch.swisstopo.pixelkarte-farbe/default/current/3857/{z}/{x}/{y}.jpeg ``` @@ -173,7 +174,18 @@ On **Google Play**, the layers sheet's **Offline Manager** section downloads its — water, roads and administrative boundaries extracted from the public Protomaps basemap dataset, drawn directly as map shapes rather than raster tiles. Frame the area you want, tap **Start Download**, and once it finishes flip **Show on map** to draw it (it's off by default so it never surprises you by covering up the -live map). You can still import a pre-made `.mbtiles` archive here too, as before. **Desktop** has neither. +live map). You can still import a pre-made `.mbtiles` archive here too, as before. **Desktop** has neither, though +the offline terrain described next does work there. + +### Offline terrain + +Hillshade and elevation contours can be downloaded for offline use as well, so a downloaded area still +shows why a link fails when there is no network. The terrain data comes from Mapterhorn and is credited on +the map. On **Google Play**, each downloaded region in the **Offline Manager** list has a **Download Terrain** +button and **Hillshade** and **Contours** switches; terrain attaches to that region and shares its storage limit, and an area +too large for terrain asks you to zoom in or pick a smaller region. On **F-Droid** and **Desktop**, the +layers sheet has an **Offline Terrain** section of its own: frame the area, tap **Start Download**, and the +hillshade and contours draw from the downloaded tiles from then on. ### Going offline @@ -181,8 +193,8 @@ An **Offline** pill appears over the map whenever the device has no network conn builds, if you've imported a local `.mbtiles` archive, the map switches to it automatically the moment the network drops — no toggle to remember — and switches back once you're reconnected, as long as you haven't picked a different base map yourself in between. On **F-Droid**, a downloaded offline area keeps rendering on -its own; the pill is purely informational there. **Desktop** has no offline downloads at all yet, so the pill -is informational there too. +its own; the pill is purely informational there. **Desktop** downloads offline terrain but no base-map +packs, so the pill is informational there too. ## Related Topics diff --git a/docs/cs-rCZ/user/messages-and-channels.md b/docs/cs-rCZ/user/messages-and-channels.md index 55731e6f7e..d87b138648 100644 --- a/docs/cs-rCZ/user/messages-and-channels.md +++ b/docs/cs-rCZ/user/messages-and-channels.md @@ -2,7 +2,7 @@ title: Messages & Channels parent: User Guide nav_order: 3 -last_updated: 2026-08-30 +last_updated: 2026-09-14 description: Send and receive messages, manage channels, configure encryption, search conversations, and use quick chat, reactions, and message actions. aliases: - channels @@ -50,6 +50,21 @@ Key length alone does not change the icon: a 128-bit key and a 256-bit key both Tapping an existing channel opens the same editor, where you can change the name, the PSK, MQTT uplink and downlink, and position precision. Every edit on this screen — adding, editing, deleting, or dragging a channel into a new order — waits on **Send** the same way. +### Archived Channels + +A conversation belongs to the channel it was held on, not to the slot that channel occupied. When a channel leaves your radio — you scan a different channel QR, replace a channel's key, or switch the modem preset — its conversation is **archived** rather than deleted or merged into whatever takes the slot. + +An archived conversation: + +- stays in the **Channels** list under the name the channel had, below the channels currently on your radio, marked with a history icon; +- keeps its full message history, searchable as usual; +- is read-only — you cannot send, reply, or react, because the channel is no longer on the radio to send on, and it is not offered as a share target; +- comes back automatically if you re-add that exact channel, taking its messages back to the live slot. + +Changing the modem preset archives the channel too: the preset is part of a default channel's identity, so **LongFast** and **MediumFast** are different channels even though neither the name nor the key changed. Changing only the region does not archive anything. + +To remove an archived conversation for good, long-press it in the conversation list and delete it. + ## Direct Messages Direct messages (DMs) go to one specific node. When both radios hold each other's public keys, your radio encrypts the message to that node's public key, so no one else on the mesh can read it — not even nodes that share your channel. diff --git a/docs/cs-rCZ/user/mqtt.md b/docs/cs-rCZ/user/mqtt.md index 6a811ac128..65fcdefc87 100644 --- a/docs/cs-rCZ/user/mqtt.md +++ b/docs/cs-rCZ/user/mqtt.md @@ -2,7 +2,7 @@ title: MQTT parent: User Guide nav_order: 11 -last_updated: 2026-08-30 +last_updated: 2026-09-11 description: Bridge your mesh to the internet — MQTT broker setup, encryption layers, and map reporting. aliases: - mqtt @@ -25,7 +25,7 @@ The MQTT module connects your node to an MQTT broker, allowing: ## How It Works -``` +```text [Your Node] → Radio → [Gateway Node with Wi-Fi] → MQTT Broker → [Remote Gateway] → Radio → [Remote Node] ``` diff --git a/docs/cs-rCZ/user/node-metrics.md b/docs/cs-rCZ/user/node-metrics.md index 909ca70961..9b6784b94f 100644 --- a/docs/cs-rCZ/user/node-metrics.md +++ b/docs/cs-rCZ/user/node-metrics.md @@ -2,7 +2,7 @@ title: Node Metrics parent: User Guide nav_order: 5 -last_updated: 2026-08-30 +last_updated: 2026-09-09 description: Telemetry dashboards for each mesh node — device health, environment sensors, air quality, signal quality, power, traceroute, and position history. aliases: - metrics @@ -114,7 +114,7 @@ Radio signal quality information: ### Signal Quality Reference -Signal quality is rated from **SNR relative to the active LoRa modem preset's demodulation floor**, not from fixed thresholds — a given SNR means different things on different presets (e.g. −15 dB is fine on LongSlow but unusable on ShortFast). RSSI is shown but is not part of the rating. In the table, _limit_ is the preset's SNR limit. +Signal quality is rated from **SNR relative to the active LoRa modem preset's demodulation floor**, not from fixed thresholds — a given SNR means different things on different presets (e.g. −15 dB is fine on LongSlow but unusable on ShortFast). When RSSI and a noise-floor reading are both available, the app also rates their difference against the preset limit and uses the worse rating. Otherwise, RSSI is display-only. In the table, _limit_ is the preset's SNR limit. | Quality | Criteria | | ------- | ---------------------------------------------------------------- | diff --git a/docs/cs-rCZ/user/nodes.md b/docs/cs-rCZ/user/nodes.md index e24051e918..b7e29ea0d4 100644 --- a/docs/cs-rCZ/user/nodes.md +++ b/docs/cs-rCZ/user/nodes.md @@ -1,9 +1,9 @@ --- title: Uzly -parent: User Guide +parent: Uživatelská příručka nav_order: 4 -last_updated: 2026-09-04 -description: Browse, filter, and sort mesh nodes — view details, signal quality, roles, and quick actions. +last_updated: 2026-09-11 +description: Procházet, filtrovat a třídit uzly sítě – zobrazit podrobnosti, kvalitu signálu, role a rychlé akce. aliases: - node-list - mesh-nodes @@ -30,65 +30,69 @@ The node list shows every node your radio has heard, including: The list has two densities, set at **Settings → Node Layout**. **Complete** shows every field a node has reported and hides the ones it hasn't. **Compact** fits more nodes on screen and lets you pick the fields yourself — **Power**, **Last Heard Time**, **Relative Last Heard Time**, **Distance and Bearing**, **Hops Away**, **Signal (Direct Only)**, **Channel**, and **Device & Role**. The **Environment Metrics** toggle applies to both densities. A preview above the toggles shows the effect before you leave the screen. -### Node Status Indicators +### Indikátory stavu uzlu -| Indicator | Meaning | -| --------------------- | ---------------------------------------------- | -| Green last-heard time | Node heard within the last 2 hours | -| Plain last-heard time | Node not heard for over 2 hours | -| ⭐ Favorite | Node you marked as a favorite. | +| Indikátor | Význam | +| ------------------------------ | ------------------------------------------------- | +| Zelený - čas posledního příjmu | Uzel byl slyšet během posledních 2 hodin | +| Běžný - čas posledního příjmu | Uzel nebyl slyšet déle než 2 hodiny | +| ⭐ Oblíbený | Uzel jste označili jako oblíbený. | There is no separate "away" tier. -### Node Roles +### Role uzlu -Nodes can be configured with different roles that affect their mesh behavior: +Uzlům lze nastavit různé role, které ovlivňují jejich chování v mesh síti: -| Role | Popis | -| ----------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ | -| Client | Standard end-user node | -| Client Base | Treats favorited-node traffic as Router Late priority; all other traffic as Client | -| Client Mute | Receives but doesn't retransmit | -| Client Hidden | Like Client Mute, plus hides from node list | -| Router | Prioritizes message forwarding; stays awake to relay | -| Router Late | Infrastructure node that rebroadcasts once, but only after all other modes (provides supplemental coverage) | -| ~~Router Client~~ | ⚠️ **Deprecated** (removed in firmware 2.3.15) — no longer selectable; use Router or Client instead | -| ~~Repeater~~ | ⚠️ **Deprecated** (removed in firmware 2.7.11) — no longer selectable; use Router instead | -| Tracker | Optimized for position reporting at regular intervals | -| Senzor | Optimized for telemetry reporting | -| TAK | Interoperates with TAK systems (sends/receives CoT) | -| TAK Tracker | TAK position reporting only | -| Lost and Found | Sends its position to the default channel as a text message at regular intervals, to help recover a lost radio | +| Role | Popis | +| ----------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Client | Standardní uzel pro koncové uživatele | +| Client Base | Provoz z oblíbených uzlů považuje za prioritu Router Late; veškerý ostatní provoz za Client | +| Client Mute | Přijímá, ale nepřeposílá | +| Client Hidden | Stejně jako Client Mute, navíc skryje uzel ze seznamu uzlů | +| Router | Upřednostňuje přeposílání zpráv a zůstává vzhůru, aby je mohl přeposílat | +| Router Late | Infrastrukturní uzel, který jednou přepošle paket, ale až po všech ostatních režimech (poskytuje doplňkové pokrytí) | +| ~~Router Client~~ | ⚠️ **Zastaralé** (odstraněno ve firmwaru 2.3.15) – již nelze vybrat; použijte místo něj Router nebo Client | +| ~~Repeater~~ | ⚠️ **Zastaralé** (odstraněno ve firmwaru 2.7.11) – již nelze vybrat; použijte místo něj Router | +| Tracker | Optimalizováno pro pravidelné odesílání polohy | +| Senzor | Optimalizováno pro hlášení telemetrie | +| TAK | Spolupracuje se systémy TAK (odesílá/přijímá CoT) | +| TAK Tracker | Pouze odesílání polohy TAK | +| Lost and Found | Pravidelně odesílá svou polohu na výchozí kanál jako textovou zprávu, aby pomohl najít ztracené rádio | -### Choosing a Role +### Výběr role -Most users should keep the default **Client** role. Consider a different role when: +Většina uživatelů by měla ponechat výchozí roli **Client**. Zvažte jinou roli, pokud: -- **Router** — You have a node in a fixed, elevated location with reliable power (rooftop, hilltop). Routers stay awake continuously to relay messages for others and are essential for extending mesh coverage. Don't use Router on battery-powered handheld radios. -- **Router Late** — An infrastructure node that always rebroadcasts packets once but only after all other routing modes have had their turn. Provides supplemental coverage for local clusters without competing with primary routers. -- **Client Base** — Treats traffic from/to your favorited nodes with Router Late priority (ensuring those messages get extra relay coverage) while handling everything else as a normal Client. -- **Client Mute** — You want to receive mesh traffic but not contribute to relaying. Useful for monitoring-only radios or to reduce congestion in dense areas. -- **Tracker** — An unattended radio whose sole purpose is broadcasting its GPS position (e.g., a vehicle, pet, or asset). Sleeps between broadcasts to conserve battery. -- **Sensor** — An unattended radio reporting environmental telemetry (temperature, humidity, air quality). Similar power profile to Tracker. -- **TAK / TAK Tracker** — Only needed if interoperating with ATAK/WinTAK systems. See [TAK Integration](tak) for details. +- \*\*Router \*\*— Máte uzel na pevném, vyvýšeném místě se spolehlivým napájením (střecha, vrchol kopce). Routery zůstávají nepřetržitě aktivní, aby přeposílaly zprávy ostatních, a jsou klíčové pro rozšiřování pokrytí mesh sítě. Nepoužívejte roli Router u bateriově napájených ručních rádií. +- **Router Late** — Infrastrukturní uzel, který vždy jednou přepošle paket, ale až poté, co dostaly prostor všechny ostatní směrovací režimy. Poskytuje doplňkové pokrytí místním skupinám uzlů, aniž by konkuroval hlavním routerům. +- **Client Base** — Zachází s provozem z/na vaše oblíbené uzly s prioritou Router Late (zajišťuje těmto zprávám dodatečné přeposílání), zatímco vše ostatní zpracovává jako běžný Client. +- **Client Mute** — Chcete přijímat provoz mesh sítě, ale nechcete se podílet na jeho přeposílání. Vhodné pro rádia určená pouze k monitorování nebo ke snížení zahlcení v hustě osídlených oblastech. +- Neobsluhované rádio, jehož jediným účelem je vysílat svou GPS polohu (např. vozidlo, domácí mazlíček nebo sledovaný majetek). Sleeps between broadcasts to conserve battery. +- **Senzor** – Neobsluhované rádio které odesílá údaje z okolního prostředí (teplota, vlhkost, kvalita ovzduší). Podobná spotřeba energie jako Tracker. +- **TAK / TAK Tracker** – Potřebujete pouze pro spolupráci se systémy ATAK/WinTAK. Podrobnosti viz [TAK integrace](tak). -> 💡 **Tip:** The mesh works best when most nodes are **Client** or **Router**. Too many Client Mute nodes reduce mesh resilience; too many Routers in a dense area can cause congestion. A good rule of thumb: one Router per 5–10 Clients in your area. +> 💡 **Tip:** Mesh síť funguje nejlépe, když je většina uzlů v roli **Client** nebo **Router**. Příliš mnoho uzlů Client Mute snižuje odolnost mesh sítě; příliš mnoho routerů v hustě osídlené oblasti může způsobit zahlcení. Dobrým orientačním pravidlem je jeden Router na 5–10 uzlů Client ve vaší oblasti. -### Encryption Indicators +### Security indicators -Nodes display encryption status icons next to their name: +Each node carries one security icon beside its name in the node list. Tap it to read what it means, and choose **Show All Meanings** in that dialog for the full legend. The detail screen shows the same state in words, as a **Security** row that opens the same dialog. -| Icon | Meaning | -| ----------- | ------------------------------------------------------------------------------------------------------------------------------------------------- | -| 🔒 Locked | Communication uses PKI (public key infrastructure) — end-to-end encrypted with verified identity | -| 🔓 Unlocked | No public key has been received for this node, so it cannot be direct messaged — use **Request User Info** on the node detail page to ask for one | -| ⚠️ Mismatch | Public key mismatch — the node's key has changed since last seen (investigate before trusting) | +| Icon | Meaning | Shown for | +| --------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------- | +| Person with a shield check (green) | **Verified contact** — you verified this node's key in person by exchanging contact QR codes, so its identity is confirmed. The strongest trust the list shows. Your own connected node carries it too | Any firmware version | +| Nodes icon with a shield check (green) | **Signed node** — this node signs its broadcasts with its identity key, so its identity is consistent over time, but you have not verified it in person. On firmware 2.8 the icon appears from the version alone, before any signed broadcast has been heard | Firmware 2.8 or newer, and any node whose signed broadcast your node has verified | +| 🔒 Closed lock | A public key is on file and matches, so direct messages to this node are encrypted | Firmware before 2.8, or no reported version | +| 🔓 Open lock | No public key has been received for this node, so it cannot be direct messaged — use **Request User Info** on the node detail page to ask for one | Firmware before 2.8, or no reported version | +| ⚠️ Mismatch | **Public key mismatch** — a different key arrived for this node after one was stored. Investigate before trusting | Any firmware version | -> 💡 **Tip:** Direct messages always use PKI, so the radio needs the other node's public key before it can send one. It refuses the send rather than falling back to channel encryption. Keys arrive inside node info, which is why an open lock usually clears itself once that node is heard from properly. If you see a key mismatch warning, the node may have been reset or compromised. +Every node on firmware 2.8 or newer signs its broadcasts, so on that firmware the signed state is the baseline and the locks are not shown. -To clear a mismatch, first confirm through another trusted channel that the key change was intentional — a factory reset causes one. Then touch & hold the node, choose **Remove**, and let the two radios exchange keys again the next time yours hears it. +Direct messages always use public-key encryption, so your node needs the other node's public key before it can send one. It refuses the send rather than falling back to channel encryption. Keys arrive inside node info, which is why an open lock usually clears itself once that node is heard from properly. -## Quick Actions +A mismatch never replaces the key you already hold. The app keeps the first key it recorded and refuses the new one, as the firmware does, so a stray or hostile node info cannot silently break encrypted messaging to a contact. To clear a mismatch, first confirm through another trusted channel that the key change was intentional — a factory reset causes one. Then touch & hold the node, choose **Remove**, and let the two nodes exchange keys again the next time yours hears it. + +## Rychlé akce From the node list, you can: @@ -106,9 +110,11 @@ User settings screen with the cursor already in the Status Message field. It onl radio is connected and running firmware 2.8 or newer — see [Settings — Radio & User](settings-radio-user.md) for the field itself. -## Sharing a Contact +## Sdílet kontakt -On a node's detail screen, tap **Share Contact** to produce a link and a QR code for that node. From the same dialog, **Write to NFC tag** saves the link to a writable NFC tag that anyone can tap to open. +On a node's detail screen, tap **Share Contact** to produce a link and a QR code for that node. From the same dialog, **Share link** opens the Android share sheet (on desktop it copies the link instead), **Write to NFC tag** saves it to a writable NFC tag, and **Copy** puts it on the clipboard. While that dialog is open and in front of you, the phone also offers the same link to any NFC reader, so someone can take the contact by tapping their phone against yours with no tag involved. + +Sharing your own contact this way marks it as verified in person, so whoever imports it sees the **Verified contact** icon rather than the signed one. Relaying someone else's contact passes on only what your app had already recorded about them. To add someone else's contact, use the import button on the node list and choose **Scan Shared Contact QR Code**, **Scan Shared Contact NFC**, or **Input Shared Contact URL**. The app asks you to confirm with **Import Shared Contact?**, and warns you when the contact is one you already have. @@ -120,14 +126,16 @@ Type in the search field to filter nodes by name or short name. The filter updat ### Filter Toggles -| Filtr | Popis | -| --------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| **Hide offline nodes** | Show only nodes heard within the last 2 hours | -| **Only show direct nodes** | Show only nodes your radio heard directly, with no relay in between | -| **Include unknown** | Show nodes that haven't sent user info yet. **On by default**, so a node heard before its info arrives stays visible; these carry a badge marking them incomplete, and cannot be direct messaged until their user info brings a public key | -| **Exclude infrastructure** | Hide infrastructure-role nodes (Router, Router Late, Client Base, and legacy Repeater nodes) and any node that cannot be messaged, whatever its role | -| **Exclude MQTT** | Hide nodes heard only via MQTT internet bridge | -| **Only show ignored Nodes** | Replace the list with the nodes you have ignored. Every other node is hidden while this is on, and a banner appears at the top of the list to take you back | +| Filtr | Popis | +| --------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **Hide offline nodes** | Show only nodes heard within the last 2 hours | +| **Only show direct nodes** | Show only nodes your radio heard directly, with no relay in between | +| **Include unknown** | Show nodes that haven't sent user info yet. **On by default**, so a node heard before its info arrives stays visible; these carry a badge marking them incomplete, and cannot be direct messaged until their user info brings a public key | +| **Exclude infrastructure** | Hide infrastructure-role nodes (Router, Router Late, Client Base, and legacy Repeater nodes) and any node that cannot be messaged, whatever its role | +| **Exclude MQTT** | Hide nodes heard only via MQTT internet bridge | +| **Signed only** | Show only nodes whose signed broadcasts your node has actually heard and verified. Stricter than the icon: a node on 2.8 shows as signed by its firmware version before any signed broadcast arrives, and a contact verified in person on older firmware is not signed at all | +| **Encrypted only** | Show nodes with a matching public key on file, the key an encrypted direct message needs. A node with a key mismatch is excluded | +| **Only show ignored Nodes** | Replace the list with the nodes you have ignored. Every other node is hidden while this is on, and a banner appears at the top of the list to take you back | ### Sort Options @@ -145,10 +153,12 @@ Type in the search field to filter nodes by name or short name. The filter updat Tap the hop-histogram icon in the node list's app bar to open a bar chart of how many nodes sit at each hop distance (0 = direct, 1 = one relay away, and so on). Filter the chart to a **last heard** window — All time, 1 hour, 8 hours, or 24 hours — to see how the mesh looks right now versus over a longer period. It's a quick way to gauge how busy and spread out your local mesh is. -## Node Detail +## Detail uzlu Tapping a node opens the detail view with comprehensive information. See [Node Metrics](node-metrics) for full details on metrics and telemetry. +Signal quality is rated against your modem preset. The same SNR can be good on a long-range preset and poor on a faster one. Traceroute and neighbor-info SNR colors use that preset too. RSSI text has its own strength colors; it affects the quality rating only when a noise-floor reading is also available. + The Details card carries the node's short name, role, IDs, last heard time, hops away, uptime, and its SNR and RSSI: ![Node detail section](../../assets/screenshots/nodes_detail_section.png) @@ -179,7 +189,7 @@ The list stays empty until your radio hears another node. - **Searching for nodes** — the radio is connected and listening, but nothing has arrived yet. Check that its region and modem preset match the mesh around you, and leave **Include unknown** on so a node that has not yet sent its name still appears. See [Settings — Radio & User](settings-radio-user). - A node you expect is missing — check the filter toggles. **Only show direct nodes**, **Exclude MQTT**, and **Exclude infrastructure** each hide a whole category of node. -## Related Topics +## Související témata - [Node Metrics](node-metrics) — detailed telemetry dashboards for each node - [Messages & Channels](messages-and-channels) — send a direct message to a node diff --git a/docs/cs-rCZ/user/settings-module-admin.md b/docs/cs-rCZ/user/settings-module-admin.md index 13de2a5d39..c144ff8752 100644 --- a/docs/cs-rCZ/user/settings-module-admin.md +++ b/docs/cs-rCZ/user/settings-module-admin.md @@ -2,7 +2,7 @@ title: Settings — Modules & Admin parent: User Guide nav_order: 8 -last_updated: 2026-08-30 +last_updated: 2026-09-16 description: Configure optional feature modules (MQTT, telemetry, canned messages, TAK, and more) and perform device administration. aliases: - modules @@ -333,7 +333,7 @@ appear only when your own node is selected: ### O aplikaci -**Settings → About** carries the app's own identity rather than the radio's: +**Settings → About** carries the app's own identity rather than the node's: Three sections: @@ -341,7 +341,11 @@ Three sections: - **Apps** — opens with **Need Hardware?**, a rotating carousel of popular devices that links out to where to buy one. It also lists the GitHub repository, the running app version, and **Acknowledgements** (see the next section). -- **Project information** — links to the website and to this documentation. +- **Project information** — links to the website, to this documentation, and to the GNU GPL v3 + license the app is released under. + +The footer states the copyright and that the app is free software with no warranty, which you +may redistribute under the same license. ### Poděkování diff --git a/docs/cs-rCZ/user/settings-radio-user.md b/docs/cs-rCZ/user/settings-radio-user.md index ecf7355b33..05ecb5641c 100644 --- a/docs/cs-rCZ/user/settings-radio-user.md +++ b/docs/cs-rCZ/user/settings-radio-user.md @@ -2,7 +2,7 @@ title: Settings — Radio & User parent: User Guide nav_order: 7 -last_updated: 2026-09-04 +last_updated: 2026-09-11 description: Configure your radio hardware, LoRa presets, user profile, position sharing, power management, and security. aliases: - nastavení @@ -51,7 +51,7 @@ The footer appears as soon as you change something. **Discard** throws the chang The status message is saved with the same **Save**, but it never reboots the node — and, like the rest of this screen, it can be edited on a remote node you administer. For your own radio there is a shortcut while it is connected: touch & hold your node in the [node list](nodes.md) and choose -**Update status**. Older firmware and a disconnected radio have no shortcut — the field above is +**Update status**. Older firmware and a disconnected node have no shortcut — the field itself is still the way in. ## Nastavení @@ -77,13 +77,13 @@ On **Settings → LoRa**. | Nastavení | Popis | Výchozí | | -------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------- | -| Region | Regulatory region for frequency bands. You must set this before transmitting | Unset (must configure) | +| Region | Regulatory region for frequency bands. You must set this before transmitting. On firmware 2.8 or newer, the first region set also creates the node's identity key and gives it a new node number | Unset (must configure) | | Předvolby | Speed/range tradeoff | LongFast | | Počet skoků | Maximum retransmit hops | 3 | | Vysílací výkon | Transmission power (dBm); 0 = max allowed for region | 0 (region max) | | Ruční nastavení frekvence | Overrides the computed operating frequency outright (MHz). It does not offset the calculated value — leave at 0 unless you know you need a specific frequency | 0 (use calculated) | | Použít předvolbu | On by default. Turn it off to set Spread Factor, Coding Rate and Bandwidth by hand instead of taking them from the modem preset | On | -| Spread Factor | Manual mode only: 7–12. Higher spreads further but slower | From preset | +| Spread Factor | Manual mode only: 5–12. Higher spreads further but slower. On SX127x (RF95) radios the firmware does not accept 5 or 6 and uses 11 instead | From preset | | Coding Rate | Manual mode only: 5–8. More redundancy costs airtime | From preset | | Šířka pásma | Manual mode only: the channel bandwidth in kHz, typed in directly. On the 2.4 GHz region the app offers a list of the bandwidths your radio supports instead, and a stored value that is not on that list shows as _Unsupported_ and blocks saving until you pick a supported one | From preset | | Frekvenční slot | Which slot within the region's band to use. 0 derives it from the primary channel name | 0 (automatic) | @@ -111,13 +111,13 @@ The Lite, Narrow, Medium Turbo, and Tiny presets need firmware 2.8 or newer — | Short Slow | ~5 km | 6.25 kbps | −10 dB | Suburban short-range; moderate building density | | Medium Fast | ~5 km | 3.52 kbps | −12.5 dB | Suburban areas; moderate building density | | Medium Slow | ~8 km | 1.95 kbps | −15 dB | Suburban/rural; moderate range with slower speed | -| Long Turbo | ~10 km | 1.34 kbps | −12.5 dB | Similar range to Long Fast but with 500 kHz bandwidth; faster throughput | +| Long Turbo | ~10 km | 1.34 kbps | −17.5 dB | Similar range to Long Fast but with 500 kHz bandwidth; faster throughput | | Long Fast | ~10 km | 1.1 kbps | −17.5 dB | **General use (default)** — balanced range and speed | | Long Moderate | ~20 km | 0.34 kbps | −17.5 dB | Rural with some terrain; occasional use | | Lite Fast | ~5 km | 1.76 kbps | −12.5 dB | EU 866 MHz SRD band (125 kHz BW); comparable to Medium Fast | | Lite Slow | ~10 km | 0.98 kbps | −15 dB | EU 866 MHz SRD band (125 kHz BW); comparable to Long Fast | -| Narrow Fast | ~5 km | 2.28 kbps | −10 dB | EU 868 MHz band (62.5 kHz BW); avoids interference with other devices | -| Narrow Slow | ~10 km | 1.30 kbps | −12.5 dB | EU 868 MHz band (62.5 kHz BW); comparable to Long Fast | +| Narrow Fast | ~5 km | 2.28 kbps | −7.5 dB | EU 868 MHz band (62.5 kHz BW); avoids interference with other devices | +| Narrow Slow | ~10 km | 1.30 kbps | −10 dB | EU 868 MHz band (62.5 kHz BW); comparable to Long Fast | | Medium Turbo | ~5 km | 7.0 kbps | −12.5 dB | Like Medium Fast but with 500 kHz bandwidth; not legal in every region. Needs firmware 2.8 or newer | | Tiny Fast | ~10 km | 0.68 kbps | −7.5 dB | Amateur bands that cap occupied bandwidth; these presets use 15.6 kHz. Needs firmware 2.8 or newer, an SX126x or SX127x radio, and a TCXO of ±5 ppm or better | | Tiny Slow | ~20 km | 0.33 kbps | −10 dB | Same band restrictions as Tiny Fast, longer range. Same firmware, radio, and TCXO requirements | diff --git a/docs/cs-rCZ/user/signal-meter.md b/docs/cs-rCZ/user/signal-meter.md index b6d63d4554..563d9c2de5 100644 --- a/docs/cs-rCZ/user/signal-meter.md +++ b/docs/cs-rCZ/user/signal-meter.md @@ -2,7 +2,7 @@ title: How the Meshtastic Signal Meter Works parent: User Guide nav_order: 15 -last_updated: 2026-08-30 +last_updated: 2026-09-09 description: How the signal meter rates quality from SNR relative to the LoRa modem preset — spread spectrum, presets, and what the bars really mean. aliases: - signal @@ -32,7 +32,7 @@ Each modem preset has an SNR limit: the lowest SNR at which that preset can stil ## Rating Signal Quality -The app rates signal quality (None, Bad, Fair, or Good) from SNR alone, measured against the active preset's SNR limit. It does not factor in RSSI: without knowing the local noise floor, RSSI alone cannot say whether a signal is decodable. RSSI is still available — on the node detail screen and in the metrics charts. +The app rates signal quality (None, Bad, Fair, or Good) from SNR relative to the active preset's SNR limit. When RSSI and a noise-floor reading are both available, it also rates the difference between them against that limit and uses the worse rating. Otherwise, it uses SNR alone. Because the rating is relative to the preset, the same SNR rates differently on different presets. An SNR of −16 dB rates Good on Long Fast (SNR limit −17.5 dB) but None on Short Fast (SNR limit −7.5 dB). Let `limit` be the active preset's SNR limit: @@ -45,7 +45,7 @@ Because the rating is relative to the preset, the same SNR rates differently on The icon never goes blank, so count the bars carefully: a single bar means None, not a weak but usable link, and Good is a solid wedge rather than a set of bars. A gray three-bar icon labeled Unknown is a different state again — the packet carried no SNR measurement at all, which is not the same as measuring one and finding it too weak. -> ℹ️ **Note:** Traceroute hop colors use fixed thresholds (−7 dB / −15 dB); the per-node signal meter uses the preset-relative rating instead. +> ℹ️ **Note:** Traceroute and neighbor-info SNR colors use the active preset's limits too. RSSI text has separate strength colors: green above −115 dBm, yellow above −120 dBm, orange above −126 dBm, and red at or below −126 dBm. ## Diagnosing Local Interference diff --git a/docs/cs-rCZ/user/tak.md b/docs/cs-rCZ/user/tak.md index 8e63137d56..4a0c0eacb1 100644 --- a/docs/cs-rCZ/user/tak.md +++ b/docs/cs-rCZ/user/tak.md @@ -2,7 +2,7 @@ title: TAK Integration parent: User Guide nav_order: 10 -last_updated: 2026-08-30 +last_updated: 2026-09-11 description: Interoperate with ATAK and WinTAK — CoT position sharing, TAK roles, and plugin setup. aliases: - tak @@ -36,7 +36,7 @@ The TAK module allows Meshtastic nodes to: > ⚠️ **Warning:** The old **Meshtastic ATAK Plugin** is no longer part of this path and cannot > work. It bridged through the cross-process AIDL API, which was removed in app 2.8.0; the mesh > service is now in-process only. Do not install it. Interop today runs over the app's own local -> TAK server plus the Mesh to CoT Converter, both described below, with stock ATAK/iTAK/WinTAK. +> TAK server plus the Mesh to CoT Converter, both described in the following sections, with stock ATAK/iTAK/WinTAK. ### Nastavení diff --git a/docs/cs-rCZ/user/translate.md b/docs/cs-rCZ/user/translate.md index b0af1228ac..d033b222c8 100644 --- a/docs/cs-rCZ/user/translate.md +++ b/docs/cs-rCZ/user/translate.md @@ -1,51 +1,51 @@ --- -title: Translate the App -parent: User Guide +title: Překlad aplikace +parent: Uživatelská příručka nav_order: 17 -last_updated: 2026-08-30 -description: How the app and its documentation are translated via Crowdin, and guidelines for contributing translations. +last_updated: 2026-09-11 +description: Jak se aplikace a její dokumentace překládají pomocí Crowdin a jak přispívat k překladům. aliases: - translate - crowdin - localization --- -# Translate the App +# Překlad aplikace -The app and its in-app docs are translated on Crowdin — this page shows how to contribute. The app uses [Crowdin](https://crowdin.com/) to manage community translations for both the user interface and in-app documentation. +Aplikace a její dokumentace v aplikaci se překládají na Crowdin – tato stránka ukazuje, jak přispívat k překladům. Aplikace používá [Crowdin](https://crowdin.com/) ke správě komunitních překladů uživatelského rozhraní i dokumentace přímo v aplikaci. -## What Gets Translated +## Co se překládá -| Resource | Source Location | Poznámka | -| ----------------- | ------------------------------------------------------------------- | ---------------------------------------------------------------------- | -| UI strings | `core/resources/src/commonMain/composeResources/values/strings.xml` | Buttons, labels, messages, and all user-visible text | -| User Guide pages | `docs/en/user/*.md` | In-app documentation shown in Help & Documentation | -| Fastlane metadata | `fastlane/metadata/android/en-US/` | Google Play listing title, description, and changelogs | +| Zdroj | Umístění zdroje | Poznámka | +| ---------------------------- | ------------------------------------------------------------------- | --------------------------------------------------------------- | +| UI řetězce | `core/resources/src/commonMain/composeResources/values/strings.xml` | Tlačítka, popisky, zprávy a veškerý text zobrazený uživateli | +| Stránky uživatelské příručky | `docs/en/user/*.md` | Dokumentace v aplikaci zobrazená v části Nápověda a dokumentace | +| Fastlane metadata | `fastlane/metadata/android/en-US/` | Google Play listing title, description, and changelogs | -> ℹ️ **Note:** Developer Guide pages are English-only. Code-focused documentation targeting contributors is not translated. +> ℹ️ **Poznámka:** Stránky Vývojářské příručky jsou pouze v angličtině. Dokumentace zaměřená na kód pro přispěvatele se nepřekládá. ## Jak přispět -1. **Visit the Crowdin project.** Open the [Meshtastic Android Crowdin project](https://crowdin.com/project/meshtastic-android) and sign in or create a free account. -2. **Choose your language.** Select an existing language or request a new one by opening a [GitHub issue](https://github.com/meshtastic/Meshtastic-Android/issues/new). -3. **Translate strings.** Crowdin shows the English source on the left and your translation on the right. Translate each string and save. -4. **Review context.** Many strings include screenshots or context comments — check these to understand where the text appears in the app. A scheduled job pulls approved translations from Crowdin and opens a pull request; they ship once a maintainer merges it and a new build goes out. +1. **Navštivte projekt na Crowdinu.** Otevřete [Meshtastic Android Crowdin project](https://crowdin.com/project/meshtastic-android) a přihlaste se nebo si zdarma vytvořte účet. +2. **Vyberte svůj jazyk.** Vyberte některý z dostupných jazyků nebo požádejte o přidání nového jazyka založením [GitHub problému](https://github.com/meshtastic/Meshtastic-Android/issues/new). +3. **Přeložte řetězce.** Crowdin zobrazuje vlevo anglický zdrojový text a vpravo váš překlad. Přeložte každý řetězec a uložte ho. +4. **Zkontrolujte kontext.** Mnoho řetězců obsahuje snímky obrazovky nebo poznámky ke kontextu – podívejte se na ně, abyste zjistili, kde se text v aplikaci zobrazuje. Naplánovaná úloha stáhne schválené překlady z Crowdin a vytvoří pull request. Překlady se dostanou do aplikace, jakmile jej správce sloučí a vyjde nová verze. -> 💡 **Tip:** Keep translations short. UI strings often appear in buttons, chips, or narrow columns. If a translation is significantly longer than the English original, consider abbreviating where the meaning stays clear. +> 💡 **Tip:** Překlady udržujte krátké. UI řetězce se často zobrazují na tlačítkách, štítcích nebo v úzkých sloupcích. Pokud je překlad výrazně delší než anglický originál, zvažte jeho zkrácení, pokud zůstane význam jasný. -## Adding a New Language +## Přidání nového jazyka -If your language is not yet listed on Crowdin: +Pokud váš jazyk ještě není na Crowdinu uveden: -1. Open an issue on [GitHub](https://github.com/meshtastic/Meshtastic-Android/issues/new) requesting the new locale. -2. A maintainer will add the language to Crowdin and configure `crowdin.yml`. -3. Once added, you can begin translating immediately. +1. Otevřete problém na [GitHub](https://github.com/meshtastic/Meshtastic-Android/issues/new) s žádostí o přidání nového jazyka. +2. Správce přidá jazyk do Crowdinu a nastaví `crowdin.yml`. +3. Jakmile bude přidán, můžete začít překládat. -## How Translations Are Organized +## Jak jsou překlady uspořádány The Android app uses **Compose Multiplatform resources** for all user-visible strings: -``` +```text core/resources/src/commonMain/composeResources/ ├── values/ ← English (default) │ └── strings.xml @@ -58,7 +58,7 @@ core/resources/src/commonMain/composeResources/ In-app documentation follows a similar pattern under `docs/`: -``` +```text docs/ ├── en/user/ ← English source (default) │ ├── onboarding.md @@ -77,20 +77,20 @@ The app automatically selects the correct locale based on your phone's **Languag A page that came from Crowdin is labeled **Community translated** under its title in the app. If a page has no Crowdin translation for your language yet, the Google-flavor Android build machine-translates the English source on the fly and labels it **Auto-translated** instead; F-Droid and desktop builds show the English page. Your Crowdin translation replaces the machine one as soon as it lands. -## Translation Guidelines +## Pokyny pro překlad -- **Do not translate** technical terms like "LoRa", "MQTT", "BLE", "TAK", "SNR", or "RSSI" — these are universal. -- **Keep placeholders intact.** Strings like `%1$s` or `%d` are filled in at runtime. Do not remove or reorder them unless the grammar of your language requires it. -- **Match tone.** The app uses a friendly, direct voice. Avoid overly formal language. -- **Test if possible.** Switch your phone's language and open the app to see how translations look in context. +- Technické výrazy jako „LoRa“, „MQTT“, „BLE“, „TAK“, „SNR“ nebo „RSSI“ nepřekládejte – jsou univerzální. +- \*\*Zástupné symboly ponechte beze změny. \*\* Řetězce jako `%1$s` nebo `%d`se doplňují za běhu aplikace. Neodstraňujte je ani neměňte jejich pořadí, pokud to nevyžaduje gramatika vašeho jazyka. +- **Dodržujte tón komunikace.** Aplikace používá přátelský a přímý styl. Vyhněte se příliš formálnímu jazyku. +- **Pokud můžete, překlad otestujte.** Přepněte jazyk telefonu a otevřete aplikaci, abyste viděli, jak překlady vypadají v kontextu. -## Questions? +## Máte dotazy? -If you have questions about a specific string's context or need help getting started, open a discussion on the [Meshtastic GitHub Discussions](https://github.com/orgs/meshtastic/discussions) page. +Pokud máte dotazy ke kontextu konkrétního řetězce nebo potřebujete pomoct se začátky, založte diskuzi na stránce [Meshtastic GitHub Discussions](https://github.com/orgs/meshtastic/discussions). -Thank you for helping expand the reach of Meshtastic. +Děkujeme, že pomáháte rozšiřovat Meshtastic. -## Related Topics +## Související témata - [Units & Locale](units-and-locale) — how the app picks number, date, and unit formats for your region - [Help & Documentation](help-and-docs) — the in-app docs browser these pages are published to diff --git a/docs/cs-rCZ/user/widget.md b/docs/cs-rCZ/user/widget.md index 123e48d9b4..53492bd92a 100644 --- a/docs/cs-rCZ/user/widget.md +++ b/docs/cs-rCZ/user/widget.md @@ -1,50 +1,50 @@ --- -title: Home Screen Widget +title: Widget na domovské obrazovce parent: Uživatelská příručka nav_order: 20 last_updated: 2026-08-30 -description: Add the Meshtastic home screen widget to glance at your connected radio's local stats without opening the app. +description: Přidejte widget Meshtastic na domovskou obrazovku a mějte přehled o místních statistikách připojeného rádia bez nutnosti otevírat aplikaci. aliases: - widget - home-screen-widget - local-stats-widget --- -# Home Screen Widget +# Widget na domovské obrazovce -On Android, Meshtastic provides a home screen **widget** that shows live local statistics from your connected radio at a glance — no need to open the app. +V systému Android nabízí Meshtastic **widget** na domovskou obrazovku, který na první pohled zobrazuje aktuální místní statistiky připojeného rádia – bez nutnosti otevírat aplikaci. -## What It Shows +## Co zobrazuje -The widget displays the **connected radio's** current local stats: +Widget zobrazuje aktuální místní statistiky **připojeného rádia**: -- A **node chip** across the top, carrying the radio's short name in its own colors -- **Battery** — the radio's battery level, or _Powered_ when running on external power -- **ChUtil** — channel utilization (how busy the LoRa channel is, as a percentage) -- **AirUtil** — airtime utilization (how much of the duty cycle your radio is transmitting) -- **Traffic** — packets transmitted / received, and duplicates seen -- **Relays** — packets relayed and relay cancellations (shown when the radio is relaying) -- **Diagnostics** — a combined line carrying **Noise** (the background noise level in dBm), **Bad** (corrupt packets received), and **Dropped** (packets the radio discarded). Bad and Dropped appear only once they are above zero, so a quiet radio may show the noise reading alone -- **Heap** — free versus total memory on the radio, drawn as a bar -- **Nodes** — how many nodes are online, out of the total known -- **Uptime** — how long the radio has been running since its last reboot, shown beside Nodes -- **Updated** — the time the stats last refreshed, along the foot of the widget +- Nahoře je **štítek uzlu** s krátkým názvem rádia v jeho vlastních barvách +- **Baterie** – úroveň nabití baterie rádia, nebo _Napájeno_ při napájení z externího zdroje +- **ChUtil** — využití kanálu (jak je kanál LoRa vytížený, v procentech) +- **AirUtil** — využití vysílacího času (jakou část povoleného vysílacího cyklu rádio využívá k vysílání) +- **Provoz** – pakety odeslané / přijaté a zaznamenané duplikáty +- **Přeposláno** – přeposlané pakety a zrušení přeposílání (zobrazuje se, když rádio přeposílá pakety) +- **Diagnostiky** – společný řádek s údaji **Šum** (úroveň okolního šumu v dBm), **Poškozené** (přijaté poškozené pakety) a **Zahozené** (pakety, které rádio zahodilo). Poškozené a zahozené pakety se zobrazí až při hodnotě vyšší než nula, takže u rádia bez provozu se může zobrazovat pouze hodnota šumu +- **Halda** — volná a celková paměť rádia, znázorněná pomocí pruhu +- **Uzly** — počet uzlů online z celkového počtu známých uzlů +- **Doba provozu** — doba provozu rádia od posledního restartu, zobrazená vedle položky Uzly +- **Aktualizováno** – čas poslední aktualizace statistik, zobrazený v dolní části widgetu -Tap the widget to open the app, or use its refresh control to request fresh stats. +Klepnutím na widget otevřete aplikaci nebo pomocí tlačítka obnovení vyžádejte aktuální statistiky. -> ℹ️ **Note:** The values reflect the connected radio. If the radio disconnects, the widget replaces the stats with a status line — **Disconnected**, **Connecting**, or **Device sleeping**. It does not keep the last-known numbers on screen. +> ℹ️ **Poznámka:** Hodnoty se vztahují k připojenému rádiu. Pokud se rádio odpojí, widget nahradí statistiky stavovým řádkem – **Odpojeno**, **Připojování** nebo **Zařízení spí**. Nezobrazuje poslední známé hodnoty. -## Adding the Widget +## Přidání widgetu -1. Touch & hold an empty area of your Android home screen. -2. Tap **Widgets**. -3. Drag the **Meshtastic** widget to your home screen. The app ships one widget, so the picker entry is just the app name. -4. Resize it as needed — the layout adapts to the available space. +1. Dotkněte se prázdného místa na domovské obrazovce Androidu a podržte jej. +2. Klepněte na položku **Widgety**. +3. Přetáhněte widget Meshtastic na domovskou obrazovku. Aplikace obsahuje jeden widget, takže v nabídce widgetů se zobrazuje pouze název aplikace. +4. Podle potřeby změňte jeho velikost – rozložení se přizpůsobí dostupnému prostoru. -> ℹ️ **Note:** The widget is Android-only. It is not available on the Desktop or iOS builds. +> ℹ️ **Poznámka:** Widget je k dispozici pouze pro Android. Není k dispozici ve verzích pro Desktop ani iOS. -## Related Topics +## Související témata -- [Node Metrics](node-metrics) — the full Signal Quality and Local Stats history inside the app -- [Connections](connections) — connect to a radio so the widget has stats to show +- [Metriky uzlu](node-metrics) — kompletní historie kvality signálu a místních statistik přímo v aplikaci +- [Připojení](connections) — připojte se k rádiu, aby widget mohl zobrazovat statistiky - [Local Mesh Discovery](discovery) — channel and airtime utilization across the mesh diff --git a/docs/de-rDE/user/desktop.md b/docs/de-rDE/user/desktop.md index c2443730c1..4c9ccd44d8 100644 --- a/docs/de-rDE/user/desktop.md +++ b/docs/de-rDE/user/desktop.md @@ -2,7 +2,7 @@ title: Desktop App parent: User Guide nav_order: 14 -last_updated: 2026-08-30 +last_updated: 2026-09-11 description: Install and use the Meshtastic Desktop app on Linux, macOS, and Windows — connections, feature parity, and keyboard shortcuts. aliases: - desktop @@ -58,20 +58,20 @@ Bluetooth Low Energy is supported on desktop via the [Kable](https://github.com/ ## Feature Parity -| Funktion | Android | Desktop | Knoten | -| ----------------------------------------------------- | ------- | ------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Messaging | ✓ | ✓ | Full parity | -| Node List | ✓ | ✓ | Full parity | -| Karte | ✓ | ✓ | Interactive MapLibre map, with base map and overlay pickers and custom tile sources. No offline downloads or local `.mbtiles` archives | -| Map layers (`.kml`/`.kmz`/GeoJSON) | ✓ | ✓ | Same layer store and sheet as Android; imported files draw on the desktop map | -| Site Planner | ✓ | ✓\* | \*Opens in your browser on desktop; the estimate is not drawn on the desktop map | -| Einstellungen | ✓ | ✓ | Full parity | -| Bluetooth (BLE) | ✓ | ✓ | Via Kable on desktop | -| Firmware Aktualisierung | ✓ | ✓ | In-app USB, BLE, and Wi-Fi (ESP32) update work the same as Android. The USB maintenance flow — nRF52/RP2040 factory erase and bootloader upgrade — is Android-only | -| Benachrichtigungen | ✓ | ✓ | Native OS notifications | -| Widgets | ✓ | ✗ | Android-only | -| AI Assistant (Chirpy) | ✓\* | ✗ | Google flavor Android only | -| App Functions (system AI) | ✓† | ✗ | Google flavor Android only | +| Funktion | Android | Desktop | Knoten | +| ----------------------------------------------------- | ------- | ------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Messaging | ✓ | ✓ | Full parity | +| Node List | ✓ | ✓ | Full parity | +| Karte | ✓ | ✓ | Interactive MapLibre map, with base map and overlay pickers and custom tile sources. Offline terrain (hillshade and contours) can be downloaded; offline base-map packs and local `.mbtiles` archives cannot | +| Map layers (`.kml`/`.kmz`/GeoJSON) | ✓ | ✓ | Same layer store and sheet as Android; imported files draw on the desktop map | +| Site Planner | ✓ | ✓\* | \*Opens in your browser on desktop; the estimate is not drawn on the desktop map | +| Einstellungen | ✓ | ✓ | Full parity | +| Bluetooth (BLE) | ✓ | ✓ | Via Kable on desktop | +| Firmware Aktualisierung | ✓ | ✓ | In-app USB, BLE, and Wi-Fi (ESP32) update work the same as Android. The USB maintenance flow — nRF52/RP2040 factory erase and bootloader upgrade — is Android-only | +| Benachrichtigungen | ✓ | ✓ | Native OS notifications | +| Widgets | ✓ | ✗ | Android-only | +| AI Assistant (Chirpy) | ✓\* | ✗ | Google flavor Android only | +| App Functions (system AI) | ✓† | ✗ | Google flavor Android only | \*Chirpy AI requires Android 14+ on Google flavor builds with supported hardware. @@ -122,7 +122,7 @@ Individual doc pages render with full formatting: ## Building from Source -```bash +```shell git clone https://github.com/meshtastic/Meshtastic-Android.git cd Meshtastic-Android ./gradlew :desktopApp:run @@ -135,7 +135,7 @@ Voraussetzungen: ## Bekannte Einschränkungen -- Offline tile downloads and local `.mbtiles` archives are not available on desktop. +- Offline base-map downloads and local `.mbtiles` archives are not available on desktop. Offline terrain is — see [Map & Waypoints](map-and-waypoints). - `.kml`/`.kmz`/GeoJSON layer import works — see [Map & Waypoints](map-and-waypoints#map-layers). Site Planner opens in your browser rather than in the app; to bring its coverage estimate onto the map, click the transmitter pin diff --git a/docs/de-rDE/user/discovery.md b/docs/de-rDE/user/discovery.md index 565653e2ce..e354e13e5c 100644 --- a/docs/de-rDE/user/discovery.md +++ b/docs/de-rDE/user/discovery.md @@ -2,7 +2,7 @@ title: Local Mesh Discovery parent: User Guide nav_order: 12 -last_updated: 2026-08-30 +last_updated: 2026-09-11 description: Explore your mesh network — the Local Mesh Discovery scanner, traceroute paths, neighbor maps, and node discovery tools. aliases: - discovery @@ -97,7 +97,7 @@ Mesh Beacon lets nodes invite others to join their mesh. A beaconing node period Configure it under **Settings → Module configuration → Mesh Beacon**. The entry appears only on radios running firmware 2.8.0 or newer. A read-only **Region** row at the top of the screen shows the region the beacon advertises: that region, and the preset, are always the ones the radio itself uses, so a beacon cannot invite anyone onto settings your radio is not running. - **Listen for beacons** — receive invitations broadcast by other nodes. -- **Broadcast a beacon** — periodically advertise this mesh to nearby nodes, with an optional **Beacon message** of up to 100 bytes, a **Broadcast interval** picked from fixed intervals between 1 hour and 72 hours, and an **Offered channel** chosen from your radio's own channels. The offered channel is required, and defaults to your primary channel. +- **Broadcast a beacon** — periodically advertise this mesh to nearby nodes, with an optional **Beacon message** of up to 100 bytes, a **Broadcast interval** picked from fixed intervals between 1 hour and 72 hours, and an **Offered channel** chosen from your radio's own channels. The offered channel is required, and defaults to your primary channel. Over remote admin the picker offers the primary channel only. - **Broadcast targets** — optional extra destinations beyond the offered channel. **Add target** appends a row; each row picks a **Channel** and a **Transmit preset**, and **Remove target** deletes it. With no targets, the beacon goes out on the offered channel alone. Two conditions block beacon setup: diff --git a/docs/de-rDE/user/help-and-docs.md b/docs/de-rDE/user/help-and-docs.md index 3ccf049786..bddd7be107 100644 --- a/docs/de-rDE/user/help-and-docs.md +++ b/docs/de-rDE/user/help-and-docs.md @@ -2,7 +2,7 @@ title: Help & In-App Docs parent: Benutzerhandbuch nav_order: 21 -last_updated: 2026-08-30 +last_updated: 2026-09-11 description: Browse this documentation inside the app, search it, and ask Chirpy — the on-device AI assistant — questions about Meshtastic. aliases: - Hilfe @@ -23,7 +23,7 @@ The docs browser lists every user-guide page. Tap a page to read it; images and ### Suchen -The search box sits at the top of the docs browser. Type in it to filter pages by title and keywords — results update as you type, and the **Clear search** button (✕) empties the box. +Tap the search field at the top of the docs browser to open a full-screen search view. Type to filter pages by title and keywords — results update as you type, under the same field. The back arrow, or the keyboard's search action, closes the view. The field hides while you scroll down the page list and comes back when you scroll up. ![Searching the in-app documentation](../../assets/screenshots/docs-browser_search.png) diff --git a/docs/de-rDE/user/map-and-waypoints.md b/docs/de-rDE/user/map-and-waypoints.md index 9200443fe6..bf362012c6 100644 --- a/docs/de-rDE/user/map-and-waypoints.md +++ b/docs/de-rDE/user/map-and-waypoints.md @@ -2,7 +2,7 @@ title: Map & Waypoints parent: User Guide nav_order: 6 -last_updated: 2026-09-01 +last_updated: 2026-09-11 description: View node positions on the map, create and share waypoints, manage map layers and Site Planner, and control position sharing and privacy. aliases: - map @@ -44,7 +44,7 @@ The floating toolbar provides quick access to the compass, the map type and laye ### Filtering the Map -Tap the filter button in the floating toolbar to open **Filter map**. **Display** controls what is drawn: **Only Favorites**, **Show Waypoints**, **Show Precision Circles**, and a slider that hides nodes not heard from recently. **Node roles** is a chip per device role, plus **All** to show every role; a selected chip means that role is shown. **Nodes** narrows the set further with **Hide offline nodes**, **Only show direct nodes**, **Exclude MQTT**, **Show ignored nodes**, and **Include unknown**. +Tap the filter button in the floating toolbar to open **Filter map**. **Display** controls what is drawn: **Only Favorites**, **Show Waypoints**, **Show Precision Circles**, and a slider that hides nodes not heard from recently. **Node roles** is a chip per device role, plus **All** to show every role; a selected chip means that role is shown. **Nodes** narrows the set further with **Hide offline nodes**, **Only show direct nodes**, **Signed only**, **Encrypted only**, **Exclude MQTT**, **Show ignored nodes**, and **Include unknown** — the same rules as the node list's [filter toggles](nodes). A dot on the filter button means at least one filter is hiding something — check it before concluding the mesh is quiet. Turning **Show Waypoints** off hides every waypoint, including your own. **Show ignored nodes** adds them to the map rather than showing only them — unlike the node list's **Only show ignored Nodes**. @@ -147,8 +147,9 @@ picker, all three offer the same raster base maps: Overlays can be toggled on top of any base map, from the layers sheet: - **Weather radar** — NOAA NEXRAD reflectivity (US coverage) -- **Hillshade** — terrain relief, on **F-Droid** and **Desktop** only. Useful for understanding why a - link fails, since LoRa range is limited by terrain +- **Hillshade** — terrain relief from an online tile source, on **F-Droid** and **Desktop** only; Google + Play draws hillshade from downloaded offline terrain instead. Useful for understanding why a link + fails, since LoRa range is limited by terrain ### Adding your own tile source @@ -156,7 +157,7 @@ Any XYZ tile endpoint can be added as a base map, on every flavor and on desktop Tile Sources** at the foot of the base map picker and paste a URL template using `{z}`, `{x}` and `{y}` — plus `{s}` if the provider uses rotating subdomains. A national mapping service, for example: -``` +```text https://wmts.geo.admin.ch/1.0.0/ch.swisstopo.pixelkarte-farbe/default/current/3857/{z}/{x}/{y}.jpeg ``` @@ -173,7 +174,18 @@ On **Google Play**, the layers sheet's **Offline Manager** section downloads its — water, roads and administrative boundaries extracted from the public Protomaps basemap dataset, drawn directly as map shapes rather than raster tiles. Frame the area you want, tap **Start Download**, and once it finishes flip **Show on map** to draw it (it's off by default so it never surprises you by covering up the -live map). You can still import a pre-made `.mbtiles` archive here too, as before. **Desktop** has neither. +live map). You can still import a pre-made `.mbtiles` archive here too, as before. **Desktop** has neither, though +the offline terrain described next does work there. + +### Offline terrain + +Hillshade and elevation contours can be downloaded for offline use as well, so a downloaded area still +shows why a link fails when there is no network. The terrain data comes from Mapterhorn and is credited on +the map. On **Google Play**, each downloaded region in the **Offline Manager** list has a **Download Terrain** +button and **Hillshade** and **Contours** switches; terrain attaches to that region and shares its storage limit, and an area +too large for terrain asks you to zoom in or pick a smaller region. On **F-Droid** and **Desktop**, the +layers sheet has an **Offline Terrain** section of its own: frame the area, tap **Start Download**, and the +hillshade and contours draw from the downloaded tiles from then on. ### Going offline @@ -181,8 +193,8 @@ An **Offline** pill appears over the map whenever the device has no network conn builds, if you've imported a local `.mbtiles` archive, the map switches to it automatically the moment the network drops — no toggle to remember — and switches back once you're reconnected, as long as you haven't picked a different base map yourself in between. On **F-Droid**, a downloaded offline area keeps rendering on -its own; the pill is purely informational there. **Desktop** has no offline downloads at all yet, so the pill -is informational there too. +its own; the pill is purely informational there. **Desktop** downloads offline terrain but no base-map +packs, so the pill is informational there too. ## Related Topics diff --git a/docs/de-rDE/user/messages-and-channels.md b/docs/de-rDE/user/messages-and-channels.md index 71a0cd1306..c05d5adf5c 100644 --- a/docs/de-rDE/user/messages-and-channels.md +++ b/docs/de-rDE/user/messages-and-channels.md @@ -2,7 +2,7 @@ title: Messages & Channels parent: User Guide nav_order: 3 -last_updated: 2026-08-30 +last_updated: 2026-09-14 description: Send and receive messages, manage channels, configure encryption, search conversations, and use quick chat, reactions, and message actions. aliases: - channels @@ -50,6 +50,21 @@ Key length alone does not change the icon: a 128-bit key and a 256-bit key both Tapping an existing channel opens the same editor, where you can change the name, the PSK, MQTT uplink and downlink, and position precision. Every edit on this screen — adding, editing, deleting, or dragging a channel into a new order — waits on **Send** the same way. +### Archived Channels + +A conversation belongs to the channel it was held on, not to the slot that channel occupied. When a channel leaves your radio — you scan a different channel QR, replace a channel's key, or switch the modem preset — its conversation is **archived** rather than deleted or merged into whatever takes the slot. + +An archived conversation: + +- stays in the **Channels** list under the name the channel had, below the channels currently on your radio, marked with a history icon; +- keeps its full message history, searchable as usual; +- is read-only — you cannot send, reply, or react, because the channel is no longer on the radio to send on, and it is not offered as a share target; +- comes back automatically if you re-add that exact channel, taking its messages back to the live slot. + +Changing the modem preset archives the channel too: the preset is part of a default channel's identity, so **LongFast** and **MediumFast** are different channels even though neither the name nor the key changed. Changing only the region does not archive anything. + +To remove an archived conversation for good, long-press it in the conversation list and delete it. + ## Direktnachrichten Direct messages (DMs) go to one specific node. When both radios hold each other's public keys, your radio encrypts the message to that node's public key, so no one else on the mesh can read it — not even nodes that share your channel. diff --git a/docs/de-rDE/user/mqtt.md b/docs/de-rDE/user/mqtt.md index 844c67c661..8d554c5088 100644 --- a/docs/de-rDE/user/mqtt.md +++ b/docs/de-rDE/user/mqtt.md @@ -2,7 +2,7 @@ title: MQTT parent: User Guide nav_order: 11 -last_updated: 2026-08-30 +last_updated: 2026-09-11 description: Bridge your mesh to the internet — MQTT broker setup, encryption layers, and map reporting. aliases: - mqtt @@ -25,7 +25,7 @@ The MQTT module connects your node to an MQTT broker, allowing: ## So funktioniert's -``` +```text [Your Node] → Radio → [Gateway Node with Wi-Fi] → MQTT Broker → [Remote Gateway] → Radio → [Remote Node] ``` diff --git a/docs/de-rDE/user/node-metrics.md b/docs/de-rDE/user/node-metrics.md index 235c4dd311..12cff861ca 100644 --- a/docs/de-rDE/user/node-metrics.md +++ b/docs/de-rDE/user/node-metrics.md @@ -2,7 +2,7 @@ title: Node Metrics parent: User Guide nav_order: 5 -last_updated: 2026-08-30 +last_updated: 2026-09-09 description: Telemetry dashboards for each mesh node — device health, environment sensors, air quality, signal quality, power, traceroute, and position history. aliases: - metrics @@ -114,7 +114,7 @@ Radio signal quality information: ### Signal Quality Reference -Die Signalqualität wird auf der Grundlage des **Signal-Rauschabstand im Verhältnis zur Demodulationsschwelle der aktiven LoRa-Modem-Voreinstellung** bewertet, nicht anhand fester Schwellenwerte – ein bestimmter Wert des Signal-Rauschabstand hat je nach Voreinstellung unterschiedliche Bedeutungen (z. B. sind −15 dB bei „LongSlow“ in Ordnung, bei „ShortFast“ jedoch unbrauchbar). RSSI is shown but is not part of the rating. In the table, _limit_ is the preset's SNR limit. +Die Signalqualität wird auf der Grundlage des **Signal-Rauschabstand im Verhältnis zur Demodulationsschwelle der aktiven LoRa-Modem-Voreinstellung** bewertet, nicht anhand fester Schwellenwerte – ein bestimmter Wert des Signal-Rauschabstand hat je nach Voreinstellung unterschiedliche Bedeutungen (z. B. sind −15 dB bei „LongSlow“ in Ordnung, bei „ShortFast“ jedoch unbrauchbar). When RSSI and a noise-floor reading are both available, the app also rates their difference against the preset limit and uses the worse rating. Otherwise, RSSI is display-only. In the table, _limit_ is the preset's SNR limit. | Quality | Criteria | | ------------------------ | ---------------------------------------------------------------- | diff --git a/docs/de-rDE/user/nodes.md b/docs/de-rDE/user/nodes.md index 47b57be0bf..fdbe8543bb 100644 --- a/docs/de-rDE/user/nodes.md +++ b/docs/de-rDE/user/nodes.md @@ -2,7 +2,7 @@ title: Knoten parent: User Guide nav_order: 4 -last_updated: 2026-09-04 +last_updated: 2026-09-11 description: Browse, filter, and sort mesh nodes — view details, signal quality, roles, and quick actions. aliases: - node-list @@ -74,19 +74,23 @@ Most users should keep the default **Client** role. Consider a different role wh > 💡 **Tip:** The mesh works best when most nodes are **Client** or **Router**. Too many Client Mute nodes reduce mesh resilience; too many Routers in a dense area can cause congestion. A good rule of thumb: one Router per 5–10 Clients in your area. -### Encryption Indicators +### Security indicators -Nodes display encryption status icons next to their name: +Each node carries one security icon beside its name in the node list. Tap it to read what it means, and choose **Show All Meanings** in that dialog for the full legend. The detail screen shows the same state in words, as a **Security** row that opens the same dialog. -| Symbol | Meaning | -| ----------- | ------------------------------------------------------------------------------------------------------------------------------------------------- | -| 🔒 Locked | Communication uses PKI (public key infrastructure) — end-to-end encrypted with verified identity | -| 🔓 Unlocked | No public key has been received for this node, so it cannot be direct messaged — use **Request User Info** on the node detail page to ask for one | -| ⚠️ Mismatch | Public key mismatch — the node's key has changed since last seen (investigate before trusting) | +| Symbol | Meaning | Shown for | +| --------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------- | +| Person with a shield check (green) | **Verified contact** — you verified this node's key in person by exchanging contact QR codes, so its identity is confirmed. The strongest trust the list shows. Your own connected node carries it too | Any firmware version | +| Nodes icon with a shield check (green) | **Signed node** — this node signs its broadcasts with its identity key, so its identity is consistent over time, but you have not verified it in person. On firmware 2.8 the icon appears from the version alone, before any signed broadcast has been heard | Firmware 2.8 or newer, and any node whose signed broadcast your node has verified | +| 🔒 Closed lock | A public key is on file and matches, so direct messages to this node are encrypted | Firmware before 2.8, or no reported version | +| 🔓 Open lock | No public key has been received for this node, so it cannot be direct messaged — use **Request User Info** on the node detail page to ask for one | Firmware before 2.8, or no reported version | +| ⚠️ Mismatch | **Public key mismatch** — a different key arrived for this node after one was stored. Investigate before trusting | Any firmware version | -> 💡 **Tip:** Direct messages always use PKI, so the radio needs the other node's public key before it can send one. It refuses the send rather than falling back to channel encryption. Keys arrive inside node info, which is why an open lock usually clears itself once that node is heard from properly. If you see a key mismatch warning, the node may have been reset or compromised. +Every node on firmware 2.8 or newer signs its broadcasts, so on that firmware the signed state is the baseline and the locks are not shown. -To clear a mismatch, first confirm through another trusted channel that the key change was intentional — a factory reset causes one. Then touch & hold the node, choose **Remove**, and let the two radios exchange keys again the next time yours hears it. +Direct messages always use public-key encryption, so your node needs the other node's public key before it can send one. It refuses the send rather than falling back to channel encryption. Keys arrive inside node info, which is why an open lock usually clears itself once that node is heard from properly. + +A mismatch never replaces the key you already hold. The app keeps the first key it recorded and refuses the new one, as the firmware does, so a stray or hostile node info cannot silently break encrypted messaging to a contact. To clear a mismatch, first confirm through another trusted channel that the key change was intentional — a factory reset causes one. Then touch & hold the node, choose **Remove**, and let the two nodes exchange keys again the next time yours hears it. ## Quick Actions @@ -108,7 +112,9 @@ radio is connected and running firmware 2.8 or newer — see ## Sharing a Contact -On a node's detail screen, tap **Share Contact** to produce a link and a QR code for that node. From the same dialog, **Write to NFC tag** saves the link to a writable NFC tag that anyone can tap to open. +On a node's detail screen, tap **Share Contact** to produce a link and a QR code for that node. From the same dialog, **Share link** opens the Android share sheet (on desktop it copies the link instead), **Write to NFC tag** saves it to a writable NFC tag, and **Copy** puts it on the clipboard. While that dialog is open and in front of you, the phone also offers the same link to any NFC reader, so someone can take the contact by tapping their phone against yours with no tag involved. + +Sharing your own contact this way marks it as verified in person, so whoever imports it sees the **Verified contact** icon rather than the signed one. Relaying someone else's contact passes on only what your app had already recorded about them. To add someone else's contact, use the import button on the node list and choose **Scan Shared Contact QR Code**, **Scan Shared Contact NFC**, or **Input Shared Contact URL**. The app asks you to confirm with **Import Shared Contact?**, and warns you when the contact is one you already have. @@ -120,14 +126,16 @@ Type in the search field to filter nodes by name or short name. The filter updat ### Filter Toggles -| Filter | Beschreibung | -| --------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| **Hide offline nodes** | Show only nodes heard within the last 2 hours | -| **Only show direct nodes** | Show only nodes your radio heard directly, with no relay in between | -| **Include unknown** | Show nodes that haven't sent user info yet. **On by default**, so a node heard before its info arrives stays visible; these carry a badge marking them incomplete, and cannot be direct messaged until their user info brings a public key | -| **Exclude infrastructure** | Hide infrastructure-role nodes (Router, Router Late, Client Base, and legacy Repeater nodes) and any node that cannot be messaged, whatever its role | -| **Exclude MQTT** | Hide nodes heard only via MQTT internet bridge | -| **Only show ignored Nodes** | Replace the list with the nodes you have ignored. Every other node is hidden while this is on, and a banner appears at the top of the list to take you back | +| Filter | Beschreibung | +| --------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **Hide offline nodes** | Show only nodes heard within the last 2 hours | +| **Only show direct nodes** | Show only nodes your radio heard directly, with no relay in between | +| **Include unknown** | Show nodes that haven't sent user info yet. **On by default**, so a node heard before its info arrives stays visible; these carry a badge marking them incomplete, and cannot be direct messaged until their user info brings a public key | +| **Exclude infrastructure** | Hide infrastructure-role nodes (Router, Router Late, Client Base, and legacy Repeater nodes) and any node that cannot be messaged, whatever its role | +| **Exclude MQTT** | Hide nodes heard only via MQTT internet bridge | +| **Signed only** | Show only nodes whose signed broadcasts your node has actually heard and verified. Stricter than the icon: a node on 2.8 shows as signed by its firmware version before any signed broadcast arrives, and a contact verified in person on older firmware is not signed at all | +| **Encrypted only** | Show nodes with a matching public key on file, the key an encrypted direct message needs. A node with a key mismatch is excluded | +| **Only show ignored Nodes** | Replace the list with the nodes you have ignored. Every other node is hidden while this is on, and a banner appears at the top of the list to take you back | ### Sort Options @@ -149,6 +157,8 @@ Tap the hop-histogram icon in the node list's app bar to open a bar chart of how Tapping a node opens the detail view with comprehensive information. See [Node Metrics](node-metrics) for full details on metrics and telemetry. +Signal quality is rated against your modem preset. The same SNR can be good on a long-range preset and poor on a faster one. Traceroute and neighbor-info SNR colors use that preset too. RSSI text has its own strength colors; it affects the quality rating only when a noise-floor reading is also available. + The Details card carries the node's short name, role, IDs, last heard time, hops away, uptime, and its SNR and RSSI: ![Node detail section](../../assets/screenshots/nodes_detail_section.png) diff --git a/docs/de-rDE/user/settings-module-admin.md b/docs/de-rDE/user/settings-module-admin.md index b603825aca..6cd5661d10 100644 --- a/docs/de-rDE/user/settings-module-admin.md +++ b/docs/de-rDE/user/settings-module-admin.md @@ -2,7 +2,7 @@ title: Settings — Modules & Admin parent: User Guide nav_order: 8 -last_updated: 2026-08-30 +last_updated: 2026-09-16 description: Configure optional feature modules (MQTT, telemetry, canned messages, TAK, and more) and perform device administration. aliases: - modules @@ -333,7 +333,7 @@ appear only when your own node is selected: ### Über -**Settings → About** carries the app's own identity rather than the radio's: +**Settings → About** carries the app's own identity rather than the node's: Three sections: @@ -341,7 +341,11 @@ Three sections: - **Apps** — opens with **Need Hardware?**, a rotating carousel of popular devices that links out to where to buy one. It also lists the GitHub repository, the running app version, and **Acknowledgements** (see the next section). -- **Project information** — links to the website and to this documentation. +- **Project information** — links to the website, to this documentation, and to the GNU GPL v3 + license the app is released under. + +The footer states the copyright and that the app is free software with no warranty, which you +may redistribute under the same license. ### Danksagungen diff --git a/docs/de-rDE/user/settings-radio-user.md b/docs/de-rDE/user/settings-radio-user.md index ece9942983..8077bac73b 100644 --- a/docs/de-rDE/user/settings-radio-user.md +++ b/docs/de-rDE/user/settings-radio-user.md @@ -2,7 +2,7 @@ title: Settings — Radio & User parent: User Guide nav_order: 7 -last_updated: 2026-09-04 +last_updated: 2026-09-11 description: Configure your radio hardware, LoRa presets, user profile, position sharing, power management, and security. aliases: - Einstellungen @@ -51,7 +51,7 @@ The footer appears as soon as you change something. **Discard** throws the chang The status message is saved with the same **Save**, but it never reboots the node — and, like the rest of this screen, it can be edited on a remote node you administer. For your own radio there is a shortcut while it is connected: touch & hold your node in the [node list](nodes.md) and choose -**Update status**. Older firmware and a disconnected radio have no shortcut — the field above is +**Update status**. Older firmware and a disconnected node have no shortcut — the field itself is still the way in. ## Einstellungen @@ -77,13 +77,13 @@ On **Settings → LoRa**. | Einstellung | Beschreibung | Standardwert | | -------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------- | -| Region | Regulatory region for frequency bands. You must set this before transmitting | Unset (must configure) | +| Region | Regulatory region for frequency bands. You must set this before transmitting. On firmware 2.8 or newer, the first region set also creates the node's identity key and gives it a new node number | Unset (must configure) | | Voreinstellungen | Speed/range tradeoff | LongFast | | Anzahl der Weiterleitungen | Maximum retransmit hops | 3 | | Sendeleistung | Transmission power (dBm); 0 = max allowed for region | 0 (region max) | | Frequenz überschreiben | Overrides the computed operating frequency outright (MHz). It does not offset the calculated value — leave at 0 unless you know you need a specific frequency | 0 (use calculated) | | Voreinstellung verwenden | On by default. Turn it off to set Spread Factor, Coding Rate and Bandwidth by hand instead of taking them from the modem preset | On | -| Spreizfaktor | Manual mode only: 7–12. Higher spreads further but slower | From preset | +| Spreizfaktor | Manual mode only: 5–12. Higher spreads further but slower. On SX127x (RF95) radios the firmware does not accept 5 or 6 and uses 11 instead | From preset | | Fehlerkorrektur | Manual mode only: 5–8. More redundancy costs airtime | From preset | | Bandbreite | Manual mode only: the channel bandwidth in kHz, typed in directly. On the 2.4 GHz region the app offers a list of the bandwidths your radio supports instead, and a stored value that is not on that list shows as _Unsupported_ and blocks saving until you pick a supported one | From preset | | Frequenzschlitz | Which slot within the region's band to use. 0 derives it from the primary channel name | 0 (automatic) | @@ -111,13 +111,13 @@ The Lite, Narrow, Medium Turbo, and Tiny presets need firmware 2.8 or newer — | Short Slow | ~5 km | 6,25 kbit/s | −10 dB | Suburban short-range; moderate building density | | MEDIUM_FAST | ~5 km | 3,52 kbit/s | −12.5 dB | Suburban areas; moderate building density | | MEDIUM_SLOW | ~8 km | 1,95 kbit/s | −15 dB | Suburban/rural; moderate range with slower speed | -| Long Turbo | ~10 km | 1.34 kbps | −12.5 dB | Similar range to Long Fast but with 500 kHz bandwidth; faster throughput | +| Long Turbo | ~10 km | 1.34 kbps | −17.5 dB | Similar range to Long Fast but with 500 kHz bandwidth; faster throughput | | Long Fast | ~10 km | 1.1 kbps | −17.5 dB | **General use (default)** — balanced range and speed | | Long Moderate | ~20 km | 0,34 kbit/s | −17.5 dB | Rural with some terrain; occasional use | | Lite Fast | ~5 km | 1.76 kbps | −12.5 dB | EU 866 MHz SRD band (125 kHz BW); comparable to Medium Fast | | Lite Slow | ~10 km | 0.98 kbps | −15 dB | EU 866 MHz SRD band (125 kHz BW); comparable to Long Fast | -| Narrow Fast | ~5 km | 2.28 kbps | −10 dB | EU 868 MHz band (62.5 kHz BW); avoids interference with other devices | -| Narrow Slow | ~10 km | 1.30 kbps | −12.5 dB | EU 868 MHz band (62.5 kHz BW); comparable to Long Fast | +| Narrow Fast | ~5 km | 2.28 kbps | −7,5 dB | EU 868 MHz band (62.5 kHz BW); avoids interference with other devices | +| Narrow Slow | ~10 km | 1.30 kbps | −10 dB | EU 868 MHz band (62.5 kHz BW); comparable to Long Fast | | Medium Turbo | ~5 km | 7.0 kbps | −12,5 dB | Like Medium Fast but with 500 kHz bandwidth; not legal in every region. Needs firmware 2.8 or newer | | Tiny Fast | ~10 km | 0.68 kbps | −7,5 dB | Amateur bands that cap occupied bandwidth; these presets use 15.6 kHz. Needs firmware 2.8 or newer, an SX126x or SX127x radio, and a TCXO of ±5 ppm or better | | Tiny Slow | ~20 km | 0.33 kbps | −10 dB | Same band restrictions as Tiny Fast, longer range. Same firmware, radio, and TCXO requirements | diff --git a/docs/de-rDE/user/signal-meter.md b/docs/de-rDE/user/signal-meter.md index f7fb23ec2f..b08adf4f12 100644 --- a/docs/de-rDE/user/signal-meter.md +++ b/docs/de-rDE/user/signal-meter.md @@ -2,7 +2,7 @@ title: Wie das Meshtastic Signal Meter funktioniert parent: User Guide nav_order: 15 -last_updated: 2026-08-30 +last_updated: 2026-09-09 description: Wie das Signal Meter die Qualität von SNR relativ zum Modem preset bewertet - spread spectrum, presets, und was die Balken wirklich bedeuten. aliases: - Signal @@ -32,7 +32,7 @@ Each modem preset has an SNR limit: the lowest SNR at which that preset can stil ## Rating Signal Quality -The app rates signal quality (None, Bad, Fair, or Good) from SNR alone, measured against the active preset's SNR limit. It does not factor in RSSI: without knowing the local noise floor, RSSI alone cannot say whether a signal is decodable. RSSI is still available — on the node detail screen and in the metrics charts. +The app rates signal quality (None, Bad, Fair, or Good) from SNR relative to the active preset's SNR limit. When RSSI and a noise-floor reading are both available, it also rates the difference between them against that limit and uses the worse rating. Otherwise, it uses SNR alone. Because the rating is relative to the preset, the same SNR rates differently on different presets. An SNR of −16 dB rates Good on Long Fast (SNR limit −17.5 dB) but None on Short Fast (SNR limit −7.5 dB). Let `limit` be the active preset's SNR limit: @@ -45,7 +45,7 @@ Because the rating is relative to the preset, the same SNR rates differently on The icon never goes blank, so count the bars carefully: a single bar means None, not a weak but usable link, and Good is a solid wedge rather than a set of bars. A gray three-bar icon labeled Unknown is a different state again — the packet carried no SNR measurement at all, which is not the same as measuring one and finding it too weak. -> ℹ️ **Note:** Traceroute hop colors use fixed thresholds (−7 dB / −15 dB); the per-node signal meter uses the preset-relative rating instead. +> ℹ️ **Note:** Traceroute and neighbor-info SNR colors use the active preset's limits too. RSSI text has separate strength colors: green above −115 dBm, yellow above −120 dBm, orange above −126 dBm, and red at or below −126 dBm. ## Diagnosing Local Interference diff --git a/docs/de-rDE/user/tak.md b/docs/de-rDE/user/tak.md index a44977c9b8..5bb1531318 100644 --- a/docs/de-rDE/user/tak.md +++ b/docs/de-rDE/user/tak.md @@ -2,7 +2,7 @@ title: TAK Integration parent: User Guide nav_order: 10 -last_updated: 2026-08-30 +last_updated: 2026-09-11 description: Interoperate with ATAK and WinTAK — CoT position sharing, TAK roles, and plugin setup. aliases: - tak @@ -36,7 +36,7 @@ The TAK module allows Meshtastic nodes to: > ⚠️ **Warning:** The old **Meshtastic ATAK Plugin** is no longer part of this path and cannot > work. It bridged through the cross-process AIDL API, which was removed in app 2.8.0; the mesh > service is now in-process only. Do not install it. Interop today runs over the app's own local -> TAK server plus the Mesh to CoT Converter, both described below, with stock ATAK/iTAK/WinTAK. +> TAK server plus the Mesh to CoT Converter, both described in the following sections, with stock ATAK/iTAK/WinTAK. ### Einstellungen diff --git a/docs/de-rDE/user/translate.md b/docs/de-rDE/user/translate.md index 911e988efe..7c25f5ef5b 100644 --- a/docs/de-rDE/user/translate.md +++ b/docs/de-rDE/user/translate.md @@ -2,7 +2,7 @@ title: Translate the App parent: User Guide nav_order: 17 -last_updated: 2026-08-30 +last_updated: 2026-09-11 description: How the app and its documentation are translated via Crowdin, and guidelines for contributing translations. aliases: - translate @@ -45,7 +45,7 @@ If your language is not yet listed on Crowdin: The Android app uses **Compose Multiplatform resources** for all user-visible strings: -``` +```text core/resources/src/commonMain/composeResources/ ├── values/ ← Englisch (Standard) │ └── strings.xml @@ -58,7 +58,7 @@ core/resources/src/commonMain/composeResources/ In-app documentation follows a similar pattern under `docs/`: -``` +```text docs/ ├── en/user/ ← Englisch (Standard) │ ├── onboarding.md diff --git a/docs/el-rGR/user/desktop.md b/docs/el-rGR/user/desktop.md index 4f294cac28..1e8b760deb 100644 --- a/docs/el-rGR/user/desktop.md +++ b/docs/el-rGR/user/desktop.md @@ -2,7 +2,7 @@ title: Desktop App parent: User Guide nav_order: 14 -last_updated: 2026-08-30 +last_updated: 2026-09-11 description: Install and use the Meshtastic Desktop app on Linux, macOS, and Windows — connections, feature parity, and keyboard shortcuts. aliases: - desktop @@ -58,20 +58,20 @@ Bluetooth Low Energy is supported on desktop via the [Kable](https://github.com/ ## Feature Parity -| Feature | Android | Desktop | Notes | -| ----------------------------------------------------- | ------- | ------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Messaging | ✓ | ✓ | Full parity | -| Node List | ✓ | ✓ | Full parity | -| Χάρτης | ✓ | ✓ | Interactive MapLibre map, with base map and overlay pickers and custom tile sources. No offline downloads or local `.mbtiles` archives | -| Map layers (`.kml`/`.kmz`/GeoJSON) | ✓ | ✓ | Same layer store and sheet as Android; imported files draw on the desktop map | -| Site Planner | ✓ | ✓\* | \*Opens in your browser on desktop; the estimate is not drawn on the desktop map | -| Ρυθμίσεις | ✓ | ✓ | Full parity | -| Bluetooth (BLE) | ✓ | ✓ | Via Kable on desktop | -| Firmware Update | ✓ | ✓ | In-app USB, BLE, and Wi-Fi (ESP32) update work the same as Android. The USB maintenance flow — nRF52/RP2040 factory erase and bootloader upgrade — is Android-only | -| Notifications | ✓ | ✓ | Native OS notifications | -| Widgets | ✓ | ✗ | Android-only | -| AI Assistant (Chirpy) | ✓\* | ✗ | Google flavor Android only | -| App Functions (system AI) | ✓† | ✗ | Google flavor Android only | +| Feature | Android | Desktop | Notes | +| ----------------------------------------------------- | ------- | ------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Messaging | ✓ | ✓ | Full parity | +| Node List | ✓ | ✓ | Full parity | +| Χάρτης | ✓ | ✓ | Interactive MapLibre map, with base map and overlay pickers and custom tile sources. Offline terrain (hillshade and contours) can be downloaded; offline base-map packs and local `.mbtiles` archives cannot | +| Map layers (`.kml`/`.kmz`/GeoJSON) | ✓ | ✓ | Same layer store and sheet as Android; imported files draw on the desktop map | +| Site Planner | ✓ | ✓\* | \*Opens in your browser on desktop; the estimate is not drawn on the desktop map | +| Ρυθμίσεις | ✓ | ✓ | Full parity | +| Bluetooth (BLE) | ✓ | ✓ | Via Kable on desktop | +| Firmware Update | ✓ | ✓ | In-app USB, BLE, and Wi-Fi (ESP32) update work the same as Android. The USB maintenance flow — nRF52/RP2040 factory erase and bootloader upgrade — is Android-only | +| Notifications | ✓ | ✓ | Native OS notifications | +| Widgets | ✓ | ✗ | Android-only | +| AI Assistant (Chirpy) | ✓\* | ✗ | Google flavor Android only | +| App Functions (system AI) | ✓† | ✗ | Google flavor Android only | \*Chirpy AI requires Android 14+ on Google flavor builds with supported hardware. @@ -122,7 +122,7 @@ Individual doc pages render with full formatting: ## Building from Source -```bash +```shell git clone https://github.com/meshtastic/Meshtastic-Android.git cd Meshtastic-Android ./gradlew :desktopApp:run @@ -135,7 +135,7 @@ Requirements: ## Known Limitations -- Offline tile downloads and local `.mbtiles` archives are not available on desktop. +- Offline base-map downloads and local `.mbtiles` archives are not available on desktop. Offline terrain is — see [Map & Waypoints](map-and-waypoints). - `.kml`/`.kmz`/GeoJSON layer import works — see [Map & Waypoints](map-and-waypoints#map-layers). Site Planner opens in your browser rather than in the app; to bring its coverage estimate onto the map, click the transmitter pin diff --git a/docs/el-rGR/user/discovery.md b/docs/el-rGR/user/discovery.md index 777b4dde11..de09873c13 100644 --- a/docs/el-rGR/user/discovery.md +++ b/docs/el-rGR/user/discovery.md @@ -2,7 +2,7 @@ title: Local Mesh Discovery parent: User Guide nav_order: 12 -last_updated: 2026-08-30 +last_updated: 2026-09-11 description: Explore your mesh network — the Local Mesh Discovery scanner, traceroute paths, neighbor maps, and node discovery tools. aliases: - discovery @@ -97,7 +97,7 @@ Mesh Beacon lets nodes invite others to join their mesh. A beaconing node period Configure it under **Settings → Module configuration → Mesh Beacon**. The entry appears only on radios running firmware 2.8.0 or newer. A read-only **Region** row at the top of the screen shows the region the beacon advertises: that region, and the preset, are always the ones the radio itself uses, so a beacon cannot invite anyone onto settings your radio is not running. - **Listen for beacons** — receive invitations broadcast by other nodes. -- **Broadcast a beacon** — periodically advertise this mesh to nearby nodes, with an optional **Beacon message** of up to 100 bytes, a **Broadcast interval** picked from fixed intervals between 1 hour and 72 hours, and an **Offered channel** chosen from your radio's own channels. The offered channel is required, and defaults to your primary channel. +- **Broadcast a beacon** — periodically advertise this mesh to nearby nodes, with an optional **Beacon message** of up to 100 bytes, a **Broadcast interval** picked from fixed intervals between 1 hour and 72 hours, and an **Offered channel** chosen from your radio's own channels. The offered channel is required, and defaults to your primary channel. Over remote admin the picker offers the primary channel only. - **Broadcast targets** — optional extra destinations beyond the offered channel. **Add target** appends a row; each row picks a **Channel** and a **Transmit preset**, and **Remove target** deletes it. With no targets, the beacon goes out on the offered channel alone. Two conditions block beacon setup: diff --git a/docs/el-rGR/user/help-and-docs.md b/docs/el-rGR/user/help-and-docs.md index f306cde7f7..c83898cea3 100644 --- a/docs/el-rGR/user/help-and-docs.md +++ b/docs/el-rGR/user/help-and-docs.md @@ -2,7 +2,7 @@ title: Help & In-App Docs parent: User Guide nav_order: 21 -last_updated: 2026-08-30 +last_updated: 2026-09-11 description: Browse this documentation inside the app, search it, and ask Chirpy — the on-device AI assistant — questions about Meshtastic. aliases: - help @@ -23,7 +23,7 @@ The docs browser lists every user-guide page. Tap a page to read it; images and ### Search -The search box sits at the top of the docs browser. Type in it to filter pages by title and keywords — results update as you type, and the **Clear search** button (✕) empties the box. +Tap the search field at the top of the docs browser to open a full-screen search view. Type to filter pages by title and keywords — results update as you type, under the same field. The back arrow, or the keyboard's search action, closes the view. The field hides while you scroll down the page list and comes back when you scroll up. ![Searching the in-app documentation](../../assets/screenshots/docs-browser_search.png) diff --git a/docs/el-rGR/user/map-and-waypoints.md b/docs/el-rGR/user/map-and-waypoints.md index 9e0f1b207c..2fb1466f8c 100644 --- a/docs/el-rGR/user/map-and-waypoints.md +++ b/docs/el-rGR/user/map-and-waypoints.md @@ -2,7 +2,7 @@ title: Map & Waypoints parent: User Guide nav_order: 6 -last_updated: 2026-09-01 +last_updated: 2026-09-11 description: View node positions on the map, create and share waypoints, manage map layers and Site Planner, and control position sharing and privacy. aliases: - map @@ -44,7 +44,7 @@ The floating toolbar provides quick access to the compass, the map type and laye ### Filtering the Map -Tap the filter button in the floating toolbar to open **Filter map**. **Display** controls what is drawn: **Only Favorites**, **Show Waypoints**, **Show Precision Circles**, and a slider that hides nodes not heard from recently. **Node roles** is a chip per device role, plus **All** to show every role; a selected chip means that role is shown. **Nodes** narrows the set further with **Hide offline nodes**, **Only show direct nodes**, **Exclude MQTT**, **Show ignored nodes**, and **Include unknown**. +Tap the filter button in the floating toolbar to open **Filter map**. **Display** controls what is drawn: **Only Favorites**, **Show Waypoints**, **Show Precision Circles**, and a slider that hides nodes not heard from recently. **Node roles** is a chip per device role, plus **All** to show every role; a selected chip means that role is shown. **Nodes** narrows the set further with **Hide offline nodes**, **Only show direct nodes**, **Signed only**, **Encrypted only**, **Exclude MQTT**, **Show ignored nodes**, and **Include unknown** — the same rules as the node list's [filter toggles](nodes). A dot on the filter button means at least one filter is hiding something — check it before concluding the mesh is quiet. Turning **Show Waypoints** off hides every waypoint, including your own. **Show ignored nodes** adds them to the map rather than showing only them — unlike the node list's **Only show ignored Nodes**. @@ -147,8 +147,9 @@ picker, all three offer the same raster base maps: Overlays can be toggled on top of any base map, from the layers sheet: - **Weather radar** — NOAA NEXRAD reflectivity (US coverage) -- **Hillshade** — terrain relief, on **F-Droid** and **Desktop** only. Useful for understanding why a - link fails, since LoRa range is limited by terrain +- **Hillshade** — terrain relief from an online tile source, on **F-Droid** and **Desktop** only; Google + Play draws hillshade from downloaded offline terrain instead. Useful for understanding why a link + fails, since LoRa range is limited by terrain ### Adding your own tile source @@ -156,7 +157,7 @@ Any XYZ tile endpoint can be added as a base map, on every flavor and on desktop Tile Sources** at the foot of the base map picker and paste a URL template using `{z}`, `{x}` and `{y}` — plus `{s}` if the provider uses rotating subdomains. A national mapping service, for example: -``` +```text https://wmts.geo.admin.ch/1.0.0/ch.swisstopo.pixelkarte-farbe/default/current/3857/{z}/{x}/{y}.jpeg ``` @@ -173,7 +174,18 @@ On **Google Play**, the layers sheet's **Offline Manager** section downloads its — water, roads and administrative boundaries extracted from the public Protomaps basemap dataset, drawn directly as map shapes rather than raster tiles. Frame the area you want, tap **Start Download**, and once it finishes flip **Show on map** to draw it (it's off by default so it never surprises you by covering up the -live map). You can still import a pre-made `.mbtiles` archive here too, as before. **Desktop** has neither. +live map). You can still import a pre-made `.mbtiles` archive here too, as before. **Desktop** has neither, though +the offline terrain described next does work there. + +### Offline terrain + +Hillshade and elevation contours can be downloaded for offline use as well, so a downloaded area still +shows why a link fails when there is no network. The terrain data comes from Mapterhorn and is credited on +the map. On **Google Play**, each downloaded region in the **Offline Manager** list has a **Download Terrain** +button and **Hillshade** and **Contours** switches; terrain attaches to that region and shares its storage limit, and an area +too large for terrain asks you to zoom in or pick a smaller region. On **F-Droid** and **Desktop**, the +layers sheet has an **Offline Terrain** section of its own: frame the area, tap **Start Download**, and the +hillshade and contours draw from the downloaded tiles from then on. ### Going offline @@ -181,8 +193,8 @@ An **Offline** pill appears over the map whenever the device has no network conn builds, if you've imported a local `.mbtiles` archive, the map switches to it automatically the moment the network drops — no toggle to remember — and switches back once you're reconnected, as long as you haven't picked a different base map yourself in between. On **F-Droid**, a downloaded offline area keeps rendering on -its own; the pill is purely informational there. **Desktop** has no offline downloads at all yet, so the pill -is informational there too. +its own; the pill is purely informational there. **Desktop** downloads offline terrain but no base-map +packs, so the pill is informational there too. ## Related Topics diff --git a/docs/el-rGR/user/messages-and-channels.md b/docs/el-rGR/user/messages-and-channels.md index 4a2bd2ea6d..c72efa049b 100644 --- a/docs/el-rGR/user/messages-and-channels.md +++ b/docs/el-rGR/user/messages-and-channels.md @@ -2,7 +2,7 @@ title: Messages & Channels parent: User Guide nav_order: 3 -last_updated: 2026-08-30 +last_updated: 2026-09-14 description: Send and receive messages, manage channels, configure encryption, search conversations, and use quick chat, reactions, and message actions. aliases: - channels @@ -50,6 +50,21 @@ Key length alone does not change the icon: a 128-bit key and a 256-bit key both Tapping an existing channel opens the same editor, where you can change the name, the PSK, MQTT uplink and downlink, and position precision. Every edit on this screen — adding, editing, deleting, or dragging a channel into a new order — waits on **Send** the same way. +### Archived Channels + +A conversation belongs to the channel it was held on, not to the slot that channel occupied. When a channel leaves your radio — you scan a different channel QR, replace a channel's key, or switch the modem preset — its conversation is **archived** rather than deleted or merged into whatever takes the slot. + +An archived conversation: + +- stays in the **Channels** list under the name the channel had, below the channels currently on your radio, marked with a history icon; +- keeps its full message history, searchable as usual; +- is read-only — you cannot send, reply, or react, because the channel is no longer on the radio to send on, and it is not offered as a share target; +- comes back automatically if you re-add that exact channel, taking its messages back to the live slot. + +Changing the modem preset archives the channel too: the preset is part of a default channel's identity, so **LongFast** and **MediumFast** are different channels even though neither the name nor the key changed. Changing only the region does not archive anything. + +To remove an archived conversation for good, long-press it in the conversation list and delete it. + ## Direct Messages Direct messages (DMs) go to one specific node. When both radios hold each other's public keys, your radio encrypts the message to that node's public key, so no one else on the mesh can read it — not even nodes that share your channel. diff --git a/docs/el-rGR/user/mqtt.md b/docs/el-rGR/user/mqtt.md index 4fc300dc34..d06524c47d 100644 --- a/docs/el-rGR/user/mqtt.md +++ b/docs/el-rGR/user/mqtt.md @@ -2,7 +2,7 @@ title: MQTT parent: User Guide nav_order: 11 -last_updated: 2026-08-30 +last_updated: 2026-09-11 description: Bridge your mesh to the internet — MQTT broker setup, encryption layers, and map reporting. aliases: - mqtt @@ -25,7 +25,7 @@ The MQTT module connects your node to an MQTT broker, allowing: ## How It Works -``` +```text [Your Node] → Radio → [Gateway Node with Wi-Fi] → MQTT Broker → [Remote Gateway] → Radio → [Remote Node] ``` diff --git a/docs/el-rGR/user/node-metrics.md b/docs/el-rGR/user/node-metrics.md index 740943a303..8d71925f67 100644 --- a/docs/el-rGR/user/node-metrics.md +++ b/docs/el-rGR/user/node-metrics.md @@ -2,7 +2,7 @@ title: Node Metrics parent: User Guide nav_order: 5 -last_updated: 2026-08-30 +last_updated: 2026-09-09 description: Telemetry dashboards for each mesh node — device health, environment sensors, air quality, signal quality, power, traceroute, and position history. aliases: - metrics @@ -114,7 +114,7 @@ Radio signal quality information: ### Signal Quality Reference -Signal quality is rated from **SNR relative to the active LoRa modem preset's demodulation floor**, not from fixed thresholds — a given SNR means different things on different presets (e.g. −15 dB is fine on LongSlow but unusable on ShortFast). RSSI is shown but is not part of the rating. In the table, _limit_ is the preset's SNR limit. +Signal quality is rated from **SNR relative to the active LoRa modem preset's demodulation floor**, not from fixed thresholds — a given SNR means different things on different presets (e.g. −15 dB is fine on LongSlow but unusable on ShortFast). When RSSI and a noise-floor reading are both available, the app also rates their difference against the preset limit and uses the worse rating. Otherwise, RSSI is display-only. In the table, _limit_ is the preset's SNR limit. | Quality | Criteria | | ------- | ---------------------------------------------------------------- | diff --git a/docs/el-rGR/user/nodes.md b/docs/el-rGR/user/nodes.md index a91095f1cf..908c6d9290 100644 --- a/docs/el-rGR/user/nodes.md +++ b/docs/el-rGR/user/nodes.md @@ -2,7 +2,7 @@ title: Nodes parent: User Guide nav_order: 4 -last_updated: 2026-09-04 +last_updated: 2026-09-11 description: Browse, filter, and sort mesh nodes — view details, signal quality, roles, and quick actions. aliases: - node-list @@ -74,19 +74,23 @@ Most users should keep the default **Client** role. Consider a different role wh > 💡 **Tip:** The mesh works best when most nodes are **Client** or **Router**. Too many Client Mute nodes reduce mesh resilience; too many Routers in a dense area can cause congestion. A good rule of thumb: one Router per 5–10 Clients in your area. -### Encryption Indicators +### Security indicators -Nodes display encryption status icons next to their name: +Each node carries one security icon beside its name in the node list. Tap it to read what it means, and choose **Show All Meanings** in that dialog for the full legend. The detail screen shows the same state in words, as a **Security** row that opens the same dialog. -| Icon | Meaning | -| ----------- | ------------------------------------------------------------------------------------------------------------------------------------------------- | -| 🔒 Locked | Communication uses PKI (public key infrastructure) — end-to-end encrypted with verified identity | -| 🔓 Unlocked | No public key has been received for this node, so it cannot be direct messaged — use **Request User Info** on the node detail page to ask for one | -| ⚠️ Mismatch | Public key mismatch — the node's key has changed since last seen (investigate before trusting) | +| Icon | Meaning | Shown for | +| --------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------- | +| Person with a shield check (green) | **Verified contact** — you verified this node's key in person by exchanging contact QR codes, so its identity is confirmed. The strongest trust the list shows. Your own connected node carries it too | Any firmware version | +| Nodes icon with a shield check (green) | **Signed node** — this node signs its broadcasts with its identity key, so its identity is consistent over time, but you have not verified it in person. On firmware 2.8 the icon appears from the version alone, before any signed broadcast has been heard | Firmware 2.8 or newer, and any node whose signed broadcast your node has verified | +| 🔒 Closed lock | A public key is on file and matches, so direct messages to this node are encrypted | Firmware before 2.8, or no reported version | +| 🔓 Open lock | No public key has been received for this node, so it cannot be direct messaged — use **Request User Info** on the node detail page to ask for one | Firmware before 2.8, or no reported version | +| ⚠️ Mismatch | **Public key mismatch** — a different key arrived for this node after one was stored. Investigate before trusting | Any firmware version | -> 💡 **Tip:** Direct messages always use PKI, so the radio needs the other node's public key before it can send one. It refuses the send rather than falling back to channel encryption. Keys arrive inside node info, which is why an open lock usually clears itself once that node is heard from properly. If you see a key mismatch warning, the node may have been reset or compromised. +Every node on firmware 2.8 or newer signs its broadcasts, so on that firmware the signed state is the baseline and the locks are not shown. -To clear a mismatch, first confirm through another trusted channel that the key change was intentional — a factory reset causes one. Then touch & hold the node, choose **Remove**, and let the two radios exchange keys again the next time yours hears it. +Direct messages always use public-key encryption, so your node needs the other node's public key before it can send one. It refuses the send rather than falling back to channel encryption. Keys arrive inside node info, which is why an open lock usually clears itself once that node is heard from properly. + +A mismatch never replaces the key you already hold. The app keeps the first key it recorded and refuses the new one, as the firmware does, so a stray or hostile node info cannot silently break encrypted messaging to a contact. To clear a mismatch, first confirm through another trusted channel that the key change was intentional — a factory reset causes one. Then touch & hold the node, choose **Remove**, and let the two nodes exchange keys again the next time yours hears it. ## Quick Actions @@ -108,7 +112,9 @@ radio is connected and running firmware 2.8 or newer — see ## Sharing a Contact -On a node's detail screen, tap **Share Contact** to produce a link and a QR code for that node. From the same dialog, **Write to NFC tag** saves the link to a writable NFC tag that anyone can tap to open. +On a node's detail screen, tap **Share Contact** to produce a link and a QR code for that node. From the same dialog, **Share link** opens the Android share sheet (on desktop it copies the link instead), **Write to NFC tag** saves it to a writable NFC tag, and **Copy** puts it on the clipboard. While that dialog is open and in front of you, the phone also offers the same link to any NFC reader, so someone can take the contact by tapping their phone against yours with no tag involved. + +Sharing your own contact this way marks it as verified in person, so whoever imports it sees the **Verified contact** icon rather than the signed one. Relaying someone else's contact passes on only what your app had already recorded about them. To add someone else's contact, use the import button on the node list and choose **Scan Shared Contact QR Code**, **Scan Shared Contact NFC**, or **Input Shared Contact URL**. The app asks you to confirm with **Import Shared Contact?**, and warns you when the contact is one you already have. @@ -120,14 +126,16 @@ Type in the search field to filter nodes by name or short name. The filter updat ### Filter Toggles -| Φίλτρο | Περιγραφή | -| --------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| **Hide offline nodes** | Show only nodes heard within the last 2 hours | -| **Only show direct nodes** | Show only nodes your radio heard directly, with no relay in between | -| **Include unknown** | Show nodes that haven't sent user info yet. **On by default**, so a node heard before its info arrives stays visible; these carry a badge marking them incomplete, and cannot be direct messaged until their user info brings a public key | -| **Exclude infrastructure** | Hide infrastructure-role nodes (Router, Router Late, Client Base, and legacy Repeater nodes) and any node that cannot be messaged, whatever its role | -| **Exclude MQTT** | Hide nodes heard only via MQTT internet bridge | -| **Only show ignored Nodes** | Replace the list with the nodes you have ignored. Every other node is hidden while this is on, and a banner appears at the top of the list to take you back | +| Φίλτρο | Περιγραφή | +| --------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **Hide offline nodes** | Show only nodes heard within the last 2 hours | +| **Only show direct nodes** | Show only nodes your radio heard directly, with no relay in between | +| **Include unknown** | Show nodes that haven't sent user info yet. **On by default**, so a node heard before its info arrives stays visible; these carry a badge marking them incomplete, and cannot be direct messaged until their user info brings a public key | +| **Exclude infrastructure** | Hide infrastructure-role nodes (Router, Router Late, Client Base, and legacy Repeater nodes) and any node that cannot be messaged, whatever its role | +| **Exclude MQTT** | Hide nodes heard only via MQTT internet bridge | +| **Signed only** | Show only nodes whose signed broadcasts your node has actually heard and verified. Stricter than the icon: a node on 2.8 shows as signed by its firmware version before any signed broadcast arrives, and a contact verified in person on older firmware is not signed at all | +| **Encrypted only** | Show nodes with a matching public key on file, the key an encrypted direct message needs. A node with a key mismatch is excluded | +| **Only show ignored Nodes** | Replace the list with the nodes you have ignored. Every other node is hidden while this is on, and a banner appears at the top of the list to take you back | ### Sort Options @@ -149,6 +157,8 @@ Tap the hop-histogram icon in the node list's app bar to open a bar chart of how Tapping a node opens the detail view with comprehensive information. See [Node Metrics](node-metrics) for full details on metrics and telemetry. +Signal quality is rated against your modem preset. The same SNR can be good on a long-range preset and poor on a faster one. Traceroute and neighbor-info SNR colors use that preset too. RSSI text has its own strength colors; it affects the quality rating only when a noise-floor reading is also available. + The Details card carries the node's short name, role, IDs, last heard time, hops away, uptime, and its SNR and RSSI: ![Node detail section](../../assets/screenshots/nodes_detail_section.png) diff --git a/docs/el-rGR/user/settings-module-admin.md b/docs/el-rGR/user/settings-module-admin.md index 8235788222..0a8306884f 100644 --- a/docs/el-rGR/user/settings-module-admin.md +++ b/docs/el-rGR/user/settings-module-admin.md @@ -2,7 +2,7 @@ title: Settings — Modules & Admin parent: User Guide nav_order: 8 -last_updated: 2026-08-30 +last_updated: 2026-09-16 description: Configure optional feature modules (MQTT, telemetry, canned messages, TAK, and more) and perform device administration. aliases: - modules @@ -333,7 +333,7 @@ appear only when your own node is selected: ### Σχετικά -**Settings → About** carries the app's own identity rather than the radio's: +**Settings → About** carries the app's own identity rather than the node's: Three sections: @@ -341,7 +341,11 @@ Three sections: - **Apps** — opens with **Need Hardware?**, a rotating carousel of popular devices that links out to where to buy one. It also lists the GitHub repository, the running app version, and **Acknowledgements** (see the next section). -- **Project information** — links to the website and to this documentation. +- **Project information** — links to the website, to this documentation, and to the GNU GPL v3 + license the app is released under. + +The footer states the copyright and that the app is free software with no warranty, which you +may redistribute under the same license. ### Acknowledgements diff --git a/docs/el-rGR/user/settings-radio-user.md b/docs/el-rGR/user/settings-radio-user.md index facb5c5132..751499a764 100644 --- a/docs/el-rGR/user/settings-radio-user.md +++ b/docs/el-rGR/user/settings-radio-user.md @@ -2,7 +2,7 @@ title: Settings — Radio & User parent: User Guide nav_order: 7 -last_updated: 2026-09-04 +last_updated: 2026-09-11 description: Configure your radio hardware, LoRa presets, user profile, position sharing, power management, and security. aliases: - ρυθμίσεις @@ -51,7 +51,7 @@ The footer appears as soon as you change something. **Discard** throws the chang The status message is saved with the same **Save**, but it never reboots the node — and, like the rest of this screen, it can be edited on a remote node you administer. For your own radio there is a shortcut while it is connected: touch & hold your node in the [node list](nodes.md) and choose -**Update status**. Older firmware and a disconnected radio have no shortcut — the field above is +**Update status**. Older firmware and a disconnected node have no shortcut — the field itself is still the way in. ## Configuration @@ -77,13 +77,13 @@ On **Settings → LoRa**. | Setting | Περιγραφή | Default | | ------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------- | -| Περιφέρεια | Regulatory region for frequency bands. You must set this before transmitting | Unset (must configure) | +| Περιφέρεια | Regulatory region for frequency bands. You must set this before transmitting. On firmware 2.8 or newer, the first region set also creates the node's identity key and gives it a new node number | Unset (must configure) | | Presets | Speed/range tradeoff | LongFast | | Number of Hops | Maximum retransmit hops | 3 | | Transmit Power | Transmission power (dBm); 0 = max allowed for region | 0 (region max) | | Frequency Override | Overrides the computed operating frequency outright (MHz). It does not offset the calculated value — leave at 0 unless you know you need a specific frequency | 0 (use calculated) | | Use Preset | On by default. Turn it off to set Spread Factor, Coding Rate and Bandwidth by hand instead of taking them from the modem preset | On | -| Spread Factor | Manual mode only: 7–12. Higher spreads further but slower | From preset | +| Spread Factor | Manual mode only: 5–12. Higher spreads further but slower. On SX127x (RF95) radios the firmware does not accept 5 or 6 and uses 11 instead | From preset | | Coding Rate | Manual mode only: 5–8. More redundancy costs airtime | From preset | | Bandwidth | Manual mode only: the channel bandwidth in kHz, typed in directly. On the 2.4 GHz region the app offers a list of the bandwidths your radio supports instead, and a stored value that is not on that list shows as _Unsupported_ and blocks saving until you pick a supported one | From preset | | Frequency Slot | Which slot within the region's band to use. 0 derives it from the primary channel name | 0 (automatic) | @@ -111,13 +111,13 @@ The Lite, Narrow, Medium Turbo, and Tiny presets need firmware 2.8 or newer — | Short Slow | ~5 km | 6.25 kbps | −10 dB | Suburban short-range; moderate building density | | Medium Fast | ~5 km | 3.52 kbps | −12.5 dB | Suburban areas; moderate building density | | Medium Slow | ~8 km | 1.95 kbps | −15 dB | Suburban/rural; moderate range with slower speed | -| Long Turbo | ~10 km | 1.34 kbps | −12.5 dB | Similar range to Long Fast but with 500 kHz bandwidth; faster throughput | +| Long Turbo | ~10 km | 1.34 kbps | −17.5 dB | Similar range to Long Fast but with 500 kHz bandwidth; faster throughput | | Long Fast | ~10 km | 1.1 kbps | −17.5 dB | **General use (default)** — balanced range and speed | | Long Moderate | ~20 km | 0.34 kbps | −17.5 dB | Rural with some terrain; occasional use | | Lite Fast | ~5 km | 1.76 kbps | −12.5 dB | EU 866 MHz SRD band (125 kHz BW); comparable to Medium Fast | | Lite Slow | ~10 km | 0.98 kbps | −15 dB | EU 866 MHz SRD band (125 kHz BW); comparable to Long Fast | -| Narrow Fast | ~5 km | 2.28 kbps | −10 dB | EU 868 MHz band (62.5 kHz BW); avoids interference with other devices | -| Narrow Slow | ~10 km | 1.30 kbps | −12.5 dB | EU 868 MHz band (62.5 kHz BW); comparable to Long Fast | +| Narrow Fast | ~5 km | 2.28 kbps | −7.5 dB | EU 868 MHz band (62.5 kHz BW); avoids interference with other devices | +| Narrow Slow | ~10 km | 1.30 kbps | −10 dB | EU 868 MHz band (62.5 kHz BW); comparable to Long Fast | | Medium Turbo | ~5 km | 7.0 kbps | −12.5 dB | Like Medium Fast but with 500 kHz bandwidth; not legal in every region. Needs firmware 2.8 or newer | | Tiny Fast | ~10 km | 0.68 kbps | −7.5 dB | Amateur bands that cap occupied bandwidth; these presets use 15.6 kHz. Needs firmware 2.8 or newer, an SX126x or SX127x radio, and a TCXO of ±5 ppm or better | | Tiny Slow | ~20 km | 0.33 kbps | −10 dB | Same band restrictions as Tiny Fast, longer range. Same firmware, radio, and TCXO requirements | diff --git a/docs/el-rGR/user/signal-meter.md b/docs/el-rGR/user/signal-meter.md index c4e7c190bd..12d6aaade0 100644 --- a/docs/el-rGR/user/signal-meter.md +++ b/docs/el-rGR/user/signal-meter.md @@ -2,7 +2,7 @@ title: How the Meshtastic Signal Meter Works parent: User Guide nav_order: 15 -last_updated: 2026-08-30 +last_updated: 2026-09-09 description: How the signal meter rates quality from SNR relative to the LoRa modem preset — spread spectrum, presets, and what the bars really mean. aliases: - signal @@ -32,7 +32,7 @@ Each modem preset has an SNR limit: the lowest SNR at which that preset can stil ## Rating Signal Quality -The app rates signal quality (None, Bad, Fair, or Good) from SNR alone, measured against the active preset's SNR limit. It does not factor in RSSI: without knowing the local noise floor, RSSI alone cannot say whether a signal is decodable. RSSI is still available — on the node detail screen and in the metrics charts. +The app rates signal quality (None, Bad, Fair, or Good) from SNR relative to the active preset's SNR limit. When RSSI and a noise-floor reading are both available, it also rates the difference between them against that limit and uses the worse rating. Otherwise, it uses SNR alone. Because the rating is relative to the preset, the same SNR rates differently on different presets. An SNR of −16 dB rates Good on Long Fast (SNR limit −17.5 dB) but None on Short Fast (SNR limit −7.5 dB). Let `limit` be the active preset's SNR limit: @@ -45,7 +45,7 @@ Because the rating is relative to the preset, the same SNR rates differently on The icon never goes blank, so count the bars carefully: a single bar means None, not a weak but usable link, and Good is a solid wedge rather than a set of bars. A gray three-bar icon labeled Unknown is a different state again — the packet carried no SNR measurement at all, which is not the same as measuring one and finding it too weak. -> ℹ️ **Note:** Traceroute hop colors use fixed thresholds (−7 dB / −15 dB); the per-node signal meter uses the preset-relative rating instead. +> ℹ️ **Note:** Traceroute and neighbor-info SNR colors use the active preset's limits too. RSSI text has separate strength colors: green above −115 dBm, yellow above −120 dBm, orange above −126 dBm, and red at or below −126 dBm. ## Diagnosing Local Interference diff --git a/docs/el-rGR/user/tak.md b/docs/el-rGR/user/tak.md index 7f102b6746..4834c0b87d 100644 --- a/docs/el-rGR/user/tak.md +++ b/docs/el-rGR/user/tak.md @@ -2,7 +2,7 @@ title: TAK Integration parent: User Guide nav_order: 10 -last_updated: 2026-08-30 +last_updated: 2026-09-11 description: Interoperate with ATAK and WinTAK — CoT position sharing, TAK roles, and plugin setup. aliases: - tak @@ -36,7 +36,7 @@ The TAK module allows Meshtastic nodes to: > ⚠️ **Warning:** The old **Meshtastic ATAK Plugin** is no longer part of this path and cannot > work. It bridged through the cross-process AIDL API, which was removed in app 2.8.0; the mesh > service is now in-process only. Do not install it. Interop today runs over the app's own local -> TAK server plus the Mesh to CoT Converter, both described below, with stock ATAK/iTAK/WinTAK. +> TAK server plus the Mesh to CoT Converter, both described in the following sections, with stock ATAK/iTAK/WinTAK. ### Configuration diff --git a/docs/el-rGR/user/translate.md b/docs/el-rGR/user/translate.md index 67d69e2d96..cdc8b6d87f 100644 --- a/docs/el-rGR/user/translate.md +++ b/docs/el-rGR/user/translate.md @@ -2,7 +2,7 @@ title: Translate the App parent: User Guide nav_order: 17 -last_updated: 2026-08-30 +last_updated: 2026-09-11 description: How the app and its documentation are translated via Crowdin, and guidelines for contributing translations. aliases: - translate @@ -45,7 +45,7 @@ If your language is not yet listed on Crowdin: The Android app uses **Compose Multiplatform resources** for all user-visible strings: -``` +```text core/resources/src/commonMain/composeResources/ ├── values/ ← English (default) │ └── strings.xml @@ -58,7 +58,7 @@ core/resources/src/commonMain/composeResources/ In-app documentation follows a similar pattern under `docs/`: -``` +```text docs/ ├── en/user/ ← English source (default) │ ├── onboarding.md diff --git a/docs/en/developer.md b/docs/en/developer.md index 5c8ec2c23e..24ceeabce6 100644 --- a/docs/en/developer.md +++ b/docs/en/developer.md @@ -30,6 +30,12 @@ Things that trip up first-time contributors — check these before requesting re Keep the last 5–8 entries and trim older ones from the bottom. --> +**September 2026** — [Navigation & Deep Links](developer/navigation-and-deep-links) — Deep links route through Navigation 3's `UriDeepLinkMatcher` instead of a hand-rolled `when` block, and patterns are anchored, so an unmodelled trailing path no longer opens the family root. + +**September 2026** — [Measurement & Formatting](developer/measurement) — `NumberFormatter.format` follows the OS locale; `formatInvariant` is the fixed-dot one for payloads another system parses. + +**September 2026** — [Documentation Style](developer/documentation-style) — Section 11 of the Meshtastic design standards is now the style guide for `docs/en/`; this page keeps only the repository mechanics, the in-app renderer's admonition form, and the prose rules the standards leave open. + **August 2026** — [Documentation Style](developer/documentation-style) — New page: the house style guide for `docs/en/` prose, with rule IDs, a project word list, and the reasoning behind each convention. **August 2026** — Map tile sources are one shared catalogue in `feature/map` (`MapTileCatalogue`, `RasterTileSpec`), so both flavors draw the same raster base maps and overlays from one definition. @@ -40,11 +46,5 @@ Keep the last 5–8 entries and trim older ones from the bottom. **August 2026** — New module `feature/map-maplibre`: the F-Droid flavor and Desktop now render every map surface (main map, node track, traceroute, discovery, inline mini-map) through `maplibre-compose` from one multiplatform module, and `osmdroid` is gone. The shared rules both renderers must agree on live in `feature/map` policy classes. -**August 2026** — Android Auto removed from all build variants (#6779). `feature/car` is gone — the module, its Car App Library dependencies, the `automotive_app_desc.xml` manifest entry and the `google` flavor's `FlavorModule` registration. - -**August 2026** — [Testing](developer/testing) — CI gained a fourth runner tier: `ubuntu-slim` (#6674, #6677) now carries the lightweight jobs. It is single-CPU, unprivileged, x64-only and capped at 15 minutes, so anything needing `sudo`, Docker or a full-history clone stays on `ubuntu-24.04-arm`. Picking rules are in `.github/instructions/ci-workflows.instructions.md`. - -**August 2026** — Flatpak sources are generated inside each architecture's own offline build (#6919) rather than committed, and the flatpak-sources plugin resolves platform dependencies transitively (0.2.x), so the hand-tracked entries are gone. - diff --git a/docs/en/developer/adding-a-feature-module.md b/docs/en/developer/adding-a-feature-module.md index 1572443b8a..cf3bd8ee2f 100644 --- a/docs/en/developer/adding-a-feature-module.md +++ b/docs/en/developer/adding-a-feature-module.md @@ -16,7 +16,7 @@ Step-by-step guide for creating a new KMP feature module in the Meshtastic proje ## Create the Module Directory -```bash +```shell mkdir -p feature/my-feature/src/{commonMain,commonTest,androidMain,jvmMain,iosMain}/kotlin/org/meshtastic/feature/myfeature ``` diff --git a/docs/en/developer/architecture.md b/docs/en/developer/architecture.md index 62506d0c52..137e010d2b 100644 --- a/docs/en/developer/architecture.md +++ b/docs/en/developer/architecture.md @@ -2,7 +2,7 @@ title: Architecture parent: Developer Guide nav_order: 1 -last_updated: 2026-08-29 +last_updated: 2026-09-11 description: How the Android and Desktop apps split into androidApp/desktopApp, feature modules, and core modules, and how radio control and navigation are layered across them. aliases: - layers @@ -17,7 +17,7 @@ The Meshtastic Android and Desktop apps follow a modular Kotlin Multiplatform (K ## Layer Overview -``` +```text ┌─────────────────────────────────────────────┐ │ androidApp / desktopApp │ Platform entry points ├─────────────────────────────────────────────┤ @@ -105,7 +105,7 @@ Protobuf models come from the external `org.meshtastic:protobufs` Maven artifact Each module uses the standard KMP source set hierarchy: -``` +```text src/ ├── commonMain/ ← Shared code (all platforms) ├── commonTest/ ← Shared tests diff --git a/docs/en/developer/codebase.md b/docs/en/developer/codebase.md index b2045eef63..81029dfc2b 100644 --- a/docs/en/developer/codebase.md +++ b/docs/en/developer/codebase.md @@ -2,7 +2,7 @@ title: Codebase parent: Developer Guide nav_order: 2 -last_updated: 2026-08-29 +last_updated: 2026-09-11 description: Repository layout, package namespacing, and the Gradle build system — convention plugins, build variants, and key tasks. aliases: - repository-layout @@ -16,7 +16,7 @@ Repository layout, namespacing conventions, and build system overview. ## Repository Structure -``` +```text Meshtastic-Android/ ├── androidApp/ # Android application module │ ├── src/main/ # Shared Android code @@ -75,7 +75,7 @@ Meshtastic-Android/ ## Namespacing Convention All Kotlin packages follow the pattern: -``` +```text org.meshtastic.{layer}.{module}.{subpackage} ``` @@ -123,7 +123,7 @@ block rather than assuming a plugin does or does not exist. ### Key Gradle Tasks -```bash +```shell # Compile check across all KMP targets ./gradlew kmpSmokeCompile diff --git a/docs/en/developer/contributing.md b/docs/en/developer/contributing.md index 4431f648d3..ce09fe50ae 100644 --- a/docs/en/developer/contributing.md +++ b/docs/en/developer/contributing.md @@ -2,7 +2,7 @@ title: Contributing parent: Developer Guide nav_order: 8 -last_updated: 2026-08-29 +last_updated: 2026-09-11 description: Branch naming, commit style, PR workflow, and the verification gates a change must pass before merge. aliases: - contributing @@ -48,7 +48,7 @@ Examples: ## Commit Messages Follow conventional commit style: -``` +```text feat(docs): add in-app documentation browser fix(ble): handle reconnection timeout refactor(navigation): migrate to typed routes @@ -76,7 +76,7 @@ Before submitting: - **Line length:** 120 characters maximum Run formatting: -```bash +```shell ./gradlew spotlessApply ``` @@ -91,16 +91,16 @@ Run formatting: ## Verification Full pre-merge verification: -```bash +```shell ./gradlew spotlessCheck detekt kmpSmokeCompile test allTests ``` For docs-specific changes, also run: -```bash +```shell ./gradlew generateDocsBundle validateDocsBundle ``` -Prose in `docs/en/` follows the house style — see [Documentation Style](documentation-style). +Prose in `docs/en/` follows Section 11 of the [Meshtastic Client Design Standards](https://github.com/meshtastic/design/tree/master/standards) — see [Documentation Style](documentation-style) for what's specific to this repository. ## Getting Help diff --git a/docs/en/developer/documentation-style.md b/docs/en/developer/documentation-style.md index 516cbf37c1..0d46f98910 100644 --- a/docs/en/developer/documentation-style.md +++ b/docs/en/developer/documentation-style.md @@ -2,8 +2,8 @@ title: Documentation Style parent: Developer Guide nav_order: 11 -last_updated: 2026-08-30 -description: House style for the user and developer docs — voice, wording, formatting, page structure, and the decisions behind them. +last_updated: 2026-09-14 +description: How this repository's docs work, and the one prose rule the Meshtastic design standards leave to us. aliases: - style - style-guide @@ -13,77 +13,41 @@ aliases: # Documentation Style -House style for everything under `docs/en/`. These pages ship to three places — the in-app docs browser, the GitHub Pages site, and meshtastic.org — so one page has to read well in all of them, and its English source is the translation base for 40+ locales on Crowdin. +Section 11 of the [Meshtastic Client Design Standards](https://github.com/meshtastic/design/tree/master/standards) is the style guide for everything under `docs/en/`. Read it first. It decides voice, plain language, terminology, page structure, instructions, cross-platform coverage, code and CLI examples, media, accessibility, admonition use, units, translation, versioning, and in-product text, and it closes with a checklist of the rules a reviewer can check without judgment. -This guide synthesizes the [Google developer documentation style guide](https://developers.google.com/style), the [Microsoft Writing Style Guide](https://learn.microsoft.com/en-us/style-guide/welcome/), the Apple Style Guide's gesture vocabulary, the US [plain language guidelines](https://www.plainlanguage.gov/guidelines/) and [18F content guide](https://github.com/18F/content-guide), and Red Hat's admonition rules — adjudicated against this repo's own established conventions. Where an entry here decides something, follow it; for anything not covered, follow Google's guide. +This page carries what Section 11 doesn't: how documentation works in this repository, two forms that differ here, and the one prose rule the standards leave open. Where the two ever disagree on something not listed here, Section 11 wins and this page is wrong. -Every rule has an ID so review feedback and audit findings can cite it. +Pages under `docs/en/` ship to three places — the in-app docs browser, the GitHub Pages site, and meshtastic.org — so one page has to read well in all of them, and its English source is the translation base for 40+ locales on Crowdin. Section 11.1 describes the sync: user pages land in `docs/software/android/` on the documentation site weekly, taken from the latest release, and pull requests against those paths there are reverted. -## Voice and Tone +Rules keep the IDs they had so review feedback and audit findings can still cite them. Rules that Section 11 now covers are retired, and their IDs aren't reused. -- **VOICE-1** — Address the reader as *you*. *We* means the Meshtastic project team, nothing else. Never *the user* for the reader. -- **VOICE-2** — Warm, plain, and direct. Contractions are house voice (*you'll*, *doesn't*, *it's*) — but in warnings, write *do not*: negative contractions are too easy to misread when the cost of misreading is data loss. -- **VOICE-3** — No hype and no filler: no exclamation points, no *simply*, *just*, *easy*, *easily*, or *quickly* in instructions, no *please*, no scare quotes, no pop-culture jokes. If a task is genuinely hard, say what makes it hard instead of calling it easy. -- **VOICE-4** — Voice stays constant; tone flexes with the reader's situation. A troubleshooting section addresses someone who is stuck and possibly annoyed — that is the calmest, most concrete writing on the page. Clear beats entertaining, always. -- **VOICE-5** — Active voice by default. Passive is fine when the actor is irrelevant (*the packet is re-encrypted*) or to avoid blaming the reader (*over 50 conflicts were found*). -- **VOICE-6** — Timeless present tense. The app *sends*, not *will send*. Reserve *will* for events genuinely later than the sentence (*the file will be removed on the next sync*). Never *currently*, *now*, *soon*, *new*, or *as of this writing* — the page outlives all of them — and never pre-announce unreleased features. +## In transition -## Language +Two rules changed when Section 11 landed. Neither is a sweep. -- **LANG-1** — American English spelling: *color*, *behavior*, *honors*, *gray*, *organize*. -- **LANG-2** — Keep sentences to roughly 25 words and one idea; keep paragraphs to about five sentences and one topic. Front-load: main point first, exceptions after the rule. A one-sentence paragraph is fine. -- **LANG-3** — Requirement words carry exact weight: *must* (obligation), *must not* (prohibition), *should* (recommendation), *can* (capability), *may* (permission). Never *shall*. -- **LANG-4** — Prefer *for example* and *such as* over *e.g.*, and *that is* over *i.e.*, in running prose. Inside table cells and parentheses, *e.g.* is acceptable where space is tight. -- **LANG-5** — One term per concept, everywhere. If the settings page calls it a *modem preset*, no page calls it a *radio profile*. Keep clarifying words that ease translation: *the*, *that*, *who* (*the radios **that** you have paired*, not *the radios you paired*). -- **LANG-6** — Inclusive, literal language: *allowlist*/*blocklist*, singular *they*, no ableist idioms (*final check*, not *sanity check*), no violent metaphors (*the app stops responding*, not *hangs*), no culture-bound idioms that defeat translators. -- **LANG-7** — Oxford comma (*Android, iOS, and Windows*). Em dashes are spaced — like this — matching the entire existing corpus. -- **LANG-8** — Spell out zero through nine in prose; numerals for 10 and up, for all measurements and units (*3 dB*, *915 MHz*), for values the reader enters, and with `%`. Dates spell the month: *June 12, 2026*. -- **LANG-9** — Don't use *above* and *below* to point at other text — say *earlier*, *the following*, or link to the section. (Literal technical use is fine: *below the noise floor*.) +- **Headings are sentence case** (11.5, and a quick check in 11.16). Existing Title Case headings stay until the page is edited for another reason. Re-casing a heading invalidates its translation memory across 40+ locales, so it happens as pages are touched and never as a pass of its own. Frontmatter `title` values are the nav labels the in-app browser and the site share, so they change with the rest of the nav rather than one page at a time. +- **The hardware is a node** (11.4). This repo used *radio* to disambiguate the hardware from the phone, and roughly 300 uses of it remain. Prefer *node* in new writing and drop *radio* when you edit a page. *Phone* still means the Android handset the app runs on, which Section 11 has no word for. -## Word List - -Project- and platform-specific decisions, alphabetical. Google's word list covers the rest. - -| Term | Rule | -|---|---| -| app | The Meshtastic client this repo builds. Not *application*. | -| channel | Lowercase in prose; bold only when naming a UI element. | -| direct message | Lowercase; *DM* is fine after the page has spelled it out once. | -| email | No hyphen. | -| firmware | Lowercase, even when referring to the Meshtastic firmware project. | -| internet | Lowercase. | -| LoRa | Exactly this casing, everywhere. | -| mesh | Lowercase: *your mesh*, *the mesh network*. | -| node | A participant in the mesh — yours or anyone's — as it appears in the node list and on the map. | -| phone | The Android handset the app runs on. Use *phone* even when a tablet also works, unless the distinction matters. | -| radio | The Meshtastic hardware the app connects to. Prefer this over the ambiguous *device* — see the note after this table. | -| set up / setup | Verb two words, noun/adjective one word. | -| sign in | Verb; *sign-in* as adjective. Not *log in* or *login*. | -| tap | The interaction verb for the app's touch UI. *Click* only in desktop-app or web contexts; never *tap on* or *click on*. | -| touch & hold | Exactly this form (Google's Android convention). Not *long press*, not *tap and hold*. | -| Wi-Fi | Never *WiFi* or *wifi* — in docs and in the app's own strings alike (this word outranks PROC-1's match-the-UI rule if a stray label slips back in). | - -**radio vs. phone vs. node vs. device:** the corpus's biggest ambiguity is *device*, which has meant both the radio and the phone. Use *radio* for the Meshtastic hardware, *phone* for the Android handset, and *node* for a mesh participant. Use *device* only when quoting Android's own UI (the **Nearby devices** permission, *Devices* system pages) or in established compounds (*device metrics*, *device firmware* where the UI uses them). - -## Headings - -- **HEAD-1** — Title Case for page titles and all headings: *Where Signal Information Appears*, not *Where signal information appears*. This deliberately deviates from Google, Microsoft, and gov.uk (all sentence case) — the entire corpus is Title Case, and re-casing every heading would invalidate the translation memory for 40+ locales with zero reader benefit. See [Decisions](#decisions-and-why). -- **HEAD-2** — One `#` H1 per page, and it matches the frontmatter `title` exactly. -- **HEAD-3** — Never skip a level (H2 → H4). No links, no trailing punctuation, and no numbering in headings — the one exception is a genuine question heading in troubleshooting content (*Which Channels Can Obtainium Reach?*), which research says helps readers find answers. -- **HEAD-4** — Headings are scannable noun or verb phrases, shorter than the text they cover. No bare code identifiers as an entire heading — add a noun. - -## Page Structure +## Repository mechanics - **STRUCT-1** — Complete frontmatter on every page, user *and* developer: `title`, `parent`, `nav_order`, `last_updated` (bump it whenever content changes — CI checks freshness), `description` (one sentence, ~160 characters, used by search and link previews), and `aliases` (search terms the in-app browser resolves). -- **STRUCT-2** — Open with a one-to-three-sentence lede that says what the page covers and who needs it. No *Welcome!* preamble. -- **STRUCT-3** — User pages end with a `## Related Topics` section: bulleted links, each with an em-dash clause saying why you'd go there. -- **STRUCT-4** — No horizontal rules (`---`) in page bodies, and especially not at the end of the file — the site layout adds its own footer rule, so a trailing `---` renders as a doubled line. Headings already separate sections. -- **STRUCT-5** — New pages must be registered in `DocBundleLoader.kt` (the in-app index) — CI fails in both directions if the page and the index disagree. Screenshots go in `docs/assets/screenshots/`. +- **HEAD-2** — One `#` H1 per page, and it matches the frontmatter `title` exactly. The in-app index is built from the frontmatter, so a mismatch shows the reader two different titles for one page. +- **STRUCT-3** — User pages end with a `## Related Topics` section: bulleted links, each with an em-dash clause saying why you'd go there. The heading is a fixed section name shared by 22 pages, so like a frontmatter `title` it stays Title Case until they all change together. +- **STRUCT-4** — No horizontal rules (`---`) in page bodies, and especially not at the end of the file — the site layout adds its own footer rule, so a trailing `---` renders as a doubled line. +- **STRUCT-5** — New pages must be registered in `DocBundleLoader.kt` (the in-app index) — CI fails in both directions if the page and the index disagree. - **STRUCT-6** — Notable page changes get a *What's New* entry at the top of `user.md` or `developer.md`, in the format the HTML comment there specifies. +- **IMG-1** — Reference screenshots by relative path from `docs/assets/screenshots/`: `../../assets/screenshots/_.png`. Both the Docusaurus sync and the in-app renderer anchor on the `assets/` segment, so this one form works in all three consumers. +- **LINK-2** — Sibling pages link by bare slug (`connections`), cross-section by relative path (`../developer/testing`). `scripts/validate-doc-links.js` enforces resolvability. +- **LINK-3** — Node-side concepts (LoRa presets, firmware regions, MQTT topics) link out to the [meshtastic.org docs](https://meshtastic.org/docs/) rather than being re-explained here. One source drifts less than two. +- **CODE-1** — Code font for module paths (`core:ble`), Gradle tasks, class and function names, and setting keys, alongside the cases 11.8 covers. +- **DEV-1** — Developer pages assume Kotlin, Gradle, and Android fluency and skip the reassurance, but every rule on this page and in Section 11 still applies. Terse is good; cryptic is not. +- **DEV-3** — Structural or procedural changes get a *What's New for Developers* entry in `developer.md`. -## Admonitions +## Local forms -- **ADMON-1** — One form: a blockquote starting with an emoji and a bold label, from this closed set. +Two forms differ from Section 11. The first is a renderer requirement. The second is the entrenched house form, kept because changing it would touch every page for no reader benefit. + +- **ADMON-1** — Admonitions are a blockquote starting with an emoji and a bold label, from this closed set. The in-app renderer has no admonition component, so `:::note` would reach the reader as literal text. Section 11.11 exempts synced client documentation for exactly this reason and leaves the form to this guide; its rules on *when* to use one still apply in full, including at most one per H2 section. | Admonition | Use for | |---|---| @@ -94,65 +58,51 @@ Project- and platform-specific decisions, alphabetical. Google's word list cover | `> 🔒 **Privacy:**` | What data leaves the phone, who on the mesh can see it, and how to limit it. | | `> 🔒 **Security:**` | Cryptographic caveats — key handling, unencrypted channels, admin access. | - Privacy and Security are house-specific labels; they exist because the app's constitution makes privacy a core principle, and they should not be flattened into Note. -- **ADMON-2** — Admonitions are for information only: never put a required step inside one — if the reader must act, it's a numbered step; if inaction causes loss, it's a Warning. Use them sparingly (rarely more than one per section, never adjacent), or they stop working. -- **ADMON-3** — A Warning names the concrete consequence first, then how to avoid it: *Formatting a mounted filesystem destroys all data on it*, not *take care when formatting*. + Privacy and Security are house labels with no equivalent upstream. They exist because the app's constitution makes privacy a core principle, and they shouldn't be flattened into Note. -## Procedures and UI +- **PROC-2** — Menu paths use a spaced arrow between bold labels: **Settings → Permissions**. Never `>` or `›`. Section 11.6 writes the separator as `>` in its example; the arrow is this corpus's established form, unambiguous in Markdown, and it reads naturally in the in-app renderer. -- **PROC-1** — Bold the exact UI label: tap **Get started**. No quotation marks, and no element type (*button*, *menu*) unless it disambiguates. Match the UI's own capitalization exactly. -- **PROC-2** — Menu paths use a spaced arrow between bold labels: **Settings → Permissions**. Never `>` or `›`. -- **PROC-3** — Steps are imperative, one action each, ordered goal → location → action: *To pair a second radio, on the Connections screen, tap **Scan**.* Number steps only when order matters; prefer seven or fewer; prefix genuinely optional steps with *Optional:*. -- **PROC-4** — State the result of a step in the same paragraph as the action, not as its own step. -- **PROC-5** — Describe the task, not the widget, when you can: *turn on the **MQTT** module*, not *tap the toggle switch next to MQTT*. +## Prose rules Section 11 leaves open -## Links +One rule. [design#154](https://github.com/meshtastic/design/pull/154) moved the rest into Section 11, where they read the same, so nothing about how you write changes — the rules have one home rather than two that can drift apart. -- **LINK-1** — Link text describes the destination — the page title or a noun phrase. Never *click here*, *this page*, or a raw URL as text. The standing formula: *For more information, see [Connections](connections).* -- **LINK-2** — Sibling pages link by bare slug (`connections`), cross-section by relative path (`../developer/testing`). `scripts/validate-doc-links.js` enforces resolvability. -- **LINK-3** — Radio-side concepts (LoRa presets, firmware regions, MQTT topics) link out to [meshtastic.org docs](https://meshtastic.org/docs/) rather than re-explaining them here — one source drifts less than two. +- **LANG-7** — Em dashes are spaced — like this — matching the entire existing corpus. Section 11 doesn't mention them. -## Images +These are retired, with the section that owns each. Their IDs aren't reused. -- **IMG-1** — Reference screenshots by relative path: `../../assets/screenshots/_.png`. Both the Docusaurus sync and the in-app renderer anchor on the `assets/` segment, so this one form works in all three consumers. -- **IMG-2** — Every image has alt text that names the screen and the state it shows: *Scanning for Bluetooth devices, with a discovered radio in the list*. -- **IMG-3** — Screenshot only what words can't carry — a complex screen the reader must orient in, not a confirmation dialog. Capture the state right before the action, crop to the relevant surface, and keep version numbers, dates, and real user data out of frame. Place the image directly after the sentence it illustrates. -- **IMG-4** — Never post an image of text, code, or terminal output — use a code block. +| Retired rule | Section | +|---|---| +| VOICE-2 — contractions, and *do not* in warnings | 11.2 | +| LANG-3 — requirement words | 11.3 | +| LANG-4 — *e.g.* and *i.e.* | 11.3 | +| LANG-6 — inclusive, literal language | 11.3 | +| LANG-7 — the Oxford comma half; the em dash stays | 11.3 | +| LANG-1 — American spelling | 11.4 | +| LANG-9 — *above* and *below* | 11.5 | +| LANG-8 — number style | 11.12 | -## Code +## Word list -- **CODE-1** — Code font for file paths, Gradle tasks, module paths (`core:ble`), class and function names, setting keys, values the reader types, and protocol identifiers (`LongFast`, `!a1b2c3d4`). -- **CODE-2** — Not code font: product names, and URLs the reader is meant to visit (link those instead). -- **CODE-3** — Fenced blocks with a language tag; shell blocks show the bare command with no `$` prompt, so it copies cleanly. -- **CODE-4** — Placeholders in angle brackets: `docs/`, `--tests ""`. +Terms this repository needs that the Section 11 tables don't cover. Section 11.4 decides the rest. -## Developer Pages +| Term | Rule | +|---|---| +| email | No hyphen. | +| internet | Lowercase. | +| phone | The Android handset the app runs on. Use *phone* even when a tablet also works, unless the distinction matters. | +| set up / setup | Verb two words, noun/adjective one word. | +| sign in | Verb; *sign-in* as adjective. Not *log in* or *login*. | +| tap | The interaction verb for the app's touch UI. These pages are mobile-specific, so *tap* stands where 11.7 would prefer *select*. Never *tap on*. | +| touch & hold | Exactly this form (Google's Android convention). Not *long press*, not *tap and hold*. | -- **DEV-1** — Same voice, different reader: assume Kotlin, Gradle, and Android fluency, and skip the reassurance — but the rules above still apply. Terse is good; cryptic is not. -- **DEV-2** — All frontmatter rules apply, including `description` — developer pages are indexed by the same in-app search as user pages. -- **DEV-3** — Structural or procedural changes get a *What's New for Developers* entry in `developer.md`. - -## Decisions and Why - -Where this guide deviates from the external guides it synthesizes, the deviation is deliberate: - -| Decision | External guides say | Why we differ | -|---|---|---| -| Title Case headings (HEAD-1) | Google, Microsoft, gov.uk: sentence case | The whole corpus is Title Case; re-casing invalidates ~150 headings' translations across 40+ locales for no reader benefit. | -| `→` in menu paths (PROC-2) | Google, Microsoft: `>` | `→` is the entrenched house form (29:2 in the corpus), unambiguous in Markdown, and reads naturally in the in-app renderer. | -| Emoji admonition labels (ADMON-1) | Red Hat, SUSE: plain labeled blocks | The in-app renderer has no admonition component; the emoji-plus-bold-blockquote form is the established house pattern and survives all three renderers. | -| Privacy/Security labels (ADMON-1) | Not in any external set | They map directly to the constitution's Privacy First principle and deserve more visual weight than Note. | -| Relative image paths (IMG-1) | (Repo history preferred root-relative) | The in-app image transformer documents the relative form as canonical, the sync script rewrites either form, and every existing page already uses it. | -| Spaced em dashes (LANG-7) | Microsoft: unspaced | 477:0 in the corpus; also kinder to narrow in-app line widths. | -| Contractions (VOICE-2) | gov.uk warns on negative contractions | We follow Microsoft/18F (contractions are the house voice) and adopt gov.uk's caution only inside warnings. | - -## New Page Checklist +## New page checklist 1. Create `docs/en/user/.md` or `docs/en/developer/.md` with complete frontmatter (STRUCT-1). 2. Register the page in `feature/docs/.../data/DocBundleLoader.kt` with keywords and aliases. 3. Put screenshots in `docs/assets/screenshots/`, referenced per IMG-1. 4. Add a *What's New* entry (STRUCT-6). 5. Validate locally: `node scripts/validate-doc-links.js docs/en`, `node scripts/check-doc-coverage.js .`, `node scripts/check-doc-aliases.js .` (every alias in step 1's frontmatter must also be in step 2's loader entry — only the loader's list reaches in-app search), and the docs bundle Gradle checks in [Contributing](contributing). +6. Run the Section 11.16 quick checks over the page before you open the pull request. ## Related Topics diff --git a/docs/en/developer/measurement.md b/docs/en/developer/measurement.md index 7b33ae3969..c20a5aaac0 100644 Binary files a/docs/en/developer/measurement.md and b/docs/en/developer/measurement.md differ diff --git a/docs/en/developer/navigation-and-deep-links.md b/docs/en/developer/navigation-and-deep-links.md index cb969b20fa..e03e4976e2 100644 --- a/docs/en/developer/navigation-and-deep-links.md +++ b/docs/en/developer/navigation-and-deep-links.md @@ -2,7 +2,7 @@ title: Navigation & Deep Links parent: Developer Guide nav_order: 4 -last_updated: 2026-08-29 +last_updated: 2026-09-19 description: How typed Navigation 3 routes and DeepLinkRouter work together, the supported deep link URIs, and how to add a new one. aliases: - deeplinks @@ -14,11 +14,11 @@ aliases: The app uses **Navigation 3** with typed, serializable routes and centralized deep link resolution. -## Route Architecture +## Route architecture All routes are defined in `core/navigation/src/commonMain/kotlin/org/meshtastic/core/navigation/Routes.kt`. -### Route Hierarchy +### Route hierarchy ```kotlin interface Route : NavKey // All routes implement NavKey @@ -42,11 +42,16 @@ sealed interface SettingsRoute : Route { - Group related routes under a `sealed interface` - Graph entry points implement both the route interface and `Graph` -## Deep Link Router +## Deep link router -`DeepLinkRouter` in `core/navigation` maps URI deep links to typed backstack lists. +`DeepLinkRouter` in `core/navigation` maps URI deep links to typed backstack lists. Each supported link is a +Navigation 3 `UriDeepLinkMatcher` pattern whose placeholders decode into the target route's fields, wrapped in +`withBackStack` to synthesize the parents. -### URI Format +Patterns are anchored, so a path the patterns don't model returns null rather than falling back to the family +root: `/firmware/anything-else` no longer opens the firmware screen. + +### URI format Both forms resolve through the same `DeepLinkRouter`, so any deep link path works with either scheme: @@ -62,7 +67,7 @@ For the `https` form to open in-app, each top-level path segment must also be de `android:pathPrefix` in the `android:autoVerify` intent-filter in `androidApp/src/main/AndroidManifest.xml` — otherwise the link opens in the browser. Adding a new top-level route therefore takes three steps: add the segment to `DeepLinkRouter.topLevelPathSegments` (the router refuses to dispatch segments outside that set), -add its `when` branch in `DeepLinkRouter.route()`, and add the matching `pathPrefix` to the manifest. +add its matcher to `DeepLinkRouter.matchers`, and add the matching `pathPrefix` to the manifest. `DeepLinkManifestConsistencyTest` (androidApp unit tests) checks the manifest against the set, so a missing manifest entry fails CI. @@ -70,22 +75,22 @@ manifest entry fails CI. - The always-current list of top-level segments is `topLevelPathSegments` in [`DeepLinkRouter`](https://github.com/meshtastic/Meshtastic-Android/blob/main/core/navigation/src/commonMain/kotlin/org/meshtastic/core/navigation/DeepLinkRouter.kt). -- Sub-paths live in the `route()` `when` block plus its helper maps (`settingsSubRoutes`, `nodeDetailSubRoutes`). +- Sub-paths live in the `matchers` list plus its helper maps (`settingsSubRoutes`, `nodeDetailSubRoutes`). - The class-level KDoc on the `DeepLinkRouter` object lists example mappings, but it's illustrative, not exhaustive. - The executable spec is [`DeepLinkRouterTest.kt`](https://github.com/meshtastic/Meshtastic-Android/blob/main/core/navigation/src/commonTest/kotlin/org/meshtastic/core/navigation/DeepLinkRouterTest.kt). - The following table is a snapshot for quick reference — check those two files if it looks out of date. -### Supported Deep Links +### Supported deep links | URI Path | Route | Notes | |----------|-------|-------| | `/connections` | `ConnectionsRoute.Connections(null)` | Connections screen | -| `/connections?address={prefixedAddress}` | `ConnectionsRoute.Connections(address)` | Auto-connects to a radio without manual selection — the address uses the app's internal transport-prefixed format: `t192.168.1.1:4403` (TCP), `xAA:BB:CC:DD:EE:FF` (BLE), `s/dev/ttyUSB0` (serial). Intended for scripts/AI tooling driving the app. | -| `/connections?address=n` | `ConnectionsRoute.Connections("n")` | Disconnects the current radio instead of connecting (`n` = the internal "no device selected" sentinel). | +| `/connections?address={prefixedAddress}` | `ConnectionsRoute.Connections(address)` | Auto-connects to a node without manual selection — the address uses the app's internal transport-prefixed format: `t192.168.1.1:4403` (TCP), `xAA:BB:CC:DD:EE:FF` (BLE), `s/dev/ttyUSB0` (serial). Intended for scripts/AI tooling driving the app. | +| `/connections?address=n` | `ConnectionsRoute.Connections("n")` | Disconnects the current node instead of connecting (`n` = the internal "no device selected" sentinel). | | `/wifi-provision` | `WifiProvisionRoute.WifiProvision(null)` | Wi-Fi provisioning screen | -| `/wifi-provision?address={mac}` | `WifiProvisionRoute.WifiProvision(mac)` | Provisioning targeting a specific radio MAC | +| `/wifi-provision?address={mac}` | `WifiProvisionRoute.WifiProvision(mac)` | Provisioning targeting a specific node MAC | | `/settings` | `SettingsRoute.Settings(null)` | Settings root | | `/settings/helpDocs` | `SettingsRoute.HelpDocs` | Docs browser | | `/settings/helpDocs/{pageId}` | `SettingsRoute.HelpDocPage(pageId)` | Specific doc page | @@ -105,7 +110,7 @@ manifest entry fails CI. | `/firmware` | `FirmwareRoute.FirmwareGraph` | Firmware screen | | `/firmware/update` | `FirmwareRoute.FirmwareUpdate` | Firmware update flow | -### Backstack Synthesis +### Backstack synthesis Deep links synthesize a full backstack, not just the target screen: @@ -120,15 +125,16 @@ listOf( This ensures the user can navigate "up" correctly. -## Adding a Deep Link +## Adding a deep link 1. Define the typed route in `Routes.kt`. -2. Add the mapping in `DeepLinkRouter.settingsSubRoutes` (or equivalent for other graphs). +2. Add the mapping in `DeepLinkRouter.settingsSubRoutes` (or equivalent for other graphs), and a matcher in + `DeepLinkRouter.matchers` if the path shape is new. 3. Add a test in `DeepLinkRouterTest.kt`. 4. Register the navigation entry in the appropriate feature module. 5. Update the illustrative KDoc list on the `DeepLinkRouter` object (the class-level doc comment, not `route()`'s own KDoc) and the preceding table — both are quick-reference snapshots, not the source of truth. See the Source of Truth list earlier in this page for the authoritative places. -## Navigation Entry Registration +## Navigation entry registration Each feature module provides entries via an extension function: @@ -144,6 +150,6 @@ These are called from the settings navigation composition. ## Testing Deep link routing is tested in: -``` +```text core/navigation/src/commonTest/kotlin/org/meshtastic/core/navigation/DeepLinkRouterTest.kt ``` diff --git a/docs/en/developer/testing.md b/docs/en/developer/testing.md index 11a21898b2..cb302d502f 100644 Binary files a/docs/en/developer/testing.md and b/docs/en/developer/testing.md differ diff --git a/docs/en/user.md b/docs/en/user.md index df3872edc5..f2336be269 100644 --- a/docs/en/user.md +++ b/docs/en/user.md @@ -17,21 +17,21 @@ Documentation for using the Meshtastic Android and Desktop app. Keep the last 5–8 entries and archive older ones by removing them. --> +**September 2026** — [Nodes](user/nodes) — Nodes your node has not heard since its LoRa settings changed now carry an orange marker, a **Hide unheard nodes** filter, and a banner whose **Remove** action deletes them in bulk. + +**September 2026** — [Settings — Radio & User](user/settings-radio-user) — Lockdown's debug-port lock is not reversible from the app, Managed Mode locks the whole configuration list, and the preset list is filtered to what your region permits. + +**September 2026** — [Local Mesh Discovery](user/discovery) — Signal colours are relative to the preset's demodulation floor rather than fixed thresholds, and a beacon's broadcast targets always hold at least one row. + +**September 2026** — [Nodes](user/nodes) — Nodes on firmware 2.8 show signed and verified identity icons in place of the PKI lock, and **Signed only** and **Encrypted only** filters join the node list and the map. + +**September 2026** — [Map & Waypoints](user/map-and-waypoints) — Offline terrain (hillshade and contours) can be downloaded on Google Play, F-Droid, and Desktop. + **August 2026** — [Local Mesh Discovery](user/discovery) — Mesh Beacon advertises the region and preset your radio actually uses, requires a region and a standard modem preset before it will broadcast, and hides invitations to channels your radio already has. **August 2026** — [Map & Waypoints](user/map-and-waypoints) — Filter the map by node role and by how a node was heard, from a new filter sheet. **August 2026** — [Map & Waypoints](user/map-and-waypoints) — Every map layer and overlay now has its own opacity slider. -**August 2026** — [Map & Waypoints](user/map-and-waypoints) — F-Droid builds draw the map with MapLibre, and the Desktop app has an interactive map for the first time. - -**August 2026** — [Settings — Radio & User](user/settings-radio-user) — The status message moved into the User Profile, beside Long Name and Short Name; its separate module screen is gone. - -**August 2026** — [Messages & Channels](user/messages-and-channels) — Swipe a message right to reply, double-tap it to react, and day separators group the thread. - -**August 2026** — [Messages & Channels](user/messages-and-channels) — The conversation list keeps per-contact drafts and adds pinning, mark-unread, and swipe-to-mute/delete, and a conversation can be opened as a floating bubble. - -**August 2026** — [Units & Locale](user/units-and-locale) — New page. Units now follow your device region and OS locale, with a Units setting to override them, and all number formatting goes through ICU. - diff --git a/docs/en/user/desktop.md b/docs/en/user/desktop.md index 34cb454d48..57dba50579 100644 --- a/docs/en/user/desktop.md +++ b/docs/en/user/desktop.md @@ -2,7 +2,7 @@ title: Desktop App parent: User Guide nav_order: 14 -last_updated: 2026-08-30 +last_updated: 2026-09-11 description: Install and use the Meshtastic Desktop app on Linux, macOS, and Windows — connections, feature parity, and keyboard shortcuts. aliases: - desktop @@ -62,7 +62,7 @@ Bluetooth Low Energy is supported on desktop via the [Kable](https://github.com/ |---------|---------|---------|-------| | Messaging | ✓ | ✓ | Full parity | | Node List | ✓ | ✓ | Full parity | -| Map | ✓ | ✓ | Interactive MapLibre map, with base map and overlay pickers and custom tile sources. No offline downloads or local `.mbtiles` archives | +| Map | ✓ | ✓ | Interactive MapLibre map, with base map and overlay pickers and custom tile sources. Offline terrain (hillshade and contours) can be downloaded; offline base-map packs and local `.mbtiles` archives cannot | | Map layers (`.kml`/`.kmz`/GeoJSON) | ✓ | ✓ | Same layer store and sheet as Android; imported files draw on the desktop map | | Site Planner | ✓ | ✓* | *Opens in your browser on desktop; the estimate is not drawn on the desktop map | | Settings | ✓ | ✓ | Full parity | @@ -122,7 +122,7 @@ Individual doc pages render with full formatting: ## Building from Source -```bash +```shell git clone https://github.com/meshtastic/Meshtastic-Android.git cd Meshtastic-Android ./gradlew :desktopApp:run @@ -134,7 +134,7 @@ Requirements: ## Known Limitations -- Offline tile downloads and local `.mbtiles` archives are not available on desktop. +- Offline base-map downloads and local `.mbtiles` archives are not available on desktop. Offline terrain is — see [Map & Waypoints](map-and-waypoints). - `.kml`/`.kmz`/GeoJSON layer import works — see [Map & Waypoints](map-and-waypoints#map-layers). Site Planner opens in your browser rather than in the app; to bring its coverage estimate onto the map, click the transmitter pin diff --git a/docs/en/user/discovery.md b/docs/en/user/discovery.md index 458e1a8b09..6c3988a285 100644 --- a/docs/en/user/discovery.md +++ b/docs/en/user/discovery.md @@ -2,7 +2,7 @@ title: Local Mesh Discovery parent: User Guide nav_order: 12 -last_updated: 2026-08-30 +last_updated: 2026-09-19 description: Explore your mesh network — the Local Mesh Discovery scanner, traceroute paths, neighbor maps, and node discovery tools. aliases: - discovery @@ -20,18 +20,18 @@ Discovery tools help you understand **how** your mesh network is connected — w The app offers two complementary approaches: -- **Local Mesh Discovery (Scanner)** — an automated mode that cycles your connected radio through different LoRa presets, listens on each, and ranks which preset performs best at your location. +- **Local Mesh Discovery (Scanner)** — an automated mode that cycles your connected node through different LoRa presets, listens on each, and ranks which preset performs best at your location. - **Manual exploration** — traceroute, Neighbor Info, and the node list, which you can use at any time to investigate specific paths and topology. ## Local Mesh Discovery (Scanner) -Local Mesh Discovery is a dedicated scanning mode that helps you find the best LoRa modem preset for your location and see which nodes are active on each preset. It cycles your connected radio through one or more presets you choose, dwells on each one — listens for a set time — to collect packets, then analyzes and ranks the results. +Local Mesh Discovery is a dedicated scanning mode that helps you find the best LoRa modem preset for your location and see which nodes are active on each preset. It cycles your connected node through one or more presets you choose, dwells on each one — listens for a set time — to collect packets, then analyzes and ranks the results. -Connect your radio, then open **Settings → Advanced → Local Mesh Discovery**. On Android the **Advanced** section stays grayed out until a radio is connected and the app has finished reading its configuration, and every entry in it is disabled on a managed device. On desktop, Local Mesh Discovery has its own entry on the Settings screen, with no such gate. +Connect your node, then open **Settings → Advanced → Local Mesh Discovery**. On Android the **Advanced** section appears only for a locally connected node, never over remote admin, and stays grayed out until the app has finished reading the node's configuration. On a managed device its entries are disabled, except **Debug Panel**, which reads app-local logs and stays available. On desktop, Local Mesh Discovery has its own entry on the Settings screen, with no such gate. -> ℹ️ **Note:** Discovery temporarily changes your radio's LoRa settings while it scans, then restores your original configuration when it finishes. +> ℹ️ **Note:** Discovery temporarily changes your node's LoRa settings while it scans, then restores your original configuration when it finishes. -### Setting Up a Scan +### Setting up a scan Before starting, configure these controls: @@ -39,34 +39,34 @@ Before starting, configure these controls: |---------|-------------| | **LoRa preset picker** | Select one or more presets to scan. Discovery dwells on each selected preset in turn. | | **Dwell time** | Time to listen on each preset. Choose from 1, 5, 15, 30, 45, 60, 90, 120, or 180 minutes. Longer dwell times collect more packets and give a clearer picture, but take longer. | -| **Keep screen awake** | Keeps the phone out of Android Doze mode, which would otherwise drop radio packets during a long scan. Recommended — a scan run with it off can under-count what the radio heard. | +| **Keep screen awake** | Keeps the display on for the scan. The scan itself holds a CPU wake lock for its whole run and posts a **Scanning LoRa presets…** notification, so it keeps collecting with the screen off or the app in the background. | The **Start Scan** button stays disabled — with an explanation of why — until the scan can run. Common reasons it's disabled: -- The radio is **not connected**. +- The node is **not connected**. - **No presets** have been selected to scan. - The selected preset uses **2.4 GHz**, which your hardware doesn't support. -### Live Progress +### Live progress While a scan runs, Discovery shows its current stage: | Stage | What's happening | |-------|------------------| | **Preparing scan** | Saving your current configuration and getting ready to scan. | -| **Shifting to \** | Switching the radio to the next preset to test. | +| **Shifting to \** | Switching the node to the next preset to test. | | **Reconnecting on \** | Re-establishing the connection after the preset change. | | **Dwelling on \** | Listening on the current preset to collect packets, with a countdown to the next step. | | **Analyzing results** | Processing the collected packets and ranking the presets. | | **Restoring home preset** | Putting your original LoRa configuration back. | | **Cancelling scan** | You tapped **Stop Scan**; partial results are saved before the original preset is restored. | -| **Scan failed: \** | The scan could not continue — most often the radio did not come back within a minute of a preset change. The results collected so far are saved, and the original preset is restored automatically. | +| **Scan failed: \** | The scan could not continue — most often the node didn't come back within a minute of a preset change. The results collected so far are saved, and the original preset is restored automatically. | ![Dwell countdown showing time remaining on the current preset](../../assets/screenshots/discovery_dwell_progress.png) -If a scan is interrupted — the app is closed, or the radio goes away — the app restores your original preset the next time it reconnects to that radio, and tells you it has done so. Reconnect the same radio to let that happen; until you do, the radio stays on whichever preset the scan left it on. +If a scan is interrupted — the app is closed, or the node goes away — the app restores your original preset the next time it reconnects to that node, and tells you it has done so. Reconnect the same node to let that happen; until you do, the node stays on whichever preset the scan left it on. -### Reading the Results +### Reading the results When the scan completes, Discovery presents a per-preset result card for each preset it tested, plus an overall summary. @@ -94,28 +94,28 @@ Additional features available from the results: Mesh Beacon lets nodes invite others to join their mesh. A beaconing node periodically broadcasts an invitation — optionally advertising a channel, region, and modem preset — that nearby nodes can hear even before they share a configuration. -Configure it under **Settings → Module configuration → Mesh Beacon**. The entry appears only on radios running firmware 2.8.0 or newer. A read-only **Region** row at the top of the screen shows the region the beacon advertises: that region, and the preset, are always the ones the radio itself uses, so a beacon cannot invite anyone onto settings your radio is not running. +Configure it under **Settings → Module configuration → Mesh Beacon**. The entry appears only on nodes running firmware 2.8.0 or newer. A read-only **Region** row at the top of the screen shows the region the beacon advertises: that region, and the preset, are always the ones the node itself uses, so a beacon can't invite anyone onto settings your node isn't running. - **Listen for beacons** — receive invitations broadcast by other nodes. -- **Broadcast a beacon** — periodically advertise this mesh to nearby nodes, with an optional **Beacon message** of up to 100 bytes, a **Broadcast interval** picked from fixed intervals between 1 hour and 72 hours, and an **Offered channel** chosen from your radio's own channels. The offered channel is required, and defaults to your primary channel. -- **Broadcast targets** — optional extra destinations beyond the offered channel. **Add target** appends a row; each row picks a **Channel** and a **Transmit preset**, and **Remove target** deletes it. With no targets, the beacon goes out on the offered channel alone. +- **Broadcast a beacon** — periodically advertise this mesh to nearby nodes, with an optional **Beacon message** of up to 100 bytes, a **Broadcast interval** picked from fixed intervals between 1 hour and 72 hours, and an **Offered channel** chosen from your node's own channels. The offered channel is required, and defaults to your primary channel. Over remote admin the picker offers the primary channel only. +- **Broadcast targets** — where the beacon actually transmits. The list always holds at least one row: the first is the beacon's own transmission, not an extra. Each row picks a **Channel** and a **Transmit preset**. **Add target** appends a row, and **Remove target** deletes one — removing the last row replaces it with a fresh default rather than emptying the list. Two conditions block beacon setup: -- **The radio has no region set.** The screen shows nothing but *Set your radio's region before setting up a beacon.* Set the region on **Settings → LoRa** first. -- **The radio uses custom LoRa settings.** A beacon advertises a modem preset for others to join, so a radio with **Use Preset** turned off has no standard preset to offer. In that state **Broadcast a beacon** can be turned off but not on, and the broadcast settings are read-only. Listening for beacons is unaffected. +- **The node has no region set.** The screen shows nothing but *Set your node's region before setting up a beacon.* Set the region on **Settings → LoRa** first. +- **The node uses custom LoRa settings.** A beacon advertises a modem preset for others to join, so a node with **Use Preset** turned off has no standard preset to offer. In that state **Broadcast a beacon** can be turned off but not on, and the broadcast settings are read-only. Listening for beacons is unaffected. Received invitations appear as **Mesh invitations** cards on the Discovery screen. Each card shows the sender's message plus the offered channel, region, preset, and signal quality, with these actions: -- **Join** — switch to the offered channel and preset (retunes the radio and reboots). When the offer matches your current frequency slot, an **Add channel** action adds it without a reboot. +- **Join** — switch to the offered channel and preset (retunes the node and reboots). When the offer matches your current frequency slot, an **Add channel** action adds it without a reboot. - **Discover** — seed a Discovery scan with the offered preset so you can survey that mesh before joining (shown only when the beacon offers a preset). - **Dismiss** — ignore the invitation. Channels advertised by beacons also show up in the scan setup as **Beacon channels** — select one to include it as a scan target. -An invitation to a mesh your radio is already on is suppressed: no card, no notification, and no **Beacon channels** entry. A channel counts as one you already have only when both its name and its key match a channel on your radio — the same name with a different key is a different mesh, so that invitation still reaches you. +An invitation to a mesh your node is already on is suppressed: no card, no notification, and no **Beacon channels** entry. A channel counts as one you already have only when both its name and its key match a channel on your node — the same name with a different key is a different mesh, so that invitation still reaches you. -## Manual Exploration +## Manual exploration The following tools are available at any time from the node list and node detail screens. Use them to investigate specific paths and build a topology picture, alongside or instead of a full scan. @@ -128,7 +128,7 @@ Traceroute reveals the exact path a message takes from your node to any other no 1. Navigate to **Nodes** and tap the node you want to trace. 2. On the node detail screen, find **Traceroute** in the **Telemetry** section and tap its request button. Once a result arrives, a second button on the same row opens the traceroute log, where each hop is listed with its signal quality. -#### Reading the Results +#### Reading the results A traceroute result looks like this: @@ -142,12 +142,12 @@ Route traced toward destination: ■ Target Node (TGT1) ``` -Each `⇊` line between two nodes is one relay hop, and the SNR on that line is the quality of that segment alone. The app colors it green at or above −7 dB, yellow at or above −15 dB, and orange below that. A request that also gets a reply adds a second block under **Route traced back to us:**. +Each `⇊` line between two nodes is one relay hop, and the SNR on that line is the quality of that segment alone. The app colors it against the demodulation floor of the preset in use, not a fixed number: green above the floor, yellow within 5.5 dB below it, orange within 7.5 dB, and red beyond that. The floor is −7.5 dB on Short Fast and improves 2.5 dB per spreading-factor step, so it is −17.5 dB on Long Fast — the same SNR reads differently on different presets. See [Signal Meter](signal-meter). A request that also gets a reply adds a second block under **Route traced back to us:**. | What to look for | What it means | |------------------|---------------| -| All hops show Good SNR (≥ −7 dB, green) | Healthy path — messages flow reliably | -| One hop shows a poor SNR (below −15 dB, orange) | Weak link — this relay segment is fragile | +| All hops show Good SNR (green) | Healthy path — messages flow reliably | +| One hop shows a poor SNR (orange or red) | Weak link — this relay segment is fragile | | Many hops (4+) | Long path — consider repositioning a node to shorten it | | Different path on retry | Mesh is adapting — multiple routes exist (this is good!) | @@ -158,7 +158,7 @@ Each `⇊` line between two nodes is one relay hop, and the SNR on that line is - **No Response** — The traceroute got nothing back. The target node may be offline, out of range, or on a different channel. Check that both nodes share at least one channel with the same encryption key. - **Traceroute times out** — The path may be too long (exceeds hop limit) or a relay node is congested. Try increasing the hop limit in **Settings → LoRa**. - **Cannot show traceroute map because the start or destination node has no position information** — The path was traced, but one end has never shared a position. -- **Asymmetric paths** — A traceroute from A→B may take a different path than B→A. This is normal — radio propagation is not always symmetric. +- **Asymmetric paths** — A traceroute from A→B may take a different path than B→A. This is normal — radio propagation isn't always symmetric. ### Neighbor Info @@ -168,42 +168,42 @@ The Neighbor Info module lets each node broadcast a list of the nodes it can **d 1. Navigate to **Settings → Module configuration → Neighbor Info**. 2. Enable the module. -3. Set **Update interval (seconds)**. The default is 21600 seconds (6 hours), and the firmware minimum is 14400 seconds (4 hours) — a smaller value is rejected and reset to the default. -4. Turn on **Transmit over LoRa**. Without it, your neighbor list goes only to MQTT and to this app, never over the air. It is unavailable on a channel that still uses the default name and key, so set up your own channel first — see [Messages & Channels](messages-and-channels). +3. Set **Update interval (seconds)**. The app accepts whatever you type; the firmware enforces its own minimum and resets a value below it. +4. Turn on **Transmit over LoRa**. Without it, your neighbor list goes only to MQTT and to this app, never over the air. Once enabled and transmitting over LoRa, your node periodically broadcasts its neighbor list. Other nodes with Neighbor Info enabled do the same. -#### Viewing Neighbor Data +#### Viewing neighbor data - Open a node's detail screen and find **Neighbor Info** in the **Telemetry** section. The request button asks the node for its current neighbor list; once the app has received one, a second button on the same row opens the log of everything that node has reported. The row appears only on nodes that can answer a neighbor request, or that have already reported neighbors. - Each neighbor entry shows the node that was directly heard and its signal quality. - Combine neighbor data from multiple nodes to understand the full mesh topology. -> ℹ️ **Note:** Neighbor Info increases airtime usage because every enabled node periodically broadcasts its neighbor list. The firmware does not accept an interval shorter than 14400 seconds (4 hours) for this reason; on busy meshes, leave it at the 21600-second default or raise it further. +> ℹ️ **Note:** Neighbor Info increases airtime usage because every enabled node periodically broadcasts its neighbor list. The firmware doesn't accept an interval shorter than 14400 seconds (4 hours) for this reason; on busy meshes, leave it at the 21600-second default or raise it further. -### Node List as a Discovery Tool +### Node list as a discovery tool The node list itself is a powerful discovery tool when you use its filtering and sorting features effectively. -#### Finding New Nodes +#### Finding new nodes - Sort by **Last heard** to see the most recently active nodes at the top. -- Enable **Include unknown** to see nodes that have appeared on the mesh but haven't sent user info yet — these are often newly powered-on radios. +- Enable **Include unknown** to see nodes that have appeared on the mesh but haven't sent user info yet — these are often newly powered-on nodes. -#### Assessing Connectivity +#### Assessing connectivity - Sort by **Hops away** to see which nodes are directly reachable (0 hops) versus relayed. - Sort by **Distance** to find nearby nodes and verify they're reachable. -- Use **Exclude MQTT** to focus on nodes reachable over radio (not via internet bridge). +- Use **Exclude MQTT** to focus on nodes reachable over LoRa (not via internet bridge). -#### Infrastructure Audit +#### Infrastructure audit - Disable **Exclude infrastructure** to see Router, Router Late, and Client Base nodes. - Check their signal quality and last-heard times to verify your infrastructure nodes are healthy. See [Nodes](nodes) for full details on filtering and sorting options. -## Tips for Mesh Exploration +## Tips for Mesh exploration - **Start with traceroute** — it gives you immediate, actionable information about a specific path. - **Enable Neighbor Info on key nodes** — especially routers and repeaters, to build a picture of the backbone. diff --git a/docs/en/user/help-and-docs.md b/docs/en/user/help-and-docs.md index f306cde7f7..c83898cea3 100644 --- a/docs/en/user/help-and-docs.md +++ b/docs/en/user/help-and-docs.md @@ -2,7 +2,7 @@ title: Help & In-App Docs parent: User Guide nav_order: 21 -last_updated: 2026-08-30 +last_updated: 2026-09-11 description: Browse this documentation inside the app, search it, and ask Chirpy — the on-device AI assistant — questions about Meshtastic. aliases: - help @@ -23,7 +23,7 @@ The docs browser lists every user-guide page. Tap a page to read it; images and ### Search -The search box sits at the top of the docs browser. Type in it to filter pages by title and keywords — results update as you type, and the **Clear search** button (✕) empties the box. +Tap the search field at the top of the docs browser to open a full-screen search view. Type to filter pages by title and keywords — results update as you type, under the same field. The back arrow, or the keyboard's search action, closes the view. The field hides while you scroll down the page list and comes back when you scroll up. ![Searching the in-app documentation](../../assets/screenshots/docs-browser_search.png) diff --git a/docs/en/user/map-and-waypoints.md b/docs/en/user/map-and-waypoints.md index 28267b1902..b777873679 100644 --- a/docs/en/user/map-and-waypoints.md +++ b/docs/en/user/map-and-waypoints.md @@ -2,7 +2,7 @@ title: Map & Waypoints parent: User Guide nav_order: 6 -last_updated: 2026-09-01 +last_updated: 2026-09-11 description: View node positions on the map, create and share waypoints, manage map layers and Site Planner, and control position sharing and privacy. aliases: - map @@ -43,7 +43,7 @@ The floating toolbar provides quick access to the compass, the map type and laye ### Filtering the Map -Tap the filter button in the floating toolbar to open **Filter map**. **Display** controls what is drawn: **Only Favorites**, **Show Waypoints**, **Show Precision Circles**, and a slider that hides nodes not heard from recently. **Node roles** is a chip per device role, plus **All** to show every role; a selected chip means that role is shown. **Nodes** narrows the set further with **Hide offline nodes**, **Only show direct nodes**, **Exclude MQTT**, **Show ignored nodes**, and **Include unknown**. +Tap the filter button in the floating toolbar to open **Filter map**. **Display** controls what is drawn: **Only Favorites**, **Show Waypoints**, **Show Precision Circles**, and a slider that hides nodes not heard from recently. **Node roles** is a chip per device role, plus **All** to show every role; a selected chip means that role is shown. **Nodes** narrows the set further with **Hide offline nodes**, **Only show direct nodes**, **Signed only**, **Encrypted only**, **Exclude MQTT**, **Show ignored nodes**, and **Include unknown** — the same rules as the node list's [filter toggles](nodes). A dot on the filter button means at least one filter is hiding something — check it before concluding the mesh is quiet. Turning **Show Waypoints** off hides every waypoint, including your own. **Show ignored nodes** adds them to the map rather than showing only them — unlike the node list's **Only show ignored Nodes**. @@ -144,8 +144,9 @@ picker, all three offer the same raster base maps: Overlays can be toggled on top of any base map, from the layers sheet: - **Weather radar** — NOAA NEXRAD reflectivity (US coverage) -- **Hillshade** — terrain relief, on **F-Droid** and **Desktop** only. Useful for understanding why a - link fails, since LoRa range is limited by terrain +- **Hillshade** — terrain relief from an online tile source, on **F-Droid** and **Desktop** only; Google + Play draws hillshade from downloaded offline terrain instead. Useful for understanding why a link + fails, since LoRa range is limited by terrain ### Adding your own tile source @@ -153,7 +154,7 @@ Any XYZ tile endpoint can be added as a base map, on every flavor and on desktop Tile Sources** at the foot of the base map picker and paste a URL template using `{z}`, `{x}` and `{y}` — plus `{s}` if the provider uses rotating subdomains. A national mapping service, for example: -``` +```text https://wmts.geo.admin.ch/1.0.0/ch.swisstopo.pixelkarte-farbe/default/current/3857/{z}/{x}/{y}.jpeg ``` @@ -170,7 +171,18 @@ On **Google Play**, the layers sheet's **Offline Manager** section downloads its — water, roads and administrative boundaries extracted from the public Protomaps basemap dataset, drawn directly as map shapes rather than raster tiles. Frame the area you want, tap **Start Download**, and once it finishes flip **Show on map** to draw it (it's off by default so it never surprises you by covering up the -live map). You can still import a pre-made `.mbtiles` archive here too, as before. **Desktop** has neither. +live map). You can still import a pre-made `.mbtiles` archive here too, as before. **Desktop** has neither, though +the offline terrain described next does work there. + +### Offline terrain + +Hillshade and elevation contours can be downloaded for offline use as well, so a downloaded area still +shows why a link fails when there is no network. The terrain data comes from Mapterhorn and is credited on +the map. On **Google Play**, each downloaded region in the **Offline Manager** list has a **Download Terrain** +button and **Hillshade** and **Contours** switches; terrain attaches to that region and shares its storage limit, and an area +too large for terrain asks you to zoom in or pick a smaller region. On **F-Droid** and **Desktop**, the +layers sheet has an **Offline Terrain** section of its own: frame the area, tap **Start Download**, and the +hillshade and contours draw from the downloaded tiles from then on. ### Going offline @@ -178,8 +190,8 @@ An **Offline** pill appears over the map whenever the device has no network conn builds, if you've imported a local `.mbtiles` archive, the map switches to it automatically the moment the network drops — no toggle to remember — and switches back once you're reconnected, as long as you haven't picked a different base map yourself in between. On **F-Droid**, a downloaded offline area keeps rendering on -its own; the pill is purely informational there. **Desktop** has no offline downloads at all yet, so the pill -is informational there too. +its own; the pill is purely informational there. **Desktop** downloads offline terrain but no base-map +packs, so the pill is informational there too. ## Related Topics diff --git a/docs/en/user/messages-and-channels.md b/docs/en/user/messages-and-channels.md index 030ef50a37..87ed74a9d0 100644 --- a/docs/en/user/messages-and-channels.md +++ b/docs/en/user/messages-and-channels.md @@ -2,7 +2,7 @@ title: Messages & Channels parent: User Guide nav_order: 3 -last_updated: 2026-08-30 +last_updated: 2026-09-14 description: Send and receive messages, manage channels, configure encryption, search conversations, and use quick chat, reactions, and message actions. aliases: - channels @@ -50,6 +50,21 @@ Key length alone does not change the icon: a 128-bit key and a 256-bit key both Tapping an existing channel opens the same editor, where you can change the name, the PSK, MQTT uplink and downlink, and position precision. Every edit on this screen — adding, editing, deleting, or dragging a channel into a new order — waits on **Send** the same way. +### Archived Channels + +A conversation belongs to the channel it was held on, not to the slot that channel occupied. When a channel leaves your radio — you scan a different channel QR, replace a channel's key, or switch the modem preset — its conversation is **archived** rather than deleted or merged into whatever takes the slot. + +An archived conversation: + +- stays in the **Channels** list under the name the channel had, below the channels currently on your radio, marked with a history icon; +- keeps its full message history, searchable as usual; +- is read-only — you cannot send, reply, or react, because the channel is no longer on the radio to send on, and it is not offered as a share target; +- comes back automatically if you re-add that exact channel, taking its messages back to the live slot. + +Changing the modem preset archives the channel too: the preset is part of a default channel's identity, so **LongFast** and **MediumFast** are different channels even though neither the name nor the key changed. Changing only the region does not archive anything. + +To remove an archived conversation for good, long-press it in the conversation list and delete it. + ## Direct Messages Direct messages (DMs) go to one specific node. When both radios hold each other's public keys, your radio encrypts the message to that node's public key, so no one else on the mesh can read it — not even nodes that share your channel. diff --git a/docs/en/user/mqtt.md b/docs/en/user/mqtt.md index ba61c095e8..adab74117e 100644 --- a/docs/en/user/mqtt.md +++ b/docs/en/user/mqtt.md @@ -2,7 +2,7 @@ title: MQTT parent: User Guide nav_order: 11 -last_updated: 2026-08-30 +last_updated: 2026-09-11 description: Bridge your mesh to the internet — MQTT broker setup, encryption layers, and map reporting. aliases: - mqtt @@ -24,7 +24,7 @@ The MQTT module connects your node to an MQTT broker, allowing: ## How It Works -``` +```text [Your Node] → Radio → [Gateway Node with Wi-Fi] → MQTT Broker → [Remote Gateway] → Radio → [Remote Node] ``` diff --git a/docs/en/user/node-metrics.md b/docs/en/user/node-metrics.md index 90e9f29c2a..afe552ffe8 100644 --- a/docs/en/user/node-metrics.md +++ b/docs/en/user/node-metrics.md @@ -2,7 +2,7 @@ title: Node Metrics parent: User Guide nav_order: 5 -last_updated: 2026-08-30 +last_updated: 2026-09-09 description: Telemetry dashboards for each mesh node — device health, environment sensors, air quality, signal quality, power, traceroute, and position history. aliases: - metrics @@ -114,7 +114,7 @@ Radio signal quality information: ### Signal Quality Reference -Signal quality is rated from **SNR relative to the active LoRa modem preset's demodulation floor**, not from fixed thresholds — a given SNR means different things on different presets (e.g. −15 dB is fine on LongSlow but unusable on ShortFast). RSSI is shown but is not part of the rating. In the table, *limit* is the preset's SNR limit. +Signal quality is rated from **SNR relative to the active LoRa modem preset's demodulation floor**, not from fixed thresholds — a given SNR means different things on different presets (e.g. −15 dB is fine on LongSlow but unusable on ShortFast). When RSSI and a noise-floor reading are both available, the app also rates their difference against the preset limit and uses the worse rating. Otherwise, RSSI is display-only. In the table, *limit* is the preset's SNR limit. | Quality | Criteria | |---------|----------| diff --git a/docs/en/user/nodes.md b/docs/en/user/nodes.md index de67e6b502..732807b593 100644 --- a/docs/en/user/nodes.md +++ b/docs/en/user/nodes.md @@ -2,7 +2,7 @@ title: Nodes parent: User Guide nav_order: 4 -last_updated: 2026-09-04 +last_updated: 2026-09-19 description: Browse, filter, and sort mesh nodes — view details, signal quality, roles, and quick actions. aliases: - node-list @@ -15,31 +15,35 @@ aliases: The Nodes screen lists every node visible on your mesh. -## Node List +## Node list -The node list shows every node your radio has heard, including: +The node list shows every node your node has heard, including: - **Node name** — user-configured long name - **Short name** — 4-character identifier -- **Signal quality** — SNR, RSSI, and a quality word, shown only for nodes your radio heard directly. In the Complete layout a node reached through a relay shows its hop count here instead; a node heard only over MQTT shows neither +- **Signal quality** — SNR, RSSI, and a quality word, shown only for nodes your node heard directly. In the Complete layout a node reached through a relay shows its hop count here instead; a node heard only over MQTT shows neither - **Last heard** — time since last communication - **Distance** — estimated distance (if positions are shared) - **Battery** — remote node battery level (if telemetry is enabled) -### Choosing What the List Shows +### Choosing what the list shows The list has two densities, set at **Settings → Node Layout**. **Complete** shows every field a node has reported and hides the ones it hasn't. **Compact** fits more nodes on screen and lets you pick the fields yourself — **Power**, **Last Heard Time**, **Relative Last Heard Time**, **Distance and Bearing**, **Hops Away**, **Signal (Direct Only)**, **Channel**, and **Device & Role**. The **Environment Metrics** toggle applies to both densities. A preview above the toggles shows the effect before you leave the screen. -### Node Status Indicators +### Node Status indicators | Indicator | Meaning | |-------|---------| | Green last-heard time | Node heard within the last 2 hours | | Plain last-heard time | Node not heard for over 2 hours | +| Orange last-heard time with a crossed-out signal icon | Not heard since your node's LoRa settings changed, so it can't be reached from here | +| Struck-through name | Node you have ignored | | ⭐ Favorite | Node you marked as a favorite. | -There is no separate "away" tier. +There is no separate "away" tier, but the orange unreachable state takes precedence over the +green one: a node can be online and still be unreachable on your current settings. Nodes known +only over MQTT are never shown as unreachable. -### Node Roles +### Node roles Nodes can be configured with different roles that affect their mesh behavior: @@ -57,35 +61,39 @@ Nodes can be configured with different roles that affect their mesh behavior: | Sensor | Optimized for telemetry reporting | | TAK | Interoperates with TAK systems (sends/receives CoT) | | TAK Tracker | TAK position reporting only | -| Lost and Found | Sends its position to the default channel as a text message at regular intervals, to help recover a lost radio | +| Lost and Found | Sends its position to the default channel as a text message at regular intervals, to help recover a lost node | -### Choosing a Role +### Choosing a role Most users should keep the default **Client** role. Consider a different role when: -- **Router** — You have a node in a fixed, elevated location with reliable power (rooftop, hilltop). Routers stay awake continuously to relay messages for others and are essential for extending mesh coverage. Don't use Router on battery-powered handheld radios. +- **Router** — You have a node in a fixed, elevated location with reliable power (rooftop, hilltop). Routers stay awake continuously to relay messages for others and are essential for extending mesh coverage. Don't use Router on battery-powered handheld nodes. - **Router Late** — An infrastructure node that always rebroadcasts packets once but only after all other routing modes have had their turn. Provides supplemental coverage for local clusters without competing with primary routers. - **Client Base** — Treats traffic from/to your favorited nodes with Router Late priority (ensuring those messages get extra relay coverage) while handling everything else as a normal Client. -- **Client Mute** — You want to receive mesh traffic but not contribute to relaying. Useful for monitoring-only radios or to reduce congestion in dense areas. -- **Tracker** — An unattended radio whose sole purpose is broadcasting its GPS position (e.g., a vehicle, pet, or asset). Sleeps between broadcasts to conserve battery. -- **Sensor** — An unattended radio reporting environmental telemetry (temperature, humidity, air quality). Similar power profile to Tracker. +- **Client Mute** — You want to receive mesh traffic but not contribute to relaying. Useful for monitoring-only nodes or to reduce congestion in dense areas. +- **Tracker** — An unattended node whose sole purpose is broadcasting its GPS position (e.g., a vehicle, pet, or asset). Sleeps between broadcasts to conserve battery. +- **Sensor** — An unattended node reporting environmental telemetry (temperature, humidity, air quality). Similar power profile to Tracker. - **TAK / TAK Tracker** — Only needed if interoperating with ATAK/WinTAK systems. See [TAK Integration](tak) for details. > 💡 **Tip:** The mesh works best when most nodes are **Client** or **Router**. Too many Client Mute nodes reduce mesh resilience; too many Routers in a dense area can cause congestion. A good rule of thumb: one Router per 5–10 Clients in your area. -### Encryption Indicators +### Security indicators -Nodes display encryption status icons next to their name: +Each node carries one security icon beside its name in the node list. Tap it to read what it means, and choose **Show All Meanings** in that dialog for the full legend. The detail screen shows the same state in words, as a **Security** row that opens the same dialog. -| Icon | Meaning | -|------|---------| -| 🔒 Locked | Communication uses PKI (public key infrastructure) — end-to-end encrypted with verified identity | -| 🔓 Unlocked | No public key has been received for this node, so it cannot be direct messaged — use **Request User Info** on the node detail page to ask for one | -| ⚠️ Mismatch | Public key mismatch — the node's key has changed since last seen (investigate before trusting) | +| Icon | Meaning | Shown for | +|------|---------|-----------| +| Person with a shield check (green) | **Verified contact** — you verified this node's key in person by exchanging contact QR codes, so its identity is confirmed. The strongest trust the list shows. Your own connected node carries it too | Any firmware version | +| Nodes icon with a shield check (green) | **Signed node** — this node signs its broadcasts with its identity key, so its identity is consistent over time, but you haven't verified it in person. On firmware 2.8 the icon appears from the version alone, before any signed broadcast has been heard | Firmware 2.8 or newer, and any node whose signed broadcast your node has verified | +| 🔒 Closed lock | A public key is on file and matches, so direct messages to this node are encrypted | Firmware before 2.8, or no reported version | +| 🔓 Open lock | No public key has been received for this node, so it can't be direct messaged — use **Request User Info** on the node detail page to ask for one | Firmware before 2.8, or no reported version | +| ⚠️ Mismatch | **Public key mismatch** — a different key arrived for this node after one was stored. Investigate before trusting | Any firmware version | -> 💡 **Tip:** Direct messages always use PKI, so the radio needs the other node's public key before it can send one. It refuses the send rather than falling back to channel encryption. Keys arrive inside node info, which is why an open lock usually clears itself once that node is heard from properly. If you see a key mismatch warning, the node may have been reset or compromised. +Every node on firmware 2.8 or newer signs its broadcasts, so on that firmware the signed state is the baseline and the locks aren't shown. -To clear a mismatch, first confirm through another trusted channel that the key change was intentional — a factory reset causes one. Then touch & hold the node, choose **Remove**, and let the two radios exchange keys again the next time yours hears it. +Direct messages always use public-key encryption, so your node needs the other node's public key before it can send one. It refuses the send rather than falling back to channel encryption. Keys arrive inside node info, which is why an open lock usually clears itself once that node is heard from properly. + +A mismatch never replaces the key you already hold. The app keeps the first key it recorded and refuses the new one, as the firmware does, so a stray or hostile node info can't silently break encrypted messaging to a contact. To clear a mismatch, first confirm through another trusted channel that the key change was intentional — a factory reset causes one. Then touch & hold the node, choose **Remove**, and let the two nodes exchange keys again the next time yours hears it. ## Quick Actions @@ -101,33 +109,55 @@ From the node list, you can: Touch & hold **your own node** instead and you get one action, **Update status**, which opens the User settings screen with the cursor already in the Status Message field. It only appears while the -radio is connected and running firmware 2.8 or newer — see +node is connected and running firmware 2.8 or newer — see [Settings — Radio & User](settings-radio-user.md) for the field itself. -## Sharing a Contact +## Sharing a contact -On a node's detail screen, tap **Share Contact** to produce a link and a QR code for that node. From the same dialog, **Write to NFC tag** saves the link to a writable NFC tag that anyone can tap to open. +On a node's detail screen, tap **Share Contact** to produce a link and a QR code for that node. From the same dialog, **Share link** opens the Android share sheet (on desktop it copies the link instead), **Write to NFC tag** saves it to a writable NFC tag, and **Copy** puts it on the clipboard. While that dialog is open and in front of you, the phone also offers the same link to any NFC reader, so someone can take the contact by tapping their phone against yours with no tag involved. + +Sharing your own contact this way marks it as verified in person, so whoever imports it sees the **Verified contact** icon rather than the signed one. Relaying someone else's contact passes on only what your app had already recorded about them. To add someone else's contact, use the import button on the node list and choose **Scan Shared Contact QR Code**, **Scan Shared Contact NFC**, or **Input Shared Contact URL**. The app asks you to confirm with **Import Shared Contact?**, and warns you when the contact is one you already have. -## Filtering & Sorting +## Filtering & sorting -### Text Search +### Text search Type in the search field to filter nodes by name or short name. The filter updates in real time as you type. -### Filter Toggles +### Filter toggles | Filter | Description | |--------|-------------| | **Hide offline nodes** | Show only nodes heard within the last 2 hours | -| **Only show direct nodes** | Show only nodes your radio heard directly, with no relay in between | -| **Include unknown** | Show nodes that haven't sent user info yet. **On by default**, so a node heard before its info arrives stays visible; these carry a badge marking them incomplete, and cannot be direct messaged until their user info brings a public key | -| **Exclude infrastructure** | Hide infrastructure-role nodes (Router, Router Late, Client Base, and legacy Repeater nodes) and any node that cannot be messaged, whatever its role | +| **Only show direct nodes** | Show only nodes your node heard directly, with no relay in between | +| **Include unknown** | Show nodes that haven't sent user info yet. **On by default**, so a node heard before its info arrives stays visible; these carry a badge marking them incomplete, and can't be direct messaged until their user info brings a public key | +| **Exclude infrastructure** | Hide infrastructure-role nodes (Router, Router Late, Client Base, and legacy Repeater nodes) and any node that can't be messaged, whatever its role | | **Exclude MQTT** | Hide nodes heard only via MQTT internet bridge | +| **Hide unheard nodes** | Hide nodes your own node hasn't heard since its LoRa settings changed. Off by default, and it does nothing on firmware that doesn't report whether a node was heard on the current settings | +| **Signed only** | Show only nodes whose signed broadcasts your node has actually heard and verified. Stricter than the icon: a node on 2.8 shows as signed by its firmware version before any signed broadcast arrives, and a contact verified in person on older firmware isn't signed at all | +| **Encrypted only** | Show nodes with a matching public key on file, the key an encrypted direct message needs. A node with a key mismatch is excluded | | **Only show ignored Nodes** | Replace the list with the nodes you have ignored. Every other node is hidden while this is on, and a banner appears at the top of the list to take you back | -### Sort Options +### Nodes not heard on your current Settings + +When your node's LoRa settings change — a different preset, region or frequency slot — nodes heard +under the old settings are still in the list but can no longer be reached. The app marks them with +an orange last-heard time and a crossed-out signal icon, and shows a banner at the top of the list: +**N nodes not heard on your current LoRa settings**. + +The banner offers two actions: + +- **Keep** dismisses the banner and leaves every node in place. +- **Remove** deletes those nodes from the list. Your favorites and your own node are never removed. + +**Remove** is a bulk delete and there is no undo, so use **Keep** if you expect to switch back to +the old settings. Removed nodes reappear if your node hears them again. + +The **Hide unheard nodes** filter does the same hiding without deleting anything. + +### Sort options | Sort | Description | |------|-------------| @@ -136,17 +166,19 @@ Type in the search field to filter nodes by name or short name. The filter updat | **Distance** | Nearest nodes first (requires position sharing) | | **Hops away** | Fewest relay hops first | | **Channel** | Grouped by channel index | -| **via MQTT** | Grouped by MQTT vs. radio-heard | +| **via MQTT** | Grouped by MQTT vs. node-heard | | **via Favorite** (default) | Favorited nodes first, then the rest | -## Nodes per Hop +## Nodes per hop -Tap the hop-histogram icon in the node list's app bar to open a bar chart of how many nodes sit at each hop distance (0 = direct, 1 = one relay away, and so on). Filter the chart to a **last heard** window — All time, 1 hour, 8 hours, or 24 hours — to see how the mesh looks right now versus over a longer period. It's a quick way to gauge how busy and spread out your local mesh is. +Tap the hop-histogram icon in the node list's app bar to open a bar chart of how many nodes sit at each hop distance (0 = direct, 1 = one relay away, and so on). Filter the chart to a **last heard** window — **All**, **1 Hour**, **8 Hours** or **24H** — to see how the mesh looks right now versus over a longer period. It's a quick way to gauge how busy and spread out your local mesh is. -## Node Detail +## Node detail Tapping a node opens the detail view with comprehensive information. See [Node Metrics](node-metrics) for full details on metrics and telemetry. +Signal quality is rated against your modem preset. The same SNR can be good on a long-range preset and poor on a faster one. Traceroute and neighbor-info SNR colors use that preset too. RSSI text has its own strength colors; it affects the quality rating only when a noise-floor reading is also available. + The Details card carries the node's short name, role, IDs, last heard time, hops away, uptime, and its SNR and RSSI: ![Node detail section](../../assets/screenshots/nodes_detail_section.png) @@ -161,7 +193,7 @@ Inline status indicators show key metrics at a glance: | Last heard | ![Last heard](../../assets/screenshots/nodes_last_heard.png) | | Distance | ![Distance](../../assets/screenshots/nodes_distance_info.png) | -### Device Links ("I want one") +### Device links ("I want one") When a node's hardware is recognized, the detail view shows a collapsible **"I want one"** section linking to places to buy or learn more about that device: the vendor's product page, product variants, and regional marketplace listings (such as AliExpress, Amazon, and supported retailers), filtered to your country. Each link opens through the `msh.to` redirect service. Devices with no matching links don't show the section. @@ -169,12 +201,12 @@ A full, browsable directory of every link is also available at **Settings → De Some of these are affiliate links. Both places say so above the links: product links may be affiliate links, and purchases may earn Meshtastic a commission. -## When No Nodes Appear +## When no nodes appear -The list stays empty until your radio hears another node. +The list stays empty until your node hears another node. -- **No device connected** — the app is not connected to a radio. See [Connections](connections). -- **Searching for nodes** — the radio is connected and listening, but nothing has arrived yet. Check that its region and modem preset match the mesh around you, and leave **Include unknown** on so a node that has not yet sent its name still appears. See [Settings — Radio & User](settings-radio-user). +- **No device connected** — the app isn't connected to a node. See [Connections](connections). +- **Searching for nodes** — the node is connected and listening, but nothing has arrived yet. Check that its region and modem preset match the mesh around you, and leave **Include unknown** on so a node that hasn't yet sent its name still appears. See [Settings — Radio & User](settings-radio-user). - A node you expect is missing — check the filter toggles. **Only show direct nodes**, **Exclude MQTT**, and **Exclude infrastructure** each hide a whole category of node. ## Related Topics diff --git a/docs/en/user/settings-module-admin.md b/docs/en/user/settings-module-admin.md index 4fc7534b80..ad9f57c397 100644 --- a/docs/en/user/settings-module-admin.md +++ b/docs/en/user/settings-module-admin.md @@ -2,7 +2,7 @@ title: Settings — Modules & Admin parent: User Guide nav_order: 8 -last_updated: 2026-08-30 +last_updated: 2026-09-19 description: Configure optional feature modules (MQTT, telemetry, canned messages, TAK, and more) and perform device administration. aliases: - modules @@ -14,7 +14,7 @@ aliases: Configure optional feature modules and perform device administration. Modules extend Meshtastic with specialized capabilities — each can be independently enabled or disabled. -> 💡 **Tip:** You only need to enable the modules you actually use. Disabling unused modules reduces airtime, saves battery, and simplifies your configuration. A module you expect can be missing for three reasons: your node's role does not enable it, your firmware is older than the release that added it, or the firmware build excludes it for this hardware. +> 💡 **Tip:** You only need to enable the modules you actually use. Disabling unused modules reduces airtime, saves battery, and simplifies your configuration. A module you expect can be missing for three reasons: your node's role doesn't enable it, your firmware is older than the release that added it, or the firmware build excludes it for this hardware. Module settings use a card-based layout with toggle switches, dropdowns, text fields, and sliders: @@ -26,15 +26,15 @@ Module settings use a card-based layout with toggle switches, dropdowns, text fi ![A module settings card with its title and grouped controls](../../assets/screenshots/settings_titled_card.png) -## Module Configuration +## Module configuration Every module lives under **Settings → Module configuration**. -> ⚠️ **Important:** Saving a module screen restarts the radio — the button reads **Save & restart**, and the radio is unreachable for a few seconds afterwards. External Notification and Mesh Beacon are the exceptions: their button reads **Save**, and the radio may still restart for some changes. +> ⚠️ **Important:** Saving a module screen restarts the node — the button reads **Save & restart**, and the node is unreachable for a few seconds afterwards. External Notification and Mesh Beacon are the exceptions: their button reads **Save**, and the node may still restart for some changes. -### MQTT Module +### MQTT module -Bridges mesh messages to and from an MQTT broker for internet connectivity. This is how you extend your mesh beyond radio range or integrate with home automation systems. +Bridges mesh messages to and from an MQTT broker for internet connectivity. This is how you extend your mesh beyond LoRa range or integrate with home automation systems. | Setting | Description | |---------|-------------| @@ -51,18 +51,18 @@ Bridges mesh messages to and from an MQTT broker for internet connectivity. This | Map reporting | Publish position to the public map — see the Map reporting group that follows | Turning **Map reporting** on reveals a consent card headed *Consent to Share Unencrypted Node Data -via MQTT*, with an **I agree.** switch under it. The rest of the card does not exist on screen +via MQTT*, with an **I agree.** switch under it. The rest of the card doesn't exist on screen until you agree: | Setting | Description | |---------|-------------| -| I agree. | Explicit consent to transmit your node data, including an approximate position, unencrypted. Map reporting does not start without it | +| I agree. | Explicit consent to transmit your node data, including an approximate position, unencrypted. Map reporting doesn't start without it | | Precision slider | The slider has no label of its own. It sets how coarsely your position is published, from 12 to 15; the line beneath reads *±* the resulting distance, in your own units | | Map reporting interval (seconds) | How often to report. A dropdown of fixed intervals from 1 hour to 72 hours — nothing shorter is offered | See [MQTT](mqtt) for a detailed usage guide including encryption, privacy, and broker setup. -### Serial Module +### Serial module Enables serial port communication for external device integrations (GPS modules, sensors, or custom hardware). When enabled, the node's serial port can send and receive protobuf or text data, allowing external microcontrollers or computers to interact with the mesh. @@ -76,9 +76,9 @@ Enables serial port communication for external device integrations (GPS modules, | Timeout | How long to wait before considering an incoming message complete | | Override console serial port | Take over the port the debug console normally uses | -### External Notification Module +### External Notification module -Controls buzzer, LED, or vibration alerts on your radio hardware. Useful for devices that need to physically signal when a message arrives — particularly helpful for unattended or outdoor installations. +Controls buzzer, LED, or vibration alerts on your node hardware. Useful for devices that need to physically signal when a message arrives — particularly helpful for unattended or outdoor installations. There are two independent triggers — an incoming **message**, and a received **bell** character — and each can drive the LED, the buzzer and the vibration motor separately, giving six toggles. @@ -98,7 +98,7 @@ and each can drive the LED, the buzzer and the vibration motor separately, givin | Nag timeout (seconds) | Keep repeating the alert for this long until it is acknowledged. 0 disables nagging | | Ringtone | The tone played on a PWM buzzer, in RTTTL. Can be imported from a file | -### Store & Forward Module +### Store & Forward module Buffers messages for nodes that were temporarily offline, then replays them when those nodes reconnect. Essential for meshes where nodes go in and out of range regularly — ensures messages aren't lost during brief disconnections. @@ -113,7 +113,7 @@ Buffers messages for nodes that were temporarily offline, then replays them when > 💡 **Tip:** Store and Forward works best on nodes with ample memory (ESP32 with PSRAM). Router nodes are ideal candidates since they're typically always-on. -### Range Test Module +### Range Test module > ⚠️ **Warning:** Range Test only works on a secured primary channel. As long as your primary channel > still uses the default channel key, the interval and CSV controls stay disabled — you can still @@ -126,9 +126,9 @@ Automated range testing tool for evaluating link quality between nodes. When ena |---------|-------------| | Range test enabled | Activate range testing | | Sender message interval (seconds) | Time between test transmissions, chosen from a dropdown of fixed intervals | -| Save .CSV in storage (ESP32 only) | Log received test data to the radio's own filesystem. ESP32 hardware only | +| Save .CSV in storage (ESP32 only) | Log received test data to the node's own filesystem. ESP32 hardware only | -### Telemetry Module +### Telemetry module Controls what telemetry data your node shares with the mesh. Telemetry includes device health (battery, uptime) and environmental sensor data (temperature, humidity, pressure). @@ -142,7 +142,7 @@ battery health often and sensors rarely. | Environment metrics module enabled | Report the attached environment sensors | | Environment metrics update interval | How often to report them | | Environment metrics on-screen enabled | Also show these readings on the device's own display | -| Environment metrics use Fahrenheit | Use °F on the device's display. This is the radio's screen only — the app follows your phone's locale, see [Units & Locale](units-and-locale) | +| Environment metrics use Fahrenheit | Use °F on the device's display. This is the node's screen only — the app follows your phone's locale, see [Units & Locale](units-and-locale) | | Air quality metrics module enabled | Report particulate and CO₂ sensor data | | Air quality metrics update interval | How often to report them | | Power metrics module enabled | Report the per-channel voltage and current readings | @@ -151,9 +151,9 @@ battery health often and sensors rarely. See [Telemetry & Sensors](telemetry-and-sensors) for supported sensors and configuration recommendations. -### Canned Message Module +### Canned Message module -Pre-configured messages accessible from the radio's physical buttons (for radios with rotary encoders, keypads, or similar input hardware). Define a list of quick-send messages that can be transmitted without a phone connected — ideal for field use. +Pre-configured messages accessible from the node's physical buttons (for nodes with rotary encoders, keypads, or similar input hardware). Define a list of quick-send messages that can be transmitted without a phone connected — ideal for field use. | Setting | Description | |---------|-------------| @@ -166,7 +166,7 @@ Pre-configured messages accessible from the radio's physical buttons (for radios | Up/Down/Select input enabled | A separate, simpler input scheme using up/down/select buttons rather than an encoder | | ~~Allow input source~~ | ⚠️ **Deprecated** in the protobuf schema | -### Audio Module +### Audio module Codec2 audio support for low-bandwidth voice communication over the mesh. This is an **experimental** feature that encodes voice into very small data packets using the Codec2 codec. @@ -182,11 +182,11 @@ Codec2 audio support for low-bandwidth voice communication over the mesh. This i > ℹ️ **Note:** Audio requires specific hardware (I2S microphone and speaker). Voice quality is very low-bandwidth — think "understandable radio voice," not phone-call quality. -### Remote Hardware Module +### Remote Hardware module GPIO control over the mesh network. Allows a remote node to read or write GPIO pins on another node — useful for activating relays, reading switches, or controlling external hardware from a distance. -> ⚠️ **Warning:** Turning on **Allow undefined pin access** gives remote nodes access to all GPIO pins, which could interfere with the radio's own hardware. Turn it on only on dedicated GPIO nodes. +> ⚠️ **Warning:** Turning on **Allow undefined pin access** gives remote nodes access to all GPIO pins, which could interfere with the node's own hardware. Turn it on only on dedicated GPIO nodes. | Setting | Description | |---------|-------------| @@ -194,7 +194,7 @@ GPIO control over the mesh network. Allows a remote node to read or write GPIO p | Allow undefined pin access | Allow access to any GPIO pin (security risk) | | Available pins | Up to 4 GPIO pins this node exposes for remote read/write | -### Neighbor Info Module +### Neighbor Info module Broadcasts information about directly heard neighbors, enabling mesh topology mapping. Each enabled node periodically shares a list of the other nodes it can hear and their signal quality. @@ -206,7 +206,7 @@ Broadcasts information about directly heard neighbors, enabling mesh topology ma See [Local Mesh Discovery](discovery) for how to use neighbor data for mesh topology exploration. -### Ambient Lighting Module +### Ambient Lighting module Controls onboard NeoPixel or other addressable RGB LEDs on supported hardware. Can be used for visual status indicators, notification lights, or decorative effects. @@ -216,7 +216,7 @@ Controls onboard NeoPixel or other addressable RGB LEDs on supported hardware. C | Current | LED current limit (0–31) | | Red / Green / Blue | Individual color channel values (0–255) | -### Detection Sensor Module +### Detection Sensor module Turns your node into a motion or door sensor alert system. When a GPIO pin detects a state change (motion detected, door opened), the node broadcasts an alert message over the mesh. @@ -231,7 +231,7 @@ Turns your node into a motion or door sensor alert system. When a GPIO pin detec | Send bell with alert message | Include bell character in alerts | | Friendly name | Custom name for this sensor | -### Paxcounter Module +### Paxcounter module People counter using Wi-Fi and BLE probe requests. Counts nearby devices by passively listening for probe requests that phones and laptops emit when scanning for networks. Available only on ESP32 devices. @@ -239,26 +239,26 @@ People counter using Wi-Fi and BLE probe requests. Counts nearby devices by pass |---------|-------------| | Paxcounter enabled | Activate people counting | | Update interval (seconds) | How often to report counts | -| Wi-Fi RSSI threshold | Ignore Wi-Fi probes weaker than this, so distant devices are not counted (defaults to −80 dBm) | +| Wi-Fi RSSI threshold | Ignore Wi-Fi probes weaker than this, so distant devices aren't counted (defaults to −80 dBm) | | BLE RSSI threshold | The same cut-off for BLE advertisements (defaults to −80 dBm) | > 💡 **Tip:** Paxcounter is useful for estimating foot traffic at trailheads, event venues, or other locations. Counts are approximate — one person may carry multiple devices. -### Status Message Module +### Status Message module The status message has no module screen. It is edited with the rest of the node's identity, on [Settings — Radio & User](settings-radio-user#user-profile). -### Mesh Beacon Module +### Mesh Beacon module Broadcasts an invitation to your mesh, and receives invitations from others. The entry appears only -on radios running firmware 2.8.0 or newer. See [Local Mesh Discovery](discovery) for the full +on nodes running firmware 2.8.0 or newer. See [Local Mesh Discovery](discovery) for the full walkthrough. -### TAK Module +### TAK module Team Awareness Kit integration for interoperability with ATAK and WinTAK. Two things have to be -true before the entry appears in the module list: the radio runs firmware 2.8.0 or newer, and its +true before the entry appears in the module list: the node runs firmware 2.8.0 or newer, and its **Device Role** on **Settings → Device configuration → Device** is set to `TAK` or `TAK_TRACKER`. See [TAK Integration](tak) for detailed setup and usage. @@ -281,30 +281,30 @@ Remotely configure nodes that share your admin key: | Action | What it does | |---|---| -| Set time | Sends your phone's clock to the radio | -| Reboot | Restarts the radio | -| Shutdown | Powers the radio down | +| Set time | Sends your phone's clock to the node | +| Reboot | Restarts the node | +| Shutdown | Powers the node down | | Factory reset | Returns every setting to its factory default | -| NodeDB reset | Clears the radio's node database. This dialog carries a **Preserve Favorites?** switch | +| NodeDB reset | Clears the node database. This dialog carries a **Preserve Favorites?** switch | -> ⚠️ **Warning:** Factory reset erases all settings, channels, and keys, and cannot be undone. Before you reset, use **Export configuration** to save the radio's settings and **Backup Keys** on the Security screen to save its keys, so you can put both back afterwards. +> ⚠️ **Warning:** Factory reset erases all settings, channels, and keys, and cannot be undone. Before you reset, use **Export configuration** to save the node's settings and **Backup Keys** on the Security screen to save its keys, so you can put both back afterwards. ### Backup & Restore -**Settings → Backup & Restore** writes the connected radio's whole configuration to a file with +**Settings → Backup & Restore** writes the connected node's whole configuration to a file with **Export configuration**, and reads a saved file back in with **Import configuration**. Export -before a factory reset, or to copy one radio's setup onto another. The section is shown for your -own radio only, not over remote admin. +before a factory reset, or to copy one node's setup onto another. The section is shown for your +own node only, not over remote admin. ### Advanced **Settings → Advanced** collects the tools that read or rewrite local state, and is likewise shown -for your own radio only: **Firmware Update** on OTA-capable hardware, **Clean Node Database**, +for your own node only: **Firmware Update** on OTA-capable hardware, **Clean Node Database**, **TAK Server**, **Local Mesh Discovery**, and the **Debug Panel**. #### Clean Node Database -Prunes nodes from your node database — from the app's copy *and* from the radio's own, so this is +Prunes nodes from your node database — from the app's copy *and* from the node's own, so this is not a display-only cleanup. The two filters combine rather than acting separately; the screen puts it as *Selections are additive*. @@ -314,7 +314,7 @@ it as *Selections are additive*. info. The age limit still applies on top of it. The screen lists the nodes queued for deletion as you move the filters. **Clean Now** carries the -purge out, after one more confirmation, and it cannot be undone. Favorited nodes, ignored nodes, +purge out, after one more confirmation, and it can't be undone. Favorited nodes, ignored nodes, and nodes with a public key heard in the last seven days are never removed, whatever the filters say — that is why the queued list can be shorter than you expect. @@ -324,16 +324,42 @@ Opens the **Packets** and **App logs** tabs for viewing, filtering, and exportin ### App Settings -Two easy-to-miss entries on the **Settings** screen configure the app rather than the radio, and -appear only when your own node is selected: +The **App Settings** block on the **Settings** screen configures the app rather than the node, and +appears only when your own node is selected. It is grouped rather than flat: + +**Privacy** + +- **Allow analytics and crash reporting** — opt in or out of diagnostics. +- **Provide phone location to mesh** — share this phone's position when the node has no GPS fix. +- **Homoglyph encoding** — how look-alike characters in names are handled. + +**Appearance** + +- **Units** — metric, imperial, or follow the system. This is the entry + [Units & Locale](units-and-locale) sends you to. +- **Theme** and **Language**. +- **Show full message timestamps**. + +**Persistence** + +- A cache limit for how much history is kept. +- **Export rangetest packets**, **Export all packets** and **Export node database**. + +**On their own** - **Node Layout** — how much detail each row of the node list shows. +- **Wi-Fi Provisioning for mPWRD-OS** — see [Connections](connections). - **Message Filter** — hides incoming messages that contain words you list. With no words configured it does nothing. +- **System AI** — appears only where app functions are available; see + [App Functions](app-functions). + +A **Permissions** block sits above App Settings, so a user who skipped a permission during +onboarding can get back to it, and **App Info** sits below. ### About -**Settings → About** carries the app's own identity rather than the radio's: +**Settings → About** carries the app's own identity rather than the node's: Three sections: @@ -341,14 +367,18 @@ Three sections: - **Apps** — opens with **Need Hardware?**, a rotating carousel of popular devices that links out to where to buy one. It also lists the GitHub repository, the running app version, and **Acknowledgements** (see the next section). -- **Project information** — links to the website and to this documentation. +- **Project information** — links to the website, to this documentation, and to the GNU GPL v3 + license the app is released under. + +The footer states the copyright and that the app is free software with no warranty, which you +may redistribute under the same license. ### Acknowledgements Reached from **About**, this lists every open-source library the app ships, with its license, generated at build time by AboutLibraries. It was previously called the license screen. -### Troubleshooting Remote Admin +### Troubleshooting remote admin - **"No response from target node"** — the target may be out of range, offline, or have a mismatched admin key. Verify the admin key matches on both nodes. - **Changes not applying** — some settings require a reboot to take effect. Try the Reboot action after saving. @@ -356,6 +386,6 @@ generated at build time by AboutLibraries. It was previously called the license ## Related Topics -- [Settings — Radio & User](settings-radio-user) — core radio and user profile settings +- [Settings — Radio & User](settings-radio-user) — core node and user profile settings - [Module configuration reference](https://meshtastic.org/docs/configuration/module) — detailed module docs on meshtastic.org - [FAQ](https://meshtastic.org/docs/faq/) — common questions on meshtastic.org diff --git a/docs/en/user/settings-radio-user.md b/docs/en/user/settings-radio-user.md index 9b16c0aee1..c503abeb92 100644 --- a/docs/en/user/settings-radio-user.md +++ b/docs/en/user/settings-radio-user.md @@ -2,8 +2,8 @@ title: Settings — Radio & User parent: User Guide nav_order: 7 -last_updated: 2026-09-04 -description: Configure your radio hardware, LoRa presets, user profile, position sharing, power management, and security. +last_updated: 2026-09-19 +description: Configure your node hardware, LoRa presets, user profile, position sharing, power management, and security. aliases: - settings - radio-config @@ -13,14 +13,14 @@ aliases: # Settings — Radio & User -Configure your radio's user identity, region and LoRa parameters, position and power behavior, network and Bluetooth connectivity, and security settings. +Configure your node's user identity, region and LoRa parameters, position and power behavior, network and Bluetooth connectivity, and security settings. -## How These Screens Work +## How these screens work Everything here is on the **Settings** screen. **User**, **LoRa**, **Channels** and **Security** are listed there directly. **Device**, **Position**, **Power**, **Network**, **Display** and **Bluetooth** are one level down, under **Settings → Device configuration**. **Network** appears -only on radios with Wi-Fi or Ethernet, and **Bluetooth** only on radios with Bluetooth. +only on nodes with Wi-Fi or Ethernet, and **Bluetooth** only on nodes with Bluetooth. Settings use standard preference controls — dropdowns, toggles, and sliders: @@ -40,18 +40,18 @@ On **Settings → User**. |---------|-------------| | Long Name | Your display name (up to 39 characters) | | Short Name | 4-character abbreviated name | -| Status Message | A short, public free-text status other nodes display alongside your node — up to 80 bytes, cleared with the **✕** in the field. The radio broadcasts it to the mesh when you change it and again every 12 hours. Needs firmware 2.8 or newer, and is absent otherwise | +| Status Message | A short, public free-text status other nodes display alongside your node — up to 80 bytes, cleared with the **✕** in the field. The node broadcasts it to the mesh when you change it and again every 12 hours. Needs firmware 2.8 or newer, and is absent otherwise | | Unmessageable | Marks the node as one nobody should try to message — for an unmonitored or infrastructure node. Other clients hide it from the contact list. Needs supporting firmware | -| Licensed amateur radio (Ham) | Enable if you hold an amateur radio license (permits higher power). Turning it on is staged behind a confirmation dialog. On your own radio it then relabels **Long Name** as **Call sign** and adds a separate Long Name field; over remote admin the field stays **Long Name** | +| Licensed amateur node (Ham) | Enable if you hold an amateur node license (permits higher power). Turning it on is staged behind a confirmation dialog. On your own node it then relabels **Long Name** as **Call sign** and adds a separate Long Name field; over remote admin the field stays **Long Name** | -### Applying Changes +### Applying changes -The footer appears as soon as you change something. **Discard** throws the change away, and the other button writes it to the radio: it reads **Save & restart** on the screens the firmware applies with a reboot — Position, Network, Bluetooth, Security, and most module screens — and **Save** everywhere else. +The footer appears as soon as you change something. **Discard** throws the change away, and the other button writes it to the node: it reads **Save & restart** on the screens the firmware applies with a reboot — Position, Network, Bluetooth, Security, and most module screens — and **Save** everywhere else. The status message is saved with the same **Save**, but it never reboots the node — and, like the -rest of this screen, it can be edited on a remote node you administer. For your own radio there is a +rest of this screen, it can be edited on a remote node you administer. For your own node there is a shortcut while it is connected: touch & hold your node in the [node list](nodes.md) and choose -**Update status**. Older firmware and a disconnected radio have no shortcut — the field above is +**Update status**. Older firmware and a disconnected node have no shortcut — the field itself is still the way in. ## Configuration @@ -66,7 +66,7 @@ On **Settings → Device configuration → Device**. | Rebroadcast Mode | How the node retransmits messages. As with the role, the picker lists the firmware names and describes only the selected one | `ALL` | | Node Info Broadcast Interval | How often the node re-announces itself. A dropdown of fixed intervals — Unset, then 3 to 72 hours — not a value you type in seconds | 3 hours | | Double Tap as Button | Treat a double tap as a button press | Disabled | -| Triple Click Ad Hoc Ping | Send an ad-hoc position ping on a triple click | Disabled | +| Triple Click Ad Hoc Ping | Send an ad-hoc position ping on a triple click | Enabled | | LED Heartbeat | Blink the status LED periodically | Enabled | | Time Zone | POSIX time-zone string for the device clock, with buttons to copy your phone's zone or clear it | — | | Button / Buzzer GPIO | Advanced: which pins the button and buzzer are wired to | — | @@ -77,15 +77,15 @@ On **Settings → LoRa**. | Setting | Description | Default | |---------|-------------|---------| -| Region | Regulatory region for frequency bands. You must set this before transmitting | Unset (must configure) | +| Region | Regulatory region for frequency bands. You must set this before transmitting. On firmware 2.8 or newer, the first region set also creates the node's identity key and gives it a new node number | Unset (must configure) | | Presets | Speed/range tradeoff | LongFast | | Number of Hops | Maximum retransmit hops | 3 | | Transmit Power | Transmission power (dBm); 0 = max allowed for region | 0 (region max) | -| Frequency Override | Overrides the computed operating frequency outright (MHz). It does not offset the calculated value — leave at 0 unless you know you need a specific frequency | 0 (use calculated) | +| Frequency Override | Overrides the computed operating frequency outright (MHz). It doesn't offset the calculated value — leave at 0 unless you know you need a specific frequency | 0 (use calculated) | | Use Preset | On by default. Turn it off to set Spread Factor, Coding Rate and Bandwidth by hand instead of taking them from the modem preset | On | -| Spread Factor | Manual mode only: 7–12. Higher spreads further but slower | From preset | +| Spread Factor | Manual mode only: 5–12. Higher spreads further but slower. On SX127x (RF95) radios the firmware doesn't accept 5 or 6 and uses 11 instead | From preset | | Coding Rate | Manual mode only: 5–8. More redundancy costs airtime | From preset | -| Bandwidth | Manual mode only: the channel bandwidth in kHz, typed in directly. On the 2.4 GHz region the app offers a list of the bandwidths your radio supports instead, and a stored value that is not on that list shows as *Unsupported* and blocks saving until you pick a supported one | From preset | +| Bandwidth | Manual mode only: the channel bandwidth in kHz, typed in directly. On the 2.4 GHz region the app offers a list of the bandwidths your node supports instead, and a stored value that isn't on that list shows as *Unsupported* and blocks saving until you pick a supported one | From preset | | Frequency Slot | Which slot within the region's band to use. 0 derives it from the primary channel name | 0 (automatic) | | Transmit Enabled | Turning this off makes the node receive-only | On | | Override Duty Cycle | Ignores the region's duty-cycle limit. Illegal in most regions; turn it on only where your license permits | Off | @@ -94,13 +94,19 @@ On **Settings → LoRa**. | RX Boosted Gain | Extra receive gain on SX126x radios; costs a little current | Off | | PA fan disabled | Turn off the power-amplifier fan on hardware that has one | Off | -Some regions are amateur-radio allocations whose presets only licensed operators may use. On firmware 2.8 or newer the app knows which regions those are and grays the whole **Presets** list out until **Licensed amateur radio (Ham)** is turned on for the node you are configuring; the text under the field says so while it is grayed out. +The preset list is also filtered to what your region legally permits, so a preset allowed in one +region doesn't appear in another. Changing region can therefore leave your current preset +illegal — the app repairs it for you, switching to the region's own default rather than leaving an +unusable setting in place. The region list itself is filtered by what your firmware supports, so +the newer regions appear only on 2.8 or later, alongside whichever region the node already has set. + +Some regions are amateur-node allocations whose presets only licensed operators may use. On firmware 2.8 or newer the app knows which regions those are and grays the whole **Presets** list out until **Licensed amateur node (Ham)** is turned on for the node you are configuring; the text under the field says so while it is grayed out. > ⚠️ **Important:** Operating without the correct region may violate local radio regulations. See the [region configuration guide](https://meshtastic.org/docs/getting-started/initial-config) on meshtastic.org for details. -### Modem Presets +### Modem presets -The Lite, Narrow, Medium Turbo, and Tiny presets need firmware 2.8 or newer — the app hides them on older radios. +The Lite, Narrow, Medium Turbo, and Tiny presets need firmware 2.8 or newer — the app hides them on older nodes. > 💡 **Tip:** The **SNR Limit** values are negative on purpose. LoRa can decode signals *below* the noise floor, so a more-negative limit means the preset tolerates a weaker, noisier signal (more range). See [How the Signal Meter Works](signal-meter) for the full explanation. @@ -111,22 +117,22 @@ The Lite, Narrow, Medium Turbo, and Tiny presets need firmware 2.8 or newer — | Short Slow | ~5 km | 6.25 kbps | −10 dB | Suburban short-range; moderate building density | | Medium Fast | ~5 km | 3.52 kbps | −12.5 dB | Suburban areas; moderate building density | | Medium Slow | ~8 km | 1.95 kbps | −15 dB | Suburban/rural; moderate range with slower speed | -| Long Turbo | ~10 km | 1.34 kbps | −12.5 dB | Similar range to Long Fast but with 500 kHz bandwidth; faster throughput | +| Long Turbo | ~10 km | 1.34 kbps | −17.5 dB | Similar range to Long Fast but with 500 kHz bandwidth; faster throughput | | Long Fast | ~10 km | 1.1 kbps | −17.5 dB | **General use (default)** — balanced range and speed | | Long Moderate | ~20 km | 0.34 kbps | −17.5 dB | Rural with some terrain; occasional use | | Lite Fast | ~5 km | 1.76 kbps | −12.5 dB | EU 866 MHz SRD band (125 kHz BW); comparable to Medium Fast | | Lite Slow | ~10 km | 0.98 kbps | −15 dB | EU 866 MHz SRD band (125 kHz BW); comparable to Long Fast | -| Narrow Fast | ~5 km | 2.28 kbps | −10 dB | EU 868 MHz band (62.5 kHz BW); avoids interference with other devices | -| Narrow Slow | ~10 km | 1.30 kbps | −12.5 dB | EU 868 MHz band (62.5 kHz BW); comparable to Long Fast | +| Narrow Fast | ~5 km | 2.28 kbps | −7.5 dB | EU 868 MHz band (62.5 kHz BW); avoids interference with other devices | +| Narrow Slow | ~10 km | 1.30 kbps | −10 dB | EU 868 MHz band (62.5 kHz BW); comparable to Long Fast | | Medium Turbo | ~5 km | 7.0 kbps | −12.5 dB | Like Medium Fast but with 500 kHz bandwidth; not legal in every region. Needs firmware 2.8 or newer | | Tiny Fast | ~10 km | 0.68 kbps | −7.5 dB | Amateur bands that cap occupied bandwidth; these presets use 15.6 kHz. Needs firmware 2.8 or newer, an SX126x or SX127x radio, and a TCXO of ±5 ppm or better | -| Tiny Slow | ~20 km | 0.33 kbps | −10 dB | Same band restrictions as Tiny Fast, longer range. Same firmware, radio, and TCXO requirements | +| Tiny Slow | ~20 km | 0.33 kbps | −10 dB | Same band restrictions as Tiny Fast, longer range. Same firmware, node, and TCXO requirements | | ~~Long Slow~~ | ~30 km | 0.18 kbps | −20 dB | ⚠️ **Deprecated** — still selectable but may be removed in a future firmware release | | ~~Very Long Slow~~ | ~40+ km | 0.09 kbps | −20 dB | ⚠️ **Deprecated** — still selectable but may be removed in a future firmware release | > ℹ️ **Note:** This table uses the common short names. The app's **Presets** dropdown lists the raw firmware names instead — `SHORT_FAST`, `LONG_FAST`, `LITE_FAST`, `NARROW_FAST`, and so on. Local Mesh Discovery shows the same presets as *Long Fast* and *Short Turbo*. -#### Choosing a Modem Preset +#### Choosing a modem preset The modem preset controls the fundamental tradeoff between **range** and **data rate**: @@ -141,38 +147,36 @@ The modem preset controls the fundamental tradeoff between **range** and **data - **Fixed infrastructure links:** Use **Short Turbo** or **Long Turbo** for dedicated point-to-point links with good antennas and line-of-sight. - **Mixed environments:** Stick with **Long Fast** — it's the community default and ensures compatibility with others in your area. -All nodes on the same channel must use the same modem preset. Nodes with mismatched presets cannot communicate even if they share the same frequency and encryption key. +All nodes on the same channel must use the same modem preset. Nodes with mismatched presets can't communicate even if they share the same frequency and encryption key. The range estimates in the [Modem Presets](#modem-presets) table assume flat terrain and modest antennas. Elevation advantage (hilltop, rooftop) dramatically increases effective range. A well-placed Router with Long Fast can often outperform a ground-level node with Long Slow. ### Display Config -On **Settings → Device configuration → Display**. These control the **radio's own screen**, not the app's. +On **Settings → Device configuration → Display**. These control the **node's own screen**, not the app's. | Setting | Description | |---------|-------------| | Screen on for | How long the display stays lit before sleeping | -| Carousel interval | How often the radio cycles between screens on its own | +| Carousel interval | How often the node cycles between screens on its own | | Display mode | Screen layout/density used by the firmware | -| Display units | Metric or Imperial on the radio's screen | -| Use 12h clock format | Show the radio's clock as 12-hour rather than 24-hour | +| Display units | Metric or Imperial on the node's screen | +| Use 12h clock format | Show the node's clock as 12-hour rather than 24-hour | | Bold Heading | Draw the screen's heading text in bold | | Flip screen | Rotate the display 180° for an inverted mounting | -| OLED type | Auto, SSD1306, SH1106, SH1107 | -| Wake on tap or motion | Light the screen when the radio is tapped or moved | -| Compass orientation | Rotation offset for the compass rose (0°, 90°, 180°, 270°) | +| OLED type | Six values, shown as the raw constants: `OLED_AUTO`, `OLED_SSD1306`, `OLED_SH1106`, `OLED_SH1107`, `OLED_SH1107_128_128`, `OLED_SH1107_ROTATED` | +| Wake on tap or motion | Light the screen when the node is tapped or moved | +| Compass orientation | Rotation offset for the compass rose. Eight values, shown as the raw constants `DEGREES_0` through `DEGREES_270` plus an `_INVERTED` variant of each | | Always point north | Locks the compass rose north-up instead of rotating it with your heading. Independent of Compass orientation — neither replaces the other | ### Position Config On **Settings → Device configuration → Position**. -> ⚠️ **Important:** Saving this screen always reboots the radio. - | Setting | Description | |---------|-------------| | GPS Mode (Physical Hardware) | Three-state: GPS enabled, disabled, or not present. Not a simple on/off | -| GPS Polling Interval | How often the radio asks its GPS for a fix | +| GPS Polling Interval | How often the node asks its GPS for a fix | | Broadcast Interval | How often the position is shared with the mesh | | Smart Position | Broadcast based on movement rather than purely on the clock | | Smart Interval | With Smart Position on, the shortest gap between broadcasts | @@ -187,10 +191,10 @@ On **Settings → Device configuration → Power**. | Setting | Description | |---------|-------------| -| Enable power saving mode | Let the radio sleep aggressively between activity | +| Enable power saving mode | Let the node sleep aggressively between activity | | Shutdown on power loss | Power the device down after external power disappears | | Super deep sleep duration | How long the deepest sleep state lasts | -| Minimum wake time | The shortest time the radio stays awake once woken | +| Minimum wake time | The shortest time the node stays awake once woken | | Wait for Bluetooth duration | How long to wait for a phone to connect before sleeping | | ADC multiplier override | Turn on a manual correction for battery-voltage readings | | ADC multiplier override ratio | The correction factor itself, used only when the override is on | @@ -198,13 +202,13 @@ On **Settings → Device configuration → Power**. ### Network Config -On **Settings → Device configuration → Network**, on radios with Wi-Fi or Ethernet. +On **Settings → Device configuration → Network**, on nodes with Wi-Fi or Ethernet. -> ⚠️ **Warning:** Turning on **Wi-Fi enabled** or **Ethernet enabled** ends the Bluetooth connection between your phone and the radio. Reconnect over the network afterwards from the [Connections](connections) screen, or turn Wi-Fi off again from the radio's own screen or over USB. Saving this screen also always reboots the radio. +> ⚠️ **Warning:** Turning on **Wi-Fi enabled** or **Ethernet enabled** ends the Bluetooth connection between your phone and the node. Reconnect over the network afterwards from the [Connections](connections) screen, or turn Wi-Fi off again from the node's own screen or over USB. Saving this screen also always reboots the node. | Setting | Description | |---------|-------------| -| Wi-Fi enabled | Enable the Wi-Fi radio (ESP32 radios) | +| Wi-Fi enabled | Enable the Wi-Fi node (ESP32 nodes) | | SSID | Network name to connect to. Appears only once **Wi-Fi enabled** is on, along with **Password**. **Scan Wi-Fi QR code** fills both from a standard Wi-Fi QR code; on Android, holding the phone against a Wi-Fi NFC tag while this screen is open fills them the same way, and the app offers to open system settings if NFC is turned off | | Password | Network password | | Ethernet enabled | Use a wired connection on hardware that has one | @@ -218,9 +222,7 @@ On **Settings → Device configuration → Network**, on radios with Wi-Fi or Et ### Bluetooth Config -On **Settings → Device configuration → Bluetooth**, on radios with Bluetooth. - -> ⚠️ **Important:** Saving this screen always reboots the radio. +On **Settings → Device configuration → Bluetooth**, on nodes with Bluetooth. | Setting | Description | |---------|-------------| @@ -232,18 +234,16 @@ On **Settings → Device configuration → Bluetooth**, on radios with Bluetooth On **Settings → Security**. The screen is grouped into cards: **Packet authenticity**, **Direct Message Key** (your node's key pair), **Admin Keys**, **Logs**, and **Administration**. -> ⚠️ **Important:** Saving this screen always reboots the radio. - | Setting | Description | |---------|-------------| | Public Key | Your node's public key (read-only) | | Admin Key | Keys permitted to administer this node remotely — up to three | -| Private Key | Your node's private key (handle securely). Shown redacted when you are viewing another node over remote admin — the firmware does not send it | +| Private Key | Your node's private key (handle securely). Shown redacted when you are viewing another node over remote admin — the firmware doesn't send it | | Regenerate Private Key | Issues a new keypair for this node, behind a confirmation. Every peer that knew your old key must learn the new one | | ~~Admin Channel Enabled~~ | ⚠️ Removed — now configured automatically when an admin key is set | | Serial console | Serial console over the Stream API | -| Debug log API enabled | Output live debug logging over serial, and view and export position-redacted radio logs over Bluetooth | -| Managed Mode | Restrict non-admin channel changes. Only selectable once an Admin Key is set | +| Debug log API enabled | Output live debug logging over serial, and view and export position-redacted node logs over Bluetooth | +| Managed Mode | Locks the whole Configuration list, not just channels — every setting on the node becomes read-only and only an admin can change anything. Only selectable once an Admin Key is set | | Backup Keys | Save an encrypted backup of the node's keys on this phone (Android only, and only for your own node) | | Restore Keys | Write the backed-up keys back to the node (available once a backup exists) | | Delete Key Backup | Remove the stored key backup from this phone | @@ -252,11 +252,12 @@ On **Settings → Security**. The screen is grouped into cards: **Packet authent #### Lockdown Mode Lockdown encrypts the device's storage and requires a passphrase for each connection. It needs -supporting firmware; the row does not appear otherwise. +supporting firmware; the row doesn't appear otherwise. Enabling it asks you to set and confirm a passphrase, and to acknowledge that **it locks the debug -(SWD) port on hardware that supports locking**. You can turn lockdown off again at any time with -the passphrase, and a full device erase restores the hardware regardless. +(SWD) port on hardware where the lockout takes effect**. That part isn't reversible from the app. +Turning lockdown off later decrypts your storage and reboots the device, but the port stays locked. +Reopening it takes a full chip erase with a debug probe, which destroys everything on the device. Alongside the passphrase you set the limits that end a session automatically: diff --git a/docs/en/user/signal-meter.md b/docs/en/user/signal-meter.md index 2cfccf8488..c33c9afee8 100644 --- a/docs/en/user/signal-meter.md +++ b/docs/en/user/signal-meter.md @@ -2,7 +2,7 @@ title: How the Meshtastic Signal Meter Works parent: User Guide nav_order: 15 -last_updated: 2026-08-30 +last_updated: 2026-09-09 description: How the signal meter rates quality from SNR relative to the LoRa modem preset — spread spectrum, presets, and what the bars really mean. aliases: - signal @@ -32,7 +32,7 @@ Each modem preset has an SNR limit: the lowest SNR at which that preset can stil ## Rating Signal Quality -The app rates signal quality (None, Bad, Fair, or Good) from SNR alone, measured against the active preset's SNR limit. It does not factor in RSSI: without knowing the local noise floor, RSSI alone cannot say whether a signal is decodable. RSSI is still available — on the node detail screen and in the metrics charts. +The app rates signal quality (None, Bad, Fair, or Good) from SNR relative to the active preset's SNR limit. When RSSI and a noise-floor reading are both available, it also rates the difference between them against that limit and uses the worse rating. Otherwise, it uses SNR alone. Because the rating is relative to the preset, the same SNR rates differently on different presets. An SNR of −16 dB rates Good on Long Fast (SNR limit −17.5 dB) but None on Short Fast (SNR limit −7.5 dB). Let `limit` be the active preset's SNR limit: @@ -45,7 +45,7 @@ Because the rating is relative to the preset, the same SNR rates differently on The icon never goes blank, so count the bars carefully: a single bar means None, not a weak but usable link, and Good is a solid wedge rather than a set of bars. A gray three-bar icon labeled Unknown is a different state again — the packet carried no SNR measurement at all, which is not the same as measuring one and finding it too weak. -> ℹ️ **Note:** Traceroute hop colors use fixed thresholds (−7 dB / −15 dB); the per-node signal meter uses the preset-relative rating instead. +> ℹ️ **Note:** Traceroute and neighbor-info SNR colors use the active preset's limits too. RSSI text has separate strength colors: green above −115 dBm, yellow above −120 dBm, orange above −126 dBm, and red at or below −126 dBm. ## Diagnosing Local Interference diff --git a/docs/en/user/tak.md b/docs/en/user/tak.md index cd8c4e6582..543c29764d 100644 --- a/docs/en/user/tak.md +++ b/docs/en/user/tak.md @@ -2,7 +2,7 @@ title: TAK Integration parent: User Guide nav_order: 10 -last_updated: 2026-08-30 +last_updated: 2026-09-11 description: Interoperate with ATAK and WinTAK — CoT position sharing, TAK roles, and plugin setup. aliases: - tak @@ -35,7 +35,7 @@ The TAK module allows Meshtastic nodes to: > ⚠️ **Warning:** The old **Meshtastic ATAK Plugin** is no longer part of this path and cannot > work. It bridged through the cross-process AIDL API, which was removed in app 2.8.0; the mesh > service is now in-process only. Do not install it. Interop today runs over the app's own local -> TAK server plus the Mesh to CoT Converter, both described below, with stock ATAK/iTAK/WinTAK. +> TAK server plus the Mesh to CoT Converter, both described in the following sections, with stock ATAK/iTAK/WinTAK. ### Configuration diff --git a/docs/en/user/telemetry-and-sensors.md b/docs/en/user/telemetry-and-sensors.md index 8a1045b229..3cbb4a8200 100644 --- a/docs/en/user/telemetry-and-sensors.md +++ b/docs/en/user/telemetry-and-sensors.md @@ -1,8 +1,8 @@ ---- +| Lightning, strikes in the last hour and storm distance | count, km or mi | Card and listed with each reading on the Environment Metrics screen; not charted. From an AS3935 detector. Storm distance is always in km or mi, since the detector resolves whole kilometres |--- title: Telemetry & Sensors parent: User Guide nav_order: 9 -last_updated: 2026-08-30 +last_updated: 2026-09-18 description: Sensor data on the mesh — supported environment, air quality, and power sensors, plus configuration and viewing guides. aliases: - sensors @@ -48,6 +48,7 @@ Supported environmental sensors: | BME680 | Gas Resistance / IAQ | Volatile organic compounds | | PMSA003I | PM1.0, PM2.5, PM10 | See [Air Quality Metrics](#air-quality-metrics) | | SEN55 | PM, Temp, Humidity | Multi-sensor. Its NOx and VOC indices are recorded and included in a CSV export, but are not shown as cards or charts | +| SEN6x | PM, CO₂, Temp, Humidity | Its status register is always exported; when it reports a fault (fan, RH&T, gas, CO₂, HCHO or PM), the fault names appear as a Sensor Status card and on each Air Quality log entry | ### Soil @@ -58,6 +59,8 @@ Supported environmental sensors: Both appear as info cards on the node detail screen, next to the other environment readings. +Soil probe and water-quality sonde chemistry (pH, conductivity, salinity, NPK, dissolved oxygen, ORP, turbidity, nitrate, ammonium, oxygen demand, solar irradiance) appears as a **Soil & Water** row of info cards on the node detail screen once a node reports any of it, and is included in the node database export. There is no chart or log screen for it yet. + ### Light & UV | Sensor | Metric | @@ -73,6 +76,7 @@ Both appear as info cards on the node detail screen, next to the other environme | Wind speed | km/h or mph | Card and chart. Sensors report meters per second; the app converts to match your unit setting, and the chart uses the same unit as the card | | Wind direction, gust, and lull | degrees, km/h or mph | Listed with each reading on the Environment Metrics screen; not charted | | Rainfall, last hour and last 24 hours | mm or in | Listed with each reading on the Environment Metrics screen; not charted | +| Lightning, strikes in the last hour and storm distance | count, km or mi | Card and listed with each reading on the Environment Metrics screen; not charted. From an AS3935 detector | | Radiation | µR/h | Card and chart | | Weight | kg or lb | Card only — load cells, such as a beehive scale | | Distance | mm or in | Card only — water level, from a distance sensor | diff --git a/docs/en/user/translate.md b/docs/en/user/translate.md index cdaa16a402..4fd9af00fc 100644 --- a/docs/en/user/translate.md +++ b/docs/en/user/translate.md @@ -2,7 +2,7 @@ title: Translate the App parent: User Guide nav_order: 17 -last_updated: 2026-08-30 +last_updated: 2026-09-11 description: How the app and its documentation are translated via Crowdin, and guidelines for contributing translations. aliases: - translate @@ -45,7 +45,7 @@ If your language is not yet listed on Crowdin: The Android app uses **Compose Multiplatform resources** for all user-visible strings: -``` +```text core/resources/src/commonMain/composeResources/ ├── values/ ← English (default) │ └── strings.xml @@ -58,7 +58,7 @@ core/resources/src/commonMain/composeResources/ In-app documentation follows a similar pattern under `docs/`: -``` +```text docs/ ├── en/user/ ← English source (default) │ ├── onboarding.md diff --git a/docs/es-rES/user/desktop.md b/docs/es-rES/user/desktop.md index 4cd6573141..55c447098a 100644 --- a/docs/es-rES/user/desktop.md +++ b/docs/es-rES/user/desktop.md @@ -2,7 +2,7 @@ title: Desktop App parent: User Guide nav_order: 14 -last_updated: 2026-08-30 +last_updated: 2026-09-11 description: Install and use the Meshtastic Desktop app on Linux, macOS, and Windows — connections, feature parity, and keyboard shortcuts. aliases: - desktop @@ -58,20 +58,20 @@ Bluetooth Low Energy is supported on desktop via the [Kable](https://github.com/ ## Feature Parity -| Feature | Android | Desktop | Notas | -| ----------------------------------------------------- | ------- | ------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Messaging | ✓ | ✓ | Full parity | -| Node List | ✓ | ✓ | Full parity | -| Mapa | ✓ | ✓ | Interactive MapLibre map, with base map and overlay pickers and custom tile sources. No offline downloads or local `.mbtiles` archives | -| Map layers (`.kml`/`.kmz`/GeoJSON) | ✓ | ✓ | Same layer store and sheet as Android; imported files draw on the desktop map | -| Site Planner | ✓ | ✓\* | \*Opens in your browser on desktop; the estimate is not drawn on the desktop map | -| Ajustes | ✓ | ✓ | Full parity | -| Bluetooth (BLE) | ✓ | ✓ | Via Kable on desktop | -| Actualización de firmware | ✓ | ✓ | In-app USB, BLE, and Wi-Fi (ESP32) update work the same as Android. The USB maintenance flow — nRF52/RP2040 factory erase and bootloader upgrade — is Android-only | -| Notifications | ✓ | ✓ | Native OS notifications | -| Widgets | ✓ | ✗ | Android-only | -| AI Assistant (Chirpy) | ✓\* | ✗ | Google flavor Android only | -| App Functions (system AI) | ✓† | ✗ | Google flavor Android only | +| Feature | Android | Desktop | Notas | +| ----------------------------------------------------- | ------- | ------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Messaging | ✓ | ✓ | Full parity | +| Node List | ✓ | ✓ | Full parity | +| Mapa | ✓ | ✓ | Interactive MapLibre map, with base map and overlay pickers and custom tile sources. Offline terrain (hillshade and contours) can be downloaded; offline base-map packs and local `.mbtiles` archives cannot | +| Map layers (`.kml`/`.kmz`/GeoJSON) | ✓ | ✓ | Same layer store and sheet as Android; imported files draw on the desktop map | +| Site Planner | ✓ | ✓\* | \*Opens in your browser on desktop; the estimate is not drawn on the desktop map | +| Ajustes | ✓ | ✓ | Full parity | +| Bluetooth (BLE) | ✓ | ✓ | Via Kable on desktop | +| Actualización de firmware | ✓ | ✓ | In-app USB, BLE, and Wi-Fi (ESP32) update work the same as Android. The USB maintenance flow — nRF52/RP2040 factory erase and bootloader upgrade — is Android-only | +| Notifications | ✓ | ✓ | Native OS notifications | +| Widgets | ✓ | ✗ | Android-only | +| AI Assistant (Chirpy) | ✓\* | ✗ | Google flavor Android only | +| App Functions (system AI) | ✓† | ✗ | Google flavor Android only | \*Chirpy AI requires Android 14+ on Google flavor builds with supported hardware. @@ -122,7 +122,7 @@ Individual doc pages render with full formatting: ## Building from Source -```bash +```shell git clone https://github.com/meshtastic/Meshtastic-Android.git cd Meshtastic-Android ./gradlew :desktopApp:run @@ -135,7 +135,7 @@ Requirements: ## Known Limitations -- Offline tile downloads and local `.mbtiles` archives are not available on desktop. +- Offline base-map downloads and local `.mbtiles` archives are not available on desktop. Offline terrain is — see [Map & Waypoints](map-and-waypoints). - `.kml`/`.kmz`/GeoJSON layer import works — see [Map & Waypoints](map-and-waypoints#map-layers). Site Planner opens in your browser rather than in the app; to bring its coverage estimate onto the map, click the transmitter pin diff --git a/docs/es-rES/user/discovery.md b/docs/es-rES/user/discovery.md index 63195f8abb..651fcb2b8d 100644 --- a/docs/es-rES/user/discovery.md +++ b/docs/es-rES/user/discovery.md @@ -2,7 +2,7 @@ title: Local Mesh Discovery parent: User Guide nav_order: 12 -last_updated: 2026-08-30 +last_updated: 2026-09-11 description: Explore your mesh network — the Local Mesh Discovery scanner, traceroute paths, neighbor maps, and node discovery tools. aliases: - discovery @@ -97,7 +97,7 @@ Mesh Beacon lets nodes invite others to join their mesh. A beaconing node period Configure it under **Settings → Module configuration → Mesh Beacon**. The entry appears only on radios running firmware 2.8.0 or newer. A read-only **Region** row at the top of the screen shows the region the beacon advertises: that region, and the preset, are always the ones the radio itself uses, so a beacon cannot invite anyone onto settings your radio is not running. - **Listen for beacons** — receive invitations broadcast by other nodes. -- **Broadcast a beacon** — periodically advertise this mesh to nearby nodes, with an optional **Beacon message** of up to 100 bytes, a **Broadcast interval** picked from fixed intervals between 1 hour and 72 hours, and an **Offered channel** chosen from your radio's own channels. The offered channel is required, and defaults to your primary channel. +- **Broadcast a beacon** — periodically advertise this mesh to nearby nodes, with an optional **Beacon message** of up to 100 bytes, a **Broadcast interval** picked from fixed intervals between 1 hour and 72 hours, and an **Offered channel** chosen from your radio's own channels. The offered channel is required, and defaults to your primary channel. Over remote admin the picker offers the primary channel only. - **Broadcast targets** — optional extra destinations beyond the offered channel. **Add target** appends a row; each row picks a **Channel** and a **Transmit preset**, and **Remove target** deletes it. With no targets, the beacon goes out on the offered channel alone. Two conditions block beacon setup: diff --git a/docs/es-rES/user/help-and-docs.md b/docs/es-rES/user/help-and-docs.md index f306cde7f7..c83898cea3 100644 --- a/docs/es-rES/user/help-and-docs.md +++ b/docs/es-rES/user/help-and-docs.md @@ -2,7 +2,7 @@ title: Help & In-App Docs parent: User Guide nav_order: 21 -last_updated: 2026-08-30 +last_updated: 2026-09-11 description: Browse this documentation inside the app, search it, and ask Chirpy — the on-device AI assistant — questions about Meshtastic. aliases: - help @@ -23,7 +23,7 @@ The docs browser lists every user-guide page. Tap a page to read it; images and ### Search -The search box sits at the top of the docs browser. Type in it to filter pages by title and keywords — results update as you type, and the **Clear search** button (✕) empties the box. +Tap the search field at the top of the docs browser to open a full-screen search view. Type to filter pages by title and keywords — results update as you type, under the same field. The back arrow, or the keyboard's search action, closes the view. The field hides while you scroll down the page list and comes back when you scroll up. ![Searching the in-app documentation](../../assets/screenshots/docs-browser_search.png) diff --git a/docs/es-rES/user/map-and-waypoints.md b/docs/es-rES/user/map-and-waypoints.md index 8b7f13bc22..19bc7e2f3f 100644 --- a/docs/es-rES/user/map-and-waypoints.md +++ b/docs/es-rES/user/map-and-waypoints.md @@ -2,7 +2,7 @@ title: Map & Waypoints parent: User Guide nav_order: 6 -last_updated: 2026-09-01 +last_updated: 2026-09-11 description: View node positions on the map, create and share waypoints, manage map layers and Site Planner, and control position sharing and privacy. aliases: - map @@ -44,7 +44,7 @@ The floating toolbar provides quick access to the compass, the map type and laye ### Filtering the Map -Tap the filter button in the floating toolbar to open **Filter map**. **Display** controls what is drawn: **Only Favorites**, **Show Waypoints**, **Show Precision Circles**, and a slider that hides nodes not heard from recently. **Node roles** is a chip per device role, plus **All** to show every role; a selected chip means that role is shown. **Nodes** narrows the set further with **Hide offline nodes**, **Only show direct nodes**, **Exclude MQTT**, **Show ignored nodes**, and **Include unknown**. +Tap the filter button in the floating toolbar to open **Filter map**. **Display** controls what is drawn: **Only Favorites**, **Show Waypoints**, **Show Precision Circles**, and a slider that hides nodes not heard from recently. **Node roles** is a chip per device role, plus **All** to show every role; a selected chip means that role is shown. **Nodes** narrows the set further with **Hide offline nodes**, **Only show direct nodes**, **Signed only**, **Encrypted only**, **Exclude MQTT**, **Show ignored nodes**, and **Include unknown** — the same rules as the node list's [filter toggles](nodes). A dot on the filter button means at least one filter is hiding something — check it before concluding the mesh is quiet. Turning **Show Waypoints** off hides every waypoint, including your own. **Show ignored nodes** adds them to the map rather than showing only them — unlike the node list's **Only show ignored Nodes**. @@ -147,8 +147,9 @@ picker, all three offer the same raster base maps: Overlays can be toggled on top of any base map, from the layers sheet: - **Weather radar** — NOAA NEXRAD reflectivity (US coverage) -- **Hillshade** — terrain relief, on **F-Droid** and **Desktop** only. Useful for understanding why a - link fails, since LoRa range is limited by terrain +- **Hillshade** — terrain relief from an online tile source, on **F-Droid** and **Desktop** only; Google + Play draws hillshade from downloaded offline terrain instead. Useful for understanding why a link + fails, since LoRa range is limited by terrain ### Adding your own tile source @@ -156,7 +157,7 @@ Any XYZ tile endpoint can be added as a base map, on every flavor and on desktop Tile Sources** at the foot of the base map picker and paste a URL template using `{z}`, `{x}` and `{y}` — plus `{s}` if the provider uses rotating subdomains. A national mapping service, for example: -``` +```text https://wmts.geo.admin.ch/1.0.0/ch.swisstopo.pixelkarte-farbe/default/current/3857/{z}/{x}/{y}.jpeg ``` @@ -173,7 +174,18 @@ On **Google Play**, the layers sheet's **Offline Manager** section downloads its — water, roads and administrative boundaries extracted from the public Protomaps basemap dataset, drawn directly as map shapes rather than raster tiles. Frame the area you want, tap **Start Download**, and once it finishes flip **Show on map** to draw it (it's off by default so it never surprises you by covering up the -live map). You can still import a pre-made `.mbtiles` archive here too, as before. **Desktop** has neither. +live map). You can still import a pre-made `.mbtiles` archive here too, as before. **Desktop** has neither, though +the offline terrain described next does work there. + +### Offline terrain + +Hillshade and elevation contours can be downloaded for offline use as well, so a downloaded area still +shows why a link fails when there is no network. The terrain data comes from Mapterhorn and is credited on +the map. On **Google Play**, each downloaded region in the **Offline Manager** list has a **Download Terrain** +button and **Hillshade** and **Contours** switches; terrain attaches to that region and shares its storage limit, and an area +too large for terrain asks you to zoom in or pick a smaller region. On **F-Droid** and **Desktop**, the +layers sheet has an **Offline Terrain** section of its own: frame the area, tap **Start Download**, and the +hillshade and contours draw from the downloaded tiles from then on. ### Going offline @@ -181,8 +193,8 @@ An **Offline** pill appears over the map whenever the device has no network conn builds, if you've imported a local `.mbtiles` archive, the map switches to it automatically the moment the network drops — no toggle to remember — and switches back once you're reconnected, as long as you haven't picked a different base map yourself in between. On **F-Droid**, a downloaded offline area keeps rendering on -its own; the pill is purely informational there. **Desktop** has no offline downloads at all yet, so the pill -is informational there too. +its own; the pill is purely informational there. **Desktop** downloads offline terrain but no base-map +packs, so the pill is informational there too. ## Related Topics diff --git a/docs/es-rES/user/messages-and-channels.md b/docs/es-rES/user/messages-and-channels.md index 8237405a50..1ea485b013 100644 --- a/docs/es-rES/user/messages-and-channels.md +++ b/docs/es-rES/user/messages-and-channels.md @@ -2,7 +2,7 @@ title: Messages & Channels parent: User Guide nav_order: 3 -last_updated: 2026-08-30 +last_updated: 2026-09-14 description: Send and receive messages, manage channels, configure encryption, search conversations, and use quick chat, reactions, and message actions. aliases: - channels @@ -50,6 +50,21 @@ Key length alone does not change the icon: a 128-bit key and a 256-bit key both Tapping an existing channel opens the same editor, where you can change the name, the PSK, MQTT uplink and downlink, and position precision. Every edit on this screen — adding, editing, deleting, or dragging a channel into a new order — waits on **Send** the same way. +### Archived Channels + +A conversation belongs to the channel it was held on, not to the slot that channel occupied. When a channel leaves your radio — you scan a different channel QR, replace a channel's key, or switch the modem preset — its conversation is **archived** rather than deleted or merged into whatever takes the slot. + +An archived conversation: + +- stays in the **Channels** list under the name the channel had, below the channels currently on your radio, marked with a history icon; +- keeps its full message history, searchable as usual; +- is read-only — you cannot send, reply, or react, because the channel is no longer on the radio to send on, and it is not offered as a share target; +- comes back automatically if you re-add that exact channel, taking its messages back to the live slot. + +Changing the modem preset archives the channel too: the preset is part of a default channel's identity, so **LongFast** and **MediumFast** are different channels even though neither the name nor the key changed. Changing only the region does not archive anything. + +To remove an archived conversation for good, long-press it in the conversation list and delete it. + ## Direct Messages Direct messages (DMs) go to one specific node. When both radios hold each other's public keys, your radio encrypts the message to that node's public key, so no one else on the mesh can read it — not even nodes that share your channel. diff --git a/docs/es-rES/user/mqtt.md b/docs/es-rES/user/mqtt.md index 98e7bdeb2b..32cfc30863 100644 --- a/docs/es-rES/user/mqtt.md +++ b/docs/es-rES/user/mqtt.md @@ -2,7 +2,7 @@ title: MQTT parent: User Guide nav_order: 11 -last_updated: 2026-08-30 +last_updated: 2026-09-11 description: Bridge your mesh to the internet — MQTT broker setup, encryption layers, and map reporting. aliases: - mqtt @@ -25,7 +25,7 @@ The MQTT module connects your node to an MQTT broker, allowing: ## How It Works -``` +```text [Your Node] → Radio → [Gateway Node with Wi-Fi] → MQTT Broker → [Remote Gateway] → Radio → [Remote Node] ``` diff --git a/docs/es-rES/user/node-metrics.md b/docs/es-rES/user/node-metrics.md index 12e94a19dd..0f7f20692e 100644 --- a/docs/es-rES/user/node-metrics.md +++ b/docs/es-rES/user/node-metrics.md @@ -2,7 +2,7 @@ title: Node Metrics parent: User Guide nav_order: 5 -last_updated: 2026-08-30 +last_updated: 2026-09-09 description: Telemetry dashboards for each mesh node — device health, environment sensors, air quality, signal quality, power, traceroute, and position history. aliases: - metrics @@ -114,7 +114,7 @@ Radio signal quality information: ### Signal Quality Reference -Signal quality is rated from **SNR relative to the active LoRa modem preset's demodulation floor**, not from fixed thresholds — a given SNR means different things on different presets (e.g. −15 dB is fine on LongSlow but unusable on ShortFast). RSSI is shown but is not part of the rating. In the table, _limit_ is the preset's SNR limit. +Signal quality is rated from **SNR relative to the active LoRa modem preset's demodulation floor**, not from fixed thresholds — a given SNR means different things on different presets (e.g. −15 dB is fine on LongSlow but unusable on ShortFast). When RSSI and a noise-floor reading are both available, the app also rates their difference against the preset limit and uses the worse rating. Otherwise, RSSI is display-only. In the table, _limit_ is the preset's SNR limit. | Quality | Criteria | | --------- | ---------------------------------------------------------------- | diff --git a/docs/es-rES/user/nodes.md b/docs/es-rES/user/nodes.md index ee7b939dba..64ee135730 100644 --- a/docs/es-rES/user/nodes.md +++ b/docs/es-rES/user/nodes.md @@ -2,7 +2,7 @@ title: Nodos parent: User Guide nav_order: 4 -last_updated: 2026-09-04 +last_updated: 2026-09-11 description: Browse, filter, and sort mesh nodes — view details, signal quality, roles, and quick actions. aliases: - node-list @@ -74,19 +74,23 @@ Most users should keep the default **Client** role. Consider a different role wh > 💡 **Tip:** The mesh works best when most nodes are **Client** or **Router**. Too many Client Mute nodes reduce mesh resilience; too many Routers in a dense area can cause congestion. A good rule of thumb: one Router per 5–10 Clients in your area. -### Encryption Indicators +### Security indicators -Nodes display encryption status icons next to their name: +Each node carries one security icon beside its name in the node list. Tap it to read what it means, and choose **Show All Meanings** in that dialog for the full legend. The detail screen shows the same state in words, as a **Security** row that opens the same dialog. -| Icon | Meaning | -| ----------- | ------------------------------------------------------------------------------------------------------------------------------------------------- | -| 🔒 Locked | Communication uses PKI (public key infrastructure) — end-to-end encrypted with verified identity | -| 🔓 Unlocked | No public key has been received for this node, so it cannot be direct messaged — use **Request User Info** on the node detail page to ask for one | -| ⚠️ Mismatch | Public key mismatch — the node's key has changed since last seen (investigate before trusting) | +| Icon | Meaning | Shown for | +| --------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------- | +| Person with a shield check (green) | **Verified contact** — you verified this node's key in person by exchanging contact QR codes, so its identity is confirmed. The strongest trust the list shows. Your own connected node carries it too | Any firmware version | +| Nodes icon with a shield check (green) | **Signed node** — this node signs its broadcasts with its identity key, so its identity is consistent over time, but you have not verified it in person. On firmware 2.8 the icon appears from the version alone, before any signed broadcast has been heard | Firmware 2.8 or newer, and any node whose signed broadcast your node has verified | +| 🔒 Closed lock | A public key is on file and matches, so direct messages to this node are encrypted | Firmware before 2.8, or no reported version | +| 🔓 Open lock | No public key has been received for this node, so it cannot be direct messaged — use **Request User Info** on the node detail page to ask for one | Firmware before 2.8, or no reported version | +| ⚠️ Mismatch | **Public key mismatch** — a different key arrived for this node after one was stored. Investigate before trusting | Any firmware version | -> 💡 **Tip:** Direct messages always use PKI, so the radio needs the other node's public key before it can send one. It refuses the send rather than falling back to channel encryption. Keys arrive inside node info, which is why an open lock usually clears itself once that node is heard from properly. If you see a key mismatch warning, the node may have been reset or compromised. +Every node on firmware 2.8 or newer signs its broadcasts, so on that firmware the signed state is the baseline and the locks are not shown. -To clear a mismatch, first confirm through another trusted channel that the key change was intentional — a factory reset causes one. Then touch & hold the node, choose **Remove**, and let the two radios exchange keys again the next time yours hears it. +Direct messages always use public-key encryption, so your node needs the other node's public key before it can send one. It refuses the send rather than falling back to channel encryption. Keys arrive inside node info, which is why an open lock usually clears itself once that node is heard from properly. + +A mismatch never replaces the key you already hold. The app keeps the first key it recorded and refuses the new one, as the firmware does, so a stray or hostile node info cannot silently break encrypted messaging to a contact. To clear a mismatch, first confirm through another trusted channel that the key change was intentional — a factory reset causes one. Then touch & hold the node, choose **Remove**, and let the two nodes exchange keys again the next time yours hears it. ## Quick Actions @@ -108,7 +112,9 @@ radio is connected and running firmware 2.8 or newer — see ## Sharing a Contact -On a node's detail screen, tap **Share Contact** to produce a link and a QR code for that node. From the same dialog, **Write to NFC tag** saves the link to a writable NFC tag that anyone can tap to open. +On a node's detail screen, tap **Share Contact** to produce a link and a QR code for that node. From the same dialog, **Share link** opens the Android share sheet (on desktop it copies the link instead), **Write to NFC tag** saves it to a writable NFC tag, and **Copy** puts it on the clipboard. While that dialog is open and in front of you, the phone also offers the same link to any NFC reader, so someone can take the contact by tapping their phone against yours with no tag involved. + +Sharing your own contact this way marks it as verified in person, so whoever imports it sees the **Verified contact** icon rather than the signed one. Relaying someone else's contact passes on only what your app had already recorded about them. To add someone else's contact, use the import button on the node list and choose **Scan Shared Contact QR Code**, **Scan Shared Contact NFC**, or **Input Shared Contact URL**. The app asks you to confirm with **Import Shared Contact?**, and warns you when the contact is one you already have. @@ -120,14 +126,16 @@ Type in the search field to filter nodes by name or short name. The filter updat ### Filter Toggles -| Filtro | Descripción | -| --------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| **Hide offline nodes** | Show only nodes heard within the last 2 hours | -| **Only show direct nodes** | Show only nodes your radio heard directly, with no relay in between | -| **Include unknown** | Show nodes that haven't sent user info yet. **On by default**, so a node heard before its info arrives stays visible; these carry a badge marking them incomplete, and cannot be direct messaged until their user info brings a public key | -| **Exclude infrastructure** | Hide infrastructure-role nodes (Router, Router Late, Client Base, and legacy Repeater nodes) and any node that cannot be messaged, whatever its role | -| **Exclude MQTT** | Hide nodes heard only via MQTT internet bridge | -| **Only show ignored Nodes** | Replace the list with the nodes you have ignored. Every other node is hidden while this is on, and a banner appears at the top of the list to take you back | +| Filtro | Descripción | +| --------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **Hide offline nodes** | Show only nodes heard within the last 2 hours | +| **Only show direct nodes** | Show only nodes your radio heard directly, with no relay in between | +| **Include unknown** | Show nodes that haven't sent user info yet. **On by default**, so a node heard before its info arrives stays visible; these carry a badge marking them incomplete, and cannot be direct messaged until their user info brings a public key | +| **Exclude infrastructure** | Hide infrastructure-role nodes (Router, Router Late, Client Base, and legacy Repeater nodes) and any node that cannot be messaged, whatever its role | +| **Exclude MQTT** | Hide nodes heard only via MQTT internet bridge | +| **Signed only** | Show only nodes whose signed broadcasts your node has actually heard and verified. Stricter than the icon: a node on 2.8 shows as signed by its firmware version before any signed broadcast arrives, and a contact verified in person on older firmware is not signed at all | +| **Encrypted only** | Show nodes with a matching public key on file, the key an encrypted direct message needs. A node with a key mismatch is excluded | +| **Only show ignored Nodes** | Replace the list with the nodes you have ignored. Every other node is hidden while this is on, and a banner appears at the top of the list to take you back | ### Sort Options @@ -149,6 +157,8 @@ Tap the hop-histogram icon in the node list's app bar to open a bar chart of how Tapping a node opens the detail view with comprehensive information. See [Node Metrics](node-metrics) for full details on metrics and telemetry. +Signal quality is rated against your modem preset. The same SNR can be good on a long-range preset and poor on a faster one. Traceroute and neighbor-info SNR colors use that preset too. RSSI text has its own strength colors; it affects the quality rating only when a noise-floor reading is also available. + The Details card carries the node's short name, role, IDs, last heard time, hops away, uptime, and its SNR and RSSI: ![Node detail section](../../assets/screenshots/nodes_detail_section.png) diff --git a/docs/es-rES/user/settings-module-admin.md b/docs/es-rES/user/settings-module-admin.md index b0344e0228..78bf602053 100644 --- a/docs/es-rES/user/settings-module-admin.md +++ b/docs/es-rES/user/settings-module-admin.md @@ -2,7 +2,7 @@ title: Settings — Modules & Admin parent: User Guide nav_order: 8 -last_updated: 2026-08-30 +last_updated: 2026-09-16 description: Configure optional feature modules (MQTT, telemetry, canned messages, TAK, and more) and perform device administration. aliases: - modules @@ -333,7 +333,7 @@ appear only when your own node is selected: ### Acerca de -**Settings → About** carries the app's own identity rather than the radio's: +**Settings → About** carries the app's own identity rather than the node's: Three sections: @@ -341,7 +341,11 @@ Three sections: - **Apps** — opens with **Need Hardware?**, a rotating carousel of popular devices that links out to where to buy one. It also lists the GitHub repository, the running app version, and **Acknowledgements** (see the next section). -- **Project information** — links to the website and to this documentation. +- **Project information** — links to the website, to this documentation, and to the GNU GPL v3 + license the app is released under. + +The footer states the copyright and that the app is free software with no warranty, which you +may redistribute under the same license. ### Agradecimientos diff --git a/docs/es-rES/user/settings-radio-user.md b/docs/es-rES/user/settings-radio-user.md index 42dc200e09..5c4884ad05 100644 --- a/docs/es-rES/user/settings-radio-user.md +++ b/docs/es-rES/user/settings-radio-user.md @@ -2,7 +2,7 @@ title: Settings — Radio & User parent: User Guide nav_order: 7 -last_updated: 2026-09-04 +last_updated: 2026-09-11 description: Configure your radio hardware, LoRa presets, user profile, position sharing, power management, and security. aliases: - ajustes @@ -51,7 +51,7 @@ The footer appears as soon as you change something. **Discard** throws the chang The status message is saved with the same **Save**, but it never reboots the node — and, like the rest of this screen, it can be edited on a remote node you administer. For your own radio there is a shortcut while it is connected: touch & hold your node in the [node list](nodes.md) and choose -**Update status**. Older firmware and a disconnected radio have no shortcut — the field above is +**Update status**. Older firmware and a disconnected node have no shortcut — the field itself is still the way in. ## Configuración @@ -77,13 +77,13 @@ On **Settings → LoRa**. | Setting | Descripción | Por defecto | | ----------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------- | -| Región | Regulatory region for frequency bands. You must set this before transmitting | Unset (must configure) | +| Región | Regulatory region for frequency bands. You must set this before transmitting. On firmware 2.8 or newer, the first region set also creates the node's identity key and gives it a new node number | Unset (must configure) | | Predefinidos | Speed/range tradeoff | LongFast | | Número de saltos | Maximum retransmit hops | 3 | | Potencia de transmisión | Transmission power (dBm); 0 = max allowed for region | 0 (region max) | | Sobreescribir frecuencia | Overrides the computed operating frequency outright (MHz). It does not offset the calculated value — leave at 0 unless you know you need a specific frequency | 0 (use calculated) | | Usar predefinido | On by default. Turn it off to set Spread Factor, Coding Rate and Bandwidth by hand instead of taking them from the modem preset | On | -| Factor de dispersión | Manual mode only: 7–12. Higher spreads further but slower | From preset | +| Factor de dispersión | Manual mode only: 5–12. Higher spreads further but slower. On SX127x (RF95) radios the firmware does not accept 5 or 6 and uses 11 instead | From preset | | Ratio de Codificación | Manual mode only: 5–8. More redundancy costs airtime | From preset | | Ancho de banda | Manual mode only: the channel bandwidth in kHz, typed in directly. On the 2.4 GHz region the app offers a list of the bandwidths your radio supports instead, and a stored value that is not on that list shows as _Unsupported_ and blocks saving until you pick a supported one | From preset | | Banda de Frecuencia | Which slot within the region's band to use. 0 derives it from the primary channel name | 0 (automatic) | @@ -111,13 +111,13 @@ The Lite, Narrow, Medium Turbo, and Tiny presets need firmware 2.8 or newer — | Short Slow | ~5 km | 6.25 kbps | −10 dB | Suburban short-range; moderate building density | | Medium Fast | ~5 km | 3.52 kbps | −12.5 dB | Suburban areas; moderate building density | | Medium Slow | ~8 km | 1.95 kbps | −15 dB | Suburban/rural; moderate range with slower speed | -| Long Turbo | ~10 km | 1.34 kbps | −12.5 dB | Similar range to Long Fast but with 500 kHz bandwidth; faster throughput | +| Long Turbo | ~10 km | 1.34 kbps | −17.5 dB | Similar range to Long Fast but with 500 kHz bandwidth; faster throughput | | Long Fast | ~10 km | 1.1 kbps | −17.5 dB | **General use (default)** — balanced range and speed | | Long Moderate | ~20 km | 0.34 kbps | −17.5 dB | Rural with some terrain; occasional use | | Lite Fast | ~5 km | 1.76 kbps | −12.5 dB | EU 866 MHz SRD band (125 kHz BW); comparable to Medium Fast | | Lite Slow | ~10 km | 0.98 kbps | −15 dB | EU 866 MHz SRD band (125 kHz BW); comparable to Long Fast | -| Narrow Fast | ~5 km | 2.28 kbps | −10 dB | EU 868 MHz band (62.5 kHz BW); avoids interference with other devices | -| Narrow Slow | ~10 km | 1.30 kbps | −12.5 dB | EU 868 MHz band (62.5 kHz BW); comparable to Long Fast | +| Narrow Fast | ~5 km | 2.28 kbps | −7.5 dB | EU 868 MHz band (62.5 kHz BW); avoids interference with other devices | +| Narrow Slow | ~10 km | 1.30 kbps | −10 dB | EU 868 MHz band (62.5 kHz BW); comparable to Long Fast | | Medium Turbo | ~5 km | 7.0 kbps | −12.5 dB | Like Medium Fast but with 500 kHz bandwidth; not legal in every region. Needs firmware 2.8 or newer | | Tiny Fast | ~10 km | 0.68 kbps | −7.5 dB | Amateur bands that cap occupied bandwidth; these presets use 15.6 kHz. Needs firmware 2.8 or newer, an SX126x or SX127x radio, and a TCXO of ±5 ppm or better | | Tiny Slow | ~20 km | 0.33 kbps | −10 dB | Same band restrictions as Tiny Fast, longer range. Same firmware, radio, and TCXO requirements | diff --git a/docs/es-rES/user/signal-meter.md b/docs/es-rES/user/signal-meter.md index cfeb617448..dda31f4ddb 100644 --- a/docs/es-rES/user/signal-meter.md +++ b/docs/es-rES/user/signal-meter.md @@ -2,7 +2,7 @@ title: How the Meshtastic Signal Meter Works parent: User Guide nav_order: 15 -last_updated: 2026-08-30 +last_updated: 2026-09-09 description: How the signal meter rates quality from SNR relative to the LoRa modem preset — spread spectrum, presets, and what the bars really mean. aliases: - signal @@ -32,7 +32,7 @@ Each modem preset has an SNR limit: the lowest SNR at which that preset can stil ## Rating Signal Quality -The app rates signal quality (None, Bad, Fair, or Good) from SNR alone, measured against the active preset's SNR limit. It does not factor in RSSI: without knowing the local noise floor, RSSI alone cannot say whether a signal is decodable. RSSI is still available — on the node detail screen and in the metrics charts. +The app rates signal quality (None, Bad, Fair, or Good) from SNR relative to the active preset's SNR limit. When RSSI and a noise-floor reading are both available, it also rates the difference between them against that limit and uses the worse rating. Otherwise, it uses SNR alone. Because the rating is relative to the preset, the same SNR rates differently on different presets. An SNR of −16 dB rates Good on Long Fast (SNR limit −17.5 dB) but None on Short Fast (SNR limit −7.5 dB). Let `limit` be the active preset's SNR limit: @@ -45,7 +45,7 @@ Because the rating is relative to the preset, the same SNR rates differently on The icon never goes blank, so count the bars carefully: a single bar means None, not a weak but usable link, and Good is a solid wedge rather than a set of bars. A gray three-bar icon labeled Unknown is a different state again — the packet carried no SNR measurement at all, which is not the same as measuring one and finding it too weak. -> ℹ️ **Note:** Traceroute hop colors use fixed thresholds (−7 dB / −15 dB); the per-node signal meter uses the preset-relative rating instead. +> ℹ️ **Note:** Traceroute and neighbor-info SNR colors use the active preset's limits too. RSSI text has separate strength colors: green above −115 dBm, yellow above −120 dBm, orange above −126 dBm, and red at or below −126 dBm. ## Diagnosing Local Interference diff --git a/docs/es-rES/user/tak.md b/docs/es-rES/user/tak.md index 26d779ed45..196e18f426 100644 --- a/docs/es-rES/user/tak.md +++ b/docs/es-rES/user/tak.md @@ -2,7 +2,7 @@ title: TAK Integration parent: User Guide nav_order: 10 -last_updated: 2026-08-30 +last_updated: 2026-09-11 description: Interoperate with ATAK and WinTAK — CoT position sharing, TAK roles, and plugin setup. aliases: - tak @@ -36,7 +36,7 @@ The TAK module allows Meshtastic nodes to: > ⚠️ **Warning:** The old **Meshtastic ATAK Plugin** is no longer part of this path and cannot > work. It bridged through the cross-process AIDL API, which was removed in app 2.8.0; the mesh > service is now in-process only. Do not install it. Interop today runs over the app's own local -> TAK server plus the Mesh to CoT Converter, both described below, with stock ATAK/iTAK/WinTAK. +> TAK server plus the Mesh to CoT Converter, both described in the following sections, with stock ATAK/iTAK/WinTAK. ### Configuración diff --git a/docs/es-rES/user/translate.md b/docs/es-rES/user/translate.md index dc7781ab19..6b030d3673 100644 --- a/docs/es-rES/user/translate.md +++ b/docs/es-rES/user/translate.md @@ -2,7 +2,7 @@ title: Translate the App parent: User Guide nav_order: 17 -last_updated: 2026-08-30 +last_updated: 2026-09-11 description: How the app and its documentation are translated via Crowdin, and guidelines for contributing translations. aliases: - translate @@ -45,7 +45,7 @@ If your language is not yet listed on Crowdin: The Android app uses **Compose Multiplatform resources** for all user-visible strings: -``` +```text core/resources/src/commonMain/composeResources/ ├── values/ ← English (default) │ └── strings.xml @@ -58,7 +58,7 @@ core/resources/src/commonMain/composeResources/ In-app documentation follows a similar pattern under `docs/`: -``` +```text docs/ ├── en/user/ ← English source (default) │ ├── onboarding.md diff --git a/docs/et-rEE/user/desktop.md b/docs/et-rEE/user/desktop.md index b1be8be8b5..2ffdb26b66 100644 --- a/docs/et-rEE/user/desktop.md +++ b/docs/et-rEE/user/desktop.md @@ -2,7 +2,7 @@ title: Desktop App parent: Kasutusjuhend nav_order: 14 -last_updated: 2026-08-30 +last_updated: 2026-09-11 description: Meshtastic arvuti rakendus pakub samu võrgusuhtluse funktsioone Linuxis, macOS-is ja Windowsis. aliases: - töölaud @@ -58,20 +58,20 @@ Bluetooth Low Energy is supported on desktop via the [Kable](https://github.com/ ## Feature Parity -| Feature | Android | Desktop | Sõnumid | -| ------------------------------------------------------ | ------- | ------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Messaging | ✓ | ✓ | Full parity | -| Sõlmede loend | ✓ | ✓ | Full parity | -| Kaart | ✓ | ✓ | Interactive MapLibre map, with base map and overlay pickers and custom tile sources. No offline downloads or local `.mbtiles` archives | -| Map layers (`.kml`/`.kmz`/GeoJSON) | ✓ | ✓ | Same layer store and sheet as Android; imported files draw on the desktop map | -| Saidi planeerija | ✓ | ✓\* | \*Opens in your browser on desktop; the estimate is not drawn on the desktop map | -| Sätted | ✓ | ✓ | Full parity | -| Sinihammas (BLE) | ✓ | ✓ | Kable'i kaudu töölauale | -| Püsivara uuendus | ✓ | ✓ | In-app USB, BLE, and Wi-Fi (ESP32) update work the same as Android. The USB maintenance flow — nRF52/RP2040 factory erase and bootloader upgrade — is Android-only | -| Märguanded | ✓ | ✓ | Emakeelsed op.süsteemi märguanded | -| Widgets | ✓ | ✗ | Android-only | -| TI assistent (Chirpy) | ✓\* | ✗ | Google flavor Android only | -| Rakenduse funktsioonid (süstemi TI) | ✓† | ✗ | Google flavor Android only | +| Feature | Android | Desktop | Sõnumid | +| ------------------------------------------------------ | ------- | ------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Messaging | ✓ | ✓ | Full parity | +| Sõlmede loend | ✓ | ✓ | Full parity | +| Kaart | ✓ | ✓ | Interactive MapLibre map, with base map and overlay pickers and custom tile sources. Offline terrain (hillshade and contours) can be downloaded; offline base-map packs and local `.mbtiles` archives cannot | +| Map layers (`.kml`/`.kmz`/GeoJSON) | ✓ | ✓ | Same layer store and sheet as Android; imported files draw on the desktop map | +| Saidi planeerija | ✓ | ✓\* | \*Opens in your browser on desktop; the estimate is not drawn on the desktop map | +| Sätted | ✓ | ✓ | Full parity | +| Sinihammas (BLE) | ✓ | ✓ | Kable'i kaudu töölauale | +| Püsivara uuendus | ✓ | ✓ | In-app USB, BLE, and Wi-Fi (ESP32) update work the same as Android. The USB maintenance flow — nRF52/RP2040 factory erase and bootloader upgrade — is Android-only | +| Märguanded | ✓ | ✓ | Emakeelsed op.süsteemi märguanded | +| Widgets | ✓ | ✗ | Android-only | +| TI assistent (Chirpy) | ✓\* | ✗ | Google flavor Android only | +| Rakenduse funktsioonid (süstemi TI) | ✓† | ✗ | Google flavor Android only | \*Chirpy tehisintellekti jaoks on vaja Google'i eriversioonidel Android 14+ ja toetatud riistvaraga. @@ -122,7 +122,7 @@ Individual doc pages render with full formatting: ## Building from Source -```bash +```shell git clone https://github.com/meshtastic/Meshtastic-Android.git cd Meshtastic-Android ./gradlew :desktopApp:run @@ -135,7 +135,7 @@ Nõuded: ## Known Limitations -- Offline tile downloads and local `.mbtiles` archives are not available on desktop. +- Offline base-map downloads and local `.mbtiles` archives are not available on desktop. Offline terrain is — see [Map & Waypoints](map-and-waypoints). - `.kml`/`.kmz`/GeoJSON layer import works — see [Map & Waypoints](map-and-waypoints#map-layers). Site Planner opens in your browser rather than in the app; to bring its coverage estimate onto the map, click the transmitter pin diff --git a/docs/et-rEE/user/discovery.md b/docs/et-rEE/user/discovery.md index cfdafd0503..b451a90040 100644 --- a/docs/et-rEE/user/discovery.md +++ b/docs/et-rEE/user/discovery.md @@ -2,7 +2,7 @@ title: Kohalik kärgvõrgu avastaja parent: Kasutusjuhend nav_order: 12 -last_updated: 2026-08-30 +last_updated: 2026-09-11 description: Avasta oma kärgvõrku – kohalik kärgvõrgu avastaja skanner, traceroute'i teed, naabri-kaardid ja sõlmede avastamise tööriistad. aliases: - discovery @@ -97,7 +97,7 @@ Kärgvõrgu majakas võimaldab sõlmedel kutsuda teisi oma võrguga liituma. A b Configure it under **Settings → Module configuration → Mesh Beacon**. The entry appears only on radios running firmware 2.8.0 or newer. A read-only **Region** row at the top of the screen shows the region the beacon advertises: that region, and the preset, are always the ones the radio itself uses, so a beacon cannot invite anyone onto settings your radio is not running. - **Kuula majakaid** — võta vastu teiste sõlmede edastatud kutseid. -- **Broadcast a beacon** — periodically advertise this mesh to nearby nodes, with an optional **Beacon message** of up to 100 bytes, a **Broadcast interval** picked from fixed intervals between 1 hour and 72 hours, and an **Offered channel** chosen from your radio's own channels. The offered channel is required, and defaults to your primary channel. +- **Broadcast a beacon** — periodically advertise this mesh to nearby nodes, with an optional **Beacon message** of up to 100 bytes, a **Broadcast interval** picked from fixed intervals between 1 hour and 72 hours, and an **Offered channel** chosen from your radio's own channels. The offered channel is required, and defaults to your primary channel. Over remote admin the picker offers the primary channel only. - **Broadcast targets** — optional extra destinations beyond the offered channel. **Add target** appends a row; each row picks a **Channel** and a **Transmit preset**, and **Remove target** deletes it. With no targets, the beacon goes out on the offered channel alone. Two conditions block beacon setup: diff --git a/docs/et-rEE/user/help-and-docs.md b/docs/et-rEE/user/help-and-docs.md index f1e6279759..9ac43fd472 100644 --- a/docs/et-rEE/user/help-and-docs.md +++ b/docs/et-rEE/user/help-and-docs.md @@ -2,7 +2,7 @@ title: Help & In-App Docs parent: Kasutaja juhis nav_order: 21 -last_updated: 2026-08-30 +last_updated: 2026-09-11 description: Sirvi seda dokumentatsiooni rakenduses, otsi seda ja küsi Chirpylt – seadmesisesele TI assistendile – küsimusi Meshtasticu kohta. aliases: - abi @@ -23,7 +23,7 @@ Dokumendibrauser loetleb kõik kasutusjuhendi lehed. Selle lugemiseks klõpsa le ### Otsi -The search box sits at the top of the docs browser. Type in it to filter pages by title and keywords — results update as you type, and the **Clear search** button (✕) empties the box. +Tap the search field at the top of the docs browser to open a full-screen search view. Type to filter pages by title and keywords — results update as you type, under the same field. The back arrow, or the keyboard's search action, closes the view. The field hides while you scroll down the page list and comes back when you scroll up. ![Rakendusesisese dokumentatsiooni otsimine](../../assets/screenshots/docs-browser_search.png) diff --git a/docs/et-rEE/user/map-and-waypoints.md b/docs/et-rEE/user/map-and-waypoints.md index 1ac7ba8799..4a5494d993 100644 --- a/docs/et-rEE/user/map-and-waypoints.md +++ b/docs/et-rEE/user/map-and-waypoints.md @@ -2,7 +2,7 @@ title: Kaart ja teekonnapunktid parent: Kasutusjuhend nav_order: 6 -last_updated: 2026-09-01 +last_updated: 2026-09-11 description: Vaata sõlmede asukohti kaardil, loo ja jaga teekonnapunkte ning halda asukoha jagamist ja privaatsust. aliases: - kaart @@ -44,7 +44,7 @@ The floating toolbar provides quick access to the compass, the map type and laye ### Filtering the Map -Tap the filter button in the floating toolbar to open **Filter map**. **Display** controls what is drawn: **Only Favorites**, **Show Waypoints**, **Show Precision Circles**, and a slider that hides nodes not heard from recently. **Node roles** is a chip per device role, plus **All** to show every role; a selected chip means that role is shown. **Nodes** narrows the set further with **Hide offline nodes**, **Only show direct nodes**, **Exclude MQTT**, **Show ignored nodes**, and **Include unknown**. +Tap the filter button in the floating toolbar to open **Filter map**. **Display** controls what is drawn: **Only Favorites**, **Show Waypoints**, **Show Precision Circles**, and a slider that hides nodes not heard from recently. **Node roles** is a chip per device role, plus **All** to show every role; a selected chip means that role is shown. **Nodes** narrows the set further with **Hide offline nodes**, **Only show direct nodes**, **Signed only**, **Encrypted only**, **Exclude MQTT**, **Show ignored nodes**, and **Include unknown** — the same rules as the node list's [filter toggles](nodes). A dot on the filter button means at least one filter is hiding something — check it before concluding the mesh is quiet. Turning **Show Waypoints** off hides every waypoint, including your own. **Show ignored nodes** adds them to the map rather than showing only them — unlike the node list's **Only show ignored Nodes**. @@ -147,8 +147,9 @@ picker, all three offer the same raster base maps: Overlays can be toggled on top of any base map, from the layers sheet: - **Weather radar** — NOAA NEXRAD reflectivity (US coverage) -- **Hillshade** — terrain relief, on **F-Droid** and **Desktop** only. Useful for understanding why a - link fails, since LoRa range is limited by terrain +- **Hillshade** — terrain relief from an online tile source, on **F-Droid** and **Desktop** only; Google + Play draws hillshade from downloaded offline terrain instead. Useful for understanding why a link + fails, since LoRa range is limited by terrain ### Adding your own tile source @@ -156,7 +157,7 @@ Any XYZ tile endpoint can be added as a base map, on every flavor and on desktop Tile Sources** at the foot of the base map picker and paste a URL template using `{z}`, `{x}` and `{y}` — plus `{s}` if the provider uses rotating subdomains. A national mapping service, for example: -``` +```text https://wmts.geo.admin.ch/1.0.0/ch.swisstopo.pixelkarte-farbe/default/current/3857/{z}/{x}/{y}.jpeg ``` @@ -173,7 +174,18 @@ On **Google Play**, the layers sheet's **Offline Manager** section downloads its — water, roads and administrative boundaries extracted from the public Protomaps basemap dataset, drawn directly as map shapes rather than raster tiles. Frame the area you want, tap **Start Download**, and once it finishes flip **Show on map** to draw it (it's off by default so it never surprises you by covering up the -live map). You can still import a pre-made `.mbtiles` archive here too, as before. **Desktop** has neither. +live map). You can still import a pre-made `.mbtiles` archive here too, as before. **Desktop** has neither, though +the offline terrain described next does work there. + +### Offline terrain + +Hillshade and elevation contours can be downloaded for offline use as well, so a downloaded area still +shows why a link fails when there is no network. The terrain data comes from Mapterhorn and is credited on +the map. On **Google Play**, each downloaded region in the **Offline Manager** list has a **Download Terrain** +button and **Hillshade** and **Contours** switches; terrain attaches to that region and shares its storage limit, and an area +too large for terrain asks you to zoom in or pick a smaller region. On **F-Droid** and **Desktop**, the +layers sheet has an **Offline Terrain** section of its own: frame the area, tap **Start Download**, and the +hillshade and contours draw from the downloaded tiles from then on. ### Going offline @@ -181,8 +193,8 @@ An **Offline** pill appears over the map whenever the device has no network conn builds, if you've imported a local `.mbtiles` archive, the map switches to it automatically the moment the network drops — no toggle to remember — and switches back once you're reconnected, as long as you haven't picked a different base map yourself in between. On **F-Droid**, a downloaded offline area keeps rendering on -its own; the pill is purely informational there. **Desktop** has no offline downloads at all yet, so the pill -is informational there too. +its own; the pill is purely informational there. **Desktop** downloads offline terrain but no base-map +packs, so the pill is informational there too. ## Seotud teemad diff --git a/docs/et-rEE/user/messages-and-channels.md b/docs/et-rEE/user/messages-and-channels.md index f324379e87..dec9a0291d 100644 --- a/docs/et-rEE/user/messages-and-channels.md +++ b/docs/et-rEE/user/messages-and-channels.md @@ -2,7 +2,7 @@ title: Sõnumid ja kanalid parent: Kasutusjuhend nav_order: 3 -last_updated: 2026-08-30 +last_updated: 2026-09-14 description: Saada ja võta vastu sõnumeid, halda kanaleid, konfigureeri krüpteerimist ning kasuta kiirvestlust, reaktsioone ja sõnumitoiminguid. aliases: - kanalid @@ -50,6 +50,21 @@ Key length alone does not change the icon: a 128-bit key and a 256-bit key both Tapping an existing channel opens the same editor, where you can change the name, the PSK, MQTT uplink and downlink, and position precision. Every edit on this screen — adding, editing, deleting, or dragging a channel into a new order — waits on **Send** the same way. +### Archived Channels + +A conversation belongs to the channel it was held on, not to the slot that channel occupied. When a channel leaves your radio — you scan a different channel QR, replace a channel's key, or switch the modem preset — its conversation is **archived** rather than deleted or merged into whatever takes the slot. + +An archived conversation: + +- stays in the **Channels** list under the name the channel had, below the channels currently on your radio, marked with a history icon; +- keeps its full message history, searchable as usual; +- is read-only — you cannot send, reply, or react, because the channel is no longer on the radio to send on, and it is not offered as a share target; +- comes back automatically if you re-add that exact channel, taking its messages back to the live slot. + +Changing the modem preset archives the channel too: the preset is part of a default channel's identity, so **LongFast** and **MediumFast** are different channels even though neither the name nor the key changed. Changing only the region does not archive anything. + +To remove an archived conversation for good, long-press it in the conversation list and delete it. + ## Otsesõnumid Direct messages (DMs) go to one specific node. When both radios hold each other's public keys, your radio encrypts the message to that node's public key, so no one else on the mesh can read it — not even nodes that share your channel. diff --git a/docs/et-rEE/user/mqtt.md b/docs/et-rEE/user/mqtt.md index 2cf499e6a2..9a264ba6e7 100644 --- a/docs/et-rEE/user/mqtt.md +++ b/docs/et-rEE/user/mqtt.md @@ -2,7 +2,7 @@ title: MQTT parent: Kasutusjuhend nav_order: 11 -last_updated: 2026-08-30 +last_updated: 2026-09-11 description: Silda oma võrk internetiga – MQTT maakleri seadistamine, krüpteerimiskihid ja kaardiaruandlus. aliases: - mqtt @@ -25,7 +25,7 @@ MQTT moodul ühendab sinu sõlme MQTT vahendajaga, võimaldades: ## Kuidas see toimib -``` +```text [Your Node] → Radio → [Gateway Node with Wi-Fi] → MQTT Broker → [Remote Gateway] → Radio → [Remote Node] ``` diff --git a/docs/et-rEE/user/node-metrics.md b/docs/et-rEE/user/node-metrics.md index e2f2de2d10..e7a0fed97a 100644 --- a/docs/et-rEE/user/node-metrics.md +++ b/docs/et-rEE/user/node-metrics.md @@ -2,7 +2,7 @@ title: Sõlme mõõdikud parent: Kasutusjuhend nav_order: 5 -last_updated: 2026-08-30 +last_updated: 2026-09-09 description: Telemeetria armatuurlauad iga võrgusõlme kohta – seadme tervis, keskkonnaandurid, õhu kvaliteet, signaali kvaliteet, võimsus, marsruut ja asukoha ajalugu. aliases: - meetriline @@ -114,7 +114,7 @@ Radio signal quality information: ### Signal Quality Reference -Signaali kvaliteeti hinnatakse **SNR-i põhjal, mis on seotud aktiivse LoRa modemi eelseadistuse demodulatsiooni alumise piiriga**, mitte fikseeritud läviväärtuste põhjal – antud SNR tähendab erinevatel eelseadistustel erinevat väärtust (nt −15 dB on LongSlow režiimil hea, kuid ShortFast režiimil mittekasutatav). RSSI on kuvatud, aga see ei ole osa hinnangust. In the table, _limit_ is the preset's SNR limit. +Signaali kvaliteeti hinnatakse **SNR-i põhjal, mis on seotud aktiivse LoRa modemi eelseadistuse demodulatsiooni alumise piiriga**, mitte fikseeritud läviväärtuste põhjal – antud SNR tähendab erinevatel eelseadistustel erinevat väärtust (nt −15 dB on LongSlow režiimil hea, kuid ShortFast režiimil mittekasutatav). When RSSI and a noise-floor reading are both available, the app also rates their difference against the preset limit and uses the worse rating. Otherwise, RSSI is display-only. In the table, _limit_ is the preset's SNR limit. | Kvaliteet | Kriteerium | | --------- | ----------------------------------------- | diff --git a/docs/et-rEE/user/nodes.md b/docs/et-rEE/user/nodes.md index 6f51f3b4ad..0d9ac8af43 100644 --- a/docs/et-rEE/user/nodes.md +++ b/docs/et-rEE/user/nodes.md @@ -2,7 +2,7 @@ title: Sõlmed parent: Kasutusjuhend nav_order: 4 -last_updated: 2026-09-04 +last_updated: 2026-09-11 description: Browse, filter, and sort mesh nodes — view details, signal quality, roles, and quick actions. aliases: - sõlmede loend @@ -74,19 +74,23 @@ Most users should keep the default **Client** role. Consider a different role wh > 💡 **Vihje:** Kärgvõrk töötab kõige paremini, kui enamik sõlmi on **klient** või **ruuter**. Too many Client Mute nodes reduce mesh resilience; too many Routers in a dense area can cause congestion. A good rule of thumb: one Router per 5–10 Clients in your area. -### Encryption Indicators +### Security indicators -Sõlmed kuvavad oma nime kõrval krüpteerimisoleku ikoone: +Each node carries one security icon beside its name in the node list. Tap it to read what it means, and choose **Show All Meanings** in that dialog for the full legend. The detail screen shows the same state in words, as a **Security** row that opens the same dialog. -| Ikoon | Tähendus | -| --------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- | -| 🔒 Lukustatud | Suhtlus kasutab PKI-d (avaliku võtme infrastruktuuri) – otsast lõpuni krüpteeritud kontrollitud identiteediga | -| 🔓 Lukust lahti | No public key has been received for this node, so it cannot be direct messaged — use **Request User Info** on the node detail page to ask for one | -| ⚠️ Ebakõla | Avaliku võtme mittevastavus — sõlme võti on viimasest nägemisest saadik muutunud (enne usaldamist uuri) | +| Ikoon | Tähendus | Shown for | +| --------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------- | +| Person with a shield check (green) | **Verified contact** — you verified this node's key in person by exchanging contact QR codes, so its identity is confirmed. The strongest trust the list shows. Your own connected node carries it too | Any firmware version | +| Nodes icon with a shield check (green) | **Signed node** — this node signs its broadcasts with its identity key, so its identity is consistent over time, but you have not verified it in person. On firmware 2.8 the icon appears from the version alone, before any signed broadcast has been heard | Firmware 2.8 or newer, and any node whose signed broadcast your node has verified | +| 🔒 Closed lock | A public key is on file and matches, so direct messages to this node are encrypted | Firmware before 2.8, or no reported version | +| 🔓 Open lock | No public key has been received for this node, so it cannot be direct messaged — use **Request User Info** on the node detail page to ask for one | Firmware before 2.8, or no reported version | +| ⚠️ Ebakõla | **Public key mismatch** — a different key arrived for this node after one was stored. Investigate before trusting | Any firmware version | -> 💡 **Tip:** Direct messages always use PKI, so the radio needs the other node's public key before it can send one. It refuses the send rather than falling back to channel encryption. Keys arrive inside node info, which is why an open lock usually clears itself once that node is heard from properly. Kui näed võtme mittevastavuse hoiatust, võib sõlm olla lähtestatud või ohustatud. +Every node on firmware 2.8 or newer signs its broadcasts, so on that firmware the signed state is the baseline and the locks are not shown. -To clear a mismatch, first confirm through another trusted channel that the key change was intentional — a factory reset causes one. Then touch & hold the node, choose **Remove**, and let the two radios exchange keys again the next time yours hears it. +Direct messages always use public-key encryption, so your node needs the other node's public key before it can send one. It refuses the send rather than falling back to channel encryption. Keys arrive inside node info, which is why an open lock usually clears itself once that node is heard from properly. + +A mismatch never replaces the key you already hold. The app keeps the first key it recorded and refuses the new one, as the firmware does, so a stray or hostile node info cannot silently break encrypted messaging to a contact. To clear a mismatch, first confirm through another trusted channel that the key change was intentional — a factory reset causes one. Then touch & hold the node, choose **Remove**, and let the two nodes exchange keys again the next time yours hears it. ## Quick Actions @@ -108,7 +112,9 @@ radio is connected and running firmware 2.8 or newer — see ## Sharing a Contact -On a node's detail screen, tap **Share Contact** to produce a link and a QR code for that node. From the same dialog, **Write to NFC tag** saves the link to a writable NFC tag that anyone can tap to open. +On a node's detail screen, tap **Share Contact** to produce a link and a QR code for that node. From the same dialog, **Share link** opens the Android share sheet (on desktop it copies the link instead), **Write to NFC tag** saves it to a writable NFC tag, and **Copy** puts it on the clipboard. While that dialog is open and in front of you, the phone also offers the same link to any NFC reader, so someone can take the contact by tapping their phone against yours with no tag involved. + +Sharing your own contact this way marks it as verified in person, so whoever imports it sees the **Verified contact** icon rather than the signed one. Relaying someone else's contact passes on only what your app had already recorded about them. To add someone else's contact, use the import button on the node list and choose **Scan Shared Contact QR Code**, **Scan Shared Contact NFC**, or **Input Shared Contact URL**. The app asks you to confirm with **Import Shared Contact?**, and warns you when the contact is one you already have. @@ -120,14 +126,16 @@ Sõlmede filtreerimiseks nime või lühinime järgi tipi otsinguväljal. Filter ### Filter Toggles -| Filtreeri | Kirjeldus | -| --------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| **Hide offline nodes** | Näita ainult viimase 2 tunni jooksul kuuldud sõlmi | -| **Only show direct nodes** | Show only nodes your radio heard directly, with no relay in between | -| **Include unknown** | Show nodes that haven't sent user info yet. **On by default**, so a node heard before its info arrives stays visible; these carry a badge marking them incomplete, and cannot be direct messaged until their user info brings a public key | -| **Exclude infrastructure** | Hide infrastructure-role nodes (Router, Router Late, Client Base, and legacy Repeater nodes) and any node that cannot be messaged, whatever its role | -| **Välista MQTT** | Peida ainult MQTT internetisilla kaudu kuuldavad sõlmed | -| **Only show ignored Nodes** | Replace the list with the nodes you have ignored. Every other node is hidden while this is on, and a banner appears at the top of the list to take you back | +| Filtreeri | Kirjeldus | +| --------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **Hide offline nodes** | Näita ainult viimase 2 tunni jooksul kuuldud sõlmi | +| **Only show direct nodes** | Show only nodes your radio heard directly, with no relay in between | +| **Include unknown** | Show nodes that haven't sent user info yet. **On by default**, so a node heard before its info arrives stays visible; these carry a badge marking them incomplete, and cannot be direct messaged until their user info brings a public key | +| **Exclude infrastructure** | Hide infrastructure-role nodes (Router, Router Late, Client Base, and legacy Repeater nodes) and any node that cannot be messaged, whatever its role | +| **Välista MQTT** | Peida ainult MQTT internetisilla kaudu kuuldavad sõlmed | +| **Signed only** | Show only nodes whose signed broadcasts your node has actually heard and verified. Stricter than the icon: a node on 2.8 shows as signed by its firmware version before any signed broadcast arrives, and a contact verified in person on older firmware is not signed at all | +| **Encrypted only** | Show nodes with a matching public key on file, the key an encrypted direct message needs. A node with a key mismatch is excluded | +| **Only show ignored Nodes** | Replace the list with the nodes you have ignored. Every other node is hidden while this is on, and a banner appears at the top of the list to take you back | ### Sorteerimisvalikud @@ -149,6 +157,8 @@ Puuduta sõlmede loendi rakenduse ribal hüppehistogrammi ikooni, et avada tulpd Sõlmel klõpsamine avab detailvaate koos põhjaliku teabega. See [Node Metrics](node-metrics) for full details on metrics and telemetry. +Signal quality is rated against your modem preset. The same SNR can be good on a long-range preset and poor on a faster one. Traceroute and neighbor-info SNR colors use that preset too. RSSI text has its own strength colors; it affects the quality rating only when a noise-floor reading is also available. + The Details card carries the node's short name, role, IDs, last heard time, hops away, uptime, and its SNR and RSSI: ![Sõlme üksikasjade jaotis](../../assets/screenshots/nodes_detail_section.png) diff --git a/docs/et-rEE/user/settings-module-admin.md b/docs/et-rEE/user/settings-module-admin.md index 1193332c0b..771ceeb5e0 100644 --- a/docs/et-rEE/user/settings-module-admin.md +++ b/docs/et-rEE/user/settings-module-admin.md @@ -2,7 +2,7 @@ title: Sätted - moodulid & admin parent: Kasutusjuhend nav_order: 8 -last_updated: 2026-08-30 +last_updated: 2026-09-16 description: Muuda valikulisi funktsioonimooduleid (MQTT, telemeetria, salvestatud sõnumid, TAK ja palju muud) ja teosta seadme haldamist. aliases: - moodul @@ -333,7 +333,7 @@ appear only when your own node is selected: ### Teave -**Settings → About** carries the app's own identity rather than the radio's: +**Settings → About** carries the app's own identity rather than the node's: Three sections: @@ -341,7 +341,11 @@ Three sections: - **Apps** — opens with **Need Hardware?**, a rotating carousel of popular devices that links out to where to buy one. It also lists the GitHub repository, the running app version, and **Acknowledgements** (see the next section). -- **Project information** — links to the website and to this documentation. +- **Project information** — links to the website, to this documentation, and to the GNU GPL v3 + license the app is released under. + +The footer states the copyright and that the app is free software with no warranty, which you +may redistribute under the same license. ### Tänusõnad diff --git a/docs/et-rEE/user/settings-radio-user.md b/docs/et-rEE/user/settings-radio-user.md index 7238bfe597..1f67d3aa47 100644 --- a/docs/et-rEE/user/settings-radio-user.md +++ b/docs/et-rEE/user/settings-radio-user.md @@ -2,7 +2,7 @@ title: Seaded — raadio ja kasutaja parent: Kasutusjuhend nav_order: 7 -last_updated: 2026-09-04 +last_updated: 2026-09-11 description: Configure your radio hardware, LoRa presets, user profile, position sharing, power management, and security. aliases: - sätted @@ -51,7 +51,7 @@ The footer appears as soon as you change something. **Discard** throws the chang The status message is saved with the same **Save**, but it never reboots the node — and, like the rest of this screen, it can be edited on a remote node you administer. For your own radio there is a shortcut while it is connected: touch & hold your node in the [node list](nodes.md) and choose -**Update status**. Older firmware and a disconnected radio have no shortcut — the field above is +**Update status**. Older firmware and a disconnected node have no shortcut — the field itself is still the way in. ## Sätted @@ -77,13 +77,13 @@ On **Settings → LoRa**. | Sätted | Kirjeldus | Vaikimisi | | ------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------- | -| Regioon | Regulatory region for frequency bands. You must set this before transmitting | Määramata (tuleb seadistada) | +| Regioon | Regulatory region for frequency bands. You must set this before transmitting. On firmware 2.8 or newer, the first region set also creates the node's identity key and gives it a new node number | Määramata (tuleb seadistada) | | Eelseadistused | Speed/range tradeoff | LongFast | | Hüpete arv | Maks uuesti saadetud hüpet | 3 | | Saatevõimsus | Transmission power (dBm); 0 = max allowed for region | 0 (regiooni maks) | | Tühista sagedus | Overrides the computed operating frequency outright (MHz). It does not offset the calculated value — leave at 0 unless you know you need a specific frequency | 0 (use calculated) | | Kasuta eelseadistust | On by default. Turn it off to set Spread Factor, Coding Rate and Bandwidth by hand instead of taking them from the modem preset | On | -| Levitustegur | Manual mode only: 7–12. Higher spreads further but slower | From preset | +| Levitustegur | Manual mode only: 5–12. Higher spreads further but slower. On SX127x (RF95) radios the firmware does not accept 5 or 6 and uses 11 instead | From preset | | Kodeerimiskiirus | Manual mode only: 5–8. More redundancy costs airtime | From preset | | Ribalaius | Manual mode only: the channel bandwidth in kHz, typed in directly. On the 2.4 GHz region the app offers a list of the bandwidths your radio supports instead, and a stored value that is not on that list shows as _Unsupported_ and blocks saving until you pick a supported one | From preset | | Sageduspesa | Which slot within the region's band to use. 0 derives it from the primary channel name | 0 (automatic) | @@ -111,13 +111,13 @@ The Lite, Narrow, Medium Turbo, and Tiny presets need firmware 2.8 or newer — | Short Slow | ~5 km | 6.25 kbps | −10 dB | Äärelinna lühimaa; mõõdukas hoonestustihedus | | Medium Fast | ~5 km | 3.52 kbps | −12,5 dB | Suburban areas; moderate building density | | Medium Slow | ~8 km | 1.95 kbps | −15 dB | Suburban/rural; moderate range with slower speed | -| Long Turbo | ~10 km | 1.34 kbps | −12,5 dB | Sarnane ulatus kui Pikk Kauge, aga 500 kHz ribalaiusega; kiirem läbilaskevõime | +| Long Turbo | ~10 km | 1.34 kbps | −17,5 dB | Sarnane ulatus kui Pikk Kauge, aga 500 kHz ribalaiusega; kiirem läbilaskevõime | | Long Fast | ~10 km | 1,1 kbps | −17,5 dB | **General use (default)** — balanced range and speed | | Long Moderate | ~20 km | 0,34 kbps | −17,5 dB | Maapiirkond, mõningase maastikuga; aeg-ajalt kasutatav | | Lite Fast | ~5 km | 1.76 kbps | −12,5 dB | EL 866 MHz SRD sagedusala (125 kHz ribalaius); võrreldav Medium Fast | | Lite Slow | ~10 km | 0.98 kbps | −15 dB | EL 866 MHz SRD sagedusala (125 kHz ribalaius); võrreldav Long Fast | -| Narrow Fast | ~5 km | 2.28 kbps | −10 dB | EL 868 MHz sagedusala (62,5 kHz sagedusriba); väldib häireid teiste seadmetega | -| Narrow Slow | ~10 km | 1.30 kbps | −12,5 dB | EL 868 MHz sagedusala (62,5 kHz ribalaius); võrreldav Long Fast | +| Narrow Fast | ~5 km | 2.28 kbps | −7,5 dB | EL 868 MHz sagedusala (62,5 kHz sagedusriba); väldib häireid teiste seadmetega | +| Narrow Slow | ~10 km | 1.30 kbps | −10 dB | EL 868 MHz sagedusala (62,5 kHz ribalaius); võrreldav Long Fast | | Medium Turbo | ~5 km | 7.0 kbps | −12,5 dB | Like Medium Fast but with 500 kHz bandwidth; not legal in every region. Needs firmware 2.8 or newer | | Tiny Fast | ~10 km | 0.68 kbps | −7,5 dB | Amateur bands that cap occupied bandwidth; these presets use 15.6 kHz. Needs firmware 2.8 or newer, an SX126x or SX127x radio, and a TCXO of ±5 ppm or better | | Tiny Slow | ~20 km | 0.33 kbps | −10 dB | Same band restrictions as Tiny Fast, longer range. Same firmware, radio, and TCXO requirements | diff --git a/docs/et-rEE/user/signal-meter.md b/docs/et-rEE/user/signal-meter.md index f84356933c..350cf4bcd7 100644 --- a/docs/et-rEE/user/signal-meter.md +++ b/docs/et-rEE/user/signal-meter.md @@ -2,7 +2,7 @@ title: Kuidas Meshtastic signaalimõõtur töötab parent: Kasutusjuhend nav_order: 15 -last_updated: 2026-08-30 +last_updated: 2026-09-09 description: How the signal meter rates quality from SNR relative to the LoRa modem preset — spread spectrum, presets, and what the bars really mean. aliases: - signaal @@ -32,7 +32,7 @@ Each modem preset has an SNR limit: the lowest SNR at which that preset can stil ## Rating Signal Quality -The app rates signal quality (None, Bad, Fair, or Good) from SNR alone, measured against the active preset's SNR limit. It does not factor in RSSI: without knowing the local noise floor, RSSI alone cannot say whether a signal is decodable. RSSI is still available — on the node detail screen and in the metrics charts. +The app rates signal quality (None, Bad, Fair, or Good) from SNR relative to the active preset's SNR limit. When RSSI and a noise-floor reading are both available, it also rates the difference between them against that limit and uses the worse rating. Otherwise, it uses SNR alone. Because the rating is relative to the preset, the same SNR rates differently on different presets. An SNR of −16 dB rates Good on Long Fast (SNR limit −17.5 dB) but None on Short Fast (SNR limit −7.5 dB). Let `limit` be the active preset's SNR limit: @@ -45,7 +45,7 @@ Because the rating is relative to the preset, the same SNR rates differently on The icon never goes blank, so count the bars carefully: a single bar means None, not a weak but usable link, and Good is a solid wedge rather than a set of bars. A gray three-bar icon labeled Unknown is a different state again — the packet carried no SNR measurement at all, which is not the same as measuring one and finding it too weak. -> ℹ️ **Note:** Traceroute hop colors use fixed thresholds (−7 dB / −15 dB); the per-node signal meter uses the preset-relative rating instead. +> ℹ️ **Note:** Traceroute and neighbor-info SNR colors use the active preset's limits too. RSSI text has separate strength colors: green above −115 dBm, yellow above −120 dBm, orange above −126 dBm, and red at or below −126 dBm. ## Diagnosing Local Interference diff --git a/docs/et-rEE/user/tak.md b/docs/et-rEE/user/tak.md index 259806b830..5196abc110 100644 --- a/docs/et-rEE/user/tak.md +++ b/docs/et-rEE/user/tak.md @@ -2,7 +2,7 @@ title: TAK integratsioon parent: Kasutusjuhend nav_order: 10 -last_updated: 2026-08-30 +last_updated: 2026-09-11 description: Koostöö ATAKi ja WinTAKiga — CoT asukoha jagamine, TAK rollid ja pluginate seadistamine. aliases: - tak @@ -36,7 +36,7 @@ TAK moodul võimaldab Meshtastic sõlmedel: > ⚠️ **Warning:** The old **Meshtastic ATAK Plugin** is no longer part of this path and cannot > work. It bridged through the cross-process AIDL API, which was removed in app 2.8.0; the mesh > service is now in-process only. Do not install it. Interop today runs over the app's own local -> TAK server plus the Mesh to CoT Converter, both described below, with stock ATAK/iTAK/WinTAK. +> TAK server plus the Mesh to CoT Converter, both described in the following sections, with stock ATAK/iTAK/WinTAK. ### Sätted diff --git a/docs/et-rEE/user/translate.md b/docs/et-rEE/user/translate.md index 455f39fa8f..52395b3e50 100644 --- a/docs/et-rEE/user/translate.md +++ b/docs/et-rEE/user/translate.md @@ -2,7 +2,7 @@ title: Tõlgi rakendus parent: Kasutusjuhend nav_order: 17 -last_updated: 2026-08-30 +last_updated: 2026-09-11 description: Kuidas rakendust ja selle dokumentatsiooni Crowdini kaudu tõlgitakse ja tõlgete panustamise juhised. aliases: - tõlgi @@ -45,7 +45,7 @@ Kui teie keelt Crowdinis veel pole: The Android app uses **Compose Multiplatform resources** for all user-visible strings: -``` +```text core/resources/src/commonMain/composeResources/ ├── values/ ← English (default) │ └── strings.xml @@ -58,7 +58,7 @@ core/resources/src/commonMain/composeResources/ Rakendusesisene dokumentatsioon järgib sarnast mustrit jaotises `docs/`: -``` +```text docs/ ├── en/user/ ← English source (default) │ ├── onboarding.md diff --git a/docs/fi-rFI/user/desktop.md b/docs/fi-rFI/user/desktop.md index 037ef4e5b2..52eaaf8e6d 100644 --- a/docs/fi-rFI/user/desktop.md +++ b/docs/fi-rFI/user/desktop.md @@ -2,7 +2,7 @@ title: Työpöytäsovellus parent: Käyttöopas nav_order: 14 -last_updated: 2026-08-30 +last_updated: 2026-09-11 description: Asenna ja käytä Meshtastic-työpöytäsovellusta Linuxilla, macOS:llä ja Windowsilla — yhteydet, ominaisuuksien yhtenevyys ja pikanäppäimet. aliases: - työpöytä @@ -58,20 +58,20 @@ Bluetooth Low Energy on tuettu Työpöydällä [Kable](https://github.com/JuulLa ## Ominaisuuksien yhtenevyys -| Ominaisuus | Android | Työpöytä | Viestit | -| ----------------------------------------------------------- | ------- | -------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Viestit | ✓ | ✓ | Täysi yhtenevyys | -| Radiolista | ✓ | ✓ | Täysi yhtenevyys | -| Kartta | ✓ | ✓ | Vuorovaikutteinen MapLibre-kartta, jossa on taustakartan ja karttatasojen valitsimet sekä mukautetut karttalähteet. Ei offline-latauksia eikä paikallisia `.mbtiles`-arkistoja | -| Karttatasot (`.kml` / `.kmz` / GeoJSON) | ✓ | ✓ | Sama karttatasojen hallinta kuin Androidissa. Tuodut tiedostot piirretään Työpöytä-kartalle | -| Site Planner | ✓ | ✓\* | \*Avautuu selaimessa Työpöydällä, eikä arviota piirretä Työpöytä-kartalle | -| Asetukset | ✓ | ✓ | Täysi yhtenevyys | -| Bluetooth (BLE) | ✓ | ✓ | Työpöydällä Kable-kirjaston kautta | -| Laiteohjelmiston päivitys | ✓ | ✓ | In-app USB, BLE, and Wi-Fi (ESP32) update work the same as Android. The USB maintenance flow — nRF52/RP2040 factory erase and bootloader upgrade — is Android-only | -| Ilmoitukset | ✓ | ✓ | Käyttöjärjestelmän natiivit ilmoitukset | -| Widgetit | ✓ | ✗ | Vain Android | -| Tekoälyavustaja (Chirpy) | ✓\* | ✗ | Vain Google-version Android-laitteissa | -| Sovellustoiminnot (järjestelmän tekoäly) | ✓† | ✗ | Vain Google-version Android-laitteissa | +| Ominaisuus | Android | Työpöytä | Viestit | +| ----------------------------------------------------------- | ------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| Viestit | ✓ | ✓ | Täysi yhtenevyys | +| Radiolista | ✓ | ✓ | Täysi yhtenevyys | +| Kartta | ✓ | ✓ | Vuorovaikutteinen MapLibre-kartta, jossa on taustakartan ja karttatasojen valitsimet sekä mukautetut karttalähteet. Offline terrain (hillshade and contours) can be downloaded; offline base-map packs and local `.mbtiles` archives cannot | +| Karttatasot (`.kml` / `.kmz` / GeoJSON) | ✓ | ✓ | Sama karttatasojen hallinta kuin Androidissa. Tuodut tiedostot piirretään Työpöytä-kartalle | +| Site Planner | ✓ | ✓\* | \*Avautuu selaimessa Työpöydällä, eikä arviota piirretä Työpöytä-kartalle | +| Asetukset | ✓ | ✓ | Täysi yhtenevyys | +| Bluetooth (BLE) | ✓ | ✓ | Työpöydällä Kable-kirjaston kautta | +| Laiteohjelmiston päivitys | ✓ | ✓ | In-app USB, BLE, and Wi-Fi (ESP32) update work the same as Android. The USB maintenance flow — nRF52/RP2040 factory erase and bootloader upgrade — is Android-only | +| Ilmoitukset | ✓ | ✓ | Käyttöjärjestelmän natiivit ilmoitukset | +| Widgetit | ✓ | ✗ | Vain Android | +| Tekoälyavustaja (Chirpy) | ✓\* | ✗ | Vain Google-version Android-laitteissa | +| Sovellustoiminnot (järjestelmän tekoäly) | ✓† | ✗ | Vain Google-version Android-laitteissa | \*Chirpy AI vaatii Android 14+ -version Google-version Android-laitteissa, joissa on tuettu laitteisto. @@ -122,7 +122,7 @@ Yksittäiset dokumenttisivut renderöidään täydellä muotoilulla: ## Rakentaminen lähdekoodista -```bash +```shell git clone https://github.com/meshtastic/Meshtastic-Android.git cd Meshtastic-Android ./gradlew :desktopApp:run @@ -135,7 +135,7 @@ Vaatimukset: ## Tunnetut rajoitukset -- Offline-kartta-alueiden lataukset ja paikalliset `.mbtiles`-arkistot eivät ole käytettävissä Työpöydällä. +- Offline base-map downloads and local `.mbtiles` archives are not available on desktop. Offline terrain is — see [Map & Waypoints](map-and-waypoints). - `.kml`-, `.kmz`- ja GeoJSON-karttatasojen tuonti toimii — katso [Kartta ja reittipisteet](map-and-waypoints#map-layers). Site Planner opens in your browser rather than in the app; to bring its coverage estimate onto the map, click the transmitter pin in the browser and use the planner's GeoJSON export, then add the file as a layer — not the KML diff --git a/docs/fi-rFI/user/discovery.md b/docs/fi-rFI/user/discovery.md index d2f3dfc30d..4d669f5cfe 100644 --- a/docs/fi-rFI/user/discovery.md +++ b/docs/fi-rFI/user/discovery.md @@ -2,7 +2,7 @@ title: Paikallisen mesh-verkon skannaus parent: Käyttöopas nav_order: 12 -last_updated: 2026-08-30 +last_updated: 2026-09-11 description: Tutki mesh-verkkoasi — paikallinen verkon haku, reitinselvitykset, naapurikartat ja radion hakuun liittyvät työkalut. aliases: - haku @@ -97,7 +97,7 @@ Mesh Beacon antaa radioille mahdollisuuden kutsua muita liittymään mesh-verkko Configure it under **Settings → Module configuration → Mesh Beacon**. The entry appears only on radios running firmware 2.8.0 or newer. A read-only **Region** row at the top of the screen shows the region the beacon advertises: that region, and the preset, are always the ones the radio itself uses, so a beacon cannot invite anyone onto settings your radio is not running. - **Kuuntele majakoita** – vastaanota muiden radioiden lähettämiä liittymiskutsuja. -- **Broadcast a beacon** — periodically advertise this mesh to nearby nodes, with an optional **Beacon message** of up to 100 bytes, a **Broadcast interval** picked from fixed intervals between 1 hour and 72 hours, and an **Offered channel** chosen from your radio's own channels. The offered channel is required, and defaults to your primary channel. +- **Broadcast a beacon** — periodically advertise this mesh to nearby nodes, with an optional **Beacon message** of up to 100 bytes, a **Broadcast interval** picked from fixed intervals between 1 hour and 72 hours, and an **Offered channel** chosen from your radio's own channels. The offered channel is required, and defaults to your primary channel. Over remote admin the picker offers the primary channel only. - **Broadcast targets** — optional extra destinations beyond the offered channel. **Add target** appends a row; each row picks a **Channel** and a **Transmit preset**, and **Remove target** deletes it. With no targets, the beacon goes out on the offered channel alone. Two conditions block beacon setup: diff --git a/docs/fi-rFI/user/help-and-docs.md b/docs/fi-rFI/user/help-and-docs.md index 330b087cb7..329e155d58 100644 --- a/docs/fi-rFI/user/help-and-docs.md +++ b/docs/fi-rFI/user/help-and-docs.md @@ -2,7 +2,7 @@ title: Ohjeet jasovelluksen sisäinen dokumentaatio parent: Käyttöopas nav_order: 21 -last_updated: 2026-08-30 +last_updated: 2026-09-11 description: Selaa tätä dokumentaatiota sovelluksessa, hae siitä tietoa ja kysy Meshtasticiin liittyviä kysymyksiä Chirpyltä — laitteella toimivalta tekoälyavustajalta. aliases: - apua @@ -23,7 +23,7 @@ Dokumentaation selain näyttää kaikki käyttöohjeen sivut. Avaa sivu napautta ### Haku -The search box sits at the top of the docs browser. Type in it to filter pages by title and keywords — results update as you type, and the **Clear search** button (✕) empties the box. +Tap the search field at the top of the docs browser to open a full-screen search view. Type to filter pages by title and keywords — results update as you type, under the same field. The back arrow, or the keyboard's search action, closes the view. The field hides while you scroll down the page list and comes back when you scroll up. ![Haku sovelluksen sisäisessä dokumentaatiossa](../../assets/screenshots/docs-browser_search.png) diff --git a/docs/fi-rFI/user/map-and-waypoints.md b/docs/fi-rFI/user/map-and-waypoints.md index a8475b5c7e..e4072cfca6 100644 --- a/docs/fi-rFI/user/map-and-waypoints.md +++ b/docs/fi-rFI/user/map-and-waypoints.md @@ -2,7 +2,7 @@ title: Kartta ja reittipisteet parent: Käyttöopas nav_order: 6 -last_updated: 2026-09-01 +last_updated: 2026-09-11 description: Näytä radioiden sijainnit kartalla, luo ja jaa reittipisteitä, hallitse karttatasoja ja Site Planneria sekä säädä sijainnin jakamista ja tietosuoja-asetuksia. aliases: - kartta @@ -44,7 +44,7 @@ The floating toolbar provides quick access to the compass, the map type and laye ### Filtering the Map -Tap the filter button in the floating toolbar to open **Filter map**. **Display** controls what is drawn: **Only Favorites**, **Show Waypoints**, **Show Precision Circles**, and a slider that hides nodes not heard from recently. **Node roles** is a chip per device role, plus **All** to show every role; a selected chip means that role is shown. **Nodes** narrows the set further with **Hide offline nodes**, **Only show direct nodes**, **Exclude MQTT**, **Show ignored nodes**, and **Include unknown**. +Tap the filter button in the floating toolbar to open **Filter map**. **Display** controls what is drawn: **Only Favorites**, **Show Waypoints**, **Show Precision Circles**, and a slider that hides nodes not heard from recently. **Node roles** is a chip per device role, plus **All** to show every role; a selected chip means that role is shown. **Nodes** narrows the set further with **Hide offline nodes**, **Only show direct nodes**, **Signed only**, **Encrypted only**, **Exclude MQTT**, **Show ignored nodes**, and **Include unknown** — the same rules as the node list's [filter toggles](nodes). A dot on the filter button means at least one filter is hiding something — check it before concluding the mesh is quiet. Turning **Show Waypoints** off hides every waypoint, including your own. **Show ignored nodes** adds them to the map rather than showing only them — unlike the node list's **Only show ignored Nodes**. @@ -145,7 +145,9 @@ Jokaisessa versiossa kartan työkalurivillä on karttapohjan valitsin. **Google Peitekuvat voidaan ottaa käyttöön minkä tahansa karttapohjan päällä karttatasovalikosta: - **Säätutka** — NOAA NEXRAD -heijastavuus (Yhdysvaltojen kattavuus) -- **Varjostus** — maaston korkeuserot, vain **F-Droid**- ja **työpöytä**-versioissa. Hyödyllinen, kun halutaan ymmärtää, miksi LoRa-yhteys katkeaa maaston vuoksi +- **Hillshade** — terrain relief from an online tile source, on **F-Droid** and **Desktop** only; Google + Play draws hillshade from downloaded offline terrain instead. Useful for understanding why a link + fails, since LoRa range is limited by terrain ### Oman karttatiilipalvelun lisääminen @@ -153,7 +155,7 @@ Mikä tahansa XYZ-karttatiilipalvelu voidaan lisätä karttapohjaksi kaikkiin ve Tile Sources** at the foot of the base map picker and paste a URL template using `{z}`, `{x}` and `{y}` — plus `{s}` if the provider uses rotating subdomains. Esimerkiksi kansallinen karttapalvelu: -``` +```text https://wmts.geo.admin.ch/1.0.0/ch.swisstopo.pixelkarte-farbe/default/current/3857/{z}/{x}/{y}.jpeg ``` @@ -170,7 +172,18 @@ On **Google Play**, the layers sheet's **Offline Manager** section downloads its — water, roads and administrative boundaries extracted from the public Protomaps basemap dataset, drawn directly as map shapes rather than raster tiles. Frame the area you want, tap **Start Download**, and once it finishes flip **Show on map** to draw it (it's off by default so it never surprises you by covering up the -live map). You can still import a pre-made `.mbtiles` archive here too, as before. **Desktop** has neither. +live map). You can still import a pre-made `.mbtiles` archive here too, as before. **Desktop** has neither, though +the offline terrain described next does work there. + +### Offline terrain + +Hillshade and elevation contours can be downloaded for offline use as well, so a downloaded area still +shows why a link fails when there is no network. The terrain data comes from Mapterhorn and is credited on +the map. On **Google Play**, each downloaded region in the **Offline Manager** list has a **Download Terrain** +button and **Hillshade** and **Contours** switches; terrain attaches to that region and shares its storage limit, and an area +too large for terrain asks you to zoom in or pick a smaller region. On **F-Droid** and **Desktop**, the +layers sheet has an **Offline Terrain** section of its own: frame the area, tap **Start Download**, and the +hillshade and contours draw from the downloaded tiles from then on. ### Going offline @@ -178,8 +191,8 @@ An **Offline** pill appears over the map whenever the device has no network conn builds, if you've imported a local `.mbtiles` archive, the map switches to it automatically the moment the network drops — no toggle to remember — and switches back once you're reconnected, as long as you haven't picked a different base map yourself in between. On **F-Droid**, a downloaded offline area keeps rendering on -its own; the pill is purely informational there. **Desktop** has no offline downloads at all yet, so the pill -is informational there too. +its own; the pill is purely informational there. **Desktop** downloads offline terrain but no base-map +packs, so the pill is informational there too. ## Aiheeseen liittyvät aiheet diff --git a/docs/fi-rFI/user/messages-and-channels.md b/docs/fi-rFI/user/messages-and-channels.md index 213679497d..63975a9c09 100644 --- a/docs/fi-rFI/user/messages-and-channels.md +++ b/docs/fi-rFI/user/messages-and-channels.md @@ -2,7 +2,7 @@ title: Viestit ja kanavat parent: Käyttöopas nav_order: 3 -last_updated: 2026-08-30 +last_updated: 2026-09-14 description: Lähetä ja vastaanota viestejä, hallitse kanavia, määritä salaus, hae keskusteluja sekä käytä pikachatia, reaktioita ja viestitoimintoja. aliases: - kanavat @@ -50,6 +50,21 @@ Key length alone does not change the icon: a 128-bit key and a 256-bit key both Tapping an existing channel opens the same editor, where you can change the name, the PSK, MQTT uplink and downlink, and position precision. Every edit on this screen — adding, editing, deleting, or dragging a channel into a new order — waits on **Send** the same way. +### Archived Channels + +A conversation belongs to the channel it was held on, not to the slot that channel occupied. When a channel leaves your radio — you scan a different channel QR, replace a channel's key, or switch the modem preset — its conversation is **archived** rather than deleted or merged into whatever takes the slot. + +An archived conversation: + +- stays in the **Channels** list under the name the channel had, below the channels currently on your radio, marked with a history icon; +- keeps its full message history, searchable as usual; +- is read-only — you cannot send, reply, or react, because the channel is no longer on the radio to send on, and it is not offered as a share target; +- comes back automatically if you re-add that exact channel, taking its messages back to the live slot. + +Changing the modem preset archives the channel too: the preset is part of a default channel's identity, so **LongFast** and **MediumFast** are different channels even though neither the name nor the key changed. Changing only the region does not archive anything. + +To remove an archived conversation for good, long-press it in the conversation list and delete it. + ## Yksityisviestit Direct messages (DMs) go to one specific node. When both radios hold each other's public keys, your radio encrypts the message to that node's public key, so no one else on the mesh can read it — not even nodes that share your channel. diff --git a/docs/fi-rFI/user/mqtt.md b/docs/fi-rFI/user/mqtt.md index 6c7daee589..6b0a3ce8b2 100644 --- a/docs/fi-rFI/user/mqtt.md +++ b/docs/fi-rFI/user/mqtt.md @@ -2,7 +2,7 @@ title: MQTT parent: Käyttöopas nav_order: 11 -last_updated: 2026-08-30 +last_updated: 2026-09-11 description: Siltaa mesh-verkko internetiin — MQTT-välityspalvelimen käyttöönotto, salauskerrokset ja karttadatan välitys. aliases: - mqtt @@ -25,7 +25,7 @@ MQTT-moduuli yhdistää radion MQTT-välityspalvelimeen, mahdollistaen: ## Kuinka se toimii -``` +```text [Oma radio] → Radio → [Yhdyskäytäväradio, jossa WiFi] → MQTT-välityspalvelin → [Etäyhdyskäytävä] → Radio → [Etäradio] ``` diff --git a/docs/fi-rFI/user/node-metrics.md b/docs/fi-rFI/user/node-metrics.md index f24f02a1ff..1a4ccc5ee3 100644 --- a/docs/fi-rFI/user/node-metrics.md +++ b/docs/fi-rFI/user/node-metrics.md @@ -2,7 +2,7 @@ title: Radion mittarit parent: Käyttöopas nav_order: 5 -last_updated: 2026-08-30 +last_updated: 2026-09-09 description: Telemetrianäkymät jokaiselle verkon radiolle — laitteen kunto, ympäristöanturit, ilmanlaatu, signaalin laatu, virta, reitinselvitys ja sijaintihistoria. aliases: - mittarit @@ -114,7 +114,7 @@ Radiosignaalin laatutiedot: ### Signaalin laadun viitearvot -Signaalin laatu arvioidaan **SNR**-arvon perusteella suhteessa käytössä olevan LoRa-modeemiesiasetuksen **demodulaation alarajaan**, ei kiinteiden raja-arvojen perusteella. Sama SNR-arvo voi tarkoittaa eri asioita eri esiasetuksilla (esim. `-15 dB` on hyvä LongSlow-esiasetuksella, mutta käyttökelvoton ShortFast-esiasetuksella). RSSI näytetään, mutta sitä ei käytetä arvioinnissa. Taulukossa _raja_ tarkoittaa esiasetuksen SNR-rajaa. +Signaalin laatu arvioidaan **SNR**-arvon perusteella suhteessa käytössä olevan LoRa-modeemiesiasetuksen **demodulaation alarajaan**, ei kiinteiden raja-arvojen perusteella. Sama SNR-arvo voi tarkoittaa eri asioita eri esiasetuksilla (esim. `-15 dB` on hyvä LongSlow-esiasetuksella, mutta käyttökelvoton ShortFast-esiasetuksella). When RSSI and a noise-floor reading are both available, the app also rates their difference against the preset limit and uses the worse rating. Otherwise, RSSI is display-only. Taulukossa _raja_ tarkoittaa esiasetuksen SNR-rajaa. | Laatu | Kriteerit | | ----------- | ---------------------------------- | diff --git a/docs/fi-rFI/user/nodes.md b/docs/fi-rFI/user/nodes.md index 22b5658249..814e8ab1df 100644 --- a/docs/fi-rFI/user/nodes.md +++ b/docs/fi-rFI/user/nodes.md @@ -2,7 +2,7 @@ title: Laitteet parent: Käyttöopas nav_order: 4 -last_updated: 2026-09-04 +last_updated: 2026-09-11 description: Selaa, suodata ja lajittele verkon radioita — tarkastele tietoja, signaalin laatua, rooleja ja pikatoimintoja. aliases: - radiolista @@ -74,19 +74,23 @@ Useimpien käyttäjien kannattaa käyttää oletusarvoista **Client**-roolia. Ha > 💡 **Vinkki:** Verkko toimii parhaiten, kun suurin osa radioista käyttää **Client**- tai **Router**-roolia. Liian suuri määrä Client Mute (mykistetty) -radioita heikentää mesh-verkon vikasietoisuutta. Liian useat Router -roolin radiot tiheällä alueella voivat aiheuttaa ruuhkaa. Hyvä nyrkkisääntö on yksi Router jokaista 5–10 Client-roolia kohden alueellasi. -### Salausilmaisimet +### Security indicators -Radiot näyttävät nimensä vieressä salauksen tilaa kuvaavat kuvakkeet: +Each node carries one security icon beside its name in the node list. Tap it to read what it means, and choose **Show All Meanings** in that dialog for the full legend. The detail screen shows the same state in words, as a **Security** row that opens the same dialog. -| Kuvake | Merkitys | -| --------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -| 🔒 Lukittu | Yhteys käyttää PKI:tä (julkisen avaimen infrastruktuuria) — päästä päähän salattu ja varmennetulla identiteetillä suojattu. | -| 🔓 Lukitsematon | No public key has been received for this node, so it cannot be direct messaged — use **Request User Info** on the node detail page to ask for one | -| ⚠️ Ei täsmää | Julkinen avain ei täsmää — radion avain on muuttunut viime näkemän jälkeen (tutki ennen luottamista). | +| Kuvake | Merkitys | Shown for | +| --------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------- | +| Person with a shield check (green) | **Verified contact** — you verified this node's key in person by exchanging contact QR codes, so its identity is confirmed. The strongest trust the list shows. Your own connected node carries it too | Any firmware version | +| Nodes icon with a shield check (green) | **Signed node** — this node signs its broadcasts with its identity key, so its identity is consistent over time, but you have not verified it in person. On firmware 2.8 the icon appears from the version alone, before any signed broadcast has been heard | Firmware 2.8 or newer, and any node whose signed broadcast your node has verified | +| 🔒 Closed lock | A public key is on file and matches, so direct messages to this node are encrypted | Firmware before 2.8, or no reported version | +| 🔓 Open lock | No public key has been received for this node, so it cannot be direct messaged — use **Request User Info** on the node detail page to ask for one | Firmware before 2.8, or no reported version | +| ⚠️ Ei täsmää | **Public key mismatch** — a different key arrived for this node after one was stored. Investigate before trusting | Any firmware version | -> 💡 **Tip:** Direct messages always use PKI, so the radio needs the other node's public key before it can send one. It refuses the send rather than falling back to channel encryption. Keys arrive inside node info, which is why an open lock usually clears itself once that node is heard from properly. Jos näet avaimen täsmäämättömyysvaroituksen, radio on voitu nollata tai sen tietoturva on voinut vaarantua. +Every node on firmware 2.8 or newer signs its broadcasts, so on that firmware the signed state is the baseline and the locks are not shown. -To clear a mismatch, first confirm through another trusted channel that the key change was intentional — a factory reset causes one. Then touch & hold the node, choose **Remove**, and let the two radios exchange keys again the next time yours hears it. +Direct messages always use public-key encryption, so your node needs the other node's public key before it can send one. It refuses the send rather than falling back to channel encryption. Keys arrive inside node info, which is why an open lock usually clears itself once that node is heard from properly. + +A mismatch never replaces the key you already hold. The app keeps the first key it recorded and refuses the new one, as the firmware does, so a stray or hostile node info cannot silently break encrypted messaging to a contact. To clear a mismatch, first confirm through another trusted channel that the key change was intentional — a factory reset causes one. Then touch & hold the node, choose **Remove**, and let the two nodes exchange keys again the next time yours hears it. ## Pikatoiminnot @@ -108,7 +112,9 @@ radio is connected and running firmware 2.8 or newer — see ## Sharing a Contact -On a node's detail screen, tap **Share Contact** to produce a link and a QR code for that node. From the same dialog, **Write to NFC tag** saves the link to a writable NFC tag that anyone can tap to open. +On a node's detail screen, tap **Share Contact** to produce a link and a QR code for that node. From the same dialog, **Share link** opens the Android share sheet (on desktop it copies the link instead), **Write to NFC tag** saves it to a writable NFC tag, and **Copy** puts it on the clipboard. While that dialog is open and in front of you, the phone also offers the same link to any NFC reader, so someone can take the contact by tapping their phone against yours with no tag involved. + +Sharing your own contact this way marks it as verified in person, so whoever imports it sees the **Verified contact** icon rather than the signed one. Relaying someone else's contact passes on only what your app had already recorded about them. To add someone else's contact, use the import button on the node list and choose **Scan Shared Contact QR Code**, **Scan Shared Contact NFC**, or **Input Shared Contact URL**. The app asks you to confirm with **Import Shared Contact?**, and warns you when the contact is one you already have. @@ -120,14 +126,16 @@ Kirjoita hakukenttään suodattaaksesi radioita nimen tai lyhyen nimen perusteel ### Suodatusvalinnat -| Suodatus | Kuvaus | -| --------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| **Hide offline nodes** | Näytä vain radiot, joista on kuultu viimeisten 2 tunnin aikana | -| **Only show direct nodes** | Show only nodes your radio heard directly, with no relay in between | -| **Näytä tuntemattomat** | Näytä radiot, jotka eivät ole vielä lähettäneet käyttäjätietoja. **On by default**, so a node heard before its info arrives stays visible; these carry a badge marking them incomplete, and cannot be direct messaged until their user info brings a public key | -| **Ohita infrastruktuurilaitteet** | Hide infrastructure-role nodes (Router, Router Late, Client Base, and legacy Repeater nodes) and any node that cannot be messaged, whatever its role | -| **Rajaa MQTT pois** | Piilottaa radiot, joista on kuultu vain MQTT-internetsillan kautta | -| **Only show ignored Nodes** | Replace the list with the nodes you have ignored. Every other node is hidden while this is on, and a banner appears at the top of the list to take you back | +| Suodatus | Kuvaus | +| --------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **Hide offline nodes** | Näytä vain radiot, joista on kuultu viimeisten 2 tunnin aikana | +| **Only show direct nodes** | Show only nodes your radio heard directly, with no relay in between | +| **Näytä tuntemattomat** | Näytä radiot, jotka eivät ole vielä lähettäneet käyttäjätietoja. **On by default**, so a node heard before its info arrives stays visible; these carry a badge marking them incomplete, and cannot be direct messaged until their user info brings a public key | +| **Ohita infrastruktuurilaitteet** | Hide infrastructure-role nodes (Router, Router Late, Client Base, and legacy Repeater nodes) and any node that cannot be messaged, whatever its role | +| **Rajaa MQTT pois** | Piilottaa radiot, joista on kuultu vain MQTT-internetsillan kautta | +| **Signed only** | Show only nodes whose signed broadcasts your node has actually heard and verified. Stricter than the icon: a node on 2.8 shows as signed by its firmware version before any signed broadcast arrives, and a contact verified in person on older firmware is not signed at all | +| **Encrypted only** | Show nodes with a matching public key on file, the key an encrypted direct message needs. A node with a key mismatch is excluded | +| **Only show ignored Nodes** | Replace the list with the nodes you have ignored. Every other node is hidden while this is on, and a banner appears at the top of the list to take you back | ### Lajitteluvaihtoehdot @@ -149,6 +157,8 @@ Avaa pylväskaavio, joka näyttää radioiden määrän kullakin hyppyetäisyyde Radion napauttaminen avaa tietonäkymän, jossa on kattavat tiedot. Katso [Radion mittarit](node-metrics) saadaksesi täydelliset tiedot mittareista ja telemetriasta. +Signal quality is rated against your modem preset. The same SNR can be good on a long-range preset and poor on a faster one. Traceroute and neighbor-info SNR colors use that preset too. RSSI text has its own strength colors; it affects the quality rating only when a noise-floor reading is also available. + The Details card carries the node's short name, role, IDs, last heard time, hops away, uptime, and its SNR and RSSI: ![Radion tietonäkymän osio](../../assets/screenshots/nodes_detail_section.png) diff --git a/docs/fi-rFI/user/settings-module-admin.md b/docs/fi-rFI/user/settings-module-admin.md index 29eb4dd8b8..f188c1bd1c 100644 --- a/docs/fi-rFI/user/settings-module-admin.md +++ b/docs/fi-rFI/user/settings-module-admin.md @@ -2,7 +2,7 @@ title: Asetukset — Moduulit ja ylläpito parent: Käyttöopas nav_order: 8 -last_updated: 2026-08-30 +last_updated: 2026-09-16 description: Määritä valinnaiset ominaisuusmoduulit (MQTT, telemetria, valmiit viestit, TAK ja muut) sekä suorita laitteen ylläpitotoimia. aliases: - moduulit @@ -330,13 +330,17 @@ appear only when your own node is selected: ### Tietoja -**Asetukset → Tietoja** sisältää sovelluksen tiedot, ei radion tietoja: +**Settings → About** carries the app's own identity rather than the node's: Kolme osiota: - **Mikä on Meshtastic?** – lyhyt kuvaus projektista. - **Sovellukset** — avaa **Tarvitsetko laitteiston?** -näkymän, jossa on suosittujen laitteiden karuselli ja linkit niiden ostopaikkoihin. Siellä näytetään myös GitHub-tietovarasto, käytössä oleva sovellusversio ja **Tekijätiedot** (katso seuraava osio). -- **Projektitiedot** – linkit verkkosivustolle ja tähän dokumentaatioon. +- **Project information** — links to the website, to this documentation, and to the GNU GPL v3 + license the app is released under. + +The footer states the copyright and that the app is free software with no warranty, which you +may redistribute under the same license. ### Kiitokset diff --git a/docs/fi-rFI/user/settings-radio-user.md b/docs/fi-rFI/user/settings-radio-user.md index 79004600c8..8eaf4fc852 100644 --- a/docs/fi-rFI/user/settings-radio-user.md +++ b/docs/fi-rFI/user/settings-radio-user.md @@ -2,7 +2,7 @@ title: Asetukset — Radio ja käyttäjä parent: Käyttöopas nav_order: 7 -last_updated: 2026-09-04 +last_updated: 2026-09-11 description: Määritä radion laitteisto, LoRa-esiasetukset, käyttäjäprofiili, sijainnin jakaminen, virranhallinta ja tietoturva. aliases: - asetukset @@ -50,7 +50,7 @@ The footer appears as soon as you change something. **Discard** throws the chang Tilaviesti tallennetaan samalla **Tallenna**-painikkeella, mutta se ei koskaan käynnistä radiota uudelleen. Kuten muitakin tämän näkymän asetuksia, sitä voidaan muokata etähallittavassa radiossa. For your own radio there is a shortcut while it is connected: touch & hold your node in the [node list](nodes.md) and choose -**Update status**. Older firmware and a disconnected radio have no shortcut — the field above is +**Update status**. Older firmware and a disconnected node have no shortcut — the field itself is still the way in. ## Asetukset @@ -76,13 +76,13 @@ On **Settings → LoRa**. | Asetus | Kuvaus | Oletus | | -------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------ | -| Alue | Taajuusalueen sääntelyalue. Tämä on määritettävä ennen lähettämistä | Ei asetettu (on määritettävä) | +| Alue | Taajuusalueen sääntelyalue. You must set this before transmitting. On firmware 2.8 or newer, the first region set also creates the node's identity key and gives it a new node number | Ei asetettu (on määritettävä) | | Esiasetukset | Nopeuden ja kantaman välinen kompromissi | LongFast | | Hyppyjen määrä | Suurin hyppyjen määrä | 3 | | Lähetysteho | Lähetysteho (dBm): 0 = alueen sallima enimmäisteho | 0 (alueen enimmäisteho) | | Taajuuden ohitus | Ohittaa lasketun käyttötaajuuden (MHz). Ei siirrä laskettua arvoa – jätä arvoksi 0, ellet tarvitse tiettyä taajuutta | 0 (käytä laskettua arvoa) | | Käytä esiasetusta | Oletusarvoisesti käytössä. Poista tämä käytöstä, jos haluat määrittää hajotuskerroin-, koodausnopeus- ja kaistanleveysasetukset käsin modeemiesiasetuksen sijaan | Käytössä | -| Levennyskerroin (Spread Factor) | Vain manuaalitilassa: 7–12. Suurempi hajotuskerroin lisää kantamaa, mutta hidastaa tiedonsiirtoa | Esiasetuksesta | +| Levennyskerroin (Spread Factor) | Manual mode only: 5–12. Higher spreads further but slower. On SX127x (RF95) radios the firmware does not accept 5 or 6 and uses 11 instead | Esiasetuksesta | | Koodausnopeus | Vain manuaalitilassa: 5–8. Suurempi virheenkorjaus lisää lähetysaikaa | Esiasetuksesta | | Kaistanleveys | Manual mode only: the channel bandwidth in kHz, typed in directly. On the 2.4 GHz region the app offers a list of the bandwidths your radio supports instead, and a stored value that is not on that list shows as _Unsupported_ and blocks saving until you pick a supported one | Esiasetuksesta | | Taajuuspaikka | Määrittää, mitä alueen taajuusväliä käytetään. 0 muodostetaan ensisijaisen kanavan nimestä | 0 (automaattinen) | @@ -110,13 +110,13 @@ The Lite, Narrow, Medium Turbo, and Tiny presets need firmware 2.8 or newer — | Short Slow | ~5 km | 6.25 kbps | −10 dB | Lyhyen kantaman esikaupunkialueet; kohtalainen rakennustiheys | | Medium Fast | ~5 km | 3.52 kbps | −12.5 dB | Esikaupunkialueet; kohtalainen rakennustiheys | | Medium Slow | ~8 km | 1.95 kbps | −15 dB | Esikaupunki-/maaseutualueet; kohtalainen kantama ja hitaampi nopeus | -| Long Turbo | ~10 km | 1.34 kbps | −12.5 dB | Samankaltainen kantama kuin Long Fast -asetuksella, mutta 500 kHz:n kaistanleveydellä; suurempi tiedonsiirtonopeus | +| Long Turbo | ~10 km | 1.34 kbps | −17.5 dB | Samankaltainen kantama kuin Long Fast -asetuksella, mutta 500 kHz:n kaistanleveydellä; suurempi tiedonsiirtonopeus | | Long Fast | ~10 km | 1.1 kbps | −17.5 dB | **Yleiskäyttö (oletus)** — tasapaino kantaman ja nopeuden välillä | | Long Moderate | ~20 km | 0.34 kbps | −17.5 dB | Maaseutualueet, joissa on jonkin verran maastonmuotoja; satunnainen käyttö | | Lite Fast | ~5 km | 1.76 kbps | −12.5 dB | EU 866 MHz SRD -alue (125 kHz BW); verrattavissa Medium Fast -asetukseen | | Lite Slow | ~10 km | 0.98 kbps | −15 dB | EU 866 MHz SRD -alue (125 kHz BW); verrattavissa Long Fast -asetukseen | -| Narrow Fast | ~5 km | 2.28 kbps | −10 dB | EU 868 MHz -alue (62,5 kHz BW); välttää häiriöitä muiden laitteiden kanssa | -| Narrow Slow | ~10 km | 1.30 kbps | −12.5 dB | EU 868 MHz -alue (62,5 kHz BW); verrattavissa Long Fast -asetukseen | +| Narrow Fast | ~5 km | 2.28 kbps | −7.5 dB | EU 868 MHz -alue (62,5 kHz BW); välttää häiriöitä muiden laitteiden kanssa | +| Narrow Slow | ~10 km | 1.30 kbps | −10 dB | EU 868 MHz -alue (62,5 kHz BW); verrattavissa Long Fast -asetukseen | | Medium Turbo | ~5 km | 7.0 kbps | −12.5 dB | Like Medium Fast but with 500 kHz bandwidth; not legal in every region. Needs firmware 2.8 or newer | | Tiny Fast | ~10 km | 0.68 kbps | −7.5 dB | Amateur bands that cap occupied bandwidth; these presets use 15.6 kHz. Needs firmware 2.8 or newer, an SX126x or SX127x radio, and a TCXO of ±5 ppm or better | | Tiny Slow | ~20 km | 0.33 kbps | −10 dB | Same band restrictions as Tiny Fast, longer range. Same firmware, radio, and TCXO requirements | diff --git a/docs/fi-rFI/user/signal-meter.md b/docs/fi-rFI/user/signal-meter.md index f96d82adc4..1498d4d4f0 100644 --- a/docs/fi-rFI/user/signal-meter.md +++ b/docs/fi-rFI/user/signal-meter.md @@ -2,7 +2,7 @@ title: Kuinka Meshtastic-signaalimittari toimii parent: Käyttöopas nav_order: 15 -last_updated: 2026-08-30 +last_updated: 2026-09-09 description: Miten signaalimittari arvioi signaalin laadun SNR-arvon perusteella suhteessa LoRa-modeemiesiasetukseen — hajaspektri, esiasetukset ja mitä palkit todellisuudessa tarkoittavat. aliases: - signaali @@ -32,7 +32,7 @@ Jokaisella modeemiesiasetuksella on SNR-raja: pienin SNR-arvo, jolla kyseinen es ## Signaalin laadun arviointi -Sovellus arvioi signaalin laadun (Ei mitään, Huono, Tyydyttävä tai Hyvä) pelkästään SNR:n perusteella vertaamalla sitä käytössä olevan esiasetuksen SNR-rajaan. RSSI:tä ei oteta huomioon: ilman tietoa paikallisesta kohinatasosta pelkkä RSSI ei kerro, voidaanko signaali purkaa. RSSI on silti käytettävissä — radion tiedoissa ja mittarikaavioissa. +The app rates signal quality (None, Bad, Fair, or Good) from SNR relative to the active preset's SNR limit. When RSSI and a noise-floor reading are both available, it also rates the difference between them against that limit and uses the worse rating. Otherwise, it uses SNR alone. Koska arviointi suhteutetaan esiasetukseen, sama SNR saa eri arvioinnin eri esiasetuksilla. SNR −16 dB arvioidaan Hyväksi Long Fast -esiasetuksella (SNR-raja −17.5 dB), mutta Ei mitään -tasolle Short Fast -esiasetuksella (SNR-raja −7.5 dB). Jos `raja` tarkoittaa käytössä olevan esiasetuksen SNR-rajaa: @@ -45,7 +45,7 @@ Koska arviointi suhteutetaan esiasetukseen, sama SNR saa eri arvioinnin eri esia The icon never goes blank, so count the bars carefully: a single bar means None, not a weak but usable link, and Good is a solid wedge rather than a set of bars. A gray three-bar icon labeled Unknown is a different state again — the packet carried no SNR measurement at all, which is not the same as measuring one and finding it too weak. -> ℹ️ **Huomautus:** Reitiselvityksen hyppyjen värit käyttävät kiinteitä raja-arvoja (−7 dB / −15 dB); radion signaalimittari käyttää sen sijaan esiasetukseen suhteutettua arviointia. +> ℹ️ **Note:** Traceroute and neighbor-info SNR colors use the active preset's limits too. RSSI text has separate strength colors: green above −115 dBm, yellow above −120 dBm, orange above −126 dBm, and red at or below −126 dBm. ## Paikallisten häiriöiden tunnistaminen diff --git a/docs/fi-rFI/user/tak.md b/docs/fi-rFI/user/tak.md index 2d9ccef5ba..77af46783e 100644 --- a/docs/fi-rFI/user/tak.md +++ b/docs/fi-rFI/user/tak.md @@ -2,7 +2,7 @@ title: TAK-integraatio parent: Käyttöopas nav_order: 10 -last_updated: 2026-08-30 +last_updated: 2026-09-11 description: ATAK:n ja WinTAK:n yhteentoimivuus — CoT-sijaintijako, TAK-roolit ja lisäosien käyttöönotto. aliases: - tak @@ -33,7 +33,8 @@ TAK-moduuli mahdollistaa Meshtastic-radioille: stores it, so the app hides the TAK module entry below that version even when the role is set correctly. See [Firmware Updates](firmware) -> ⚠️ **Varoitus:** Vanha **Meshtastic ATAK Plugin** ei enää kuulu tähän kokonaisuuteen eikä toimi. Yhteentoimivuus toteutetaan sovelluksen sisäisen paikallisen AIDL-rajapinnan kautta, joka poistettiin sovelluksesta 2.8.0. Mesh-palvelu toimii nyt vain sovelluksen sisäisenä. Älä asenna sitä. Yhteentoimivuus toimii nykyisin sovelluksen oman paikallisen TAK-palvelimen sekä Mesh to CoT Converterin kautta, jotka kuvataan jäljempänä, yhdessä vakio-ATAKin, iTAKin ja WinTAKin kanssa. +> ⚠️ **Varoitus:** Vanha **Meshtastic ATAK Plugin** ei enää kuulu tähän kokonaisuuteen eikä toimi. Yhteentoimivuus toteutetaan sovelluksen sisäisen paikallisen AIDL-rajapinnan kautta, joka poistettiin sovelluksesta 2.8.0. Mesh-palvelu toimii nyt vain sovelluksen sisäisenä. Älä asenna sitä. Interop today runs over the app's own local +> TAK server plus the Mesh to CoT Converter, both described in the following sections, with stock ATAK/iTAK/WinTAK. ### Asetukset diff --git a/docs/fi-rFI/user/translate.md b/docs/fi-rFI/user/translate.md index 13d5b5301f..ba05115084 100644 --- a/docs/fi-rFI/user/translate.md +++ b/docs/fi-rFI/user/translate.md @@ -2,7 +2,7 @@ title: Käännä sovellus parent: Käyttöopas nav_order: 17 -last_updated: 2026-08-30 +last_updated: 2026-09-11 description: Miten sovellus ja sen dokumentaatio käännetään Crowdinin avulla sekä ohjeet käännöksiin osallistumiseen. aliases: - käännä @@ -45,7 +45,7 @@ Jos kieltäsi ei ole vielä listattu Crowdinissa: Android-sovellus käyttää **Compose Multiplatform -resursseja** kaikille käyttäjälle näkyville teksteille: -``` +```text core/resources/src/commonMain/composeResources/ ├── values/ ← English (default) │ └── strings.xml @@ -58,7 +58,7 @@ core/resources/src/commonMain/composeResources/ Sovelluksen sisäinen dokumentaatio noudattaa samaa rakennetta `docs/` -hakemistossa: -``` +```text docs/ ├── en/user/ ← English source (default) │ ├── onboarding.md diff --git a/docs/fr-rCA/user/desktop.md b/docs/fr-rCA/user/desktop.md index c678e09c01..114d3613be 100644 --- a/docs/fr-rCA/user/desktop.md +++ b/docs/fr-rCA/user/desktop.md @@ -2,7 +2,7 @@ title: Desktop App parent: User Guide nav_order: 14 -last_updated: 2026-08-30 +last_updated: 2026-09-11 description: Install and use the Meshtastic Desktop app on Linux, macOS, and Windows — connections, feature parity, and keyboard shortcuts. aliases: - desktop @@ -58,20 +58,20 @@ Bluetooth Low Energy is supported on desktop via the [Kable](https://github.com/ ## Feature Parity -| Feature | Android | Desktop | Notes | -| ----------------------------------------------------- | ------- | ------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Messaging | ✓ | ✓ | Full parity | -| Node List | ✓ | ✓ | Full parity | -| Carte | ✓ | ✓ | Interactive MapLibre map, with base map and overlay pickers and custom tile sources. No offline downloads or local `.mbtiles` archives | -| Map layers (`.kml`/`.kmz`/GeoJSON) | ✓ | ✓ | Same layer store and sheet as Android; imported files draw on the desktop map | -| Site Planner | ✓ | ✓\* | \*Opens in your browser on desktop; the estimate is not drawn on the desktop map | -| Réglages | ✓ | ✓ | Full parity | -| Bluetooth (BLE) | ✓ | ✓ | Via Kable on desktop | -| Mise à jour du firmware | ✓ | ✓ | In-app USB, BLE, and Wi-Fi (ESP32) update work the same as Android. The USB maintenance flow — nRF52/RP2040 factory erase and bootloader upgrade — is Android-only | -| Notifications | ✓ | ✓ | Native OS notifications | -| Widgets | ✓ | ✗ | Android-only | -| AI Assistant (Chirpy) | ✓\* | ✗ | Google flavor Android only | -| App Functions (system AI) | ✓† | ✗ | Google flavor Android only | +| Feature | Android | Desktop | Notes | +| ----------------------------------------------------- | ------- | ------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Messaging | ✓ | ✓ | Full parity | +| Node List | ✓ | ✓ | Full parity | +| Carte | ✓ | ✓ | Interactive MapLibre map, with base map and overlay pickers and custom tile sources. Offline terrain (hillshade and contours) can be downloaded; offline base-map packs and local `.mbtiles` archives cannot | +| Map layers (`.kml`/`.kmz`/GeoJSON) | ✓ | ✓ | Same layer store and sheet as Android; imported files draw on the desktop map | +| Site Planner | ✓ | ✓\* | \*Opens in your browser on desktop; the estimate is not drawn on the desktop map | +| Réglages | ✓ | ✓ | Full parity | +| Bluetooth (BLE) | ✓ | ✓ | Via Kable on desktop | +| Mise à jour du firmware | ✓ | ✓ | In-app USB, BLE, and Wi-Fi (ESP32) update work the same as Android. The USB maintenance flow — nRF52/RP2040 factory erase and bootloader upgrade — is Android-only | +| Notifications | ✓ | ✓ | Native OS notifications | +| Widgets | ✓ | ✗ | Android-only | +| AI Assistant (Chirpy) | ✓\* | ✗ | Google flavor Android only | +| App Functions (system AI) | ✓† | ✗ | Google flavor Android only | \*Chirpy AI requires Android 14+ on Google flavor builds with supported hardware. @@ -122,7 +122,7 @@ Individual doc pages render with full formatting: ## Building from Source -```bash +```shell git clone https://github.com/meshtastic/Meshtastic-Android.git cd Meshtastic-Android ./gradlew :desktopApp:run @@ -135,7 +135,7 @@ Requirements: ## Known Limitations -- Offline tile downloads and local `.mbtiles` archives are not available on desktop. +- Offline base-map downloads and local `.mbtiles` archives are not available on desktop. Offline terrain is — see [Map & Waypoints](map-and-waypoints). - `.kml`/`.kmz`/GeoJSON layer import works — see [Map & Waypoints](map-and-waypoints#map-layers). Site Planner opens in your browser rather than in the app; to bring its coverage estimate onto the map, click the transmitter pin diff --git a/docs/fr-rCA/user/discovery.md b/docs/fr-rCA/user/discovery.md index 2a17535a05..262207e631 100644 --- a/docs/fr-rCA/user/discovery.md +++ b/docs/fr-rCA/user/discovery.md @@ -2,7 +2,7 @@ title: Découverte de maille locale parent: User Guide nav_order: 12 -last_updated: 2026-08-30 +last_updated: 2026-09-11 description: Explore your mesh network — the Local Mesh Discovery scanner, traceroute paths, neighbor maps, and node discovery tools. aliases: - discovery @@ -97,7 +97,7 @@ Mesh Beacon lets nodes invite others to join their mesh. A beaconing node period Configure it under **Settings → Module configuration → Mesh Beacon**. The entry appears only on radios running firmware 2.8.0 or newer. A read-only **Region** row at the top of the screen shows the region the beacon advertises: that region, and the preset, are always the ones the radio itself uses, so a beacon cannot invite anyone onto settings your radio is not running. - **Listen for beacons** — receive invitations broadcast by other nodes. -- **Broadcast a beacon** — periodically advertise this mesh to nearby nodes, with an optional **Beacon message** of up to 100 bytes, a **Broadcast interval** picked from fixed intervals between 1 hour and 72 hours, and an **Offered channel** chosen from your radio's own channels. The offered channel is required, and defaults to your primary channel. +- **Broadcast a beacon** — periodically advertise this mesh to nearby nodes, with an optional **Beacon message** of up to 100 bytes, a **Broadcast interval** picked from fixed intervals between 1 hour and 72 hours, and an **Offered channel** chosen from your radio's own channels. The offered channel is required, and defaults to your primary channel. Over remote admin the picker offers the primary channel only. - **Broadcast targets** — optional extra destinations beyond the offered channel. **Add target** appends a row; each row picks a **Channel** and a **Transmit preset**, and **Remove target** deletes it. With no targets, the beacon goes out on the offered channel alone. Two conditions block beacon setup: diff --git a/docs/fr-rCA/user/help-and-docs.md b/docs/fr-rCA/user/help-and-docs.md index 120abbe648..3e47771be6 100644 --- a/docs/fr-rCA/user/help-and-docs.md +++ b/docs/fr-rCA/user/help-and-docs.md @@ -2,7 +2,7 @@ title: Help & In-App Docs parent: Guide de l'utilisateur nav_order: 21 -last_updated: 2026-08-30 +last_updated: 2026-09-11 description: Browse this documentation inside the app, search it, and ask Chirpy — the on-device AI assistant — questions about Meshtastic. aliases: - help @@ -23,7 +23,7 @@ The docs browser lists every user-guide page. Tap a page to read it; images and ### Search -The search box sits at the top of the docs browser. Type in it to filter pages by title and keywords — results update as you type, and the **Clear search** button (✕) empties the box. +Tap the search field at the top of the docs browser to open a full-screen search view. Type to filter pages by title and keywords — results update as you type, under the same field. The back arrow, or the keyboard's search action, closes the view. The field hides while you scroll down the page list and comes back when you scroll up. ![Searching the in-app documentation](../../assets/screenshots/docs-browser_search.png) diff --git a/docs/fr-rCA/user/map-and-waypoints.md b/docs/fr-rCA/user/map-and-waypoints.md index a84dfd6d0f..7c471e40cd 100644 --- a/docs/fr-rCA/user/map-and-waypoints.md +++ b/docs/fr-rCA/user/map-and-waypoints.md @@ -2,7 +2,7 @@ title: Map & Waypoints parent: User Guide nav_order: 6 -last_updated: 2026-09-01 +last_updated: 2026-09-11 description: View node positions on the map, create and share waypoints, manage map layers and Site Planner, and control position sharing and privacy. aliases: - map @@ -44,7 +44,7 @@ The floating toolbar provides quick access to the compass, the map type and laye ### Filtering the Map -Tap the filter button in the floating toolbar to open **Filter map**. **Display** controls what is drawn: **Only Favorites**, **Show Waypoints**, **Show Precision Circles**, and a slider that hides nodes not heard from recently. **Node roles** is a chip per device role, plus **All** to show every role; a selected chip means that role is shown. **Nodes** narrows the set further with **Hide offline nodes**, **Only show direct nodes**, **Exclude MQTT**, **Show ignored nodes**, and **Include unknown**. +Tap the filter button in the floating toolbar to open **Filter map**. **Display** controls what is drawn: **Only Favorites**, **Show Waypoints**, **Show Precision Circles**, and a slider that hides nodes not heard from recently. **Node roles** is a chip per device role, plus **All** to show every role; a selected chip means that role is shown. **Nodes** narrows the set further with **Hide offline nodes**, **Only show direct nodes**, **Signed only**, **Encrypted only**, **Exclude MQTT**, **Show ignored nodes**, and **Include unknown** — the same rules as the node list's [filter toggles](nodes). A dot on the filter button means at least one filter is hiding something — check it before concluding the mesh is quiet. Turning **Show Waypoints** off hides every waypoint, including your own. **Show ignored nodes** adds them to the map rather than showing only them — unlike the node list's **Only show ignored Nodes**. @@ -147,8 +147,9 @@ picker, all three offer the same raster base maps: Overlays can be toggled on top of any base map, from the layers sheet: - **Weather radar** — NOAA NEXRAD reflectivity (US coverage) -- **Hillshade** — terrain relief, on **F-Droid** and **Desktop** only. Useful for understanding why a - link fails, since LoRa range is limited by terrain +- **Hillshade** — terrain relief from an online tile source, on **F-Droid** and **Desktop** only; Google + Play draws hillshade from downloaded offline terrain instead. Useful for understanding why a link + fails, since LoRa range is limited by terrain ### Adding your own tile source @@ -156,7 +157,7 @@ Any XYZ tile endpoint can be added as a base map, on every flavor and on desktop Tile Sources** at the foot of the base map picker and paste a URL template using `{z}`, `{x}` and `{y}` — plus `{s}` if the provider uses rotating subdomains. A national mapping service, for example: -``` +```text https://wmts.geo.admin.ch/1.0.0/ch.swisstopo.pixelkarte-farbe/default/current/3857/{z}/{x}/{y}.jpeg ``` @@ -173,7 +174,18 @@ On **Google Play**, the layers sheet's **Offline Manager** section downloads its — water, roads and administrative boundaries extracted from the public Protomaps basemap dataset, drawn directly as map shapes rather than raster tiles. Frame the area you want, tap **Start Download**, and once it finishes flip **Show on map** to draw it (it's off by default so it never surprises you by covering up the -live map). You can still import a pre-made `.mbtiles` archive here too, as before. **Desktop** has neither. +live map). You can still import a pre-made `.mbtiles` archive here too, as before. **Desktop** has neither, though +the offline terrain described next does work there. + +### Offline terrain + +Hillshade and elevation contours can be downloaded for offline use as well, so a downloaded area still +shows why a link fails when there is no network. The terrain data comes from Mapterhorn and is credited on +the map. On **Google Play**, each downloaded region in the **Offline Manager** list has a **Download Terrain** +button and **Hillshade** and **Contours** switches; terrain attaches to that region and shares its storage limit, and an area +too large for terrain asks you to zoom in or pick a smaller region. On **F-Droid** and **Desktop**, the +layers sheet has an **Offline Terrain** section of its own: frame the area, tap **Start Download**, and the +hillshade and contours draw from the downloaded tiles from then on. ### Going offline @@ -181,8 +193,8 @@ An **Offline** pill appears over the map whenever the device has no network conn builds, if you've imported a local `.mbtiles` archive, the map switches to it automatically the moment the network drops — no toggle to remember — and switches back once you're reconnected, as long as you haven't picked a different base map yourself in between. On **F-Droid**, a downloaded offline area keeps rendering on -its own; the pill is purely informational there. **Desktop** has no offline downloads at all yet, so the pill -is informational there too. +its own; the pill is purely informational there. **Desktop** downloads offline terrain but no base-map +packs, so the pill is informational there too. ## Related Topics diff --git a/docs/fr-rCA/user/messages-and-channels.md b/docs/fr-rCA/user/messages-and-channels.md index 32c811c0f2..652114cdab 100644 --- a/docs/fr-rCA/user/messages-and-channels.md +++ b/docs/fr-rCA/user/messages-and-channels.md @@ -2,7 +2,7 @@ title: Messages & Channels parent: User Guide nav_order: 3 -last_updated: 2026-08-30 +last_updated: 2026-09-14 description: Send and receive messages, manage channels, configure encryption, search conversations, and use quick chat, reactions, and message actions. aliases: - channels @@ -50,6 +50,21 @@ Key length alone does not change the icon: a 128-bit key and a 256-bit key both Tapping an existing channel opens the same editor, where you can change the name, the PSK, MQTT uplink and downlink, and position precision. Every edit on this screen — adding, editing, deleting, or dragging a channel into a new order — waits on **Send** the same way. +### Archived Channels + +A conversation belongs to the channel it was held on, not to the slot that channel occupied. When a channel leaves your radio — you scan a different channel QR, replace a channel's key, or switch the modem preset — its conversation is **archived** rather than deleted or merged into whatever takes the slot. + +An archived conversation: + +- stays in the **Channels** list under the name the channel had, below the channels currently on your radio, marked with a history icon; +- keeps its full message history, searchable as usual; +- is read-only — you cannot send, reply, or react, because the channel is no longer on the radio to send on, and it is not offered as a share target; +- comes back automatically if you re-add that exact channel, taking its messages back to the live slot. + +Changing the modem preset archives the channel too: the preset is part of a default channel's identity, so **LongFast** and **MediumFast** are different channels even though neither the name nor the key changed. Changing only the region does not archive anything. + +To remove an archived conversation for good, long-press it in the conversation list and delete it. + ## Messages directs Direct messages (DMs) go to one specific node. When both radios hold each other's public keys, your radio encrypts the message to that node's public key, so no one else on the mesh can read it — not even nodes that share your channel. diff --git a/docs/fr-rCA/user/mqtt.md b/docs/fr-rCA/user/mqtt.md index ec5ea01125..951c3db90f 100644 --- a/docs/fr-rCA/user/mqtt.md +++ b/docs/fr-rCA/user/mqtt.md @@ -2,7 +2,7 @@ title: MQTT parent: User Guide nav_order: 11 -last_updated: 2026-08-30 +last_updated: 2026-09-11 description: Bridge your mesh to the internet — MQTT broker setup, encryption layers, and map reporting. aliases: - mqtt @@ -25,7 +25,7 @@ The MQTT module connects your node to an MQTT broker, allowing: ## How It Works -``` +```text [Your Node] → Radio → [Gateway Node with Wi-Fi] → MQTT Broker → [Remote Gateway] → Radio → [Remote Node] ``` diff --git a/docs/fr-rCA/user/node-metrics.md b/docs/fr-rCA/user/node-metrics.md index 61283e3302..1d376c77be 100644 --- a/docs/fr-rCA/user/node-metrics.md +++ b/docs/fr-rCA/user/node-metrics.md @@ -2,7 +2,7 @@ title: Node Metrics parent: User Guide nav_order: 5 -last_updated: 2026-08-30 +last_updated: 2026-09-09 description: Telemetry dashboards for each mesh node — device health, environment sensors, air quality, signal quality, power, traceroute, and position history. aliases: - metrics @@ -114,7 +114,7 @@ Radio signal quality information: ### Signal Quality Reference -Signal quality is rated from **SNR relative to the active LoRa modem preset's demodulation floor**, not from fixed thresholds — a given SNR means different things on different presets (e.g. −15 dB is fine on LongSlow but unusable on ShortFast). RSSI is shown but is not part of the rating. In the table, _limit_ is the preset's SNR limit. +Signal quality is rated from **SNR relative to the active LoRa modem preset's demodulation floor**, not from fixed thresholds — a given SNR means different things on different presets (e.g. −15 dB is fine on LongSlow but unusable on ShortFast). When RSSI and a noise-floor reading are both available, the app also rates their difference against the preset limit and uses the worse rating. Otherwise, RSSI is display-only. In the table, _limit_ is the preset's SNR limit. | Quality | Criteria | | -------- | ---------------------------------------------------------------- | diff --git a/docs/fr-rCA/user/nodes.md b/docs/fr-rCA/user/nodes.md index c22bf9e44f..5f1e658a68 100644 --- a/docs/fr-rCA/user/nodes.md +++ b/docs/fr-rCA/user/nodes.md @@ -2,7 +2,7 @@ title: Nœuds parent: User Guide nav_order: 4 -last_updated: 2026-09-04 +last_updated: 2026-09-11 description: Browse, filter, and sort mesh nodes — view details, signal quality, roles, and quick actions. aliases: - node-list @@ -74,19 +74,23 @@ Most users should keep the default **Client** role. Consider a different role wh > 💡 **Tip:** The mesh works best when most nodes are **Client** or **Router**. Too many Client Mute nodes reduce mesh resilience; too many Routers in a dense area can cause congestion. A good rule of thumb: one Router per 5–10 Clients in your area. -### Encryption Indicators +### Security indicators -Nodes display encryption status icons next to their name: +Each node carries one security icon beside its name in the node list. Tap it to read what it means, and choose **Show All Meanings** in that dialog for the full legend. The detail screen shows the same state in words, as a **Security** row that opens the same dialog. -| Icon | Meaning | -| ----------- | ------------------------------------------------------------------------------------------------------------------------------------------------- | -| 🔒 Locked | Communication uses PKI (public key infrastructure) — end-to-end encrypted with verified identity | -| 🔓 Unlocked | No public key has been received for this node, so it cannot be direct messaged — use **Request User Info** on the node detail page to ask for one | -| ⚠️ Mismatch | Public key mismatch — the node's key has changed since last seen (investigate before trusting) | +| Icon | Meaning | Shown for | +| --------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------- | +| Person with a shield check (green) | **Verified contact** — you verified this node's key in person by exchanging contact QR codes, so its identity is confirmed. The strongest trust the list shows. Your own connected node carries it too | Any firmware version | +| Nodes icon with a shield check (green) | **Signed node** — this node signs its broadcasts with its identity key, so its identity is consistent over time, but you have not verified it in person. On firmware 2.8 the icon appears from the version alone, before any signed broadcast has been heard | Firmware 2.8 or newer, and any node whose signed broadcast your node has verified | +| 🔒 Closed lock | A public key is on file and matches, so direct messages to this node are encrypted | Firmware before 2.8, or no reported version | +| 🔓 Open lock | No public key has been received for this node, so it cannot be direct messaged — use **Request User Info** on the node detail page to ask for one | Firmware before 2.8, or no reported version | +| ⚠️ Mismatch | **Public key mismatch** — a different key arrived for this node after one was stored. Investigate before trusting | Any firmware version | -> 💡 **Tip:** Direct messages always use PKI, so the radio needs the other node's public key before it can send one. It refuses the send rather than falling back to channel encryption. Keys arrive inside node info, which is why an open lock usually clears itself once that node is heard from properly. If you see a key mismatch warning, the node may have been reset or compromised. +Every node on firmware 2.8 or newer signs its broadcasts, so on that firmware the signed state is the baseline and the locks are not shown. -To clear a mismatch, first confirm through another trusted channel that the key change was intentional — a factory reset causes one. Then touch & hold the node, choose **Remove**, and let the two radios exchange keys again the next time yours hears it. +Direct messages always use public-key encryption, so your node needs the other node's public key before it can send one. It refuses the send rather than falling back to channel encryption. Keys arrive inside node info, which is why an open lock usually clears itself once that node is heard from properly. + +A mismatch never replaces the key you already hold. The app keeps the first key it recorded and refuses the new one, as the firmware does, so a stray or hostile node info cannot silently break encrypted messaging to a contact. To clear a mismatch, first confirm through another trusted channel that the key change was intentional — a factory reset causes one. Then touch & hold the node, choose **Remove**, and let the two nodes exchange keys again the next time yours hears it. ## Quick Actions @@ -108,7 +112,9 @@ radio is connected and running firmware 2.8 or newer — see ## Sharing a Contact -On a node's detail screen, tap **Share Contact** to produce a link and a QR code for that node. From the same dialog, **Write to NFC tag** saves the link to a writable NFC tag that anyone can tap to open. +On a node's detail screen, tap **Share Contact** to produce a link and a QR code for that node. From the same dialog, **Share link** opens the Android share sheet (on desktop it copies the link instead), **Write to NFC tag** saves it to a writable NFC tag, and **Copy** puts it on the clipboard. While that dialog is open and in front of you, the phone also offers the same link to any NFC reader, so someone can take the contact by tapping their phone against yours with no tag involved. + +Sharing your own contact this way marks it as verified in person, so whoever imports it sees the **Verified contact** icon rather than the signed one. Relaying someone else's contact passes on only what your app had already recorded about them. To add someone else's contact, use the import button on the node list and choose **Scan Shared Contact QR Code**, **Scan Shared Contact NFC**, or **Input Shared Contact URL**. The app asks you to confirm with **Import Shared Contact?**, and warns you when the contact is one you already have. @@ -120,14 +126,16 @@ Type in the search field to filter nodes by name or short name. The filter updat ### Filter Toggles -| Filtre | Description | -| --------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| **Hide offline nodes** | Show only nodes heard within the last 2 hours | -| **Only show direct nodes** | Show only nodes your radio heard directly, with no relay in between | -| **Include unknown** | Show nodes that haven't sent user info yet. **On by default**, so a node heard before its info arrives stays visible; these carry a badge marking them incomplete, and cannot be direct messaged until their user info brings a public key | -| **Exclude infrastructure** | Hide infrastructure-role nodes (Router, Router Late, Client Base, and legacy Repeater nodes) and any node that cannot be messaged, whatever its role | -| **Exclude MQTT** | Hide nodes heard only via MQTT internet bridge | -| **Only show ignored Nodes** | Replace the list with the nodes you have ignored. Every other node is hidden while this is on, and a banner appears at the top of the list to take you back | +| Filtre | Description | +| --------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **Hide offline nodes** | Show only nodes heard within the last 2 hours | +| **Only show direct nodes** | Show only nodes your radio heard directly, with no relay in between | +| **Include unknown** | Show nodes that haven't sent user info yet. **On by default**, so a node heard before its info arrives stays visible; these carry a badge marking them incomplete, and cannot be direct messaged until their user info brings a public key | +| **Exclude infrastructure** | Hide infrastructure-role nodes (Router, Router Late, Client Base, and legacy Repeater nodes) and any node that cannot be messaged, whatever its role | +| **Exclude MQTT** | Hide nodes heard only via MQTT internet bridge | +| **Signed only** | Show only nodes whose signed broadcasts your node has actually heard and verified. Stricter than the icon: a node on 2.8 shows as signed by its firmware version before any signed broadcast arrives, and a contact verified in person on older firmware is not signed at all | +| **Encrypted only** | Show nodes with a matching public key on file, the key an encrypted direct message needs. A node with a key mismatch is excluded | +| **Only show ignored Nodes** | Replace the list with the nodes you have ignored. Every other node is hidden while this is on, and a banner appears at the top of the list to take you back | ### Sort Options @@ -149,6 +157,8 @@ Tap the hop-histogram icon in the node list's app bar to open a bar chart of how Tapping a node opens the detail view with comprehensive information. See [Node Metrics](node-metrics) for full details on metrics and telemetry. +Signal quality is rated against your modem preset. The same SNR can be good on a long-range preset and poor on a faster one. Traceroute and neighbor-info SNR colors use that preset too. RSSI text has its own strength colors; it affects the quality rating only when a noise-floor reading is also available. + The Details card carries the node's short name, role, IDs, last heard time, hops away, uptime, and its SNR and RSSI: ![Node detail section](../../assets/screenshots/nodes_detail_section.png) diff --git a/docs/fr-rCA/user/settings-module-admin.md b/docs/fr-rCA/user/settings-module-admin.md index 085a7a7902..e20ed54729 100644 --- a/docs/fr-rCA/user/settings-module-admin.md +++ b/docs/fr-rCA/user/settings-module-admin.md @@ -2,7 +2,7 @@ title: Settings — Modules & Admin parent: User Guide nav_order: 8 -last_updated: 2026-08-30 +last_updated: 2026-09-16 description: Configure optional feature modules (MQTT, telemetry, canned messages, TAK, and more) and perform device administration. aliases: - modules @@ -333,7 +333,7 @@ appear only when your own node is selected: ### A propros -**Settings → About** carries the app's own identity rather than the radio's: +**Settings → About** carries the app's own identity rather than the node's: Three sections: @@ -341,7 +341,11 @@ Three sections: - **Apps** — opens with **Need Hardware?**, a rotating carousel of popular devices that links out to where to buy one. It also lists the GitHub repository, the running app version, and **Acknowledgements** (see the next section). -- **Project information** — links to the website and to this documentation. +- **Project information** — links to the website, to this documentation, and to the GNU GPL v3 + license the app is released under. + +The footer states the copyright and that the app is free software with no warranty, which you +may redistribute under the same license. ### Remerciements diff --git a/docs/fr-rCA/user/settings-radio-user.md b/docs/fr-rCA/user/settings-radio-user.md index e66d5043fd..eb4719523f 100644 --- a/docs/fr-rCA/user/settings-radio-user.md +++ b/docs/fr-rCA/user/settings-radio-user.md @@ -2,7 +2,7 @@ title: Settings — Radio & User parent: User Guide nav_order: 7 -last_updated: 2026-09-04 +last_updated: 2026-09-11 description: Configure your radio hardware, LoRa presets, user profile, position sharing, power management, and security. aliases: - paramètres @@ -51,7 +51,7 @@ The footer appears as soon as you change something. **Discard** throws the chang The status message is saved with the same **Save**, but it never reboots the node — and, like the rest of this screen, it can be edited on a remote node you administer. For your own radio there is a shortcut while it is connected: touch & hold your node in the [node list](nodes.md) and choose -**Update status**. Older firmware and a disconnected radio have no shortcut — the field above is +**Update status**. Older firmware and a disconnected node have no shortcut — the field itself is still the way in. ## Configuration @@ -77,13 +77,13 @@ On **Settings → LoRa**. | Setting | Description | Par défaut | | --------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------- | -| Région | Regulatory region for frequency bands. You must set this before transmitting | Unset (must configure) | +| Région | Regulatory region for frequency bands. You must set this before transmitting. On firmware 2.8 or newer, the first region set also creates the node's identity key and gives it a new node number | Unset (must configure) | | Préréglages | Speed/range tradeoff | LongFast | | Nombre de sauts | Maximum retransmit hops | 3 | | Puissance d'émission | Transmission power (dBm); 0 = max allowed for region | 0 (region max) | | Remplacer la fréquence | Overrides the computed operating frequency outright (MHz). It does not offset the calculated value — leave at 0 unless you know you need a specific frequency | 0 (use calculated) | | Utiliser un préréglage | On by default. Turn it off to set Spread Factor, Coding Rate and Bandwidth by hand instead of taking them from the modem preset | On | -| Facteur de propagation | Manual mode only: 7–12. Higher spreads further but slower | From preset | +| Facteur de propagation | Manual mode only: 5–12. Higher spreads further but slower. On SX127x (RF95) radios the firmware does not accept 5 or 6 and uses 11 instead | From preset | | Taux de codage | Manual mode only: 5–8. More redundancy costs airtime | From preset | | Bande Passante | Manual mode only: the channel bandwidth in kHz, typed in directly. On the 2.4 GHz region the app offers a list of the bandwidths your radio supports instead, and a stored value that is not on that list shows as _Unsupported_ and blocks saving until you pick a supported one | From preset | | Slot de fréquence | Which slot within the region's band to use. 0 derives it from the primary channel name | 0 (automatic) | @@ -111,13 +111,13 @@ The Lite, Narrow, Medium Turbo, and Tiny presets need firmware 2.8 or newer — | Short Slow | ~5 km | 6.25 kbps | −10 dB | Suburban short-range; moderate building density | | Medium Fast | ~5 km | 3.52 kbps | −12.5 dB | Suburban areas; moderate building density | | Medium Slow | ~8 km | 1.95 kbps | −15 dB | Suburban/rural; moderate range with slower speed | -| Long Turbo | ~10 km | 1.34 kbps | −12.5 dB | Similar range to Long Fast but with 500 kHz bandwidth; faster throughput | +| Long Turbo | ~10 km | 1.34 kbps | −17.5 dB | Similar range to Long Fast but with 500 kHz bandwidth; faster throughput | | Long Fast | ~10 km | 1.1 kbps | −17.5 dB | **General use (default)** — balanced range and speed | | Long Moderate | ~20 km | 0.34 kbps | −17.5 dB | Rural with some terrain; occasional use | | Lite Fast | ~5 km | 1.76 kbps | −12.5 dB | EU 866 MHz SRD band (125 kHz BW); comparable to Medium Fast | | Lite Slow | ~10 km | 0.98 kbps | −15 dB | EU 866 MHz SRD band (125 kHz BW); comparable to Long Fast | -| Narrow Fast | ~5 km | 2.28 kbps | −10 dB | EU 868 MHz band (62.5 kHz BW); avoids interference with other devices | -| Narrow Slow | ~10 km | 1.30 kbps | −12.5 dB | EU 868 MHz band (62.5 kHz BW); comparable to Long Fast | +| Narrow Fast | ~5 km | 2.28 kbps | −7.5 dB | EU 868 MHz band (62.5 kHz BW); avoids interference with other devices | +| Narrow Slow | ~10 km | 1.30 kbps | −10 dB | EU 868 MHz band (62.5 kHz BW); comparable to Long Fast | | Medium Turbo | ~5 km | 7.0 kbps | −12.5 dB | Like Medium Fast but with 500 kHz bandwidth; not legal in every region. Needs firmware 2.8 or newer | | Tiny Fast | ~10 km | 0.68 kbps | −7.5 dB | Amateur bands that cap occupied bandwidth; these presets use 15.6 kHz. Needs firmware 2.8 or newer, an SX126x or SX127x radio, and a TCXO of ±5 ppm or better | | Tiny Slow | ~20 km | 0.33 kbps | −10 dB | Same band restrictions as Tiny Fast, longer range. Same firmware, radio, and TCXO requirements | diff --git a/docs/fr-rCA/user/signal-meter.md b/docs/fr-rCA/user/signal-meter.md index f2f121a684..f66f81f52a 100644 --- a/docs/fr-rCA/user/signal-meter.md +++ b/docs/fr-rCA/user/signal-meter.md @@ -2,7 +2,7 @@ title: How the Meshtastic Signal Meter Works parent: User Guide nav_order: 15 -last_updated: 2026-08-30 +last_updated: 2026-09-09 description: How the signal meter rates quality from SNR relative to the LoRa modem preset — spread spectrum, presets, and what the bars really mean. aliases: - signal @@ -32,7 +32,7 @@ Each modem preset has an SNR limit: the lowest SNR at which that preset can stil ## Rating Signal Quality -The app rates signal quality (None, Bad, Fair, or Good) from SNR alone, measured against the active preset's SNR limit. It does not factor in RSSI: without knowing the local noise floor, RSSI alone cannot say whether a signal is decodable. RSSI is still available — on the node detail screen and in the metrics charts. +The app rates signal quality (None, Bad, Fair, or Good) from SNR relative to the active preset's SNR limit. When RSSI and a noise-floor reading are both available, it also rates the difference between them against that limit and uses the worse rating. Otherwise, it uses SNR alone. Because the rating is relative to the preset, the same SNR rates differently on different presets. An SNR of −16 dB rates Good on Long Fast (SNR limit −17.5 dB) but None on Short Fast (SNR limit −7.5 dB). Let `limit` be the active preset's SNR limit: @@ -45,7 +45,7 @@ Because the rating is relative to the preset, the same SNR rates differently on The icon never goes blank, so count the bars carefully: a single bar means None, not a weak but usable link, and Good is a solid wedge rather than a set of bars. A gray three-bar icon labeled Unknown is a different state again — the packet carried no SNR measurement at all, which is not the same as measuring one and finding it too weak. -> ℹ️ **Note:** Traceroute hop colors use fixed thresholds (−7 dB / −15 dB); the per-node signal meter uses the preset-relative rating instead. +> ℹ️ **Note:** Traceroute and neighbor-info SNR colors use the active preset's limits too. RSSI text has separate strength colors: green above −115 dBm, yellow above −120 dBm, orange above −126 dBm, and red at or below −126 dBm. ## Diagnosing Local Interference diff --git a/docs/fr-rCA/user/tak.md b/docs/fr-rCA/user/tak.md index c020af3a36..ed50f72cc9 100644 --- a/docs/fr-rCA/user/tak.md +++ b/docs/fr-rCA/user/tak.md @@ -2,7 +2,7 @@ title: TAK Integration parent: User Guide nav_order: 10 -last_updated: 2026-08-30 +last_updated: 2026-09-11 description: Interoperate with ATAK and WinTAK — CoT position sharing, TAK roles, and plugin setup. aliases: - tak @@ -36,7 +36,7 @@ The TAK module allows Meshtastic nodes to: > ⚠️ **Warning:** The old **Meshtastic ATAK Plugin** is no longer part of this path and cannot > work. It bridged through the cross-process AIDL API, which was removed in app 2.8.0; the mesh > service is now in-process only. Do not install it. Interop today runs over the app's own local -> TAK server plus the Mesh to CoT Converter, both described below, with stock ATAK/iTAK/WinTAK. +> TAK server plus the Mesh to CoT Converter, both described in the following sections, with stock ATAK/iTAK/WinTAK. ### Configuration diff --git a/docs/fr-rCA/user/translate.md b/docs/fr-rCA/user/translate.md index 1eefb0c0f9..9965c9c73f 100644 --- a/docs/fr-rCA/user/translate.md +++ b/docs/fr-rCA/user/translate.md @@ -2,7 +2,7 @@ title: Translate the App parent: User Guide nav_order: 17 -last_updated: 2026-08-30 +last_updated: 2026-09-11 description: How the app and its documentation are translated via Crowdin, and guidelines for contributing translations. aliases: - translate @@ -45,7 +45,7 @@ Si votre langue n'est pas encore listée sur Crowdin : L'application Android utilise des **Ressources Multiplateforme** pour tous les textes visibles par l'utilisateur: -``` +```text core/resources/src/commonMain/composeResources/ ├── values/ ← English (default) │ └── strings.xml @@ -58,7 +58,7 @@ core/resources/src/commonMain/composeResources/ La documentation dans l'application suit un modèle similaire dans `docs/`: -``` +```text docs/ ├── en/user/ ← English source (default) │ ├── onboarding.md diff --git a/docs/fr-rFR/user/desktop.md b/docs/fr-rFR/user/desktop.md index c678e09c01..114d3613be 100644 --- a/docs/fr-rFR/user/desktop.md +++ b/docs/fr-rFR/user/desktop.md @@ -2,7 +2,7 @@ title: Desktop App parent: User Guide nav_order: 14 -last_updated: 2026-08-30 +last_updated: 2026-09-11 description: Install and use the Meshtastic Desktop app on Linux, macOS, and Windows — connections, feature parity, and keyboard shortcuts. aliases: - desktop @@ -58,20 +58,20 @@ Bluetooth Low Energy is supported on desktop via the [Kable](https://github.com/ ## Feature Parity -| Feature | Android | Desktop | Notes | -| ----------------------------------------------------- | ------- | ------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Messaging | ✓ | ✓ | Full parity | -| Node List | ✓ | ✓ | Full parity | -| Carte | ✓ | ✓ | Interactive MapLibre map, with base map and overlay pickers and custom tile sources. No offline downloads or local `.mbtiles` archives | -| Map layers (`.kml`/`.kmz`/GeoJSON) | ✓ | ✓ | Same layer store and sheet as Android; imported files draw on the desktop map | -| Site Planner | ✓ | ✓\* | \*Opens in your browser on desktop; the estimate is not drawn on the desktop map | -| Réglages | ✓ | ✓ | Full parity | -| Bluetooth (BLE) | ✓ | ✓ | Via Kable on desktop | -| Mise à jour du firmware | ✓ | ✓ | In-app USB, BLE, and Wi-Fi (ESP32) update work the same as Android. The USB maintenance flow — nRF52/RP2040 factory erase and bootloader upgrade — is Android-only | -| Notifications | ✓ | ✓ | Native OS notifications | -| Widgets | ✓ | ✗ | Android-only | -| AI Assistant (Chirpy) | ✓\* | ✗ | Google flavor Android only | -| App Functions (system AI) | ✓† | ✗ | Google flavor Android only | +| Feature | Android | Desktop | Notes | +| ----------------------------------------------------- | ------- | ------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Messaging | ✓ | ✓ | Full parity | +| Node List | ✓ | ✓ | Full parity | +| Carte | ✓ | ✓ | Interactive MapLibre map, with base map and overlay pickers and custom tile sources. Offline terrain (hillshade and contours) can be downloaded; offline base-map packs and local `.mbtiles` archives cannot | +| Map layers (`.kml`/`.kmz`/GeoJSON) | ✓ | ✓ | Same layer store and sheet as Android; imported files draw on the desktop map | +| Site Planner | ✓ | ✓\* | \*Opens in your browser on desktop; the estimate is not drawn on the desktop map | +| Réglages | ✓ | ✓ | Full parity | +| Bluetooth (BLE) | ✓ | ✓ | Via Kable on desktop | +| Mise à jour du firmware | ✓ | ✓ | In-app USB, BLE, and Wi-Fi (ESP32) update work the same as Android. The USB maintenance flow — nRF52/RP2040 factory erase and bootloader upgrade — is Android-only | +| Notifications | ✓ | ✓ | Native OS notifications | +| Widgets | ✓ | ✗ | Android-only | +| AI Assistant (Chirpy) | ✓\* | ✗ | Google flavor Android only | +| App Functions (system AI) | ✓† | ✗ | Google flavor Android only | \*Chirpy AI requires Android 14+ on Google flavor builds with supported hardware. @@ -122,7 +122,7 @@ Individual doc pages render with full formatting: ## Building from Source -```bash +```shell git clone https://github.com/meshtastic/Meshtastic-Android.git cd Meshtastic-Android ./gradlew :desktopApp:run @@ -135,7 +135,7 @@ Requirements: ## Known Limitations -- Offline tile downloads and local `.mbtiles` archives are not available on desktop. +- Offline base-map downloads and local `.mbtiles` archives are not available on desktop. Offline terrain is — see [Map & Waypoints](map-and-waypoints). - `.kml`/`.kmz`/GeoJSON layer import works — see [Map & Waypoints](map-and-waypoints#map-layers). Site Planner opens in your browser rather than in the app; to bring its coverage estimate onto the map, click the transmitter pin diff --git a/docs/fr-rFR/user/discovery.md b/docs/fr-rFR/user/discovery.md index 2a17535a05..262207e631 100644 --- a/docs/fr-rFR/user/discovery.md +++ b/docs/fr-rFR/user/discovery.md @@ -2,7 +2,7 @@ title: Découverte de maille locale parent: User Guide nav_order: 12 -last_updated: 2026-08-30 +last_updated: 2026-09-11 description: Explore your mesh network — the Local Mesh Discovery scanner, traceroute paths, neighbor maps, and node discovery tools. aliases: - discovery @@ -97,7 +97,7 @@ Mesh Beacon lets nodes invite others to join their mesh. A beaconing node period Configure it under **Settings → Module configuration → Mesh Beacon**. The entry appears only on radios running firmware 2.8.0 or newer. A read-only **Region** row at the top of the screen shows the region the beacon advertises: that region, and the preset, are always the ones the radio itself uses, so a beacon cannot invite anyone onto settings your radio is not running. - **Listen for beacons** — receive invitations broadcast by other nodes. -- **Broadcast a beacon** — periodically advertise this mesh to nearby nodes, with an optional **Beacon message** of up to 100 bytes, a **Broadcast interval** picked from fixed intervals between 1 hour and 72 hours, and an **Offered channel** chosen from your radio's own channels. The offered channel is required, and defaults to your primary channel. +- **Broadcast a beacon** — periodically advertise this mesh to nearby nodes, with an optional **Beacon message** of up to 100 bytes, a **Broadcast interval** picked from fixed intervals between 1 hour and 72 hours, and an **Offered channel** chosen from your radio's own channels. The offered channel is required, and defaults to your primary channel. Over remote admin the picker offers the primary channel only. - **Broadcast targets** — optional extra destinations beyond the offered channel. **Add target** appends a row; each row picks a **Channel** and a **Transmit preset**, and **Remove target** deletes it. With no targets, the beacon goes out on the offered channel alone. Two conditions block beacon setup: diff --git a/docs/fr-rFR/user/help-and-docs.md b/docs/fr-rFR/user/help-and-docs.md index 120abbe648..3e47771be6 100644 --- a/docs/fr-rFR/user/help-and-docs.md +++ b/docs/fr-rFR/user/help-and-docs.md @@ -2,7 +2,7 @@ title: Help & In-App Docs parent: Guide de l'utilisateur nav_order: 21 -last_updated: 2026-08-30 +last_updated: 2026-09-11 description: Browse this documentation inside the app, search it, and ask Chirpy — the on-device AI assistant — questions about Meshtastic. aliases: - help @@ -23,7 +23,7 @@ The docs browser lists every user-guide page. Tap a page to read it; images and ### Search -The search box sits at the top of the docs browser. Type in it to filter pages by title and keywords — results update as you type, and the **Clear search** button (✕) empties the box. +Tap the search field at the top of the docs browser to open a full-screen search view. Type to filter pages by title and keywords — results update as you type, under the same field. The back arrow, or the keyboard's search action, closes the view. The field hides while you scroll down the page list and comes back when you scroll up. ![Searching the in-app documentation](../../assets/screenshots/docs-browser_search.png) diff --git a/docs/fr-rFR/user/map-and-waypoints.md b/docs/fr-rFR/user/map-and-waypoints.md index a84dfd6d0f..7c471e40cd 100644 --- a/docs/fr-rFR/user/map-and-waypoints.md +++ b/docs/fr-rFR/user/map-and-waypoints.md @@ -2,7 +2,7 @@ title: Map & Waypoints parent: User Guide nav_order: 6 -last_updated: 2026-09-01 +last_updated: 2026-09-11 description: View node positions on the map, create and share waypoints, manage map layers and Site Planner, and control position sharing and privacy. aliases: - map @@ -44,7 +44,7 @@ The floating toolbar provides quick access to the compass, the map type and laye ### Filtering the Map -Tap the filter button in the floating toolbar to open **Filter map**. **Display** controls what is drawn: **Only Favorites**, **Show Waypoints**, **Show Precision Circles**, and a slider that hides nodes not heard from recently. **Node roles** is a chip per device role, plus **All** to show every role; a selected chip means that role is shown. **Nodes** narrows the set further with **Hide offline nodes**, **Only show direct nodes**, **Exclude MQTT**, **Show ignored nodes**, and **Include unknown**. +Tap the filter button in the floating toolbar to open **Filter map**. **Display** controls what is drawn: **Only Favorites**, **Show Waypoints**, **Show Precision Circles**, and a slider that hides nodes not heard from recently. **Node roles** is a chip per device role, plus **All** to show every role; a selected chip means that role is shown. **Nodes** narrows the set further with **Hide offline nodes**, **Only show direct nodes**, **Signed only**, **Encrypted only**, **Exclude MQTT**, **Show ignored nodes**, and **Include unknown** — the same rules as the node list's [filter toggles](nodes). A dot on the filter button means at least one filter is hiding something — check it before concluding the mesh is quiet. Turning **Show Waypoints** off hides every waypoint, including your own. **Show ignored nodes** adds them to the map rather than showing only them — unlike the node list's **Only show ignored Nodes**. @@ -147,8 +147,9 @@ picker, all three offer the same raster base maps: Overlays can be toggled on top of any base map, from the layers sheet: - **Weather radar** — NOAA NEXRAD reflectivity (US coverage) -- **Hillshade** — terrain relief, on **F-Droid** and **Desktop** only. Useful for understanding why a - link fails, since LoRa range is limited by terrain +- **Hillshade** — terrain relief from an online tile source, on **F-Droid** and **Desktop** only; Google + Play draws hillshade from downloaded offline terrain instead. Useful for understanding why a link + fails, since LoRa range is limited by terrain ### Adding your own tile source @@ -156,7 +157,7 @@ Any XYZ tile endpoint can be added as a base map, on every flavor and on desktop Tile Sources** at the foot of the base map picker and paste a URL template using `{z}`, `{x}` and `{y}` — plus `{s}` if the provider uses rotating subdomains. A national mapping service, for example: -``` +```text https://wmts.geo.admin.ch/1.0.0/ch.swisstopo.pixelkarte-farbe/default/current/3857/{z}/{x}/{y}.jpeg ``` @@ -173,7 +174,18 @@ On **Google Play**, the layers sheet's **Offline Manager** section downloads its — water, roads and administrative boundaries extracted from the public Protomaps basemap dataset, drawn directly as map shapes rather than raster tiles. Frame the area you want, tap **Start Download**, and once it finishes flip **Show on map** to draw it (it's off by default so it never surprises you by covering up the -live map). You can still import a pre-made `.mbtiles` archive here too, as before. **Desktop** has neither. +live map). You can still import a pre-made `.mbtiles` archive here too, as before. **Desktop** has neither, though +the offline terrain described next does work there. + +### Offline terrain + +Hillshade and elevation contours can be downloaded for offline use as well, so a downloaded area still +shows why a link fails when there is no network. The terrain data comes from Mapterhorn and is credited on +the map. On **Google Play**, each downloaded region in the **Offline Manager** list has a **Download Terrain** +button and **Hillshade** and **Contours** switches; terrain attaches to that region and shares its storage limit, and an area +too large for terrain asks you to zoom in or pick a smaller region. On **F-Droid** and **Desktop**, the +layers sheet has an **Offline Terrain** section of its own: frame the area, tap **Start Download**, and the +hillshade and contours draw from the downloaded tiles from then on. ### Going offline @@ -181,8 +193,8 @@ An **Offline** pill appears over the map whenever the device has no network conn builds, if you've imported a local `.mbtiles` archive, the map switches to it automatically the moment the network drops — no toggle to remember — and switches back once you're reconnected, as long as you haven't picked a different base map yourself in between. On **F-Droid**, a downloaded offline area keeps rendering on -its own; the pill is purely informational there. **Desktop** has no offline downloads at all yet, so the pill -is informational there too. +its own; the pill is purely informational there. **Desktop** downloads offline terrain but no base-map +packs, so the pill is informational there too. ## Related Topics diff --git a/docs/fr-rFR/user/messages-and-channels.md b/docs/fr-rFR/user/messages-and-channels.md index 32c811c0f2..652114cdab 100644 --- a/docs/fr-rFR/user/messages-and-channels.md +++ b/docs/fr-rFR/user/messages-and-channels.md @@ -2,7 +2,7 @@ title: Messages & Channels parent: User Guide nav_order: 3 -last_updated: 2026-08-30 +last_updated: 2026-09-14 description: Send and receive messages, manage channels, configure encryption, search conversations, and use quick chat, reactions, and message actions. aliases: - channels @@ -50,6 +50,21 @@ Key length alone does not change the icon: a 128-bit key and a 256-bit key both Tapping an existing channel opens the same editor, where you can change the name, the PSK, MQTT uplink and downlink, and position precision. Every edit on this screen — adding, editing, deleting, or dragging a channel into a new order — waits on **Send** the same way. +### Archived Channels + +A conversation belongs to the channel it was held on, not to the slot that channel occupied. When a channel leaves your radio — you scan a different channel QR, replace a channel's key, or switch the modem preset — its conversation is **archived** rather than deleted or merged into whatever takes the slot. + +An archived conversation: + +- stays in the **Channels** list under the name the channel had, below the channels currently on your radio, marked with a history icon; +- keeps its full message history, searchable as usual; +- is read-only — you cannot send, reply, or react, because the channel is no longer on the radio to send on, and it is not offered as a share target; +- comes back automatically if you re-add that exact channel, taking its messages back to the live slot. + +Changing the modem preset archives the channel too: the preset is part of a default channel's identity, so **LongFast** and **MediumFast** are different channels even though neither the name nor the key changed. Changing only the region does not archive anything. + +To remove an archived conversation for good, long-press it in the conversation list and delete it. + ## Messages directs Direct messages (DMs) go to one specific node. When both radios hold each other's public keys, your radio encrypts the message to that node's public key, so no one else on the mesh can read it — not even nodes that share your channel. diff --git a/docs/fr-rFR/user/mqtt.md b/docs/fr-rFR/user/mqtt.md index ec5ea01125..951c3db90f 100644 --- a/docs/fr-rFR/user/mqtt.md +++ b/docs/fr-rFR/user/mqtt.md @@ -2,7 +2,7 @@ title: MQTT parent: User Guide nav_order: 11 -last_updated: 2026-08-30 +last_updated: 2026-09-11 description: Bridge your mesh to the internet — MQTT broker setup, encryption layers, and map reporting. aliases: - mqtt @@ -25,7 +25,7 @@ The MQTT module connects your node to an MQTT broker, allowing: ## How It Works -``` +```text [Your Node] → Radio → [Gateway Node with Wi-Fi] → MQTT Broker → [Remote Gateway] → Radio → [Remote Node] ``` diff --git a/docs/fr-rFR/user/node-metrics.md b/docs/fr-rFR/user/node-metrics.md index 61283e3302..1d376c77be 100644 --- a/docs/fr-rFR/user/node-metrics.md +++ b/docs/fr-rFR/user/node-metrics.md @@ -2,7 +2,7 @@ title: Node Metrics parent: User Guide nav_order: 5 -last_updated: 2026-08-30 +last_updated: 2026-09-09 description: Telemetry dashboards for each mesh node — device health, environment sensors, air quality, signal quality, power, traceroute, and position history. aliases: - metrics @@ -114,7 +114,7 @@ Radio signal quality information: ### Signal Quality Reference -Signal quality is rated from **SNR relative to the active LoRa modem preset's demodulation floor**, not from fixed thresholds — a given SNR means different things on different presets (e.g. −15 dB is fine on LongSlow but unusable on ShortFast). RSSI is shown but is not part of the rating. In the table, _limit_ is the preset's SNR limit. +Signal quality is rated from **SNR relative to the active LoRa modem preset's demodulation floor**, not from fixed thresholds — a given SNR means different things on different presets (e.g. −15 dB is fine on LongSlow but unusable on ShortFast). When RSSI and a noise-floor reading are both available, the app also rates their difference against the preset limit and uses the worse rating. Otherwise, RSSI is display-only. In the table, _limit_ is the preset's SNR limit. | Quality | Criteria | | -------- | ---------------------------------------------------------------- | diff --git a/docs/fr-rFR/user/nodes.md b/docs/fr-rFR/user/nodes.md index c22bf9e44f..5f1e658a68 100644 --- a/docs/fr-rFR/user/nodes.md +++ b/docs/fr-rFR/user/nodes.md @@ -2,7 +2,7 @@ title: Nœuds parent: User Guide nav_order: 4 -last_updated: 2026-09-04 +last_updated: 2026-09-11 description: Browse, filter, and sort mesh nodes — view details, signal quality, roles, and quick actions. aliases: - node-list @@ -74,19 +74,23 @@ Most users should keep the default **Client** role. Consider a different role wh > 💡 **Tip:** The mesh works best when most nodes are **Client** or **Router**. Too many Client Mute nodes reduce mesh resilience; too many Routers in a dense area can cause congestion. A good rule of thumb: one Router per 5–10 Clients in your area. -### Encryption Indicators +### Security indicators -Nodes display encryption status icons next to their name: +Each node carries one security icon beside its name in the node list. Tap it to read what it means, and choose **Show All Meanings** in that dialog for the full legend. The detail screen shows the same state in words, as a **Security** row that opens the same dialog. -| Icon | Meaning | -| ----------- | ------------------------------------------------------------------------------------------------------------------------------------------------- | -| 🔒 Locked | Communication uses PKI (public key infrastructure) — end-to-end encrypted with verified identity | -| 🔓 Unlocked | No public key has been received for this node, so it cannot be direct messaged — use **Request User Info** on the node detail page to ask for one | -| ⚠️ Mismatch | Public key mismatch — the node's key has changed since last seen (investigate before trusting) | +| Icon | Meaning | Shown for | +| --------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------- | +| Person with a shield check (green) | **Verified contact** — you verified this node's key in person by exchanging contact QR codes, so its identity is confirmed. The strongest trust the list shows. Your own connected node carries it too | Any firmware version | +| Nodes icon with a shield check (green) | **Signed node** — this node signs its broadcasts with its identity key, so its identity is consistent over time, but you have not verified it in person. On firmware 2.8 the icon appears from the version alone, before any signed broadcast has been heard | Firmware 2.8 or newer, and any node whose signed broadcast your node has verified | +| 🔒 Closed lock | A public key is on file and matches, so direct messages to this node are encrypted | Firmware before 2.8, or no reported version | +| 🔓 Open lock | No public key has been received for this node, so it cannot be direct messaged — use **Request User Info** on the node detail page to ask for one | Firmware before 2.8, or no reported version | +| ⚠️ Mismatch | **Public key mismatch** — a different key arrived for this node after one was stored. Investigate before trusting | Any firmware version | -> 💡 **Tip:** Direct messages always use PKI, so the radio needs the other node's public key before it can send one. It refuses the send rather than falling back to channel encryption. Keys arrive inside node info, which is why an open lock usually clears itself once that node is heard from properly. If you see a key mismatch warning, the node may have been reset or compromised. +Every node on firmware 2.8 or newer signs its broadcasts, so on that firmware the signed state is the baseline and the locks are not shown. -To clear a mismatch, first confirm through another trusted channel that the key change was intentional — a factory reset causes one. Then touch & hold the node, choose **Remove**, and let the two radios exchange keys again the next time yours hears it. +Direct messages always use public-key encryption, so your node needs the other node's public key before it can send one. It refuses the send rather than falling back to channel encryption. Keys arrive inside node info, which is why an open lock usually clears itself once that node is heard from properly. + +A mismatch never replaces the key you already hold. The app keeps the first key it recorded and refuses the new one, as the firmware does, so a stray or hostile node info cannot silently break encrypted messaging to a contact. To clear a mismatch, first confirm through another trusted channel that the key change was intentional — a factory reset causes one. Then touch & hold the node, choose **Remove**, and let the two nodes exchange keys again the next time yours hears it. ## Quick Actions @@ -108,7 +112,9 @@ radio is connected and running firmware 2.8 or newer — see ## Sharing a Contact -On a node's detail screen, tap **Share Contact** to produce a link and a QR code for that node. From the same dialog, **Write to NFC tag** saves the link to a writable NFC tag that anyone can tap to open. +On a node's detail screen, tap **Share Contact** to produce a link and a QR code for that node. From the same dialog, **Share link** opens the Android share sheet (on desktop it copies the link instead), **Write to NFC tag** saves it to a writable NFC tag, and **Copy** puts it on the clipboard. While that dialog is open and in front of you, the phone also offers the same link to any NFC reader, so someone can take the contact by tapping their phone against yours with no tag involved. + +Sharing your own contact this way marks it as verified in person, so whoever imports it sees the **Verified contact** icon rather than the signed one. Relaying someone else's contact passes on only what your app had already recorded about them. To add someone else's contact, use the import button on the node list and choose **Scan Shared Contact QR Code**, **Scan Shared Contact NFC**, or **Input Shared Contact URL**. The app asks you to confirm with **Import Shared Contact?**, and warns you when the contact is one you already have. @@ -120,14 +126,16 @@ Type in the search field to filter nodes by name or short name. The filter updat ### Filter Toggles -| Filtre | Description | -| --------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| **Hide offline nodes** | Show only nodes heard within the last 2 hours | -| **Only show direct nodes** | Show only nodes your radio heard directly, with no relay in between | -| **Include unknown** | Show nodes that haven't sent user info yet. **On by default**, so a node heard before its info arrives stays visible; these carry a badge marking them incomplete, and cannot be direct messaged until their user info brings a public key | -| **Exclude infrastructure** | Hide infrastructure-role nodes (Router, Router Late, Client Base, and legacy Repeater nodes) and any node that cannot be messaged, whatever its role | -| **Exclude MQTT** | Hide nodes heard only via MQTT internet bridge | -| **Only show ignored Nodes** | Replace the list with the nodes you have ignored. Every other node is hidden while this is on, and a banner appears at the top of the list to take you back | +| Filtre | Description | +| --------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **Hide offline nodes** | Show only nodes heard within the last 2 hours | +| **Only show direct nodes** | Show only nodes your radio heard directly, with no relay in between | +| **Include unknown** | Show nodes that haven't sent user info yet. **On by default**, so a node heard before its info arrives stays visible; these carry a badge marking them incomplete, and cannot be direct messaged until their user info brings a public key | +| **Exclude infrastructure** | Hide infrastructure-role nodes (Router, Router Late, Client Base, and legacy Repeater nodes) and any node that cannot be messaged, whatever its role | +| **Exclude MQTT** | Hide nodes heard only via MQTT internet bridge | +| **Signed only** | Show only nodes whose signed broadcasts your node has actually heard and verified. Stricter than the icon: a node on 2.8 shows as signed by its firmware version before any signed broadcast arrives, and a contact verified in person on older firmware is not signed at all | +| **Encrypted only** | Show nodes with a matching public key on file, the key an encrypted direct message needs. A node with a key mismatch is excluded | +| **Only show ignored Nodes** | Replace the list with the nodes you have ignored. Every other node is hidden while this is on, and a banner appears at the top of the list to take you back | ### Sort Options @@ -149,6 +157,8 @@ Tap the hop-histogram icon in the node list's app bar to open a bar chart of how Tapping a node opens the detail view with comprehensive information. See [Node Metrics](node-metrics) for full details on metrics and telemetry. +Signal quality is rated against your modem preset. The same SNR can be good on a long-range preset and poor on a faster one. Traceroute and neighbor-info SNR colors use that preset too. RSSI text has its own strength colors; it affects the quality rating only when a noise-floor reading is also available. + The Details card carries the node's short name, role, IDs, last heard time, hops away, uptime, and its SNR and RSSI: ![Node detail section](../../assets/screenshots/nodes_detail_section.png) diff --git a/docs/fr-rFR/user/settings-module-admin.md b/docs/fr-rFR/user/settings-module-admin.md index 085a7a7902..e20ed54729 100644 --- a/docs/fr-rFR/user/settings-module-admin.md +++ b/docs/fr-rFR/user/settings-module-admin.md @@ -2,7 +2,7 @@ title: Settings — Modules & Admin parent: User Guide nav_order: 8 -last_updated: 2026-08-30 +last_updated: 2026-09-16 description: Configure optional feature modules (MQTT, telemetry, canned messages, TAK, and more) and perform device administration. aliases: - modules @@ -333,7 +333,7 @@ appear only when your own node is selected: ### A propros -**Settings → About** carries the app's own identity rather than the radio's: +**Settings → About** carries the app's own identity rather than the node's: Three sections: @@ -341,7 +341,11 @@ Three sections: - **Apps** — opens with **Need Hardware?**, a rotating carousel of popular devices that links out to where to buy one. It also lists the GitHub repository, the running app version, and **Acknowledgements** (see the next section). -- **Project information** — links to the website and to this documentation. +- **Project information** — links to the website, to this documentation, and to the GNU GPL v3 + license the app is released under. + +The footer states the copyright and that the app is free software with no warranty, which you +may redistribute under the same license. ### Remerciements diff --git a/docs/fr-rFR/user/settings-radio-user.md b/docs/fr-rFR/user/settings-radio-user.md index e66d5043fd..eb4719523f 100644 --- a/docs/fr-rFR/user/settings-radio-user.md +++ b/docs/fr-rFR/user/settings-radio-user.md @@ -2,7 +2,7 @@ title: Settings — Radio & User parent: User Guide nav_order: 7 -last_updated: 2026-09-04 +last_updated: 2026-09-11 description: Configure your radio hardware, LoRa presets, user profile, position sharing, power management, and security. aliases: - paramètres @@ -51,7 +51,7 @@ The footer appears as soon as you change something. **Discard** throws the chang The status message is saved with the same **Save**, but it never reboots the node — and, like the rest of this screen, it can be edited on a remote node you administer. For your own radio there is a shortcut while it is connected: touch & hold your node in the [node list](nodes.md) and choose -**Update status**. Older firmware and a disconnected radio have no shortcut — the field above is +**Update status**. Older firmware and a disconnected node have no shortcut — the field itself is still the way in. ## Configuration @@ -77,13 +77,13 @@ On **Settings → LoRa**. | Setting | Description | Par défaut | | --------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------- | -| Région | Regulatory region for frequency bands. You must set this before transmitting | Unset (must configure) | +| Région | Regulatory region for frequency bands. You must set this before transmitting. On firmware 2.8 or newer, the first region set also creates the node's identity key and gives it a new node number | Unset (must configure) | | Préréglages | Speed/range tradeoff | LongFast | | Nombre de sauts | Maximum retransmit hops | 3 | | Puissance d'émission | Transmission power (dBm); 0 = max allowed for region | 0 (region max) | | Remplacer la fréquence | Overrides the computed operating frequency outright (MHz). It does not offset the calculated value — leave at 0 unless you know you need a specific frequency | 0 (use calculated) | | Utiliser un préréglage | On by default. Turn it off to set Spread Factor, Coding Rate and Bandwidth by hand instead of taking them from the modem preset | On | -| Facteur de propagation | Manual mode only: 7–12. Higher spreads further but slower | From preset | +| Facteur de propagation | Manual mode only: 5–12. Higher spreads further but slower. On SX127x (RF95) radios the firmware does not accept 5 or 6 and uses 11 instead | From preset | | Taux de codage | Manual mode only: 5–8. More redundancy costs airtime | From preset | | Bande Passante | Manual mode only: the channel bandwidth in kHz, typed in directly. On the 2.4 GHz region the app offers a list of the bandwidths your radio supports instead, and a stored value that is not on that list shows as _Unsupported_ and blocks saving until you pick a supported one | From preset | | Slot de fréquence | Which slot within the region's band to use. 0 derives it from the primary channel name | 0 (automatic) | @@ -111,13 +111,13 @@ The Lite, Narrow, Medium Turbo, and Tiny presets need firmware 2.8 or newer — | Short Slow | ~5 km | 6.25 kbps | −10 dB | Suburban short-range; moderate building density | | Medium Fast | ~5 km | 3.52 kbps | −12.5 dB | Suburban areas; moderate building density | | Medium Slow | ~8 km | 1.95 kbps | −15 dB | Suburban/rural; moderate range with slower speed | -| Long Turbo | ~10 km | 1.34 kbps | −12.5 dB | Similar range to Long Fast but with 500 kHz bandwidth; faster throughput | +| Long Turbo | ~10 km | 1.34 kbps | −17.5 dB | Similar range to Long Fast but with 500 kHz bandwidth; faster throughput | | Long Fast | ~10 km | 1.1 kbps | −17.5 dB | **General use (default)** — balanced range and speed | | Long Moderate | ~20 km | 0.34 kbps | −17.5 dB | Rural with some terrain; occasional use | | Lite Fast | ~5 km | 1.76 kbps | −12.5 dB | EU 866 MHz SRD band (125 kHz BW); comparable to Medium Fast | | Lite Slow | ~10 km | 0.98 kbps | −15 dB | EU 866 MHz SRD band (125 kHz BW); comparable to Long Fast | -| Narrow Fast | ~5 km | 2.28 kbps | −10 dB | EU 868 MHz band (62.5 kHz BW); avoids interference with other devices | -| Narrow Slow | ~10 km | 1.30 kbps | −12.5 dB | EU 868 MHz band (62.5 kHz BW); comparable to Long Fast | +| Narrow Fast | ~5 km | 2.28 kbps | −7.5 dB | EU 868 MHz band (62.5 kHz BW); avoids interference with other devices | +| Narrow Slow | ~10 km | 1.30 kbps | −10 dB | EU 868 MHz band (62.5 kHz BW); comparable to Long Fast | | Medium Turbo | ~5 km | 7.0 kbps | −12.5 dB | Like Medium Fast but with 500 kHz bandwidth; not legal in every region. Needs firmware 2.8 or newer | | Tiny Fast | ~10 km | 0.68 kbps | −7.5 dB | Amateur bands that cap occupied bandwidth; these presets use 15.6 kHz. Needs firmware 2.8 or newer, an SX126x or SX127x radio, and a TCXO of ±5 ppm or better | | Tiny Slow | ~20 km | 0.33 kbps | −10 dB | Same band restrictions as Tiny Fast, longer range. Same firmware, radio, and TCXO requirements | diff --git a/docs/fr-rFR/user/signal-meter.md b/docs/fr-rFR/user/signal-meter.md index f2f121a684..f66f81f52a 100644 --- a/docs/fr-rFR/user/signal-meter.md +++ b/docs/fr-rFR/user/signal-meter.md @@ -2,7 +2,7 @@ title: How the Meshtastic Signal Meter Works parent: User Guide nav_order: 15 -last_updated: 2026-08-30 +last_updated: 2026-09-09 description: How the signal meter rates quality from SNR relative to the LoRa modem preset — spread spectrum, presets, and what the bars really mean. aliases: - signal @@ -32,7 +32,7 @@ Each modem preset has an SNR limit: the lowest SNR at which that preset can stil ## Rating Signal Quality -The app rates signal quality (None, Bad, Fair, or Good) from SNR alone, measured against the active preset's SNR limit. It does not factor in RSSI: without knowing the local noise floor, RSSI alone cannot say whether a signal is decodable. RSSI is still available — on the node detail screen and in the metrics charts. +The app rates signal quality (None, Bad, Fair, or Good) from SNR relative to the active preset's SNR limit. When RSSI and a noise-floor reading are both available, it also rates the difference between them against that limit and uses the worse rating. Otherwise, it uses SNR alone. Because the rating is relative to the preset, the same SNR rates differently on different presets. An SNR of −16 dB rates Good on Long Fast (SNR limit −17.5 dB) but None on Short Fast (SNR limit −7.5 dB). Let `limit` be the active preset's SNR limit: @@ -45,7 +45,7 @@ Because the rating is relative to the preset, the same SNR rates differently on The icon never goes blank, so count the bars carefully: a single bar means None, not a weak but usable link, and Good is a solid wedge rather than a set of bars. A gray three-bar icon labeled Unknown is a different state again — the packet carried no SNR measurement at all, which is not the same as measuring one and finding it too weak. -> ℹ️ **Note:** Traceroute hop colors use fixed thresholds (−7 dB / −15 dB); the per-node signal meter uses the preset-relative rating instead. +> ℹ️ **Note:** Traceroute and neighbor-info SNR colors use the active preset's limits too. RSSI text has separate strength colors: green above −115 dBm, yellow above −120 dBm, orange above −126 dBm, and red at or below −126 dBm. ## Diagnosing Local Interference diff --git a/docs/fr-rFR/user/tak.md b/docs/fr-rFR/user/tak.md index c020af3a36..ed50f72cc9 100644 --- a/docs/fr-rFR/user/tak.md +++ b/docs/fr-rFR/user/tak.md @@ -2,7 +2,7 @@ title: TAK Integration parent: User Guide nav_order: 10 -last_updated: 2026-08-30 +last_updated: 2026-09-11 description: Interoperate with ATAK and WinTAK — CoT position sharing, TAK roles, and plugin setup. aliases: - tak @@ -36,7 +36,7 @@ The TAK module allows Meshtastic nodes to: > ⚠️ **Warning:** The old **Meshtastic ATAK Plugin** is no longer part of this path and cannot > work. It bridged through the cross-process AIDL API, which was removed in app 2.8.0; the mesh > service is now in-process only. Do not install it. Interop today runs over the app's own local -> TAK server plus the Mesh to CoT Converter, both described below, with stock ATAK/iTAK/WinTAK. +> TAK server plus the Mesh to CoT Converter, both described in the following sections, with stock ATAK/iTAK/WinTAK. ### Configuration diff --git a/docs/fr-rFR/user/translate.md b/docs/fr-rFR/user/translate.md index 1eefb0c0f9..9965c9c73f 100644 --- a/docs/fr-rFR/user/translate.md +++ b/docs/fr-rFR/user/translate.md @@ -2,7 +2,7 @@ title: Translate the App parent: User Guide nav_order: 17 -last_updated: 2026-08-30 +last_updated: 2026-09-11 description: How the app and its documentation are translated via Crowdin, and guidelines for contributing translations. aliases: - translate @@ -45,7 +45,7 @@ Si votre langue n'est pas encore listée sur Crowdin : L'application Android utilise des **Ressources Multiplateforme** pour tous les textes visibles par l'utilisateur: -``` +```text core/resources/src/commonMain/composeResources/ ├── values/ ← English (default) │ └── strings.xml @@ -58,7 +58,7 @@ core/resources/src/commonMain/composeResources/ La documentation dans l'application suit un modèle similaire dans `docs/`: -``` +```text docs/ ├── en/user/ ← English source (default) │ ├── onboarding.md diff --git a/docs/ga-rIE/user/desktop.md b/docs/ga-rIE/user/desktop.md index efca3c03c9..2ec0d98037 100644 --- a/docs/ga-rIE/user/desktop.md +++ b/docs/ga-rIE/user/desktop.md @@ -2,7 +2,7 @@ title: Desktop App parent: User Guide nav_order: 14 -last_updated: 2026-08-30 +last_updated: 2026-09-11 description: Install and use the Meshtastic Desktop app on Linux, macOS, and Windows — connections, feature parity, and keyboard shortcuts. aliases: - desktop @@ -58,20 +58,20 @@ Bluetooth Low Energy is supported on desktop via the [Kable](https://github.com/ ## Feature Parity -| Feature | Android | Desktop | Notes | -| ----------------------------------------------------- | ------- | ------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Messaging | ✓ | ✓ | Full parity | -| Node List | ✓ | ✓ | Full parity | -| Map | ✓ | ✓ | Interactive MapLibre map, with base map and overlay pickers and custom tile sources. No offline downloads or local `.mbtiles` archives | -| Map layers (`.kml`/`.kmz`/GeoJSON) | ✓ | ✓ | Same layer store and sheet as Android; imported files draw on the desktop map | -| Site Planner | ✓ | ✓\* | \*Opens in your browser on desktop; the estimate is not drawn on the desktop map | -| Settings | ✓ | ✓ | Full parity | -| Bluetooth (BLE) | ✓ | ✓ | Via Kable on desktop | -| Firmware Update | ✓ | ✓ | In-app USB, BLE, and Wi-Fi (ESP32) update work the same as Android. The USB maintenance flow — nRF52/RP2040 factory erase and bootloader upgrade — is Android-only | -| Notifications | ✓ | ✓ | Native OS notifications | -| Widgets | ✓ | ✗ | Android-only | -| AI Assistant (Chirpy) | ✓\* | ✗ | Google flavor Android only | -| App Functions (system AI) | ✓† | ✗ | Google flavor Android only | +| Feature | Android | Desktop | Notes | +| ----------------------------------------------------- | ------- | ------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Messaging | ✓ | ✓ | Full parity | +| Node List | ✓ | ✓ | Full parity | +| Map | ✓ | ✓ | Interactive MapLibre map, with base map and overlay pickers and custom tile sources. Offline terrain (hillshade and contours) can be downloaded; offline base-map packs and local `.mbtiles` archives cannot | +| Map layers (`.kml`/`.kmz`/GeoJSON) | ✓ | ✓ | Same layer store and sheet as Android; imported files draw on the desktop map | +| Site Planner | ✓ | ✓\* | \*Opens in your browser on desktop; the estimate is not drawn on the desktop map | +| Settings | ✓ | ✓ | Full parity | +| Bluetooth (BLE) | ✓ | ✓ | Via Kable on desktop | +| Firmware Update | ✓ | ✓ | In-app USB, BLE, and Wi-Fi (ESP32) update work the same as Android. The USB maintenance flow — nRF52/RP2040 factory erase and bootloader upgrade — is Android-only | +| Notifications | ✓ | ✓ | Native OS notifications | +| Widgets | ✓ | ✗ | Android-only | +| AI Assistant (Chirpy) | ✓\* | ✗ | Google flavor Android only | +| App Functions (system AI) | ✓† | ✗ | Google flavor Android only | \*Chirpy AI requires Android 14+ on Google flavor builds with supported hardware. @@ -122,7 +122,7 @@ Individual doc pages render with full formatting: ## Building from Source -```bash +```shell git clone https://github.com/meshtastic/Meshtastic-Android.git cd Meshtastic-Android ./gradlew :desktopApp:run @@ -135,7 +135,7 @@ Requirements: ## Known Limitations -- Offline tile downloads and local `.mbtiles` archives are not available on desktop. +- Offline base-map downloads and local `.mbtiles` archives are not available on desktop. Offline terrain is — see [Map & Waypoints](map-and-waypoints). - `.kml`/`.kmz`/GeoJSON layer import works — see [Map & Waypoints](map-and-waypoints#map-layers). Site Planner opens in your browser rather than in the app; to bring its coverage estimate onto the map, click the transmitter pin diff --git a/docs/ga-rIE/user/discovery.md b/docs/ga-rIE/user/discovery.md index 5459fce098..3c9909287c 100644 --- a/docs/ga-rIE/user/discovery.md +++ b/docs/ga-rIE/user/discovery.md @@ -2,7 +2,7 @@ title: Local Mesh Discovery parent: User Guide nav_order: 12 -last_updated: 2026-08-30 +last_updated: 2026-09-11 description: Explore your mesh network — the Local Mesh Discovery scanner, traceroute paths, neighbor maps, and node discovery tools. aliases: - discovery @@ -97,7 +97,7 @@ Mesh Beacon lets nodes invite others to join their mesh. A beaconing node period Configure it under **Settings → Module configuration → Mesh Beacon**. The entry appears only on radios running firmware 2.8.0 or newer. A read-only **Region** row at the top of the screen shows the region the beacon advertises: that region, and the preset, are always the ones the radio itself uses, so a beacon cannot invite anyone onto settings your radio is not running. - **Listen for beacons** — receive invitations broadcast by other nodes. -- **Broadcast a beacon** — periodically advertise this mesh to nearby nodes, with an optional **Beacon message** of up to 100 bytes, a **Broadcast interval** picked from fixed intervals between 1 hour and 72 hours, and an **Offered channel** chosen from your radio's own channels. The offered channel is required, and defaults to your primary channel. +- **Broadcast a beacon** — periodically advertise this mesh to nearby nodes, with an optional **Beacon message** of up to 100 bytes, a **Broadcast interval** picked from fixed intervals between 1 hour and 72 hours, and an **Offered channel** chosen from your radio's own channels. The offered channel is required, and defaults to your primary channel. Over remote admin the picker offers the primary channel only. - **Broadcast targets** — optional extra destinations beyond the offered channel. **Add target** appends a row; each row picks a **Channel** and a **Transmit preset**, and **Remove target** deletes it. With no targets, the beacon goes out on the offered channel alone. Two conditions block beacon setup: diff --git a/docs/ga-rIE/user/help-and-docs.md b/docs/ga-rIE/user/help-and-docs.md index f306cde7f7..c83898cea3 100644 --- a/docs/ga-rIE/user/help-and-docs.md +++ b/docs/ga-rIE/user/help-and-docs.md @@ -2,7 +2,7 @@ title: Help & In-App Docs parent: User Guide nav_order: 21 -last_updated: 2026-08-30 +last_updated: 2026-09-11 description: Browse this documentation inside the app, search it, and ask Chirpy — the on-device AI assistant — questions about Meshtastic. aliases: - help @@ -23,7 +23,7 @@ The docs browser lists every user-guide page. Tap a page to read it; images and ### Search -The search box sits at the top of the docs browser. Type in it to filter pages by title and keywords — results update as you type, and the **Clear search** button (✕) empties the box. +Tap the search field at the top of the docs browser to open a full-screen search view. Type to filter pages by title and keywords — results update as you type, under the same field. The back arrow, or the keyboard's search action, closes the view. The field hides while you scroll down the page list and comes back when you scroll up. ![Searching the in-app documentation](../../assets/screenshots/docs-browser_search.png) diff --git a/docs/ga-rIE/user/map-and-waypoints.md b/docs/ga-rIE/user/map-and-waypoints.md index c5c856d924..ca7948e013 100644 --- a/docs/ga-rIE/user/map-and-waypoints.md +++ b/docs/ga-rIE/user/map-and-waypoints.md @@ -2,7 +2,7 @@ title: Map & Waypoints parent: User Guide nav_order: 6 -last_updated: 2026-09-01 +last_updated: 2026-09-11 description: View node positions on the map, create and share waypoints, manage map layers and Site Planner, and control position sharing and privacy. aliases: - map @@ -44,7 +44,7 @@ The floating toolbar provides quick access to the compass, the map type and laye ### Filtering the Map -Tap the filter button in the floating toolbar to open **Filter map**. **Display** controls what is drawn: **Only Favorites**, **Show Waypoints**, **Show Precision Circles**, and a slider that hides nodes not heard from recently. **Node roles** is a chip per device role, plus **All** to show every role; a selected chip means that role is shown. **Nodes** narrows the set further with **Hide offline nodes**, **Only show direct nodes**, **Exclude MQTT**, **Show ignored nodes**, and **Include unknown**. +Tap the filter button in the floating toolbar to open **Filter map**. **Display** controls what is drawn: **Only Favorites**, **Show Waypoints**, **Show Precision Circles**, and a slider that hides nodes not heard from recently. **Node roles** is a chip per device role, plus **All** to show every role; a selected chip means that role is shown. **Nodes** narrows the set further with **Hide offline nodes**, **Only show direct nodes**, **Signed only**, **Encrypted only**, **Exclude MQTT**, **Show ignored nodes**, and **Include unknown** — the same rules as the node list's [filter toggles](nodes). A dot on the filter button means at least one filter is hiding something — check it before concluding the mesh is quiet. Turning **Show Waypoints** off hides every waypoint, including your own. **Show ignored nodes** adds them to the map rather than showing only them — unlike the node list's **Only show ignored Nodes**. @@ -147,8 +147,9 @@ picker, all three offer the same raster base maps: Overlays can be toggled on top of any base map, from the layers sheet: - **Weather radar** — NOAA NEXRAD reflectivity (US coverage) -- **Hillshade** — terrain relief, on **F-Droid** and **Desktop** only. Useful for understanding why a - link fails, since LoRa range is limited by terrain +- **Hillshade** — terrain relief from an online tile source, on **F-Droid** and **Desktop** only; Google + Play draws hillshade from downloaded offline terrain instead. Useful for understanding why a link + fails, since LoRa range is limited by terrain ### Adding your own tile source @@ -156,7 +157,7 @@ Any XYZ tile endpoint can be added as a base map, on every flavor and on desktop Tile Sources** at the foot of the base map picker and paste a URL template using `{z}`, `{x}` and `{y}` — plus `{s}` if the provider uses rotating subdomains. A national mapping service, for example: -``` +```text https://wmts.geo.admin.ch/1.0.0/ch.swisstopo.pixelkarte-farbe/default/current/3857/{z}/{x}/{y}.jpeg ``` @@ -173,7 +174,18 @@ On **Google Play**, the layers sheet's **Offline Manager** section downloads its — water, roads and administrative boundaries extracted from the public Protomaps basemap dataset, drawn directly as map shapes rather than raster tiles. Frame the area you want, tap **Start Download**, and once it finishes flip **Show on map** to draw it (it's off by default so it never surprises you by covering up the -live map). You can still import a pre-made `.mbtiles` archive here too, as before. **Desktop** has neither. +live map). You can still import a pre-made `.mbtiles` archive here too, as before. **Desktop** has neither, though +the offline terrain described next does work there. + +### Offline terrain + +Hillshade and elevation contours can be downloaded for offline use as well, so a downloaded area still +shows why a link fails when there is no network. The terrain data comes from Mapterhorn and is credited on +the map. On **Google Play**, each downloaded region in the **Offline Manager** list has a **Download Terrain** +button and **Hillshade** and **Contours** switches; terrain attaches to that region and shares its storage limit, and an area +too large for terrain asks you to zoom in or pick a smaller region. On **F-Droid** and **Desktop**, the +layers sheet has an **Offline Terrain** section of its own: frame the area, tap **Start Download**, and the +hillshade and contours draw from the downloaded tiles from then on. ### Going offline @@ -181,8 +193,8 @@ An **Offline** pill appears over the map whenever the device has no network conn builds, if you've imported a local `.mbtiles` archive, the map switches to it automatically the moment the network drops — no toggle to remember — and switches back once you're reconnected, as long as you haven't picked a different base map yourself in between. On **F-Droid**, a downloaded offline area keeps rendering on -its own; the pill is purely informational there. **Desktop** has no offline downloads at all yet, so the pill -is informational there too. +its own; the pill is purely informational there. **Desktop** downloads offline terrain but no base-map +packs, so the pill is informational there too. ## Related Topics diff --git a/docs/ga-rIE/user/messages-and-channels.md b/docs/ga-rIE/user/messages-and-channels.md index 345b74f76f..2323d29a12 100644 --- a/docs/ga-rIE/user/messages-and-channels.md +++ b/docs/ga-rIE/user/messages-and-channels.md @@ -2,7 +2,7 @@ title: Messages & Channels parent: User Guide nav_order: 3 -last_updated: 2026-08-30 +last_updated: 2026-09-14 description: Send and receive messages, manage channels, configure encryption, search conversations, and use quick chat, reactions, and message actions. aliases: - channels @@ -50,6 +50,21 @@ Key length alone does not change the icon: a 128-bit key and a 256-bit key both Tapping an existing channel opens the same editor, where you can change the name, the PSK, MQTT uplink and downlink, and position precision. Every edit on this screen — adding, editing, deleting, or dragging a channel into a new order — waits on **Send** the same way. +### Archived Channels + +A conversation belongs to the channel it was held on, not to the slot that channel occupied. When a channel leaves your radio — you scan a different channel QR, replace a channel's key, or switch the modem preset — its conversation is **archived** rather than deleted or merged into whatever takes the slot. + +An archived conversation: + +- stays in the **Channels** list under the name the channel had, below the channels currently on your radio, marked with a history icon; +- keeps its full message history, searchable as usual; +- is read-only — you cannot send, reply, or react, because the channel is no longer on the radio to send on, and it is not offered as a share target; +- comes back automatically if you re-add that exact channel, taking its messages back to the live slot. + +Changing the modem preset archives the channel too: the preset is part of a default channel's identity, so **LongFast** and **MediumFast** are different channels even though neither the name nor the key changed. Changing only the region does not archive anything. + +To remove an archived conversation for good, long-press it in the conversation list and delete it. + ## Direct Messages Direct messages (DMs) go to one specific node. When both radios hold each other's public keys, your radio encrypts the message to that node's public key, so no one else on the mesh can read it — not even nodes that share your channel. diff --git a/docs/ga-rIE/user/mqtt.md b/docs/ga-rIE/user/mqtt.md index 02d731ae0e..f15da334a3 100644 --- a/docs/ga-rIE/user/mqtt.md +++ b/docs/ga-rIE/user/mqtt.md @@ -2,7 +2,7 @@ title: MQTT parent: User Guide nav_order: 11 -last_updated: 2026-08-30 +last_updated: 2026-09-11 description: Bridge your mesh to the internet — MQTT broker setup, encryption layers, and map reporting. aliases: - mqtt @@ -25,7 +25,7 @@ The MQTT module connects your node to an MQTT broker, allowing: ## How It Works -``` +```text [Your Node] → Radio → [Gateway Node with Wi-Fi] → MQTT Broker → [Remote Gateway] → Radio → [Remote Node] ``` diff --git a/docs/ga-rIE/user/node-metrics.md b/docs/ga-rIE/user/node-metrics.md index 1b55e77a45..9feac9a889 100644 --- a/docs/ga-rIE/user/node-metrics.md +++ b/docs/ga-rIE/user/node-metrics.md @@ -2,7 +2,7 @@ title: Node Metrics parent: User Guide nav_order: 5 -last_updated: 2026-08-30 +last_updated: 2026-09-09 description: Telemetry dashboards for each mesh node — device health, environment sensors, air quality, signal quality, power, traceroute, and position history. aliases: - metrics @@ -114,7 +114,7 @@ Radio signal quality information: ### Signal Quality Reference -Signal quality is rated from **SNR relative to the active LoRa modem preset's demodulation floor**, not from fixed thresholds — a given SNR means different things on different presets (e.g. −15 dB is fine on LongSlow but unusable on ShortFast). RSSI is shown but is not part of the rating. In the table, _limit_ is the preset's SNR limit. +Signal quality is rated from **SNR relative to the active LoRa modem preset's demodulation floor**, not from fixed thresholds — a given SNR means different things on different presets (e.g. −15 dB is fine on LongSlow but unusable on ShortFast). When RSSI and a noise-floor reading are both available, the app also rates their difference against the preset limit and uses the worse rating. Otherwise, RSSI is display-only. In the table, _limit_ is the preset's SNR limit. | Quality | Criteria | | ---------------- | ---------------------------------------------------------------- | diff --git a/docs/ga-rIE/user/nodes.md b/docs/ga-rIE/user/nodes.md index a0ad5cf230..b49f8e2588 100644 --- a/docs/ga-rIE/user/nodes.md +++ b/docs/ga-rIE/user/nodes.md @@ -2,7 +2,7 @@ title: Nodes parent: User Guide nav_order: 4 -last_updated: 2026-09-04 +last_updated: 2026-09-11 description: Browse, filter, and sort mesh nodes — view details, signal quality, roles, and quick actions. aliases: - node-list @@ -74,19 +74,23 @@ Most users should keep the default **Client** role. Consider a different role wh > 💡 **Tip:** The mesh works best when most nodes are **Client** or **Router**. Too many Client Mute nodes reduce mesh resilience; too many Routers in a dense area can cause congestion. A good rule of thumb: one Router per 5–10 Clients in your area. -### Encryption Indicators +### Security indicators -Nodes display encryption status icons next to their name: +Each node carries one security icon beside its name in the node list. Tap it to read what it means, and choose **Show All Meanings** in that dialog for the full legend. The detail screen shows the same state in words, as a **Security** row that opens the same dialog. -| Icon | Meaning | -| ----------- | ------------------------------------------------------------------------------------------------------------------------------------------------- | -| 🔒 Locked | Communication uses PKI (public key infrastructure) — end-to-end encrypted with verified identity | -| 🔓 Unlocked | No public key has been received for this node, so it cannot be direct messaged — use **Request User Info** on the node detail page to ask for one | -| ⚠️ Mismatch | Public key mismatch — the node's key has changed since last seen (investigate before trusting) | +| Icon | Meaning | Shown for | +| --------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------- | +| Person with a shield check (green) | **Verified contact** — you verified this node's key in person by exchanging contact QR codes, so its identity is confirmed. The strongest trust the list shows. Your own connected node carries it too | Any firmware version | +| Nodes icon with a shield check (green) | **Signed node** — this node signs its broadcasts with its identity key, so its identity is consistent over time, but you have not verified it in person. On firmware 2.8 the icon appears from the version alone, before any signed broadcast has been heard | Firmware 2.8 or newer, and any node whose signed broadcast your node has verified | +| 🔒 Closed lock | A public key is on file and matches, so direct messages to this node are encrypted | Firmware before 2.8, or no reported version | +| 🔓 Open lock | No public key has been received for this node, so it cannot be direct messaged — use **Request User Info** on the node detail page to ask for one | Firmware before 2.8, or no reported version | +| ⚠️ Mismatch | **Public key mismatch** — a different key arrived for this node after one was stored. Investigate before trusting | Any firmware version | -> 💡 **Tip:** Direct messages always use PKI, so the radio needs the other node's public key before it can send one. It refuses the send rather than falling back to channel encryption. Keys arrive inside node info, which is why an open lock usually clears itself once that node is heard from properly. If you see a key mismatch warning, the node may have been reset or compromised. +Every node on firmware 2.8 or newer signs its broadcasts, so on that firmware the signed state is the baseline and the locks are not shown. -To clear a mismatch, first confirm through another trusted channel that the key change was intentional — a factory reset causes one. Then touch & hold the node, choose **Remove**, and let the two radios exchange keys again the next time yours hears it. +Direct messages always use public-key encryption, so your node needs the other node's public key before it can send one. It refuses the send rather than falling back to channel encryption. Keys arrive inside node info, which is why an open lock usually clears itself once that node is heard from properly. + +A mismatch never replaces the key you already hold. The app keeps the first key it recorded and refuses the new one, as the firmware does, so a stray or hostile node info cannot silently break encrypted messaging to a contact. To clear a mismatch, first confirm through another trusted channel that the key change was intentional — a factory reset causes one. Then touch & hold the node, choose **Remove**, and let the two nodes exchange keys again the next time yours hears it. ## Quick Actions @@ -108,7 +112,9 @@ radio is connected and running firmware 2.8 or newer — see ## Sharing a Contact -On a node's detail screen, tap **Share Contact** to produce a link and a QR code for that node. From the same dialog, **Write to NFC tag** saves the link to a writable NFC tag that anyone can tap to open. +On a node's detail screen, tap **Share Contact** to produce a link and a QR code for that node. From the same dialog, **Share link** opens the Android share sheet (on desktop it copies the link instead), **Write to NFC tag** saves it to a writable NFC tag, and **Copy** puts it on the clipboard. While that dialog is open and in front of you, the phone also offers the same link to any NFC reader, so someone can take the contact by tapping their phone against yours with no tag involved. + +Sharing your own contact this way marks it as verified in person, so whoever imports it sees the **Verified contact** icon rather than the signed one. Relaying someone else's contact passes on only what your app had already recorded about them. To add someone else's contact, use the import button on the node list and choose **Scan Shared Contact QR Code**, **Scan Shared Contact NFC**, or **Input Shared Contact URL**. The app asks you to confirm with **Import Shared Contact?**, and warns you when the contact is one you already have. @@ -120,14 +126,16 @@ Type in the search field to filter nodes by name or short name. The filter updat ### Filter Toggles -| Scagaire | Cur síos | -| --------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| **Hide offline nodes** | Show only nodes heard within the last 2 hours | -| **Only show direct nodes** | Show only nodes your radio heard directly, with no relay in between | -| **Include unknown** | Show nodes that haven't sent user info yet. **On by default**, so a node heard before its info arrives stays visible; these carry a badge marking them incomplete, and cannot be direct messaged until their user info brings a public key | -| **Exclude infrastructure** | Hide infrastructure-role nodes (Router, Router Late, Client Base, and legacy Repeater nodes) and any node that cannot be messaged, whatever its role | -| **Exclude MQTT** | Hide nodes heard only via MQTT internet bridge | -| **Only show ignored Nodes** | Replace the list with the nodes you have ignored. Every other node is hidden while this is on, and a banner appears at the top of the list to take you back | +| Scagaire | Cur síos | +| --------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **Hide offline nodes** | Show only nodes heard within the last 2 hours | +| **Only show direct nodes** | Show only nodes your radio heard directly, with no relay in between | +| **Include unknown** | Show nodes that haven't sent user info yet. **On by default**, so a node heard before its info arrives stays visible; these carry a badge marking them incomplete, and cannot be direct messaged until their user info brings a public key | +| **Exclude infrastructure** | Hide infrastructure-role nodes (Router, Router Late, Client Base, and legacy Repeater nodes) and any node that cannot be messaged, whatever its role | +| **Exclude MQTT** | Hide nodes heard only via MQTT internet bridge | +| **Signed only** | Show only nodes whose signed broadcasts your node has actually heard and verified. Stricter than the icon: a node on 2.8 shows as signed by its firmware version before any signed broadcast arrives, and a contact verified in person on older firmware is not signed at all | +| **Encrypted only** | Show nodes with a matching public key on file, the key an encrypted direct message needs. A node with a key mismatch is excluded | +| **Only show ignored Nodes** | Replace the list with the nodes you have ignored. Every other node is hidden while this is on, and a banner appears at the top of the list to take you back | ### Sort Options @@ -149,6 +157,8 @@ Tap the hop-histogram icon in the node list's app bar to open a bar chart of how Tapping a node opens the detail view with comprehensive information. See [Node Metrics](node-metrics) for full details on metrics and telemetry. +Signal quality is rated against your modem preset. The same SNR can be good on a long-range preset and poor on a faster one. Traceroute and neighbor-info SNR colors use that preset too. RSSI text has its own strength colors; it affects the quality rating only when a noise-floor reading is also available. + The Details card carries the node's short name, role, IDs, last heard time, hops away, uptime, and its SNR and RSSI: ![Node detail section](../../assets/screenshots/nodes_detail_section.png) diff --git a/docs/ga-rIE/user/settings-module-admin.md b/docs/ga-rIE/user/settings-module-admin.md index 6d9d4cfc3b..f0f1e5a897 100644 --- a/docs/ga-rIE/user/settings-module-admin.md +++ b/docs/ga-rIE/user/settings-module-admin.md @@ -2,7 +2,7 @@ title: Settings — Modules & Admin parent: User Guide nav_order: 8 -last_updated: 2026-08-30 +last_updated: 2026-09-16 description: Configure optional feature modules (MQTT, telemetry, canned messages, TAK, and more) and perform device administration. aliases: - modules @@ -333,7 +333,7 @@ appear only when your own node is selected: ### Maidir le -**Settings → About** carries the app's own identity rather than the radio's: +**Settings → About** carries the app's own identity rather than the node's: Three sections: @@ -341,7 +341,11 @@ Three sections: - **Apps** — opens with **Need Hardware?**, a rotating carousel of popular devices that links out to where to buy one. It also lists the GitHub repository, the running app version, and **Acknowledgements** (see the next section). -- **Project information** — links to the website and to this documentation. +- **Project information** — links to the website, to this documentation, and to the GNU GPL v3 + license the app is released under. + +The footer states the copyright and that the app is free software with no warranty, which you +may redistribute under the same license. ### Acknowledgements diff --git a/docs/ga-rIE/user/settings-radio-user.md b/docs/ga-rIE/user/settings-radio-user.md index 99b005bb2c..83ea0ddb47 100644 --- a/docs/ga-rIE/user/settings-radio-user.md +++ b/docs/ga-rIE/user/settings-radio-user.md @@ -2,7 +2,7 @@ title: Settings — Radio & User parent: User Guide nav_order: 7 -last_updated: 2026-09-04 +last_updated: 2026-09-11 description: Configure your radio hardware, LoRa presets, user profile, position sharing, power management, and security. aliases: - settings @@ -51,7 +51,7 @@ The footer appears as soon as you change something. **Discard** throws the chang The status message is saved with the same **Save**, but it never reboots the node — and, like the rest of this screen, it can be edited on a remote node you administer. For your own radio there is a shortcut while it is connected: touch & hold your node in the [node list](nodes.md) and choose -**Update status**. Older firmware and a disconnected radio have no shortcut — the field above is +**Update status**. Older firmware and a disconnected node have no shortcut — the field itself is still the way in. ## Configuration @@ -77,13 +77,13 @@ On **Settings → LoRa**. | Setting | Cur síos | Default | | ------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------- | -| Réigiún | Regulatory region for frequency bands. You must set this before transmitting | Unset (must configure) | +| Réigiún | Regulatory region for frequency bands. You must set this before transmitting. On firmware 2.8 or newer, the first region set also creates the node's identity key and gives it a new node number | Unset (must configure) | | Presets | Speed/range tradeoff | LongFast | | Number of Hops | Maximum retransmit hops | 3 | | Transmit Power | Transmission power (dBm); 0 = max allowed for region | 0 (region max) | | Frequency Override | Overrides the computed operating frequency outright (MHz). It does not offset the calculated value — leave at 0 unless you know you need a specific frequency | 0 (use calculated) | | Use Preset | On by default. Turn it off to set Spread Factor, Coding Rate and Bandwidth by hand instead of taking them from the modem preset | On | -| Spread Factor | Manual mode only: 7–12. Higher spreads further but slower | From preset | +| Spread Factor | Manual mode only: 5–12. Higher spreads further but slower. On SX127x (RF95) radios the firmware does not accept 5 or 6 and uses 11 instead | From preset | | Coding Rate | Manual mode only: 5–8. More redundancy costs airtime | From preset | | Bandwidth | Manual mode only: the channel bandwidth in kHz, typed in directly. On the 2.4 GHz region the app offers a list of the bandwidths your radio supports instead, and a stored value that is not on that list shows as _Unsupported_ and blocks saving until you pick a supported one | From preset | | Frequency Slot | Which slot within the region's band to use. 0 derives it from the primary channel name | 0 (automatic) | @@ -111,13 +111,13 @@ The Lite, Narrow, Medium Turbo, and Tiny presets need firmware 2.8 or newer — | Short Slow | ~5 km | 6.25 kbps | −10 dB | Suburban short-range; moderate building density | | Medium Fast | ~5 km | 3.52 kbps | −12.5 dB | Suburban areas; moderate building density | | Medium Slow | ~8 km | 1.95 kbps | −15 dB | Suburban/rural; moderate range with slower speed | -| Long Turbo | ~10 km | 1.34 kbps | −12.5 dB | Similar range to Long Fast but with 500 kHz bandwidth; faster throughput | +| Long Turbo | ~10 km | 1.34 kbps | −17.5 dB | Similar range to Long Fast but with 500 kHz bandwidth; faster throughput | | Long Fast | ~10 km | 1.1 kbps | −17.5 dB | **General use (default)** — balanced range and speed | | Long Moderate | ~20 km | 0.34 kbps | −17.5 dB | Rural with some terrain; occasional use | | Lite Fast | ~5 km | 1.76 kbps | −12.5 dB | EU 866 MHz SRD band (125 kHz BW); comparable to Medium Fast | | Lite Slow | ~10 km | 0.98 kbps | −15 dB | EU 866 MHz SRD band (125 kHz BW); comparable to Long Fast | -| Narrow Fast | ~5 km | 2.28 kbps | −10 dB | EU 868 MHz band (62.5 kHz BW); avoids interference with other devices | -| Narrow Slow | ~10 km | 1.30 kbps | −12.5 dB | EU 868 MHz band (62.5 kHz BW); comparable to Long Fast | +| Narrow Fast | ~5 km | 2.28 kbps | −7.5 dB | EU 868 MHz band (62.5 kHz BW); avoids interference with other devices | +| Narrow Slow | ~10 km | 1.30 kbps | −10 dB | EU 868 MHz band (62.5 kHz BW); comparable to Long Fast | | Medium Turbo | ~5 km | 7.0 kbps | −12.5 dB | Like Medium Fast but with 500 kHz bandwidth; not legal in every region. Needs firmware 2.8 or newer | | Tiny Fast | ~10 km | 0.68 kbps | −7.5 dB | Amateur bands that cap occupied bandwidth; these presets use 15.6 kHz. Needs firmware 2.8 or newer, an SX126x or SX127x radio, and a TCXO of ±5 ppm or better | | Tiny Slow | ~20 km | 0.33 kbps | −10 dB | Same band restrictions as Tiny Fast, longer range. Same firmware, radio, and TCXO requirements | diff --git a/docs/ga-rIE/user/signal-meter.md b/docs/ga-rIE/user/signal-meter.md index 44bf27080c..171b7273f5 100644 --- a/docs/ga-rIE/user/signal-meter.md +++ b/docs/ga-rIE/user/signal-meter.md @@ -2,7 +2,7 @@ title: How the Meshtastic Signal Meter Works parent: User Guide nav_order: 15 -last_updated: 2026-08-30 +last_updated: 2026-09-09 description: How the signal meter rates quality from SNR relative to the LoRa modem preset — spread spectrum, presets, and what the bars really mean. aliases: - signal @@ -32,7 +32,7 @@ Each modem preset has an SNR limit: the lowest SNR at which that preset can stil ## Rating Signal Quality -The app rates signal quality (None, Bad, Fair, or Good) from SNR alone, measured against the active preset's SNR limit. It does not factor in RSSI: without knowing the local noise floor, RSSI alone cannot say whether a signal is decodable. RSSI is still available — on the node detail screen and in the metrics charts. +The app rates signal quality (None, Bad, Fair, or Good) from SNR relative to the active preset's SNR limit. When RSSI and a noise-floor reading are both available, it also rates the difference between them against that limit and uses the worse rating. Otherwise, it uses SNR alone. Because the rating is relative to the preset, the same SNR rates differently on different presets. An SNR of −16 dB rates Good on Long Fast (SNR limit −17.5 dB) but None on Short Fast (SNR limit −7.5 dB). Let `limit` be the active preset's SNR limit: @@ -45,7 +45,7 @@ Because the rating is relative to the preset, the same SNR rates differently on The icon never goes blank, so count the bars carefully: a single bar means None, not a weak but usable link, and Good is a solid wedge rather than a set of bars. A gray three-bar icon labeled Unknown is a different state again — the packet carried no SNR measurement at all, which is not the same as measuring one and finding it too weak. -> ℹ️ **Note:** Traceroute hop colors use fixed thresholds (−7 dB / −15 dB); the per-node signal meter uses the preset-relative rating instead. +> ℹ️ **Note:** Traceroute and neighbor-info SNR colors use the active preset's limits too. RSSI text has separate strength colors: green above −115 dBm, yellow above −120 dBm, orange above −126 dBm, and red at or below −126 dBm. ## Diagnosing Local Interference diff --git a/docs/ga-rIE/user/tak.md b/docs/ga-rIE/user/tak.md index 9235c0650d..4f51aa709a 100644 --- a/docs/ga-rIE/user/tak.md +++ b/docs/ga-rIE/user/tak.md @@ -2,7 +2,7 @@ title: TAK Integration parent: User Guide nav_order: 10 -last_updated: 2026-08-30 +last_updated: 2026-09-11 description: Interoperate with ATAK and WinTAK — CoT position sharing, TAK roles, and plugin setup. aliases: - tak @@ -36,7 +36,7 @@ The TAK module allows Meshtastic nodes to: > ⚠️ **Warning:** The old **Meshtastic ATAK Plugin** is no longer part of this path and cannot > work. It bridged through the cross-process AIDL API, which was removed in app 2.8.0; the mesh > service is now in-process only. Do not install it. Interop today runs over the app's own local -> TAK server plus the Mesh to CoT Converter, both described below, with stock ATAK/iTAK/WinTAK. +> TAK server plus the Mesh to CoT Converter, both described in the following sections, with stock ATAK/iTAK/WinTAK. ### Configuration diff --git a/docs/ga-rIE/user/translate.md b/docs/ga-rIE/user/translate.md index 67d69e2d96..cdc8b6d87f 100644 --- a/docs/ga-rIE/user/translate.md +++ b/docs/ga-rIE/user/translate.md @@ -2,7 +2,7 @@ title: Translate the App parent: User Guide nav_order: 17 -last_updated: 2026-08-30 +last_updated: 2026-09-11 description: How the app and its documentation are translated via Crowdin, and guidelines for contributing translations. aliases: - translate @@ -45,7 +45,7 @@ If your language is not yet listed on Crowdin: The Android app uses **Compose Multiplatform resources** for all user-visible strings: -``` +```text core/resources/src/commonMain/composeResources/ ├── values/ ← English (default) │ └── strings.xml @@ -58,7 +58,7 @@ core/resources/src/commonMain/composeResources/ In-app documentation follows a similar pattern under `docs/`: -``` +```text docs/ ├── en/user/ ← English source (default) │ ├── onboarding.md diff --git a/docs/gl-rES/user/desktop.md b/docs/gl-rES/user/desktop.md index efca3c03c9..2ec0d98037 100644 --- a/docs/gl-rES/user/desktop.md +++ b/docs/gl-rES/user/desktop.md @@ -2,7 +2,7 @@ title: Desktop App parent: User Guide nav_order: 14 -last_updated: 2026-08-30 +last_updated: 2026-09-11 description: Install and use the Meshtastic Desktop app on Linux, macOS, and Windows — connections, feature parity, and keyboard shortcuts. aliases: - desktop @@ -58,20 +58,20 @@ Bluetooth Low Energy is supported on desktop via the [Kable](https://github.com/ ## Feature Parity -| Feature | Android | Desktop | Notes | -| ----------------------------------------------------- | ------- | ------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Messaging | ✓ | ✓ | Full parity | -| Node List | ✓ | ✓ | Full parity | -| Map | ✓ | ✓ | Interactive MapLibre map, with base map and overlay pickers and custom tile sources. No offline downloads or local `.mbtiles` archives | -| Map layers (`.kml`/`.kmz`/GeoJSON) | ✓ | ✓ | Same layer store and sheet as Android; imported files draw on the desktop map | -| Site Planner | ✓ | ✓\* | \*Opens in your browser on desktop; the estimate is not drawn on the desktop map | -| Settings | ✓ | ✓ | Full parity | -| Bluetooth (BLE) | ✓ | ✓ | Via Kable on desktop | -| Firmware Update | ✓ | ✓ | In-app USB, BLE, and Wi-Fi (ESP32) update work the same as Android. The USB maintenance flow — nRF52/RP2040 factory erase and bootloader upgrade — is Android-only | -| Notifications | ✓ | ✓ | Native OS notifications | -| Widgets | ✓ | ✗ | Android-only | -| AI Assistant (Chirpy) | ✓\* | ✗ | Google flavor Android only | -| App Functions (system AI) | ✓† | ✗ | Google flavor Android only | +| Feature | Android | Desktop | Notes | +| ----------------------------------------------------- | ------- | ------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Messaging | ✓ | ✓ | Full parity | +| Node List | ✓ | ✓ | Full parity | +| Map | ✓ | ✓ | Interactive MapLibre map, with base map and overlay pickers and custom tile sources. Offline terrain (hillshade and contours) can be downloaded; offline base-map packs and local `.mbtiles` archives cannot | +| Map layers (`.kml`/`.kmz`/GeoJSON) | ✓ | ✓ | Same layer store and sheet as Android; imported files draw on the desktop map | +| Site Planner | ✓ | ✓\* | \*Opens in your browser on desktop; the estimate is not drawn on the desktop map | +| Settings | ✓ | ✓ | Full parity | +| Bluetooth (BLE) | ✓ | ✓ | Via Kable on desktop | +| Firmware Update | ✓ | ✓ | In-app USB, BLE, and Wi-Fi (ESP32) update work the same as Android. The USB maintenance flow — nRF52/RP2040 factory erase and bootloader upgrade — is Android-only | +| Notifications | ✓ | ✓ | Native OS notifications | +| Widgets | ✓ | ✗ | Android-only | +| AI Assistant (Chirpy) | ✓\* | ✗ | Google flavor Android only | +| App Functions (system AI) | ✓† | ✗ | Google flavor Android only | \*Chirpy AI requires Android 14+ on Google flavor builds with supported hardware. @@ -122,7 +122,7 @@ Individual doc pages render with full formatting: ## Building from Source -```bash +```shell git clone https://github.com/meshtastic/Meshtastic-Android.git cd Meshtastic-Android ./gradlew :desktopApp:run @@ -135,7 +135,7 @@ Requirements: ## Known Limitations -- Offline tile downloads and local `.mbtiles` archives are not available on desktop. +- Offline base-map downloads and local `.mbtiles` archives are not available on desktop. Offline terrain is — see [Map & Waypoints](map-and-waypoints). - `.kml`/`.kmz`/GeoJSON layer import works — see [Map & Waypoints](map-and-waypoints#map-layers). Site Planner opens in your browser rather than in the app; to bring its coverage estimate onto the map, click the transmitter pin diff --git a/docs/gl-rES/user/discovery.md b/docs/gl-rES/user/discovery.md index 242d929f72..039f03023e 100644 --- a/docs/gl-rES/user/discovery.md +++ b/docs/gl-rES/user/discovery.md @@ -2,7 +2,7 @@ title: Local Mesh Discovery parent: User Guide nav_order: 12 -last_updated: 2026-08-30 +last_updated: 2026-09-11 description: Explore your mesh network — the Local Mesh Discovery scanner, traceroute paths, neighbor maps, and node discovery tools. aliases: - discovery @@ -97,7 +97,7 @@ Mesh Beacon lets nodes invite others to join their mesh. A beaconing node period Configure it under **Settings → Module configuration → Mesh Beacon**. The entry appears only on radios running firmware 2.8.0 or newer. A read-only **Region** row at the top of the screen shows the region the beacon advertises: that region, and the preset, are always the ones the radio itself uses, so a beacon cannot invite anyone onto settings your radio is not running. - **Listen for beacons** — receive invitations broadcast by other nodes. -- **Broadcast a beacon** — periodically advertise this mesh to nearby nodes, with an optional **Beacon message** of up to 100 bytes, a **Broadcast interval** picked from fixed intervals between 1 hour and 72 hours, and an **Offered channel** chosen from your radio's own channels. The offered channel is required, and defaults to your primary channel. +- **Broadcast a beacon** — periodically advertise this mesh to nearby nodes, with an optional **Beacon message** of up to 100 bytes, a **Broadcast interval** picked from fixed intervals between 1 hour and 72 hours, and an **Offered channel** chosen from your radio's own channels. The offered channel is required, and defaults to your primary channel. Over remote admin the picker offers the primary channel only. - **Broadcast targets** — optional extra destinations beyond the offered channel. **Add target** appends a row; each row picks a **Channel** and a **Transmit preset**, and **Remove target** deletes it. With no targets, the beacon goes out on the offered channel alone. Two conditions block beacon setup: diff --git a/docs/gl-rES/user/help-and-docs.md b/docs/gl-rES/user/help-and-docs.md index f306cde7f7..c83898cea3 100644 --- a/docs/gl-rES/user/help-and-docs.md +++ b/docs/gl-rES/user/help-and-docs.md @@ -2,7 +2,7 @@ title: Help & In-App Docs parent: User Guide nav_order: 21 -last_updated: 2026-08-30 +last_updated: 2026-09-11 description: Browse this documentation inside the app, search it, and ask Chirpy — the on-device AI assistant — questions about Meshtastic. aliases: - help @@ -23,7 +23,7 @@ The docs browser lists every user-guide page. Tap a page to read it; images and ### Search -The search box sits at the top of the docs browser. Type in it to filter pages by title and keywords — results update as you type, and the **Clear search** button (✕) empties the box. +Tap the search field at the top of the docs browser to open a full-screen search view. Type to filter pages by title and keywords — results update as you type, under the same field. The back arrow, or the keyboard's search action, closes the view. The field hides while you scroll down the page list and comes back when you scroll up. ![Searching the in-app documentation](../../assets/screenshots/docs-browser_search.png) diff --git a/docs/gl-rES/user/map-and-waypoints.md b/docs/gl-rES/user/map-and-waypoints.md index d1f2b8cb32..4b8d010093 100644 --- a/docs/gl-rES/user/map-and-waypoints.md +++ b/docs/gl-rES/user/map-and-waypoints.md @@ -2,7 +2,7 @@ title: Map & Waypoints parent: User Guide nav_order: 6 -last_updated: 2026-09-01 +last_updated: 2026-09-11 description: View node positions on the map, create and share waypoints, manage map layers and Site Planner, and control position sharing and privacy. aliases: - map @@ -44,7 +44,7 @@ The floating toolbar provides quick access to the compass, the map type and laye ### Filtering the Map -Tap the filter button in the floating toolbar to open **Filter map**. **Display** controls what is drawn: **Only Favorites**, **Show Waypoints**, **Show Precision Circles**, and a slider that hides nodes not heard from recently. **Node roles** is a chip per device role, plus **All** to show every role; a selected chip means that role is shown. **Nodes** narrows the set further with **Hide offline nodes**, **Only show direct nodes**, **Exclude MQTT**, **Show ignored nodes**, and **Include unknown**. +Tap the filter button in the floating toolbar to open **Filter map**. **Display** controls what is drawn: **Only Favorites**, **Show Waypoints**, **Show Precision Circles**, and a slider that hides nodes not heard from recently. **Node roles** is a chip per device role, plus **All** to show every role; a selected chip means that role is shown. **Nodes** narrows the set further with **Hide offline nodes**, **Only show direct nodes**, **Signed only**, **Encrypted only**, **Exclude MQTT**, **Show ignored nodes**, and **Include unknown** — the same rules as the node list's [filter toggles](nodes). A dot on the filter button means at least one filter is hiding something — check it before concluding the mesh is quiet. Turning **Show Waypoints** off hides every waypoint, including your own. **Show ignored nodes** adds them to the map rather than showing only them — unlike the node list's **Only show ignored Nodes**. @@ -147,8 +147,9 @@ picker, all three offer the same raster base maps: Overlays can be toggled on top of any base map, from the layers sheet: - **Weather radar** — NOAA NEXRAD reflectivity (US coverage) -- **Hillshade** — terrain relief, on **F-Droid** and **Desktop** only. Useful for understanding why a - link fails, since LoRa range is limited by terrain +- **Hillshade** — terrain relief from an online tile source, on **F-Droid** and **Desktop** only; Google + Play draws hillshade from downloaded offline terrain instead. Useful for understanding why a link + fails, since LoRa range is limited by terrain ### Adding your own tile source @@ -156,7 +157,7 @@ Any XYZ tile endpoint can be added as a base map, on every flavor and on desktop Tile Sources** at the foot of the base map picker and paste a URL template using `{z}`, `{x}` and `{y}` — plus `{s}` if the provider uses rotating subdomains. A national mapping service, for example: -``` +```text https://wmts.geo.admin.ch/1.0.0/ch.swisstopo.pixelkarte-farbe/default/current/3857/{z}/{x}/{y}.jpeg ``` @@ -173,7 +174,18 @@ On **Google Play**, the layers sheet's **Offline Manager** section downloads its — water, roads and administrative boundaries extracted from the public Protomaps basemap dataset, drawn directly as map shapes rather than raster tiles. Frame the area you want, tap **Start Download**, and once it finishes flip **Show on map** to draw it (it's off by default so it never surprises you by covering up the -live map). You can still import a pre-made `.mbtiles` archive here too, as before. **Desktop** has neither. +live map). You can still import a pre-made `.mbtiles` archive here too, as before. **Desktop** has neither, though +the offline terrain described next does work there. + +### Offline terrain + +Hillshade and elevation contours can be downloaded for offline use as well, so a downloaded area still +shows why a link fails when there is no network. The terrain data comes from Mapterhorn and is credited on +the map. On **Google Play**, each downloaded region in the **Offline Manager** list has a **Download Terrain** +button and **Hillshade** and **Contours** switches; terrain attaches to that region and shares its storage limit, and an area +too large for terrain asks you to zoom in or pick a smaller region. On **F-Droid** and **Desktop**, the +layers sheet has an **Offline Terrain** section of its own: frame the area, tap **Start Download**, and the +hillshade and contours draw from the downloaded tiles from then on. ### Going offline @@ -181,8 +193,8 @@ An **Offline** pill appears over the map whenever the device has no network conn builds, if you've imported a local `.mbtiles` archive, the map switches to it automatically the moment the network drops — no toggle to remember — and switches back once you're reconnected, as long as you haven't picked a different base map yourself in between. On **F-Droid**, a downloaded offline area keeps rendering on -its own; the pill is purely informational there. **Desktop** has no offline downloads at all yet, so the pill -is informational there too. +its own; the pill is purely informational there. **Desktop** downloads offline terrain but no base-map +packs, so the pill is informational there too. ## Related Topics diff --git a/docs/gl-rES/user/messages-and-channels.md b/docs/gl-rES/user/messages-and-channels.md index 4f9286c861..9502ea77ff 100644 --- a/docs/gl-rES/user/messages-and-channels.md +++ b/docs/gl-rES/user/messages-and-channels.md @@ -2,7 +2,7 @@ title: Messages & Channels parent: User Guide nav_order: 3 -last_updated: 2026-08-30 +last_updated: 2026-09-14 description: Send and receive messages, manage channels, configure encryption, search conversations, and use quick chat, reactions, and message actions. aliases: - channels @@ -50,6 +50,21 @@ Key length alone does not change the icon: a 128-bit key and a 256-bit key both Tapping an existing channel opens the same editor, where you can change the name, the PSK, MQTT uplink and downlink, and position precision. Every edit on this screen — adding, editing, deleting, or dragging a channel into a new order — waits on **Send** the same way. +### Archived Channels + +A conversation belongs to the channel it was held on, not to the slot that channel occupied. When a channel leaves your radio — you scan a different channel QR, replace a channel's key, or switch the modem preset — its conversation is **archived** rather than deleted or merged into whatever takes the slot. + +An archived conversation: + +- stays in the **Channels** list under the name the channel had, below the channels currently on your radio, marked with a history icon; +- keeps its full message history, searchable as usual; +- is read-only — you cannot send, reply, or react, because the channel is no longer on the radio to send on, and it is not offered as a share target; +- comes back automatically if you re-add that exact channel, taking its messages back to the live slot. + +Changing the modem preset archives the channel too: the preset is part of a default channel's identity, so **LongFast** and **MediumFast** are different channels even though neither the name nor the key changed. Changing only the region does not archive anything. + +To remove an archived conversation for good, long-press it in the conversation list and delete it. + ## Direct Messages Direct messages (DMs) go to one specific node. When both radios hold each other's public keys, your radio encrypts the message to that node's public key, so no one else on the mesh can read it — not even nodes that share your channel. diff --git a/docs/gl-rES/user/mqtt.md b/docs/gl-rES/user/mqtt.md index bb32bc20a3..ab6d00f542 100644 --- a/docs/gl-rES/user/mqtt.md +++ b/docs/gl-rES/user/mqtt.md @@ -2,7 +2,7 @@ title: MQTT parent: User Guide nav_order: 11 -last_updated: 2026-08-30 +last_updated: 2026-09-11 description: Bridge your mesh to the internet — MQTT broker setup, encryption layers, and map reporting. aliases: - mqtt @@ -25,7 +25,7 @@ The MQTT module connects your node to an MQTT broker, allowing: ## How It Works -``` +```text [Your Node] → Radio → [Gateway Node with Wi-Fi] → MQTT Broker → [Remote Gateway] → Radio → [Remote Node] ``` diff --git a/docs/gl-rES/user/node-metrics.md b/docs/gl-rES/user/node-metrics.md index e1d7d2c7c1..8f8423d758 100644 --- a/docs/gl-rES/user/node-metrics.md +++ b/docs/gl-rES/user/node-metrics.md @@ -2,7 +2,7 @@ title: Node Metrics parent: User Guide nav_order: 5 -last_updated: 2026-08-30 +last_updated: 2026-09-09 description: Telemetry dashboards for each mesh node — device health, environment sensors, air quality, signal quality, power, traceroute, and position history. aliases: - metrics @@ -114,7 +114,7 @@ Radio signal quality information: ### Signal Quality Reference -Signal quality is rated from **SNR relative to the active LoRa modem preset's demodulation floor**, not from fixed thresholds — a given SNR means different things on different presets (e.g. −15 dB is fine on LongSlow but unusable on ShortFast). RSSI is shown but is not part of the rating. In the table, _limit_ is the preset's SNR limit. +Signal quality is rated from **SNR relative to the active LoRa modem preset's demodulation floor**, not from fixed thresholds — a given SNR means different things on different presets (e.g. −15 dB is fine on LongSlow but unusable on ShortFast). When RSSI and a noise-floor reading are both available, the app also rates their difference against the preset limit and uses the worse rating. Otherwise, RSSI is display-only. In the table, _limit_ is the preset's SNR limit. | Quality | Criteria | | ------- | ---------------------------------------------------------------- | diff --git a/docs/gl-rES/user/nodes.md b/docs/gl-rES/user/nodes.md index 5453a5253d..58d981233e 100644 --- a/docs/gl-rES/user/nodes.md +++ b/docs/gl-rES/user/nodes.md @@ -2,7 +2,7 @@ title: Nodes parent: User Guide nav_order: 4 -last_updated: 2026-09-04 +last_updated: 2026-09-11 description: Browse, filter, and sort mesh nodes — view details, signal quality, roles, and quick actions. aliases: - node-list @@ -74,19 +74,23 @@ Most users should keep the default **Client** role. Consider a different role wh > 💡 **Tip:** The mesh works best when most nodes are **Client** or **Router**. Too many Client Mute nodes reduce mesh resilience; too many Routers in a dense area can cause congestion. A good rule of thumb: one Router per 5–10 Clients in your area. -### Encryption Indicators +### Security indicators -Nodes display encryption status icons next to their name: +Each node carries one security icon beside its name in the node list. Tap it to read what it means, and choose **Show All Meanings** in that dialog for the full legend. The detail screen shows the same state in words, as a **Security** row that opens the same dialog. -| Icon | Meaning | -| ----------- | ------------------------------------------------------------------------------------------------------------------------------------------------- | -| 🔒 Locked | Communication uses PKI (public key infrastructure) — end-to-end encrypted with verified identity | -| 🔓 Unlocked | No public key has been received for this node, so it cannot be direct messaged — use **Request User Info** on the node detail page to ask for one | -| ⚠️ Mismatch | Public key mismatch — the node's key has changed since last seen (investigate before trusting) | +| Icon | Meaning | Shown for | +| --------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------- | +| Person with a shield check (green) | **Verified contact** — you verified this node's key in person by exchanging contact QR codes, so its identity is confirmed. The strongest trust the list shows. Your own connected node carries it too | Any firmware version | +| Nodes icon with a shield check (green) | **Signed node** — this node signs its broadcasts with its identity key, so its identity is consistent over time, but you have not verified it in person. On firmware 2.8 the icon appears from the version alone, before any signed broadcast has been heard | Firmware 2.8 or newer, and any node whose signed broadcast your node has verified | +| 🔒 Closed lock | A public key is on file and matches, so direct messages to this node are encrypted | Firmware before 2.8, or no reported version | +| 🔓 Open lock | No public key has been received for this node, so it cannot be direct messaged — use **Request User Info** on the node detail page to ask for one | Firmware before 2.8, or no reported version | +| ⚠️ Mismatch | **Public key mismatch** — a different key arrived for this node after one was stored. Investigate before trusting | Any firmware version | -> 💡 **Tip:** Direct messages always use PKI, so the radio needs the other node's public key before it can send one. It refuses the send rather than falling back to channel encryption. Keys arrive inside node info, which is why an open lock usually clears itself once that node is heard from properly. If you see a key mismatch warning, the node may have been reset or compromised. +Every node on firmware 2.8 or newer signs its broadcasts, so on that firmware the signed state is the baseline and the locks are not shown. -To clear a mismatch, first confirm through another trusted channel that the key change was intentional — a factory reset causes one. Then touch & hold the node, choose **Remove**, and let the two radios exchange keys again the next time yours hears it. +Direct messages always use public-key encryption, so your node needs the other node's public key before it can send one. It refuses the send rather than falling back to channel encryption. Keys arrive inside node info, which is why an open lock usually clears itself once that node is heard from properly. + +A mismatch never replaces the key you already hold. The app keeps the first key it recorded and refuses the new one, as the firmware does, so a stray or hostile node info cannot silently break encrypted messaging to a contact. To clear a mismatch, first confirm through another trusted channel that the key change was intentional — a factory reset causes one. Then touch & hold the node, choose **Remove**, and let the two nodes exchange keys again the next time yours hears it. ## Quick Actions @@ -108,7 +112,9 @@ radio is connected and running firmware 2.8 or newer — see ## Sharing a Contact -On a node's detail screen, tap **Share Contact** to produce a link and a QR code for that node. From the same dialog, **Write to NFC tag** saves the link to a writable NFC tag that anyone can tap to open. +On a node's detail screen, tap **Share Contact** to produce a link and a QR code for that node. From the same dialog, **Share link** opens the Android share sheet (on desktop it copies the link instead), **Write to NFC tag** saves it to a writable NFC tag, and **Copy** puts it on the clipboard. While that dialog is open and in front of you, the phone also offers the same link to any NFC reader, so someone can take the contact by tapping their phone against yours with no tag involved. + +Sharing your own contact this way marks it as verified in person, so whoever imports it sees the **Verified contact** icon rather than the signed one. Relaying someone else's contact passes on only what your app had already recorded about them. To add someone else's contact, use the import button on the node list and choose **Scan Shared Contact QR Code**, **Scan Shared Contact NFC**, or **Input Shared Contact URL**. The app asks you to confirm with **Import Shared Contact?**, and warns you when the contact is one you already have. @@ -120,14 +126,16 @@ Type in the search field to filter nodes by name or short name. The filter updat ### Filter Toggles -| Filtro | Descrición | -| --------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| **Hide offline nodes** | Show only nodes heard within the last 2 hours | -| **Only show direct nodes** | Show only nodes your radio heard directly, with no relay in between | -| **Include unknown** | Show nodes that haven't sent user info yet. **On by default**, so a node heard before its info arrives stays visible; these carry a badge marking them incomplete, and cannot be direct messaged until their user info brings a public key | -| **Exclude infrastructure** | Hide infrastructure-role nodes (Router, Router Late, Client Base, and legacy Repeater nodes) and any node that cannot be messaged, whatever its role | -| **Exclude MQTT** | Hide nodes heard only via MQTT internet bridge | -| **Only show ignored Nodes** | Replace the list with the nodes you have ignored. Every other node is hidden while this is on, and a banner appears at the top of the list to take you back | +| Filtro | Descrición | +| --------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **Hide offline nodes** | Show only nodes heard within the last 2 hours | +| **Only show direct nodes** | Show only nodes your radio heard directly, with no relay in between | +| **Include unknown** | Show nodes that haven't sent user info yet. **On by default**, so a node heard before its info arrives stays visible; these carry a badge marking them incomplete, and cannot be direct messaged until their user info brings a public key | +| **Exclude infrastructure** | Hide infrastructure-role nodes (Router, Router Late, Client Base, and legacy Repeater nodes) and any node that cannot be messaged, whatever its role | +| **Exclude MQTT** | Hide nodes heard only via MQTT internet bridge | +| **Signed only** | Show only nodes whose signed broadcasts your node has actually heard and verified. Stricter than the icon: a node on 2.8 shows as signed by its firmware version before any signed broadcast arrives, and a contact verified in person on older firmware is not signed at all | +| **Encrypted only** | Show nodes with a matching public key on file, the key an encrypted direct message needs. A node with a key mismatch is excluded | +| **Only show ignored Nodes** | Replace the list with the nodes you have ignored. Every other node is hidden while this is on, and a banner appears at the top of the list to take you back | ### Sort Options @@ -149,6 +157,8 @@ Tap the hop-histogram icon in the node list's app bar to open a bar chart of how Tapping a node opens the detail view with comprehensive information. See [Node Metrics](node-metrics) for full details on metrics and telemetry. +Signal quality is rated against your modem preset. The same SNR can be good on a long-range preset and poor on a faster one. Traceroute and neighbor-info SNR colors use that preset too. RSSI text has its own strength colors; it affects the quality rating only when a noise-floor reading is also available. + The Details card carries the node's short name, role, IDs, last heard time, hops away, uptime, and its SNR and RSSI: ![Node detail section](../../assets/screenshots/nodes_detail_section.png) diff --git a/docs/gl-rES/user/settings-module-admin.md b/docs/gl-rES/user/settings-module-admin.md index 364a9b5572..82e11a4b60 100644 --- a/docs/gl-rES/user/settings-module-admin.md +++ b/docs/gl-rES/user/settings-module-admin.md @@ -2,7 +2,7 @@ title: Settings — Modules & Admin parent: User Guide nav_order: 8 -last_updated: 2026-08-30 +last_updated: 2026-09-16 description: Configure optional feature modules (MQTT, telemetry, canned messages, TAK, and more) and perform device administration. aliases: - modules @@ -333,7 +333,7 @@ appear only when your own node is selected: ### Acerca de -**Settings → About** carries the app's own identity rather than the radio's: +**Settings → About** carries the app's own identity rather than the node's: Three sections: @@ -341,7 +341,11 @@ Three sections: - **Apps** — opens with **Need Hardware?**, a rotating carousel of popular devices that links out to where to buy one. It also lists the GitHub repository, the running app version, and **Acknowledgements** (see the next section). -- **Project information** — links to the website and to this documentation. +- **Project information** — links to the website, to this documentation, and to the GNU GPL v3 + license the app is released under. + +The footer states the copyright and that the app is free software with no warranty, which you +may redistribute under the same license. ### Acknowledgements diff --git a/docs/gl-rES/user/settings-radio-user.md b/docs/gl-rES/user/settings-radio-user.md index 1a7066446e..db4c133ba8 100644 --- a/docs/gl-rES/user/settings-radio-user.md +++ b/docs/gl-rES/user/settings-radio-user.md @@ -2,7 +2,7 @@ title: Settings — Radio & User parent: User Guide nav_order: 7 -last_updated: 2026-09-04 +last_updated: 2026-09-11 description: Configure your radio hardware, LoRa presets, user profile, position sharing, power management, and security. aliases: - settings @@ -51,7 +51,7 @@ The footer appears as soon as you change something. **Discard** throws the chang The status message is saved with the same **Save**, but it never reboots the node — and, like the rest of this screen, it can be edited on a remote node you administer. For your own radio there is a shortcut while it is connected: touch & hold your node in the [node list](nodes.md) and choose -**Update status**. Older firmware and a disconnected radio have no shortcut — the field above is +**Update status**. Older firmware and a disconnected node have no shortcut — the field itself is still the way in. ## Configuration @@ -77,13 +77,13 @@ On **Settings → LoRa**. | Setting | Descrición | Default | | ------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------- | -| Rexión | Regulatory region for frequency bands. You must set this before transmitting | Unset (must configure) | +| Rexión | Regulatory region for frequency bands. You must set this before transmitting. On firmware 2.8 or newer, the first region set also creates the node's identity key and gives it a new node number | Unset (must configure) | | Presets | Speed/range tradeoff | LongFast | | Number of Hops | Maximum retransmit hops | 3 | | Transmit Power | Transmission power (dBm); 0 = max allowed for region | 0 (region max) | | Frequency Override | Overrides the computed operating frequency outright (MHz). It does not offset the calculated value — leave at 0 unless you know you need a specific frequency | 0 (use calculated) | | Use Preset | On by default. Turn it off to set Spread Factor, Coding Rate and Bandwidth by hand instead of taking them from the modem preset | On | -| Spread Factor | Manual mode only: 7–12. Higher spreads further but slower | From preset | +| Spread Factor | Manual mode only: 5–12. Higher spreads further but slower. On SX127x (RF95) radios the firmware does not accept 5 or 6 and uses 11 instead | From preset | | Coding Rate | Manual mode only: 5–8. More redundancy costs airtime | From preset | | Bandwidth | Manual mode only: the channel bandwidth in kHz, typed in directly. On the 2.4 GHz region the app offers a list of the bandwidths your radio supports instead, and a stored value that is not on that list shows as _Unsupported_ and blocks saving until you pick a supported one | From preset | | Frequency Slot | Which slot within the region's band to use. 0 derives it from the primary channel name | 0 (automatic) | @@ -111,13 +111,13 @@ The Lite, Narrow, Medium Turbo, and Tiny presets need firmware 2.8 or newer — | Short Slow | ~5 km | 6.25 kbps | −10 dB | Suburban short-range; moderate building density | | Medium Fast | ~5 km | 3.52 kbps | −12.5 dB | Suburban areas; moderate building density | | Medium Slow | ~8 km | 1.95 kbps | −15 dB | Suburban/rural; moderate range with slower speed | -| Long Turbo | ~10 km | 1.34 kbps | −12.5 dB | Similar range to Long Fast but with 500 kHz bandwidth; faster throughput | +| Long Turbo | ~10 km | 1.34 kbps | −17.5 dB | Similar range to Long Fast but with 500 kHz bandwidth; faster throughput | | Long Fast | ~10 km | 1.1 kbps | −17.5 dB | **General use (default)** — balanced range and speed | | Long Moderate | ~20 km | 0.34 kbps | −17.5 dB | Rural with some terrain; occasional use | | Lite Fast | ~5 km | 1.76 kbps | −12.5 dB | EU 866 MHz SRD band (125 kHz BW); comparable to Medium Fast | | Lite Slow | ~10 km | 0.98 kbps | −15 dB | EU 866 MHz SRD band (125 kHz BW); comparable to Long Fast | -| Narrow Fast | ~5 km | 2.28 kbps | −10 dB | EU 868 MHz band (62.5 kHz BW); avoids interference with other devices | -| Narrow Slow | ~10 km | 1.30 kbps | −12.5 dB | EU 868 MHz band (62.5 kHz BW); comparable to Long Fast | +| Narrow Fast | ~5 km | 2.28 kbps | −7.5 dB | EU 868 MHz band (62.5 kHz BW); avoids interference with other devices | +| Narrow Slow | ~10 km | 1.30 kbps | −10 dB | EU 868 MHz band (62.5 kHz BW); comparable to Long Fast | | Medium Turbo | ~5 km | 7.0 kbps | −12.5 dB | Like Medium Fast but with 500 kHz bandwidth; not legal in every region. Needs firmware 2.8 or newer | | Tiny Fast | ~10 km | 0.68 kbps | −7.5 dB | Amateur bands that cap occupied bandwidth; these presets use 15.6 kHz. Needs firmware 2.8 or newer, an SX126x or SX127x radio, and a TCXO of ±5 ppm or better | | Tiny Slow | ~20 km | 0.33 kbps | −10 dB | Same band restrictions as Tiny Fast, longer range. Same firmware, radio, and TCXO requirements | diff --git a/docs/gl-rES/user/signal-meter.md b/docs/gl-rES/user/signal-meter.md index c4e7c190bd..12d6aaade0 100644 --- a/docs/gl-rES/user/signal-meter.md +++ b/docs/gl-rES/user/signal-meter.md @@ -2,7 +2,7 @@ title: How the Meshtastic Signal Meter Works parent: User Guide nav_order: 15 -last_updated: 2026-08-30 +last_updated: 2026-09-09 description: How the signal meter rates quality from SNR relative to the LoRa modem preset — spread spectrum, presets, and what the bars really mean. aliases: - signal @@ -32,7 +32,7 @@ Each modem preset has an SNR limit: the lowest SNR at which that preset can stil ## Rating Signal Quality -The app rates signal quality (None, Bad, Fair, or Good) from SNR alone, measured against the active preset's SNR limit. It does not factor in RSSI: without knowing the local noise floor, RSSI alone cannot say whether a signal is decodable. RSSI is still available — on the node detail screen and in the metrics charts. +The app rates signal quality (None, Bad, Fair, or Good) from SNR relative to the active preset's SNR limit. When RSSI and a noise-floor reading are both available, it also rates the difference between them against that limit and uses the worse rating. Otherwise, it uses SNR alone. Because the rating is relative to the preset, the same SNR rates differently on different presets. An SNR of −16 dB rates Good on Long Fast (SNR limit −17.5 dB) but None on Short Fast (SNR limit −7.5 dB). Let `limit` be the active preset's SNR limit: @@ -45,7 +45,7 @@ Because the rating is relative to the preset, the same SNR rates differently on The icon never goes blank, so count the bars carefully: a single bar means None, not a weak but usable link, and Good is a solid wedge rather than a set of bars. A gray three-bar icon labeled Unknown is a different state again — the packet carried no SNR measurement at all, which is not the same as measuring one and finding it too weak. -> ℹ️ **Note:** Traceroute hop colors use fixed thresholds (−7 dB / −15 dB); the per-node signal meter uses the preset-relative rating instead. +> ℹ️ **Note:** Traceroute and neighbor-info SNR colors use the active preset's limits too. RSSI text has separate strength colors: green above −115 dBm, yellow above −120 dBm, orange above −126 dBm, and red at or below −126 dBm. ## Diagnosing Local Interference diff --git a/docs/gl-rES/user/tak.md b/docs/gl-rES/user/tak.md index 91fc3d6e1f..5418db53e8 100644 --- a/docs/gl-rES/user/tak.md +++ b/docs/gl-rES/user/tak.md @@ -2,7 +2,7 @@ title: TAK Integration parent: User Guide nav_order: 10 -last_updated: 2026-08-30 +last_updated: 2026-09-11 description: Interoperate with ATAK and WinTAK — CoT position sharing, TAK roles, and plugin setup. aliases: - tak @@ -36,7 +36,7 @@ The TAK module allows Meshtastic nodes to: > ⚠️ **Warning:** The old **Meshtastic ATAK Plugin** is no longer part of this path and cannot > work. It bridged through the cross-process AIDL API, which was removed in app 2.8.0; the mesh > service is now in-process only. Do not install it. Interop today runs over the app's own local -> TAK server plus the Mesh to CoT Converter, both described below, with stock ATAK/iTAK/WinTAK. +> TAK server plus the Mesh to CoT Converter, both described in the following sections, with stock ATAK/iTAK/WinTAK. ### Configuration diff --git a/docs/gl-rES/user/translate.md b/docs/gl-rES/user/translate.md index 67d69e2d96..cdc8b6d87f 100644 --- a/docs/gl-rES/user/translate.md +++ b/docs/gl-rES/user/translate.md @@ -2,7 +2,7 @@ title: Translate the App parent: User Guide nav_order: 17 -last_updated: 2026-08-30 +last_updated: 2026-09-11 description: How the app and its documentation are translated via Crowdin, and guidelines for contributing translations. aliases: - translate @@ -45,7 +45,7 @@ If your language is not yet listed on Crowdin: The Android app uses **Compose Multiplatform resources** for all user-visible strings: -``` +```text core/resources/src/commonMain/composeResources/ ├── values/ ← English (default) │ └── strings.xml @@ -58,7 +58,7 @@ core/resources/src/commonMain/composeResources/ In-app documentation follows a similar pattern under `docs/`: -``` +```text docs/ ├── en/user/ ← English source (default) │ ├── onboarding.md diff --git a/docs/hr-rHR/user/desktop.md b/docs/hr-rHR/user/desktop.md index efca3c03c9..2ec0d98037 100644 --- a/docs/hr-rHR/user/desktop.md +++ b/docs/hr-rHR/user/desktop.md @@ -2,7 +2,7 @@ title: Desktop App parent: User Guide nav_order: 14 -last_updated: 2026-08-30 +last_updated: 2026-09-11 description: Install and use the Meshtastic Desktop app on Linux, macOS, and Windows — connections, feature parity, and keyboard shortcuts. aliases: - desktop @@ -58,20 +58,20 @@ Bluetooth Low Energy is supported on desktop via the [Kable](https://github.com/ ## Feature Parity -| Feature | Android | Desktop | Notes | -| ----------------------------------------------------- | ------- | ------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Messaging | ✓ | ✓ | Full parity | -| Node List | ✓ | ✓ | Full parity | -| Map | ✓ | ✓ | Interactive MapLibre map, with base map and overlay pickers and custom tile sources. No offline downloads or local `.mbtiles` archives | -| Map layers (`.kml`/`.kmz`/GeoJSON) | ✓ | ✓ | Same layer store and sheet as Android; imported files draw on the desktop map | -| Site Planner | ✓ | ✓\* | \*Opens in your browser on desktop; the estimate is not drawn on the desktop map | -| Settings | ✓ | ✓ | Full parity | -| Bluetooth (BLE) | ✓ | ✓ | Via Kable on desktop | -| Firmware Update | ✓ | ✓ | In-app USB, BLE, and Wi-Fi (ESP32) update work the same as Android. The USB maintenance flow — nRF52/RP2040 factory erase and bootloader upgrade — is Android-only | -| Notifications | ✓ | ✓ | Native OS notifications | -| Widgets | ✓ | ✗ | Android-only | -| AI Assistant (Chirpy) | ✓\* | ✗ | Google flavor Android only | -| App Functions (system AI) | ✓† | ✗ | Google flavor Android only | +| Feature | Android | Desktop | Notes | +| ----------------------------------------------------- | ------- | ------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Messaging | ✓ | ✓ | Full parity | +| Node List | ✓ | ✓ | Full parity | +| Map | ✓ | ✓ | Interactive MapLibre map, with base map and overlay pickers and custom tile sources. Offline terrain (hillshade and contours) can be downloaded; offline base-map packs and local `.mbtiles` archives cannot | +| Map layers (`.kml`/`.kmz`/GeoJSON) | ✓ | ✓ | Same layer store and sheet as Android; imported files draw on the desktop map | +| Site Planner | ✓ | ✓\* | \*Opens in your browser on desktop; the estimate is not drawn on the desktop map | +| Settings | ✓ | ✓ | Full parity | +| Bluetooth (BLE) | ✓ | ✓ | Via Kable on desktop | +| Firmware Update | ✓ | ✓ | In-app USB, BLE, and Wi-Fi (ESP32) update work the same as Android. The USB maintenance flow — nRF52/RP2040 factory erase and bootloader upgrade — is Android-only | +| Notifications | ✓ | ✓ | Native OS notifications | +| Widgets | ✓ | ✗ | Android-only | +| AI Assistant (Chirpy) | ✓\* | ✗ | Google flavor Android only | +| App Functions (system AI) | ✓† | ✗ | Google flavor Android only | \*Chirpy AI requires Android 14+ on Google flavor builds with supported hardware. @@ -122,7 +122,7 @@ Individual doc pages render with full formatting: ## Building from Source -```bash +```shell git clone https://github.com/meshtastic/Meshtastic-Android.git cd Meshtastic-Android ./gradlew :desktopApp:run @@ -135,7 +135,7 @@ Requirements: ## Known Limitations -- Offline tile downloads and local `.mbtiles` archives are not available on desktop. +- Offline base-map downloads and local `.mbtiles` archives are not available on desktop. Offline terrain is — see [Map & Waypoints](map-and-waypoints). - `.kml`/`.kmz`/GeoJSON layer import works — see [Map & Waypoints](map-and-waypoints#map-layers). Site Planner opens in your browser rather than in the app; to bring its coverage estimate onto the map, click the transmitter pin diff --git a/docs/hr-rHR/user/discovery.md b/docs/hr-rHR/user/discovery.md index c056f95d7a..f095744ec9 100644 --- a/docs/hr-rHR/user/discovery.md +++ b/docs/hr-rHR/user/discovery.md @@ -2,7 +2,7 @@ title: Local Mesh Discovery parent: User Guide nav_order: 12 -last_updated: 2026-08-30 +last_updated: 2026-09-11 description: Explore your mesh network — the Local Mesh Discovery scanner, traceroute paths, neighbor maps, and node discovery tools. aliases: - discovery @@ -97,7 +97,7 @@ Mesh Beacon lets nodes invite others to join their mesh. A beaconing node period Configure it under **Settings → Module configuration → Mesh Beacon**. The entry appears only on radios running firmware 2.8.0 or newer. A read-only **Region** row at the top of the screen shows the region the beacon advertises: that region, and the preset, are always the ones the radio itself uses, so a beacon cannot invite anyone onto settings your radio is not running. - **Listen for beacons** — receive invitations broadcast by other nodes. -- **Broadcast a beacon** — periodically advertise this mesh to nearby nodes, with an optional **Beacon message** of up to 100 bytes, a **Broadcast interval** picked from fixed intervals between 1 hour and 72 hours, and an **Offered channel** chosen from your radio's own channels. The offered channel is required, and defaults to your primary channel. +- **Broadcast a beacon** — periodically advertise this mesh to nearby nodes, with an optional **Beacon message** of up to 100 bytes, a **Broadcast interval** picked from fixed intervals between 1 hour and 72 hours, and an **Offered channel** chosen from your radio's own channels. The offered channel is required, and defaults to your primary channel. Over remote admin the picker offers the primary channel only. - **Broadcast targets** — optional extra destinations beyond the offered channel. **Add target** appends a row; each row picks a **Channel** and a **Transmit preset**, and **Remove target** deletes it. With no targets, the beacon goes out on the offered channel alone. Two conditions block beacon setup: diff --git a/docs/hr-rHR/user/help-and-docs.md b/docs/hr-rHR/user/help-and-docs.md index f306cde7f7..c83898cea3 100644 --- a/docs/hr-rHR/user/help-and-docs.md +++ b/docs/hr-rHR/user/help-and-docs.md @@ -2,7 +2,7 @@ title: Help & In-App Docs parent: User Guide nav_order: 21 -last_updated: 2026-08-30 +last_updated: 2026-09-11 description: Browse this documentation inside the app, search it, and ask Chirpy — the on-device AI assistant — questions about Meshtastic. aliases: - help @@ -23,7 +23,7 @@ The docs browser lists every user-guide page. Tap a page to read it; images and ### Search -The search box sits at the top of the docs browser. Type in it to filter pages by title and keywords — results update as you type, and the **Clear search** button (✕) empties the box. +Tap the search field at the top of the docs browser to open a full-screen search view. Type to filter pages by title and keywords — results update as you type, under the same field. The back arrow, or the keyboard's search action, closes the view. The field hides while you scroll down the page list and comes back when you scroll up. ![Searching the in-app documentation](../../assets/screenshots/docs-browser_search.png) diff --git a/docs/hr-rHR/user/map-and-waypoints.md b/docs/hr-rHR/user/map-and-waypoints.md index 366b071d55..af0a337e11 100644 --- a/docs/hr-rHR/user/map-and-waypoints.md +++ b/docs/hr-rHR/user/map-and-waypoints.md @@ -2,7 +2,7 @@ title: Map & Waypoints parent: User Guide nav_order: 6 -last_updated: 2026-09-01 +last_updated: 2026-09-11 description: View node positions on the map, create and share waypoints, manage map layers and Site Planner, and control position sharing and privacy. aliases: - map @@ -44,7 +44,7 @@ The floating toolbar provides quick access to the compass, the map type and laye ### Filtering the Map -Tap the filter button in the floating toolbar to open **Filter map**. **Display** controls what is drawn: **Only Favorites**, **Show Waypoints**, **Show Precision Circles**, and a slider that hides nodes not heard from recently. **Node roles** is a chip per device role, plus **All** to show every role; a selected chip means that role is shown. **Nodes** narrows the set further with **Hide offline nodes**, **Only show direct nodes**, **Exclude MQTT**, **Show ignored nodes**, and **Include unknown**. +Tap the filter button in the floating toolbar to open **Filter map**. **Display** controls what is drawn: **Only Favorites**, **Show Waypoints**, **Show Precision Circles**, and a slider that hides nodes not heard from recently. **Node roles** is a chip per device role, plus **All** to show every role; a selected chip means that role is shown. **Nodes** narrows the set further with **Hide offline nodes**, **Only show direct nodes**, **Signed only**, **Encrypted only**, **Exclude MQTT**, **Show ignored nodes**, and **Include unknown** — the same rules as the node list's [filter toggles](nodes). A dot on the filter button means at least one filter is hiding something — check it before concluding the mesh is quiet. Turning **Show Waypoints** off hides every waypoint, including your own. **Show ignored nodes** adds them to the map rather than showing only them — unlike the node list's **Only show ignored Nodes**. @@ -147,8 +147,9 @@ picker, all three offer the same raster base maps: Overlays can be toggled on top of any base map, from the layers sheet: - **Weather radar** — NOAA NEXRAD reflectivity (US coverage) -- **Hillshade** — terrain relief, on **F-Droid** and **Desktop** only. Useful for understanding why a - link fails, since LoRa range is limited by terrain +- **Hillshade** — terrain relief from an online tile source, on **F-Droid** and **Desktop** only; Google + Play draws hillshade from downloaded offline terrain instead. Useful for understanding why a link + fails, since LoRa range is limited by terrain ### Adding your own tile source @@ -156,7 +157,7 @@ Any XYZ tile endpoint can be added as a base map, on every flavor and on desktop Tile Sources** at the foot of the base map picker and paste a URL template using `{z}`, `{x}` and `{y}` — plus `{s}` if the provider uses rotating subdomains. A national mapping service, for example: -``` +```text https://wmts.geo.admin.ch/1.0.0/ch.swisstopo.pixelkarte-farbe/default/current/3857/{z}/{x}/{y}.jpeg ``` @@ -173,7 +174,18 @@ On **Google Play**, the layers sheet's **Offline Manager** section downloads its — water, roads and administrative boundaries extracted from the public Protomaps basemap dataset, drawn directly as map shapes rather than raster tiles. Frame the area you want, tap **Start Download**, and once it finishes flip **Show on map** to draw it (it's off by default so it never surprises you by covering up the -live map). You can still import a pre-made `.mbtiles` archive here too, as before. **Desktop** has neither. +live map). You can still import a pre-made `.mbtiles` archive here too, as before. **Desktop** has neither, though +the offline terrain described next does work there. + +### Offline terrain + +Hillshade and elevation contours can be downloaded for offline use as well, so a downloaded area still +shows why a link fails when there is no network. The terrain data comes from Mapterhorn and is credited on +the map. On **Google Play**, each downloaded region in the **Offline Manager** list has a **Download Terrain** +button and **Hillshade** and **Contours** switches; terrain attaches to that region and shares its storage limit, and an area +too large for terrain asks you to zoom in or pick a smaller region. On **F-Droid** and **Desktop**, the +layers sheet has an **Offline Terrain** section of its own: frame the area, tap **Start Download**, and the +hillshade and contours draw from the downloaded tiles from then on. ### Going offline @@ -181,8 +193,8 @@ An **Offline** pill appears over the map whenever the device has no network conn builds, if you've imported a local `.mbtiles` archive, the map switches to it automatically the moment the network drops — no toggle to remember — and switches back once you're reconnected, as long as you haven't picked a different base map yourself in between. On **F-Droid**, a downloaded offline area keeps rendering on -its own; the pill is purely informational there. **Desktop** has no offline downloads at all yet, so the pill -is informational there too. +its own; the pill is purely informational there. **Desktop** downloads offline terrain but no base-map +packs, so the pill is informational there too. ## Related Topics diff --git a/docs/hr-rHR/user/messages-and-channels.md b/docs/hr-rHR/user/messages-and-channels.md index 1fefd6c4a9..1936a29ee7 100644 --- a/docs/hr-rHR/user/messages-and-channels.md +++ b/docs/hr-rHR/user/messages-and-channels.md @@ -2,7 +2,7 @@ title: Messages & Channels parent: User Guide nav_order: 3 -last_updated: 2026-08-30 +last_updated: 2026-09-14 description: Send and receive messages, manage channels, configure encryption, search conversations, and use quick chat, reactions, and message actions. aliases: - channels @@ -50,6 +50,21 @@ Key length alone does not change the icon: a 128-bit key and a 256-bit key both Tapping an existing channel opens the same editor, where you can change the name, the PSK, MQTT uplink and downlink, and position precision. Every edit on this screen — adding, editing, deleting, or dragging a channel into a new order — waits on **Send** the same way. +### Archived Channels + +A conversation belongs to the channel it was held on, not to the slot that channel occupied. When a channel leaves your radio — you scan a different channel QR, replace a channel's key, or switch the modem preset — its conversation is **archived** rather than deleted or merged into whatever takes the slot. + +An archived conversation: + +- stays in the **Channels** list under the name the channel had, below the channels currently on your radio, marked with a history icon; +- keeps its full message history, searchable as usual; +- is read-only — you cannot send, reply, or react, because the channel is no longer on the radio to send on, and it is not offered as a share target; +- comes back automatically if you re-add that exact channel, taking its messages back to the live slot. + +Changing the modem preset archives the channel too: the preset is part of a default channel's identity, so **LongFast** and **MediumFast** are different channels even though neither the name nor the key changed. Changing only the region does not archive anything. + +To remove an archived conversation for good, long-press it in the conversation list and delete it. + ## Direct Messages Direct messages (DMs) go to one specific node. When both radios hold each other's public keys, your radio encrypts the message to that node's public key, so no one else on the mesh can read it — not even nodes that share your channel. diff --git a/docs/hr-rHR/user/mqtt.md b/docs/hr-rHR/user/mqtt.md index a1ad507780..b76237c878 100644 --- a/docs/hr-rHR/user/mqtt.md +++ b/docs/hr-rHR/user/mqtt.md @@ -2,7 +2,7 @@ title: MQTT parent: User Guide nav_order: 11 -last_updated: 2026-08-30 +last_updated: 2026-09-11 description: Bridge your mesh to the internet — MQTT broker setup, encryption layers, and map reporting. aliases: - mqtt @@ -25,7 +25,7 @@ The MQTT module connects your node to an MQTT broker, allowing: ## How It Works -``` +```text [Your Node] → Radio → [Gateway Node with Wi-Fi] → MQTT Broker → [Remote Gateway] → Radio → [Remote Node] ``` diff --git a/docs/hr-rHR/user/node-metrics.md b/docs/hr-rHR/user/node-metrics.md index 464879c8ca..09910afb78 100644 --- a/docs/hr-rHR/user/node-metrics.md +++ b/docs/hr-rHR/user/node-metrics.md @@ -2,7 +2,7 @@ title: Node Metrics parent: User Guide nav_order: 5 -last_updated: 2026-08-30 +last_updated: 2026-09-09 description: Telemetry dashboards for each mesh node — device health, environment sensors, air quality, signal quality, power, traceroute, and position history. aliases: - metrics @@ -114,7 +114,7 @@ Radio signal quality information: ### Signal Quality Reference -Signal quality is rated from **SNR relative to the active LoRa modem preset's demodulation floor**, not from fixed thresholds — a given SNR means different things on different presets (e.g. −15 dB is fine on LongSlow but unusable on ShortFast). RSSI is shown but is not part of the rating. In the table, _limit_ is the preset's SNR limit. +Signal quality is rated from **SNR relative to the active LoRa modem preset's demodulation floor**, not from fixed thresholds — a given SNR means different things on different presets (e.g. −15 dB is fine on LongSlow but unusable on ShortFast). When RSSI and a noise-floor reading are both available, the app also rates their difference against the preset limit and uses the worse rating. Otherwise, RSSI is display-only. In the table, _limit_ is the preset's SNR limit. | Quality | Criteria | | ------- | ---------------------------------------------------------------- | diff --git a/docs/hr-rHR/user/nodes.md b/docs/hr-rHR/user/nodes.md index e86fed7f9a..513da64c12 100644 --- a/docs/hr-rHR/user/nodes.md +++ b/docs/hr-rHR/user/nodes.md @@ -2,7 +2,7 @@ title: Nodes parent: User Guide nav_order: 4 -last_updated: 2026-09-04 +last_updated: 2026-09-11 description: Browse, filter, and sort mesh nodes — view details, signal quality, roles, and quick actions. aliases: - node-list @@ -74,19 +74,23 @@ Most users should keep the default **Client** role. Consider a different role wh > 💡 **Tip:** The mesh works best when most nodes are **Client** or **Router**. Too many Client Mute nodes reduce mesh resilience; too many Routers in a dense area can cause congestion. A good rule of thumb: one Router per 5–10 Clients in your area. -### Encryption Indicators +### Security indicators -Nodes display encryption status icons next to their name: +Each node carries one security icon beside its name in the node list. Tap it to read what it means, and choose **Show All Meanings** in that dialog for the full legend. The detail screen shows the same state in words, as a **Security** row that opens the same dialog. -| Icon | Meaning | -| ----------- | ------------------------------------------------------------------------------------------------------------------------------------------------- | -| 🔒 Locked | Communication uses PKI (public key infrastructure) — end-to-end encrypted with verified identity | -| 🔓 Unlocked | No public key has been received for this node, so it cannot be direct messaged — use **Request User Info** on the node detail page to ask for one | -| ⚠️ Mismatch | Public key mismatch — the node's key has changed since last seen (investigate before trusting) | +| Icon | Meaning | Shown for | +| --------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------- | +| Person with a shield check (green) | **Verified contact** — you verified this node's key in person by exchanging contact QR codes, so its identity is confirmed. The strongest trust the list shows. Your own connected node carries it too | Any firmware version | +| Nodes icon with a shield check (green) | **Signed node** — this node signs its broadcasts with its identity key, so its identity is consistent over time, but you have not verified it in person. On firmware 2.8 the icon appears from the version alone, before any signed broadcast has been heard | Firmware 2.8 or newer, and any node whose signed broadcast your node has verified | +| 🔒 Closed lock | A public key is on file and matches, so direct messages to this node are encrypted | Firmware before 2.8, or no reported version | +| 🔓 Open lock | No public key has been received for this node, so it cannot be direct messaged — use **Request User Info** on the node detail page to ask for one | Firmware before 2.8, or no reported version | +| ⚠️ Mismatch | **Public key mismatch** — a different key arrived for this node after one was stored. Investigate before trusting | Any firmware version | -> 💡 **Tip:** Direct messages always use PKI, so the radio needs the other node's public key before it can send one. It refuses the send rather than falling back to channel encryption. Keys arrive inside node info, which is why an open lock usually clears itself once that node is heard from properly. If you see a key mismatch warning, the node may have been reset or compromised. +Every node on firmware 2.8 or newer signs its broadcasts, so on that firmware the signed state is the baseline and the locks are not shown. -To clear a mismatch, first confirm through another trusted channel that the key change was intentional — a factory reset causes one. Then touch & hold the node, choose **Remove**, and let the two radios exchange keys again the next time yours hears it. +Direct messages always use public-key encryption, so your node needs the other node's public key before it can send one. It refuses the send rather than falling back to channel encryption. Keys arrive inside node info, which is why an open lock usually clears itself once that node is heard from properly. + +A mismatch never replaces the key you already hold. The app keeps the first key it recorded and refuses the new one, as the firmware does, so a stray or hostile node info cannot silently break encrypted messaging to a contact. To clear a mismatch, first confirm through another trusted channel that the key change was intentional — a factory reset causes one. Then touch & hold the node, choose **Remove**, and let the two nodes exchange keys again the next time yours hears it. ## Quick Actions @@ -108,7 +112,9 @@ radio is connected and running firmware 2.8 or newer — see ## Sharing a Contact -On a node's detail screen, tap **Share Contact** to produce a link and a QR code for that node. From the same dialog, **Write to NFC tag** saves the link to a writable NFC tag that anyone can tap to open. +On a node's detail screen, tap **Share Contact** to produce a link and a QR code for that node. From the same dialog, **Share link** opens the Android share sheet (on desktop it copies the link instead), **Write to NFC tag** saves it to a writable NFC tag, and **Copy** puts it on the clipboard. While that dialog is open and in front of you, the phone also offers the same link to any NFC reader, so someone can take the contact by tapping their phone against yours with no tag involved. + +Sharing your own contact this way marks it as verified in person, so whoever imports it sees the **Verified contact** icon rather than the signed one. Relaying someone else's contact passes on only what your app had already recorded about them. To add someone else's contact, use the import button on the node list and choose **Scan Shared Contact QR Code**, **Scan Shared Contact NFC**, or **Input Shared Contact URL**. The app asks you to confirm with **Import Shared Contact?**, and warns you when the contact is one you already have. @@ -120,14 +126,16 @@ Type in the search field to filter nodes by name or short name. The filter updat ### Filter Toggles -| Filtriraj | Opis | -| --------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| **Hide offline nodes** | Show only nodes heard within the last 2 hours | -| **Only show direct nodes** | Show only nodes your radio heard directly, with no relay in between | -| **Include unknown** | Show nodes that haven't sent user info yet. **On by default**, so a node heard before its info arrives stays visible; these carry a badge marking them incomplete, and cannot be direct messaged until their user info brings a public key | -| **Exclude infrastructure** | Hide infrastructure-role nodes (Router, Router Late, Client Base, and legacy Repeater nodes) and any node that cannot be messaged, whatever its role | -| **Exclude MQTT** | Hide nodes heard only via MQTT internet bridge | -| **Only show ignored Nodes** | Replace the list with the nodes you have ignored. Every other node is hidden while this is on, and a banner appears at the top of the list to take you back | +| Filtriraj | Opis | +| --------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **Hide offline nodes** | Show only nodes heard within the last 2 hours | +| **Only show direct nodes** | Show only nodes your radio heard directly, with no relay in between | +| **Include unknown** | Show nodes that haven't sent user info yet. **On by default**, so a node heard before its info arrives stays visible; these carry a badge marking them incomplete, and cannot be direct messaged until their user info brings a public key | +| **Exclude infrastructure** | Hide infrastructure-role nodes (Router, Router Late, Client Base, and legacy Repeater nodes) and any node that cannot be messaged, whatever its role | +| **Exclude MQTT** | Hide nodes heard only via MQTT internet bridge | +| **Signed only** | Show only nodes whose signed broadcasts your node has actually heard and verified. Stricter than the icon: a node on 2.8 shows as signed by its firmware version before any signed broadcast arrives, and a contact verified in person on older firmware is not signed at all | +| **Encrypted only** | Show nodes with a matching public key on file, the key an encrypted direct message needs. A node with a key mismatch is excluded | +| **Only show ignored Nodes** | Replace the list with the nodes you have ignored. Every other node is hidden while this is on, and a banner appears at the top of the list to take you back | ### Sort Options @@ -149,6 +157,8 @@ Tap the hop-histogram icon in the node list's app bar to open a bar chart of how Tapping a node opens the detail view with comprehensive information. See [Node Metrics](node-metrics) for full details on metrics and telemetry. +Signal quality is rated against your modem preset. The same SNR can be good on a long-range preset and poor on a faster one. Traceroute and neighbor-info SNR colors use that preset too. RSSI text has its own strength colors; it affects the quality rating only when a noise-floor reading is also available. + The Details card carries the node's short name, role, IDs, last heard time, hops away, uptime, and its SNR and RSSI: ![Node detail section](../../assets/screenshots/nodes_detail_section.png) diff --git a/docs/hr-rHR/user/settings-module-admin.md b/docs/hr-rHR/user/settings-module-admin.md index 9bb294e569..190f63c959 100644 --- a/docs/hr-rHR/user/settings-module-admin.md +++ b/docs/hr-rHR/user/settings-module-admin.md @@ -2,7 +2,7 @@ title: Settings — Modules & Admin parent: User Guide nav_order: 8 -last_updated: 2026-08-30 +last_updated: 2026-09-16 description: Configure optional feature modules (MQTT, telemetry, canned messages, TAK, and more) and perform device administration. aliases: - modules @@ -333,7 +333,7 @@ appear only when your own node is selected: ### O programu -**Settings → About** carries the app's own identity rather than the radio's: +**Settings → About** carries the app's own identity rather than the node's: Three sections: @@ -341,7 +341,11 @@ Three sections: - **Apps** — opens with **Need Hardware?**, a rotating carousel of popular devices that links out to where to buy one. It also lists the GitHub repository, the running app version, and **Acknowledgements** (see the next section). -- **Project information** — links to the website and to this documentation. +- **Project information** — links to the website, to this documentation, and to the GNU GPL v3 + license the app is released under. + +The footer states the copyright and that the app is free software with no warranty, which you +may redistribute under the same license. ### Acknowledgements diff --git a/docs/hr-rHR/user/settings-radio-user.md b/docs/hr-rHR/user/settings-radio-user.md index 7b95f42258..2675696dcc 100644 --- a/docs/hr-rHR/user/settings-radio-user.md +++ b/docs/hr-rHR/user/settings-radio-user.md @@ -2,7 +2,7 @@ title: Settings — Radio & User parent: User Guide nav_order: 7 -last_updated: 2026-09-04 +last_updated: 2026-09-11 description: Configure your radio hardware, LoRa presets, user profile, position sharing, power management, and security. aliases: - settings @@ -51,7 +51,7 @@ The footer appears as soon as you change something. **Discard** throws the chang The status message is saved with the same **Save**, but it never reboots the node — and, like the rest of this screen, it can be edited on a remote node you administer. For your own radio there is a shortcut while it is connected: touch & hold your node in the [node list](nodes.md) and choose -**Update status**. Older firmware and a disconnected radio have no shortcut — the field above is +**Update status**. Older firmware and a disconnected node have no shortcut — the field itself is still the way in. ## Configuration @@ -77,13 +77,13 @@ On **Settings → LoRa**. | Setting | Opis | Zadano | | ------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------- | -| Regija | Regulatory region for frequency bands. You must set this before transmitting | Unset (must configure) | +| Regija | Regulatory region for frequency bands. You must set this before transmitting. On firmware 2.8 or newer, the first region set also creates the node's identity key and gives it a new node number | Unset (must configure) | | Presets | Speed/range tradeoff | LongFast | | Number of Hops | Maximum retransmit hops | 3 | | Transmit Power | Transmission power (dBm); 0 = max allowed for region | 0 (region max) | | Frequency Override | Overrides the computed operating frequency outright (MHz). It does not offset the calculated value — leave at 0 unless you know you need a specific frequency | 0 (use calculated) | | Use Preset | On by default. Turn it off to set Spread Factor, Coding Rate and Bandwidth by hand instead of taking them from the modem preset | On | -| Spread Factor | Manual mode only: 7–12. Higher spreads further but slower | From preset | +| Spread Factor | Manual mode only: 5–12. Higher spreads further but slower. On SX127x (RF95) radios the firmware does not accept 5 or 6 and uses 11 instead | From preset | | Coding Rate | Manual mode only: 5–8. More redundancy costs airtime | From preset | | Bandwidth | Manual mode only: the channel bandwidth in kHz, typed in directly. On the 2.4 GHz region the app offers a list of the bandwidths your radio supports instead, and a stored value that is not on that list shows as _Unsupported_ and blocks saving until you pick a supported one | From preset | | Frequency Slot | Which slot within the region's band to use. 0 derives it from the primary channel name | 0 (automatic) | @@ -111,13 +111,13 @@ The Lite, Narrow, Medium Turbo, and Tiny presets need firmware 2.8 or newer — | Short Slow | ~5 km | 6.25 kbps | −10 dB | Suburban short-range; moderate building density | | Medium Fast | ~5 km | 3.52 kbps | −12.5 dB | Suburban areas; moderate building density | | Medium Slow | ~8 km | 1.95 kbps | −15 dB | Suburban/rural; moderate range with slower speed | -| Long Turbo | ~10 km | 1.34 kbps | −12.5 dB | Similar range to Long Fast but with 500 kHz bandwidth; faster throughput | +| Long Turbo | ~10 km | 1.34 kbps | −17.5 dB | Similar range to Long Fast but with 500 kHz bandwidth; faster throughput | | Long Fast | ~10 km | 1.1 kbps | −17.5 dB | **General use (default)** — balanced range and speed | | Long Moderate | ~20 km | 0.34 kbps | −17.5 dB | Rural with some terrain; occasional use | | Lite Fast | ~5 km | 1.76 kbps | −12.5 dB | EU 866 MHz SRD band (125 kHz BW); comparable to Medium Fast | | Lite Slow | ~10 km | 0.98 kbps | −15 dB | EU 866 MHz SRD band (125 kHz BW); comparable to Long Fast | -| Narrow Fast | ~5 km | 2.28 kbps | −10 dB | EU 868 MHz band (62.5 kHz BW); avoids interference with other devices | -| Narrow Slow | ~10 km | 1.30 kbps | −12.5 dB | EU 868 MHz band (62.5 kHz BW); comparable to Long Fast | +| Narrow Fast | ~5 km | 2.28 kbps | −7.5 dB | EU 868 MHz band (62.5 kHz BW); avoids interference with other devices | +| Narrow Slow | ~10 km | 1.30 kbps | −10 dB | EU 868 MHz band (62.5 kHz BW); comparable to Long Fast | | Medium Turbo | ~5 km | 7.0 kbps | −12.5 dB | Like Medium Fast but with 500 kHz bandwidth; not legal in every region. Needs firmware 2.8 or newer | | Tiny Fast | ~10 km | 0.68 kbps | −7.5 dB | Amateur bands that cap occupied bandwidth; these presets use 15.6 kHz. Needs firmware 2.8 or newer, an SX126x or SX127x radio, and a TCXO of ±5 ppm or better | | Tiny Slow | ~20 km | 0.33 kbps | −10 dB | Same band restrictions as Tiny Fast, longer range. Same firmware, radio, and TCXO requirements | diff --git a/docs/hr-rHR/user/signal-meter.md b/docs/hr-rHR/user/signal-meter.md index c4e7c190bd..12d6aaade0 100644 --- a/docs/hr-rHR/user/signal-meter.md +++ b/docs/hr-rHR/user/signal-meter.md @@ -2,7 +2,7 @@ title: How the Meshtastic Signal Meter Works parent: User Guide nav_order: 15 -last_updated: 2026-08-30 +last_updated: 2026-09-09 description: How the signal meter rates quality from SNR relative to the LoRa modem preset — spread spectrum, presets, and what the bars really mean. aliases: - signal @@ -32,7 +32,7 @@ Each modem preset has an SNR limit: the lowest SNR at which that preset can stil ## Rating Signal Quality -The app rates signal quality (None, Bad, Fair, or Good) from SNR alone, measured against the active preset's SNR limit. It does not factor in RSSI: without knowing the local noise floor, RSSI alone cannot say whether a signal is decodable. RSSI is still available — on the node detail screen and in the metrics charts. +The app rates signal quality (None, Bad, Fair, or Good) from SNR relative to the active preset's SNR limit. When RSSI and a noise-floor reading are both available, it also rates the difference between them against that limit and uses the worse rating. Otherwise, it uses SNR alone. Because the rating is relative to the preset, the same SNR rates differently on different presets. An SNR of −16 dB rates Good on Long Fast (SNR limit −17.5 dB) but None on Short Fast (SNR limit −7.5 dB). Let `limit` be the active preset's SNR limit: @@ -45,7 +45,7 @@ Because the rating is relative to the preset, the same SNR rates differently on The icon never goes blank, so count the bars carefully: a single bar means None, not a weak but usable link, and Good is a solid wedge rather than a set of bars. A gray three-bar icon labeled Unknown is a different state again — the packet carried no SNR measurement at all, which is not the same as measuring one and finding it too weak. -> ℹ️ **Note:** Traceroute hop colors use fixed thresholds (−7 dB / −15 dB); the per-node signal meter uses the preset-relative rating instead. +> ℹ️ **Note:** Traceroute and neighbor-info SNR colors use the active preset's limits too. RSSI text has separate strength colors: green above −115 dBm, yellow above −120 dBm, orange above −126 dBm, and red at or below −126 dBm. ## Diagnosing Local Interference diff --git a/docs/hr-rHR/user/tak.md b/docs/hr-rHR/user/tak.md index d58d8d5836..1bc2ac7f03 100644 --- a/docs/hr-rHR/user/tak.md +++ b/docs/hr-rHR/user/tak.md @@ -2,7 +2,7 @@ title: TAK Integration parent: User Guide nav_order: 10 -last_updated: 2026-08-30 +last_updated: 2026-09-11 description: Interoperate with ATAK and WinTAK — CoT position sharing, TAK roles, and plugin setup. aliases: - tak @@ -36,7 +36,7 @@ The TAK module allows Meshtastic nodes to: > ⚠️ **Warning:** The old **Meshtastic ATAK Plugin** is no longer part of this path and cannot > work. It bridged through the cross-process AIDL API, which was removed in app 2.8.0; the mesh > service is now in-process only. Do not install it. Interop today runs over the app's own local -> TAK server plus the Mesh to CoT Converter, both described below, with stock ATAK/iTAK/WinTAK. +> TAK server plus the Mesh to CoT Converter, both described in the following sections, with stock ATAK/iTAK/WinTAK. ### Configuration diff --git a/docs/hr-rHR/user/translate.md b/docs/hr-rHR/user/translate.md index 67d69e2d96..cdc8b6d87f 100644 --- a/docs/hr-rHR/user/translate.md +++ b/docs/hr-rHR/user/translate.md @@ -2,7 +2,7 @@ title: Translate the App parent: User Guide nav_order: 17 -last_updated: 2026-08-30 +last_updated: 2026-09-11 description: How the app and its documentation are translated via Crowdin, and guidelines for contributing translations. aliases: - translate @@ -45,7 +45,7 @@ If your language is not yet listed on Crowdin: The Android app uses **Compose Multiplatform resources** for all user-visible strings: -``` +```text core/resources/src/commonMain/composeResources/ ├── values/ ← English (default) │ └── strings.xml @@ -58,7 +58,7 @@ core/resources/src/commonMain/composeResources/ In-app documentation follows a similar pattern under `docs/`: -``` +```text docs/ ├── en/user/ ← English source (default) │ ├── onboarding.md diff --git a/docs/ht-rHT/user/desktop.md b/docs/ht-rHT/user/desktop.md index efca3c03c9..2ec0d98037 100644 --- a/docs/ht-rHT/user/desktop.md +++ b/docs/ht-rHT/user/desktop.md @@ -2,7 +2,7 @@ title: Desktop App parent: User Guide nav_order: 14 -last_updated: 2026-08-30 +last_updated: 2026-09-11 description: Install and use the Meshtastic Desktop app on Linux, macOS, and Windows — connections, feature parity, and keyboard shortcuts. aliases: - desktop @@ -58,20 +58,20 @@ Bluetooth Low Energy is supported on desktop via the [Kable](https://github.com/ ## Feature Parity -| Feature | Android | Desktop | Notes | -| ----------------------------------------------------- | ------- | ------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Messaging | ✓ | ✓ | Full parity | -| Node List | ✓ | ✓ | Full parity | -| Map | ✓ | ✓ | Interactive MapLibre map, with base map and overlay pickers and custom tile sources. No offline downloads or local `.mbtiles` archives | -| Map layers (`.kml`/`.kmz`/GeoJSON) | ✓ | ✓ | Same layer store and sheet as Android; imported files draw on the desktop map | -| Site Planner | ✓ | ✓\* | \*Opens in your browser on desktop; the estimate is not drawn on the desktop map | -| Settings | ✓ | ✓ | Full parity | -| Bluetooth (BLE) | ✓ | ✓ | Via Kable on desktop | -| Firmware Update | ✓ | ✓ | In-app USB, BLE, and Wi-Fi (ESP32) update work the same as Android. The USB maintenance flow — nRF52/RP2040 factory erase and bootloader upgrade — is Android-only | -| Notifications | ✓ | ✓ | Native OS notifications | -| Widgets | ✓ | ✗ | Android-only | -| AI Assistant (Chirpy) | ✓\* | ✗ | Google flavor Android only | -| App Functions (system AI) | ✓† | ✗ | Google flavor Android only | +| Feature | Android | Desktop | Notes | +| ----------------------------------------------------- | ------- | ------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Messaging | ✓ | ✓ | Full parity | +| Node List | ✓ | ✓ | Full parity | +| Map | ✓ | ✓ | Interactive MapLibre map, with base map and overlay pickers and custom tile sources. Offline terrain (hillshade and contours) can be downloaded; offline base-map packs and local `.mbtiles` archives cannot | +| Map layers (`.kml`/`.kmz`/GeoJSON) | ✓ | ✓ | Same layer store and sheet as Android; imported files draw on the desktop map | +| Site Planner | ✓ | ✓\* | \*Opens in your browser on desktop; the estimate is not drawn on the desktop map | +| Settings | ✓ | ✓ | Full parity | +| Bluetooth (BLE) | ✓ | ✓ | Via Kable on desktop | +| Firmware Update | ✓ | ✓ | In-app USB, BLE, and Wi-Fi (ESP32) update work the same as Android. The USB maintenance flow — nRF52/RP2040 factory erase and bootloader upgrade — is Android-only | +| Notifications | ✓ | ✓ | Native OS notifications | +| Widgets | ✓ | ✗ | Android-only | +| AI Assistant (Chirpy) | ✓\* | ✗ | Google flavor Android only | +| App Functions (system AI) | ✓† | ✗ | Google flavor Android only | \*Chirpy AI requires Android 14+ on Google flavor builds with supported hardware. @@ -122,7 +122,7 @@ Individual doc pages render with full formatting: ## Building from Source -```bash +```shell git clone https://github.com/meshtastic/Meshtastic-Android.git cd Meshtastic-Android ./gradlew :desktopApp:run @@ -135,7 +135,7 @@ Requirements: ## Known Limitations -- Offline tile downloads and local `.mbtiles` archives are not available on desktop. +- Offline base-map downloads and local `.mbtiles` archives are not available on desktop. Offline terrain is — see [Map & Waypoints](map-and-waypoints). - `.kml`/`.kmz`/GeoJSON layer import works — see [Map & Waypoints](map-and-waypoints#map-layers). Site Planner opens in your browser rather than in the app; to bring its coverage estimate onto the map, click the transmitter pin diff --git a/docs/ht-rHT/user/discovery.md b/docs/ht-rHT/user/discovery.md index 80345a2e64..12edafcbe7 100644 --- a/docs/ht-rHT/user/discovery.md +++ b/docs/ht-rHT/user/discovery.md @@ -2,7 +2,7 @@ title: Local Mesh Discovery parent: User Guide nav_order: 12 -last_updated: 2026-08-30 +last_updated: 2026-09-11 description: Explore your mesh network — the Local Mesh Discovery scanner, traceroute paths, neighbor maps, and node discovery tools. aliases: - discovery @@ -97,7 +97,7 @@ Mesh Beacon lets nodes invite others to join their mesh. A beaconing node period Configure it under **Settings → Module configuration → Mesh Beacon**. The entry appears only on radios running firmware 2.8.0 or newer. A read-only **Region** row at the top of the screen shows the region the beacon advertises: that region, and the preset, are always the ones the radio itself uses, so a beacon cannot invite anyone onto settings your radio is not running. - **Listen for beacons** — receive invitations broadcast by other nodes. -- **Broadcast a beacon** — periodically advertise this mesh to nearby nodes, with an optional **Beacon message** of up to 100 bytes, a **Broadcast interval** picked from fixed intervals between 1 hour and 72 hours, and an **Offered channel** chosen from your radio's own channels. The offered channel is required, and defaults to your primary channel. +- **Broadcast a beacon** — periodically advertise this mesh to nearby nodes, with an optional **Beacon message** of up to 100 bytes, a **Broadcast interval** picked from fixed intervals between 1 hour and 72 hours, and an **Offered channel** chosen from your radio's own channels. The offered channel is required, and defaults to your primary channel. Over remote admin the picker offers the primary channel only. - **Broadcast targets** — optional extra destinations beyond the offered channel. **Add target** appends a row; each row picks a **Channel** and a **Transmit preset**, and **Remove target** deletes it. With no targets, the beacon goes out on the offered channel alone. Two conditions block beacon setup: diff --git a/docs/ht-rHT/user/help-and-docs.md b/docs/ht-rHT/user/help-and-docs.md index f306cde7f7..c83898cea3 100644 --- a/docs/ht-rHT/user/help-and-docs.md +++ b/docs/ht-rHT/user/help-and-docs.md @@ -2,7 +2,7 @@ title: Help & In-App Docs parent: User Guide nav_order: 21 -last_updated: 2026-08-30 +last_updated: 2026-09-11 description: Browse this documentation inside the app, search it, and ask Chirpy — the on-device AI assistant — questions about Meshtastic. aliases: - help @@ -23,7 +23,7 @@ The docs browser lists every user-guide page. Tap a page to read it; images and ### Search -The search box sits at the top of the docs browser. Type in it to filter pages by title and keywords — results update as you type, and the **Clear search** button (✕) empties the box. +Tap the search field at the top of the docs browser to open a full-screen search view. Type to filter pages by title and keywords — results update as you type, under the same field. The back arrow, or the keyboard's search action, closes the view. The field hides while you scroll down the page list and comes back when you scroll up. ![Searching the in-app documentation](../../assets/screenshots/docs-browser_search.png) diff --git a/docs/ht-rHT/user/map-and-waypoints.md b/docs/ht-rHT/user/map-and-waypoints.md index b93fa22f67..024ec5d7b5 100644 --- a/docs/ht-rHT/user/map-and-waypoints.md +++ b/docs/ht-rHT/user/map-and-waypoints.md @@ -2,7 +2,7 @@ title: Map & Waypoints parent: User Guide nav_order: 6 -last_updated: 2026-09-01 +last_updated: 2026-09-11 description: View node positions on the map, create and share waypoints, manage map layers and Site Planner, and control position sharing and privacy. aliases: - map @@ -44,7 +44,7 @@ The floating toolbar provides quick access to the compass, the map type and laye ### Filtering the Map -Tap the filter button in the floating toolbar to open **Filter map**. **Display** controls what is drawn: **Only Favorites**, **Show Waypoints**, **Show Precision Circles**, and a slider that hides nodes not heard from recently. **Node roles** is a chip per device role, plus **All** to show every role; a selected chip means that role is shown. **Nodes** narrows the set further with **Hide offline nodes**, **Only show direct nodes**, **Exclude MQTT**, **Show ignored nodes**, and **Include unknown**. +Tap the filter button in the floating toolbar to open **Filter map**. **Display** controls what is drawn: **Only Favorites**, **Show Waypoints**, **Show Precision Circles**, and a slider that hides nodes not heard from recently. **Node roles** is a chip per device role, plus **All** to show every role; a selected chip means that role is shown. **Nodes** narrows the set further with **Hide offline nodes**, **Only show direct nodes**, **Signed only**, **Encrypted only**, **Exclude MQTT**, **Show ignored nodes**, and **Include unknown** — the same rules as the node list's [filter toggles](nodes). A dot on the filter button means at least one filter is hiding something — check it before concluding the mesh is quiet. Turning **Show Waypoints** off hides every waypoint, including your own. **Show ignored nodes** adds them to the map rather than showing only them — unlike the node list's **Only show ignored Nodes**. @@ -147,8 +147,9 @@ picker, all three offer the same raster base maps: Overlays can be toggled on top of any base map, from the layers sheet: - **Weather radar** — NOAA NEXRAD reflectivity (US coverage) -- **Hillshade** — terrain relief, on **F-Droid** and **Desktop** only. Useful for understanding why a - link fails, since LoRa range is limited by terrain +- **Hillshade** — terrain relief from an online tile source, on **F-Droid** and **Desktop** only; Google + Play draws hillshade from downloaded offline terrain instead. Useful for understanding why a link + fails, since LoRa range is limited by terrain ### Adding your own tile source @@ -156,7 +157,7 @@ Any XYZ tile endpoint can be added as a base map, on every flavor and on desktop Tile Sources** at the foot of the base map picker and paste a URL template using `{z}`, `{x}` and `{y}` — plus `{s}` if the provider uses rotating subdomains. A national mapping service, for example: -``` +```text https://wmts.geo.admin.ch/1.0.0/ch.swisstopo.pixelkarte-farbe/default/current/3857/{z}/{x}/{y}.jpeg ``` @@ -173,7 +174,18 @@ On **Google Play**, the layers sheet's **Offline Manager** section downloads its — water, roads and administrative boundaries extracted from the public Protomaps basemap dataset, drawn directly as map shapes rather than raster tiles. Frame the area you want, tap **Start Download**, and once it finishes flip **Show on map** to draw it (it's off by default so it never surprises you by covering up the -live map). You can still import a pre-made `.mbtiles` archive here too, as before. **Desktop** has neither. +live map). You can still import a pre-made `.mbtiles` archive here too, as before. **Desktop** has neither, though +the offline terrain described next does work there. + +### Offline terrain + +Hillshade and elevation contours can be downloaded for offline use as well, so a downloaded area still +shows why a link fails when there is no network. The terrain data comes from Mapterhorn and is credited on +the map. On **Google Play**, each downloaded region in the **Offline Manager** list has a **Download Terrain** +button and **Hillshade** and **Contours** switches; terrain attaches to that region and shares its storage limit, and an area +too large for terrain asks you to zoom in or pick a smaller region. On **F-Droid** and **Desktop**, the +layers sheet has an **Offline Terrain** section of its own: frame the area, tap **Start Download**, and the +hillshade and contours draw from the downloaded tiles from then on. ### Going offline @@ -181,8 +193,8 @@ An **Offline** pill appears over the map whenever the device has no network conn builds, if you've imported a local `.mbtiles` archive, the map switches to it automatically the moment the network drops — no toggle to remember — and switches back once you're reconnected, as long as you haven't picked a different base map yourself in between. On **F-Droid**, a downloaded offline area keeps rendering on -its own; the pill is purely informational there. **Desktop** has no offline downloads at all yet, so the pill -is informational there too. +its own; the pill is purely informational there. **Desktop** downloads offline terrain but no base-map +packs, so the pill is informational there too. ## Related Topics diff --git a/docs/ht-rHT/user/messages-and-channels.md b/docs/ht-rHT/user/messages-and-channels.md index a207682ca2..2db2eb8b75 100644 --- a/docs/ht-rHT/user/messages-and-channels.md +++ b/docs/ht-rHT/user/messages-and-channels.md @@ -2,7 +2,7 @@ title: Messages & Channels parent: User Guide nav_order: 3 -last_updated: 2026-08-30 +last_updated: 2026-09-14 description: Send and receive messages, manage channels, configure encryption, search conversations, and use quick chat, reactions, and message actions. aliases: - channels @@ -50,6 +50,21 @@ Key length alone does not change the icon: a 128-bit key and a 256-bit key both Tapping an existing channel opens the same editor, where you can change the name, the PSK, MQTT uplink and downlink, and position precision. Every edit on this screen — adding, editing, deleting, or dragging a channel into a new order — waits on **Send** the same way. +### Archived Channels + +A conversation belongs to the channel it was held on, not to the slot that channel occupied. When a channel leaves your radio — you scan a different channel QR, replace a channel's key, or switch the modem preset — its conversation is **archived** rather than deleted or merged into whatever takes the slot. + +An archived conversation: + +- stays in the **Channels** list under the name the channel had, below the channels currently on your radio, marked with a history icon; +- keeps its full message history, searchable as usual; +- is read-only — you cannot send, reply, or react, because the channel is no longer on the radio to send on, and it is not offered as a share target; +- comes back automatically if you re-add that exact channel, taking its messages back to the live slot. + +Changing the modem preset archives the channel too: the preset is part of a default channel's identity, so **LongFast** and **MediumFast** are different channels even though neither the name nor the key changed. Changing only the region does not archive anything. + +To remove an archived conversation for good, long-press it in the conversation list and delete it. + ## Direct Messages Direct messages (DMs) go to one specific node. When both radios hold each other's public keys, your radio encrypts the message to that node's public key, so no one else on the mesh can read it — not even nodes that share your channel. diff --git a/docs/ht-rHT/user/mqtt.md b/docs/ht-rHT/user/mqtt.md index 2e7a6e53d3..b6c1d86644 100644 --- a/docs/ht-rHT/user/mqtt.md +++ b/docs/ht-rHT/user/mqtt.md @@ -2,7 +2,7 @@ title: MQTT parent: User Guide nav_order: 11 -last_updated: 2026-08-30 +last_updated: 2026-09-11 description: Bridge your mesh to the internet — MQTT broker setup, encryption layers, and map reporting. aliases: - mqtt @@ -25,7 +25,7 @@ The MQTT module connects your node to an MQTT broker, allowing: ## How It Works -``` +```text [Your Node] → Radio → [Gateway Node with Wi-Fi] → MQTT Broker → [Remote Gateway] → Radio → [Remote Node] ``` diff --git a/docs/ht-rHT/user/node-metrics.md b/docs/ht-rHT/user/node-metrics.md index ea807e9af3..a19011fd46 100644 --- a/docs/ht-rHT/user/node-metrics.md +++ b/docs/ht-rHT/user/node-metrics.md @@ -2,7 +2,7 @@ title: Node Metrics parent: User Guide nav_order: 5 -last_updated: 2026-08-30 +last_updated: 2026-09-09 description: Telemetry dashboards for each mesh node — device health, environment sensors, air quality, signal quality, power, traceroute, and position history. aliases: - metrics @@ -114,7 +114,7 @@ Radio signal quality information: ### Signal Quality Reference -Signal quality is rated from **SNR relative to the active LoRa modem preset's demodulation floor**, not from fixed thresholds — a given SNR means different things on different presets (e.g. −15 dB is fine on LongSlow but unusable on ShortFast). RSSI is shown but is not part of the rating. In the table, _limit_ is the preset's SNR limit. +Signal quality is rated from **SNR relative to the active LoRa modem preset's demodulation floor**, not from fixed thresholds — a given SNR means different things on different presets (e.g. −15 dB is fine on LongSlow but unusable on ShortFast). When RSSI and a noise-floor reading are both available, the app also rates their difference against the preset limit and uses the worse rating. Otherwise, RSSI is display-only. In the table, _limit_ is the preset's SNR limit. | Quality | Criteria | | ------- | ---------------------------------------------------------------- | diff --git a/docs/ht-rHT/user/nodes.md b/docs/ht-rHT/user/nodes.md index e60542f56a..21a368f0cb 100644 --- a/docs/ht-rHT/user/nodes.md +++ b/docs/ht-rHT/user/nodes.md @@ -2,7 +2,7 @@ title: Nodes parent: User Guide nav_order: 4 -last_updated: 2026-09-04 +last_updated: 2026-09-11 description: Browse, filter, and sort mesh nodes — view details, signal quality, roles, and quick actions. aliases: - node-list @@ -74,19 +74,23 @@ Most users should keep the default **Client** role. Consider a different role wh > 💡 **Tip:** The mesh works best when most nodes are **Client** or **Router**. Too many Client Mute nodes reduce mesh resilience; too many Routers in a dense area can cause congestion. A good rule of thumb: one Router per 5–10 Clients in your area. -### Encryption Indicators +### Security indicators -Nodes display encryption status icons next to their name: +Each node carries one security icon beside its name in the node list. Tap it to read what it means, and choose **Show All Meanings** in that dialog for the full legend. The detail screen shows the same state in words, as a **Security** row that opens the same dialog. -| Icon | Meaning | -| ----------- | ------------------------------------------------------------------------------------------------------------------------------------------------- | -| 🔒 Locked | Communication uses PKI (public key infrastructure) — end-to-end encrypted with verified identity | -| 🔓 Unlocked | No public key has been received for this node, so it cannot be direct messaged — use **Request User Info** on the node detail page to ask for one | -| ⚠️ Mismatch | Public key mismatch — the node's key has changed since last seen (investigate before trusting) | +| Icon | Meaning | Shown for | +| --------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------- | +| Person with a shield check (green) | **Verified contact** — you verified this node's key in person by exchanging contact QR codes, so its identity is confirmed. The strongest trust the list shows. Your own connected node carries it too | Any firmware version | +| Nodes icon with a shield check (green) | **Signed node** — this node signs its broadcasts with its identity key, so its identity is consistent over time, but you have not verified it in person. On firmware 2.8 the icon appears from the version alone, before any signed broadcast has been heard | Firmware 2.8 or newer, and any node whose signed broadcast your node has verified | +| 🔒 Closed lock | A public key is on file and matches, so direct messages to this node are encrypted | Firmware before 2.8, or no reported version | +| 🔓 Open lock | No public key has been received for this node, so it cannot be direct messaged — use **Request User Info** on the node detail page to ask for one | Firmware before 2.8, or no reported version | +| ⚠️ Mismatch | **Public key mismatch** — a different key arrived for this node after one was stored. Investigate before trusting | Any firmware version | -> 💡 **Tip:** Direct messages always use PKI, so the radio needs the other node's public key before it can send one. It refuses the send rather than falling back to channel encryption. Keys arrive inside node info, which is why an open lock usually clears itself once that node is heard from properly. If you see a key mismatch warning, the node may have been reset or compromised. +Every node on firmware 2.8 or newer signs its broadcasts, so on that firmware the signed state is the baseline and the locks are not shown. -To clear a mismatch, first confirm through another trusted channel that the key change was intentional — a factory reset causes one. Then touch & hold the node, choose **Remove**, and let the two radios exchange keys again the next time yours hears it. +Direct messages always use public-key encryption, so your node needs the other node's public key before it can send one. It refuses the send rather than falling back to channel encryption. Keys arrive inside node info, which is why an open lock usually clears itself once that node is heard from properly. + +A mismatch never replaces the key you already hold. The app keeps the first key it recorded and refuses the new one, as the firmware does, so a stray or hostile node info cannot silently break encrypted messaging to a contact. To clear a mismatch, first confirm through another trusted channel that the key change was intentional — a factory reset causes one. Then touch & hold the node, choose **Remove**, and let the two nodes exchange keys again the next time yours hears it. ## Quick Actions @@ -108,7 +112,9 @@ radio is connected and running firmware 2.8 or newer — see ## Sharing a Contact -On a node's detail screen, tap **Share Contact** to produce a link and a QR code for that node. From the same dialog, **Write to NFC tag** saves the link to a writable NFC tag that anyone can tap to open. +On a node's detail screen, tap **Share Contact** to produce a link and a QR code for that node. From the same dialog, **Share link** opens the Android share sheet (on desktop it copies the link instead), **Write to NFC tag** saves it to a writable NFC tag, and **Copy** puts it on the clipboard. While that dialog is open and in front of you, the phone also offers the same link to any NFC reader, so someone can take the contact by tapping their phone against yours with no tag involved. + +Sharing your own contact this way marks it as verified in person, so whoever imports it sees the **Verified contact** icon rather than the signed one. Relaying someone else's contact passes on only what your app had already recorded about them. To add someone else's contact, use the import button on the node list and choose **Scan Shared Contact QR Code**, **Scan Shared Contact NFC**, or **Input Shared Contact URL**. The app asks you to confirm with **Import Shared Contact?**, and warns you when the contact is one you already have. @@ -120,14 +126,16 @@ Type in the search field to filter nodes by name or short name. The filter updat ### Filter Toggles -| Filtre | Deskripsyon | -| --------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| **Hide offline nodes** | Show only nodes heard within the last 2 hours | -| **Only show direct nodes** | Show only nodes your radio heard directly, with no relay in between | -| **Include unknown** | Show nodes that haven't sent user info yet. **On by default**, so a node heard before its info arrives stays visible; these carry a badge marking them incomplete, and cannot be direct messaged until their user info brings a public key | -| **Exclude infrastructure** | Hide infrastructure-role nodes (Router, Router Late, Client Base, and legacy Repeater nodes) and any node that cannot be messaged, whatever its role | -| **Exclude MQTT** | Hide nodes heard only via MQTT internet bridge | -| **Only show ignored Nodes** | Replace the list with the nodes you have ignored. Every other node is hidden while this is on, and a banner appears at the top of the list to take you back | +| Filtre | Deskripsyon | +| --------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **Hide offline nodes** | Show only nodes heard within the last 2 hours | +| **Only show direct nodes** | Show only nodes your radio heard directly, with no relay in between | +| **Include unknown** | Show nodes that haven't sent user info yet. **On by default**, so a node heard before its info arrives stays visible; these carry a badge marking them incomplete, and cannot be direct messaged until their user info brings a public key | +| **Exclude infrastructure** | Hide infrastructure-role nodes (Router, Router Late, Client Base, and legacy Repeater nodes) and any node that cannot be messaged, whatever its role | +| **Exclude MQTT** | Hide nodes heard only via MQTT internet bridge | +| **Signed only** | Show only nodes whose signed broadcasts your node has actually heard and verified. Stricter than the icon: a node on 2.8 shows as signed by its firmware version before any signed broadcast arrives, and a contact verified in person on older firmware is not signed at all | +| **Encrypted only** | Show nodes with a matching public key on file, the key an encrypted direct message needs. A node with a key mismatch is excluded | +| **Only show ignored Nodes** | Replace the list with the nodes you have ignored. Every other node is hidden while this is on, and a banner appears at the top of the list to take you back | ### Sort Options @@ -149,6 +157,8 @@ Tap the hop-histogram icon in the node list's app bar to open a bar chart of how Tapping a node opens the detail view with comprehensive information. See [Node Metrics](node-metrics) for full details on metrics and telemetry. +Signal quality is rated against your modem preset. The same SNR can be good on a long-range preset and poor on a faster one. Traceroute and neighbor-info SNR colors use that preset too. RSSI text has its own strength colors; it affects the quality rating only when a noise-floor reading is also available. + The Details card carries the node's short name, role, IDs, last heard time, hops away, uptime, and its SNR and RSSI: ![Node detail section](../../assets/screenshots/nodes_detail_section.png) diff --git a/docs/ht-rHT/user/settings-module-admin.md b/docs/ht-rHT/user/settings-module-admin.md index bc271e523b..25ead49c9d 100644 --- a/docs/ht-rHT/user/settings-module-admin.md +++ b/docs/ht-rHT/user/settings-module-admin.md @@ -2,7 +2,7 @@ title: Settings — Modules & Admin parent: User Guide nav_order: 8 -last_updated: 2026-08-30 +last_updated: 2026-09-16 description: Configure optional feature modules (MQTT, telemetry, canned messages, TAK, and more) and perform device administration. aliases: - modules @@ -333,7 +333,7 @@ appear only when your own node is selected: ### Sou -**Settings → About** carries the app's own identity rather than the radio's: +**Settings → About** carries the app's own identity rather than the node's: Three sections: @@ -341,7 +341,11 @@ Three sections: - **Apps** — opens with **Need Hardware?**, a rotating carousel of popular devices that links out to where to buy one. It also lists the GitHub repository, the running app version, and **Acknowledgements** (see the next section). -- **Project information** — links to the website and to this documentation. +- **Project information** — links to the website, to this documentation, and to the GNU GPL v3 + license the app is released under. + +The footer states the copyright and that the app is free software with no warranty, which you +may redistribute under the same license. ### Acknowledgements diff --git a/docs/ht-rHT/user/settings-radio-user.md b/docs/ht-rHT/user/settings-radio-user.md index 4c711a4967..f26ef53f65 100644 --- a/docs/ht-rHT/user/settings-radio-user.md +++ b/docs/ht-rHT/user/settings-radio-user.md @@ -2,7 +2,7 @@ title: Settings — Radio & User parent: User Guide nav_order: 7 -last_updated: 2026-09-04 +last_updated: 2026-09-11 description: Configure your radio hardware, LoRa presets, user profile, position sharing, power management, and security. aliases: - settings @@ -51,7 +51,7 @@ The footer appears as soon as you change something. **Discard** throws the chang The status message is saved with the same **Save**, but it never reboots the node — and, like the rest of this screen, it can be edited on a remote node you administer. For your own radio there is a shortcut while it is connected: touch & hold your node in the [node list](nodes.md) and choose -**Update status**. Older firmware and a disconnected radio have no shortcut — the field above is +**Update status**. Older firmware and a disconnected node have no shortcut — the field itself is still the way in. ## Configuration @@ -77,13 +77,13 @@ On **Settings → LoRa**. | Setting | Deskripsyon | Default | | ------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------- | -| Rejyon | Regulatory region for frequency bands. You must set this before transmitting | Unset (must configure) | +| Rejyon | Regulatory region for frequency bands. You must set this before transmitting. On firmware 2.8 or newer, the first region set also creates the node's identity key and gives it a new node number | Unset (must configure) | | Presets | Speed/range tradeoff | LongFast | | Number of Hops | Maximum retransmit hops | 3 | | Transmit Power | Transmission power (dBm); 0 = max allowed for region | 0 (region max) | | Frequency Override | Overrides the computed operating frequency outright (MHz). It does not offset the calculated value — leave at 0 unless you know you need a specific frequency | 0 (use calculated) | | Use Preset | On by default. Turn it off to set Spread Factor, Coding Rate and Bandwidth by hand instead of taking them from the modem preset | On | -| Spread Factor | Manual mode only: 7–12. Higher spreads further but slower | From preset | +| Spread Factor | Manual mode only: 5–12. Higher spreads further but slower. On SX127x (RF95) radios the firmware does not accept 5 or 6 and uses 11 instead | From preset | | Coding Rate | Manual mode only: 5–8. More redundancy costs airtime | From preset | | Bandwidth | Manual mode only: the channel bandwidth in kHz, typed in directly. On the 2.4 GHz region the app offers a list of the bandwidths your radio supports instead, and a stored value that is not on that list shows as _Unsupported_ and blocks saving until you pick a supported one | From preset | | Frequency Slot | Which slot within the region's band to use. 0 derives it from the primary channel name | 0 (automatic) | @@ -111,13 +111,13 @@ The Lite, Narrow, Medium Turbo, and Tiny presets need firmware 2.8 or newer — | Short Slow | ~5 km | 6.25 kbps | −10 dB | Suburban short-range; moderate building density | | Medium Fast | ~5 km | 3.52 kbps | −12.5 dB | Suburban areas; moderate building density | | Medium Slow | ~8 km | 1.95 kbps | −15 dB | Suburban/rural; moderate range with slower speed | -| Long Turbo | ~10 km | 1.34 kbps | −12.5 dB | Similar range to Long Fast but with 500 kHz bandwidth; faster throughput | +| Long Turbo | ~10 km | 1.34 kbps | −17.5 dB | Similar range to Long Fast but with 500 kHz bandwidth; faster throughput | | Long Fast | ~10 km | 1.1 kbps | −17.5 dB | **General use (default)** — balanced range and speed | | Long Moderate | ~20 km | 0.34 kbps | −17.5 dB | Rural with some terrain; occasional use | | Lite Fast | ~5 km | 1.76 kbps | −12.5 dB | EU 866 MHz SRD band (125 kHz BW); comparable to Medium Fast | | Lite Slow | ~10 km | 0.98 kbps | −15 dB | EU 866 MHz SRD band (125 kHz BW); comparable to Long Fast | -| Narrow Fast | ~5 km | 2.28 kbps | −10 dB | EU 868 MHz band (62.5 kHz BW); avoids interference with other devices | -| Narrow Slow | ~10 km | 1.30 kbps | −12.5 dB | EU 868 MHz band (62.5 kHz BW); comparable to Long Fast | +| Narrow Fast | ~5 km | 2.28 kbps | −7.5 dB | EU 868 MHz band (62.5 kHz BW); avoids interference with other devices | +| Narrow Slow | ~10 km | 1.30 kbps | −10 dB | EU 868 MHz band (62.5 kHz BW); comparable to Long Fast | | Medium Turbo | ~5 km | 7.0 kbps | −12.5 dB | Like Medium Fast but with 500 kHz bandwidth; not legal in every region. Needs firmware 2.8 or newer | | Tiny Fast | ~10 km | 0.68 kbps | −7.5 dB | Amateur bands that cap occupied bandwidth; these presets use 15.6 kHz. Needs firmware 2.8 or newer, an SX126x or SX127x radio, and a TCXO of ±5 ppm or better | | Tiny Slow | ~20 km | 0.33 kbps | −10 dB | Same band restrictions as Tiny Fast, longer range. Same firmware, radio, and TCXO requirements | diff --git a/docs/ht-rHT/user/signal-meter.md b/docs/ht-rHT/user/signal-meter.md index 247346b7ce..3296b03565 100644 --- a/docs/ht-rHT/user/signal-meter.md +++ b/docs/ht-rHT/user/signal-meter.md @@ -2,7 +2,7 @@ title: How the Meshtastic Signal Meter Works parent: User Guide nav_order: 15 -last_updated: 2026-08-30 +last_updated: 2026-09-09 description: How the signal meter rates quality from SNR relative to the LoRa modem preset — spread spectrum, presets, and what the bars really mean. aliases: - signal @@ -32,7 +32,7 @@ Each modem preset has an SNR limit: the lowest SNR at which that preset can stil ## Rating Signal Quality -The app rates signal quality (None, Bad, Fair, or Good) from SNR alone, measured against the active preset's SNR limit. It does not factor in RSSI: without knowing the local noise floor, RSSI alone cannot say whether a signal is decodable. RSSI is still available — on the node detail screen and in the metrics charts. +The app rates signal quality (None, Bad, Fair, or Good) from SNR relative to the active preset's SNR limit. When RSSI and a noise-floor reading are both available, it also rates the difference between them against that limit and uses the worse rating. Otherwise, it uses SNR alone. Because the rating is relative to the preset, the same SNR rates differently on different presets. An SNR of −16 dB rates Good on Long Fast (SNR limit −17.5 dB) but None on Short Fast (SNR limit −7.5 dB). Let `limit` be the active preset's SNR limit: @@ -45,7 +45,7 @@ Because the rating is relative to the preset, the same SNR rates differently on The icon never goes blank, so count the bars carefully: a single bar means None, not a weak but usable link, and Good is a solid wedge rather than a set of bars. A gray three-bar icon labeled Unknown is a different state again — the packet carried no SNR measurement at all, which is not the same as measuring one and finding it too weak. -> ℹ️ **Note:** Traceroute hop colors use fixed thresholds (−7 dB / −15 dB); the per-node signal meter uses the preset-relative rating instead. +> ℹ️ **Note:** Traceroute and neighbor-info SNR colors use the active preset's limits too. RSSI text has separate strength colors: green above −115 dBm, yellow above −120 dBm, orange above −126 dBm, and red at or below −126 dBm. ## Diagnosing Local Interference diff --git a/docs/ht-rHT/user/tak.md b/docs/ht-rHT/user/tak.md index 61d9a60113..0a9dff760d 100644 --- a/docs/ht-rHT/user/tak.md +++ b/docs/ht-rHT/user/tak.md @@ -2,7 +2,7 @@ title: TAK Integration parent: User Guide nav_order: 10 -last_updated: 2026-08-30 +last_updated: 2026-09-11 description: Interoperate with ATAK and WinTAK — CoT position sharing, TAK roles, and plugin setup. aliases: - tak @@ -36,7 +36,7 @@ The TAK module allows Meshtastic nodes to: > ⚠️ **Warning:** The old **Meshtastic ATAK Plugin** is no longer part of this path and cannot > work. It bridged through the cross-process AIDL API, which was removed in app 2.8.0; the mesh > service is now in-process only. Do not install it. Interop today runs over the app's own local -> TAK server plus the Mesh to CoT Converter, both described below, with stock ATAK/iTAK/WinTAK. +> TAK server plus the Mesh to CoT Converter, both described in the following sections, with stock ATAK/iTAK/WinTAK. ### Configuration diff --git a/docs/ht-rHT/user/translate.md b/docs/ht-rHT/user/translate.md index 67d69e2d96..cdc8b6d87f 100644 --- a/docs/ht-rHT/user/translate.md +++ b/docs/ht-rHT/user/translate.md @@ -2,7 +2,7 @@ title: Translate the App parent: User Guide nav_order: 17 -last_updated: 2026-08-30 +last_updated: 2026-09-11 description: How the app and its documentation are translated via Crowdin, and guidelines for contributing translations. aliases: - translate @@ -45,7 +45,7 @@ If your language is not yet listed on Crowdin: The Android app uses **Compose Multiplatform resources** for all user-visible strings: -``` +```text core/resources/src/commonMain/composeResources/ ├── values/ ← English (default) │ └── strings.xml @@ -58,7 +58,7 @@ core/resources/src/commonMain/composeResources/ In-app documentation follows a similar pattern under `docs/`: -``` +```text docs/ ├── en/user/ ← English source (default) │ ├── onboarding.md diff --git a/docs/hu-rHU/user/desktop.md b/docs/hu-rHU/user/desktop.md index 4279ba79cf..f9cb686b4c 100644 --- a/docs/hu-rHU/user/desktop.md +++ b/docs/hu-rHU/user/desktop.md @@ -2,7 +2,7 @@ title: Desktop App parent: User Guide nav_order: 14 -last_updated: 2026-08-30 +last_updated: 2026-09-11 description: Install and use the Meshtastic Desktop app on Linux, macOS, and Windows — connections, feature parity, and keyboard shortcuts. aliases: - desktop @@ -58,20 +58,20 @@ Bluetooth Low Energy is supported on desktop via the [Kable](https://github.com/ ## Feature Parity -| Feature | Android | Desktop | Jegyzetek | -| ----------------------------------------------------- | ------- | ------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Messaging | ✓ | ✓ | Full parity | -| Node List | ✓ | ✓ | Full parity | -| Térkép | ✓ | ✓ | Interactive MapLibre map, with base map and overlay pickers and custom tile sources. No offline downloads or local `.mbtiles` archives | -| Map layers (`.kml`/`.kmz`/GeoJSON) | ✓ | ✓ | Same layer store and sheet as Android; imported files draw on the desktop map | -| Site Planner | ✓ | ✓\* | \*Opens in your browser on desktop; the estimate is not drawn on the desktop map | -| Beállítások | ✓ | ✓ | Full parity | -| Bluetooth (BLE) | ✓ | ✓ | Via Kable on desktop | -| Firmware Update | ✓ | ✓ | In-app USB, BLE, and Wi-Fi (ESP32) update work the same as Android. The USB maintenance flow — nRF52/RP2040 factory erase and bootloader upgrade — is Android-only | -| Notifications | ✓ | ✓ | Native OS notifications | -| Widgets | ✓ | ✗ | Android-only | -| AI Assistant (Chirpy) | ✓\* | ✗ | Google flavor Android only | -| App Functions (system AI) | ✓† | ✗ | Google flavor Android only | +| Feature | Android | Desktop | Jegyzetek | +| ----------------------------------------------------- | ------- | ------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Messaging | ✓ | ✓ | Full parity | +| Node List | ✓ | ✓ | Full parity | +| Térkép | ✓ | ✓ | Interactive MapLibre map, with base map and overlay pickers and custom tile sources. Offline terrain (hillshade and contours) can be downloaded; offline base-map packs and local `.mbtiles` archives cannot | +| Map layers (`.kml`/`.kmz`/GeoJSON) | ✓ | ✓ | Same layer store and sheet as Android; imported files draw on the desktop map | +| Site Planner | ✓ | ✓\* | \*Opens in your browser on desktop; the estimate is not drawn on the desktop map | +| Beállítások | ✓ | ✓ | Full parity | +| Bluetooth (BLE) | ✓ | ✓ | Via Kable on desktop | +| Firmware Update | ✓ | ✓ | In-app USB, BLE, and Wi-Fi (ESP32) update work the same as Android. The USB maintenance flow — nRF52/RP2040 factory erase and bootloader upgrade — is Android-only | +| Notifications | ✓ | ✓ | Native OS notifications | +| Widgets | ✓ | ✗ | Android-only | +| AI Assistant (Chirpy) | ✓\* | ✗ | Google flavor Android only | +| App Functions (system AI) | ✓† | ✗ | Google flavor Android only | \*Chirpy AI requires Android 14+ on Google flavor builds with supported hardware. @@ -122,7 +122,7 @@ Individual doc pages render with full formatting: ## Building from Source -```bash +```shell git clone https://github.com/meshtastic/Meshtastic-Android.git cd Meshtastic-Android ./gradlew :desktopApp:run @@ -135,7 +135,7 @@ Requirements: ## Known Limitations -- Offline tile downloads and local `.mbtiles` archives are not available on desktop. +- Offline base-map downloads and local `.mbtiles` archives are not available on desktop. Offline terrain is — see [Map & Waypoints](map-and-waypoints). - `.kml`/`.kmz`/GeoJSON layer import works — see [Map & Waypoints](map-and-waypoints#map-layers). Site Planner opens in your browser rather than in the app; to bring its coverage estimate onto the map, click the transmitter pin diff --git a/docs/hu-rHU/user/discovery.md b/docs/hu-rHU/user/discovery.md index 93e42feb24..2f3d3b9c5e 100644 --- a/docs/hu-rHU/user/discovery.md +++ b/docs/hu-rHU/user/discovery.md @@ -2,7 +2,7 @@ title: Local Mesh Discovery parent: User Guide nav_order: 12 -last_updated: 2026-08-30 +last_updated: 2026-09-11 description: Explore your mesh network — the Local Mesh Discovery scanner, traceroute paths, neighbor maps, and node discovery tools. aliases: - discovery @@ -97,7 +97,7 @@ Mesh Beacon lets nodes invite others to join their mesh. A beaconing node period Configure it under **Settings → Module configuration → Mesh Beacon**. The entry appears only on radios running firmware 2.8.0 or newer. A read-only **Region** row at the top of the screen shows the region the beacon advertises: that region, and the preset, are always the ones the radio itself uses, so a beacon cannot invite anyone onto settings your radio is not running. - **Listen for beacons** — receive invitations broadcast by other nodes. -- **Broadcast a beacon** — periodically advertise this mesh to nearby nodes, with an optional **Beacon message** of up to 100 bytes, a **Broadcast interval** picked from fixed intervals between 1 hour and 72 hours, and an **Offered channel** chosen from your radio's own channels. The offered channel is required, and defaults to your primary channel. +- **Broadcast a beacon** — periodically advertise this mesh to nearby nodes, with an optional **Beacon message** of up to 100 bytes, a **Broadcast interval** picked from fixed intervals between 1 hour and 72 hours, and an **Offered channel** chosen from your radio's own channels. The offered channel is required, and defaults to your primary channel. Over remote admin the picker offers the primary channel only. - **Broadcast targets** — optional extra destinations beyond the offered channel. **Add target** appends a row; each row picks a **Channel** and a **Transmit preset**, and **Remove target** deletes it. With no targets, the beacon goes out on the offered channel alone. Two conditions block beacon setup: diff --git a/docs/hu-rHU/user/help-and-docs.md b/docs/hu-rHU/user/help-and-docs.md index f306cde7f7..c83898cea3 100644 --- a/docs/hu-rHU/user/help-and-docs.md +++ b/docs/hu-rHU/user/help-and-docs.md @@ -2,7 +2,7 @@ title: Help & In-App Docs parent: User Guide nav_order: 21 -last_updated: 2026-08-30 +last_updated: 2026-09-11 description: Browse this documentation inside the app, search it, and ask Chirpy — the on-device AI assistant — questions about Meshtastic. aliases: - help @@ -23,7 +23,7 @@ The docs browser lists every user-guide page. Tap a page to read it; images and ### Search -The search box sits at the top of the docs browser. Type in it to filter pages by title and keywords — results update as you type, and the **Clear search** button (✕) empties the box. +Tap the search field at the top of the docs browser to open a full-screen search view. Type to filter pages by title and keywords — results update as you type, under the same field. The back arrow, or the keyboard's search action, closes the view. The field hides while you scroll down the page list and comes back when you scroll up. ![Searching the in-app documentation](../../assets/screenshots/docs-browser_search.png) diff --git a/docs/hu-rHU/user/map-and-waypoints.md b/docs/hu-rHU/user/map-and-waypoints.md index 5f9fbb5354..4a9dcfa916 100644 --- a/docs/hu-rHU/user/map-and-waypoints.md +++ b/docs/hu-rHU/user/map-and-waypoints.md @@ -2,7 +2,7 @@ title: Map & Waypoints parent: User Guide nav_order: 6 -last_updated: 2026-09-01 +last_updated: 2026-09-11 description: View node positions on the map, create and share waypoints, manage map layers and Site Planner, and control position sharing and privacy. aliases: - map @@ -44,7 +44,7 @@ The floating toolbar provides quick access to the compass, the map type and laye ### Filtering the Map -Tap the filter button in the floating toolbar to open **Filter map**. **Display** controls what is drawn: **Only Favorites**, **Show Waypoints**, **Show Precision Circles**, and a slider that hides nodes not heard from recently. **Node roles** is a chip per device role, plus **All** to show every role; a selected chip means that role is shown. **Nodes** narrows the set further with **Hide offline nodes**, **Only show direct nodes**, **Exclude MQTT**, **Show ignored nodes**, and **Include unknown**. +Tap the filter button in the floating toolbar to open **Filter map**. **Display** controls what is drawn: **Only Favorites**, **Show Waypoints**, **Show Precision Circles**, and a slider that hides nodes not heard from recently. **Node roles** is a chip per device role, plus **All** to show every role; a selected chip means that role is shown. **Nodes** narrows the set further with **Hide offline nodes**, **Only show direct nodes**, **Signed only**, **Encrypted only**, **Exclude MQTT**, **Show ignored nodes**, and **Include unknown** — the same rules as the node list's [filter toggles](nodes). A dot on the filter button means at least one filter is hiding something — check it before concluding the mesh is quiet. Turning **Show Waypoints** off hides every waypoint, including your own. **Show ignored nodes** adds them to the map rather than showing only them — unlike the node list's **Only show ignored Nodes**. @@ -147,8 +147,9 @@ picker, all three offer the same raster base maps: Overlays can be toggled on top of any base map, from the layers sheet: - **Weather radar** — NOAA NEXRAD reflectivity (US coverage) -- **Hillshade** — terrain relief, on **F-Droid** and **Desktop** only. Useful for understanding why a - link fails, since LoRa range is limited by terrain +- **Hillshade** — terrain relief from an online tile source, on **F-Droid** and **Desktop** only; Google + Play draws hillshade from downloaded offline terrain instead. Useful for understanding why a link + fails, since LoRa range is limited by terrain ### Adding your own tile source @@ -156,7 +157,7 @@ Any XYZ tile endpoint can be added as a base map, on every flavor and on desktop Tile Sources** at the foot of the base map picker and paste a URL template using `{z}`, `{x}` and `{y}` — plus `{s}` if the provider uses rotating subdomains. A national mapping service, for example: -``` +```text https://wmts.geo.admin.ch/1.0.0/ch.swisstopo.pixelkarte-farbe/default/current/3857/{z}/{x}/{y}.jpeg ``` @@ -173,7 +174,18 @@ On **Google Play**, the layers sheet's **Offline Manager** section downloads its — water, roads and administrative boundaries extracted from the public Protomaps basemap dataset, drawn directly as map shapes rather than raster tiles. Frame the area you want, tap **Start Download**, and once it finishes flip **Show on map** to draw it (it's off by default so it never surprises you by covering up the -live map). You can still import a pre-made `.mbtiles` archive here too, as before. **Desktop** has neither. +live map). You can still import a pre-made `.mbtiles` archive here too, as before. **Desktop** has neither, though +the offline terrain described next does work there. + +### Offline terrain + +Hillshade and elevation contours can be downloaded for offline use as well, so a downloaded area still +shows why a link fails when there is no network. The terrain data comes from Mapterhorn and is credited on +the map. On **Google Play**, each downloaded region in the **Offline Manager** list has a **Download Terrain** +button and **Hillshade** and **Contours** switches; terrain attaches to that region and shares its storage limit, and an area +too large for terrain asks you to zoom in or pick a smaller region. On **F-Droid** and **Desktop**, the +layers sheet has an **Offline Terrain** section of its own: frame the area, tap **Start Download**, and the +hillshade and contours draw from the downloaded tiles from then on. ### Going offline @@ -181,8 +193,8 @@ An **Offline** pill appears over the map whenever the device has no network conn builds, if you've imported a local `.mbtiles` archive, the map switches to it automatically the moment the network drops — no toggle to remember — and switches back once you're reconnected, as long as you haven't picked a different base map yourself in between. On **F-Droid**, a downloaded offline area keeps rendering on -its own; the pill is purely informational there. **Desktop** has no offline downloads at all yet, so the pill -is informational there too. +its own; the pill is purely informational there. **Desktop** downloads offline terrain but no base-map +packs, so the pill is informational there too. ## Related Topics diff --git a/docs/hu-rHU/user/messages-and-channels.md b/docs/hu-rHU/user/messages-and-channels.md index ed1bb862e5..859e15a40a 100644 --- a/docs/hu-rHU/user/messages-and-channels.md +++ b/docs/hu-rHU/user/messages-and-channels.md @@ -2,7 +2,7 @@ title: Messages & Channels parent: User Guide nav_order: 3 -last_updated: 2026-08-30 +last_updated: 2026-09-14 description: Send and receive messages, manage channels, configure encryption, search conversations, and use quick chat, reactions, and message actions. aliases: - channels @@ -50,6 +50,21 @@ Key length alone does not change the icon: a 128-bit key and a 256-bit key both Tapping an existing channel opens the same editor, where you can change the name, the PSK, MQTT uplink and downlink, and position precision. Every edit on this screen — adding, editing, deleting, or dragging a channel into a new order — waits on **Send** the same way. +### Archived Channels + +A conversation belongs to the channel it was held on, not to the slot that channel occupied. When a channel leaves your radio — you scan a different channel QR, replace a channel's key, or switch the modem preset — its conversation is **archived** rather than deleted or merged into whatever takes the slot. + +An archived conversation: + +- stays in the **Channels** list under the name the channel had, below the channels currently on your radio, marked with a history icon; +- keeps its full message history, searchable as usual; +- is read-only — you cannot send, reply, or react, because the channel is no longer on the radio to send on, and it is not offered as a share target; +- comes back automatically if you re-add that exact channel, taking its messages back to the live slot. + +Changing the modem preset archives the channel too: the preset is part of a default channel's identity, so **LongFast** and **MediumFast** are different channels even though neither the name nor the key changed. Changing only the region does not archive anything. + +To remove an archived conversation for good, long-press it in the conversation list and delete it. + ## Direct Messages Direct messages (DMs) go to one specific node. When both radios hold each other's public keys, your radio encrypts the message to that node's public key, so no one else on the mesh can read it — not even nodes that share your channel. diff --git a/docs/hu-rHU/user/mqtt.md b/docs/hu-rHU/user/mqtt.md index 8ce6f22241..ed35339d9a 100644 --- a/docs/hu-rHU/user/mqtt.md +++ b/docs/hu-rHU/user/mqtt.md @@ -2,7 +2,7 @@ title: MQTT parent: User Guide nav_order: 11 -last_updated: 2026-08-30 +last_updated: 2026-09-11 description: Bridge your mesh to the internet — MQTT broker setup, encryption layers, and map reporting. aliases: - mqtt @@ -25,7 +25,7 @@ The MQTT module connects your node to an MQTT broker, allowing: ## How It Works -``` +```text [Your Node] → Radio → [Gateway Node with Wi-Fi] → MQTT Broker → [Remote Gateway] → Radio → [Remote Node] ``` diff --git a/docs/hu-rHU/user/node-metrics.md b/docs/hu-rHU/user/node-metrics.md index db3731918d..fb3299f6da 100644 --- a/docs/hu-rHU/user/node-metrics.md +++ b/docs/hu-rHU/user/node-metrics.md @@ -2,7 +2,7 @@ title: Node Metrics parent: User Guide nav_order: 5 -last_updated: 2026-08-30 +last_updated: 2026-09-09 description: Telemetry dashboards for each mesh node — device health, environment sensors, air quality, signal quality, power, traceroute, and position history. aliases: - metrics @@ -114,7 +114,7 @@ Radio signal quality information: ### Signal Quality Reference -Signal quality is rated from **SNR relative to the active LoRa modem preset's demodulation floor**, not from fixed thresholds — a given SNR means different things on different presets (e.g. −15 dB is fine on LongSlow but unusable on ShortFast). RSSI is shown but is not part of the rating. In the table, _limit_ is the preset's SNR limit. +Signal quality is rated from **SNR relative to the active LoRa modem preset's demodulation floor**, not from fixed thresholds — a given SNR means different things on different presets (e.g. −15 dB is fine on LongSlow but unusable on ShortFast). When RSSI and a noise-floor reading are both available, the app also rates their difference against the preset limit and uses the worse rating. Otherwise, RSSI is display-only. In the table, _limit_ is the preset's SNR limit. | Quality | Criteria | | --------- | ---------------------------------------------------------------- | diff --git a/docs/hu-rHU/user/nodes.md b/docs/hu-rHU/user/nodes.md index 719186d32b..efe82de447 100644 --- a/docs/hu-rHU/user/nodes.md +++ b/docs/hu-rHU/user/nodes.md @@ -2,7 +2,7 @@ title: Csomópontok parent: User Guide nav_order: 4 -last_updated: 2026-09-04 +last_updated: 2026-09-11 description: Browse, filter, and sort mesh nodes — view details, signal quality, roles, and quick actions. aliases: - node-list @@ -74,19 +74,23 @@ Most users should keep the default **Client** role. Consider a different role wh > 💡 **Tip:** The mesh works best when most nodes are **Client** or **Router**. Too many Client Mute nodes reduce mesh resilience; too many Routers in a dense area can cause congestion. A good rule of thumb: one Router per 5–10 Clients in your area. -### Encryption Indicators +### Security indicators -Nodes display encryption status icons next to their name: +Each node carries one security icon beside its name in the node list. Tap it to read what it means, and choose **Show All Meanings** in that dialog for the full legend. The detail screen shows the same state in words, as a **Security** row that opens the same dialog. -| Icon | Meaning | -| ----------- | ------------------------------------------------------------------------------------------------------------------------------------------------- | -| 🔒 Locked | Communication uses PKI (public key infrastructure) — end-to-end encrypted with verified identity | -| 🔓 Unlocked | No public key has been received for this node, so it cannot be direct messaged — use **Request User Info** on the node detail page to ask for one | -| ⚠️ Mismatch | Public key mismatch — the node's key has changed since last seen (investigate before trusting) | +| Icon | Meaning | Shown for | +| --------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------- | +| Person with a shield check (green) | **Verified contact** — you verified this node's key in person by exchanging contact QR codes, so its identity is confirmed. The strongest trust the list shows. Your own connected node carries it too | Any firmware version | +| Nodes icon with a shield check (green) | **Signed node** — this node signs its broadcasts with its identity key, so its identity is consistent over time, but you have not verified it in person. On firmware 2.8 the icon appears from the version alone, before any signed broadcast has been heard | Firmware 2.8 or newer, and any node whose signed broadcast your node has verified | +| 🔒 Closed lock | A public key is on file and matches, so direct messages to this node are encrypted | Firmware before 2.8, or no reported version | +| 🔓 Open lock | No public key has been received for this node, so it cannot be direct messaged — use **Request User Info** on the node detail page to ask for one | Firmware before 2.8, or no reported version | +| ⚠️ Mismatch | **Public key mismatch** — a different key arrived for this node after one was stored. Investigate before trusting | Any firmware version | -> 💡 **Tip:** Direct messages always use PKI, so the radio needs the other node's public key before it can send one. It refuses the send rather than falling back to channel encryption. Keys arrive inside node info, which is why an open lock usually clears itself once that node is heard from properly. If you see a key mismatch warning, the node may have been reset or compromised. +Every node on firmware 2.8 or newer signs its broadcasts, so on that firmware the signed state is the baseline and the locks are not shown. -To clear a mismatch, first confirm through another trusted channel that the key change was intentional — a factory reset causes one. Then touch & hold the node, choose **Remove**, and let the two radios exchange keys again the next time yours hears it. +Direct messages always use public-key encryption, so your node needs the other node's public key before it can send one. It refuses the send rather than falling back to channel encryption. Keys arrive inside node info, which is why an open lock usually clears itself once that node is heard from properly. + +A mismatch never replaces the key you already hold. The app keeps the first key it recorded and refuses the new one, as the firmware does, so a stray or hostile node info cannot silently break encrypted messaging to a contact. To clear a mismatch, first confirm through another trusted channel that the key change was intentional — a factory reset causes one. Then touch & hold the node, choose **Remove**, and let the two nodes exchange keys again the next time yours hears it. ## Quick Actions @@ -108,7 +112,9 @@ radio is connected and running firmware 2.8 or newer — see ## Sharing a Contact -On a node's detail screen, tap **Share Contact** to produce a link and a QR code for that node. From the same dialog, **Write to NFC tag** saves the link to a writable NFC tag that anyone can tap to open. +On a node's detail screen, tap **Share Contact** to produce a link and a QR code for that node. From the same dialog, **Share link** opens the Android share sheet (on desktop it copies the link instead), **Write to NFC tag** saves it to a writable NFC tag, and **Copy** puts it on the clipboard. While that dialog is open and in front of you, the phone also offers the same link to any NFC reader, so someone can take the contact by tapping their phone against yours with no tag involved. + +Sharing your own contact this way marks it as verified in person, so whoever imports it sees the **Verified contact** icon rather than the signed one. Relaying someone else's contact passes on only what your app had already recorded about them. To add someone else's contact, use the import button on the node list and choose **Scan Shared Contact QR Code**, **Scan Shared Contact NFC**, or **Input Shared Contact URL**. The app asks you to confirm with **Import Shared Contact?**, and warns you when the contact is one you already have. @@ -120,14 +126,16 @@ Type in the search field to filter nodes by name or short name. The filter updat ### Filter Toggles -| Filter | Leírás | -| --------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| **Hide offline nodes** | Show only nodes heard within the last 2 hours | -| **Only show direct nodes** | Show only nodes your radio heard directly, with no relay in between | -| **Include unknown** | Show nodes that haven't sent user info yet. **On by default**, so a node heard before its info arrives stays visible; these carry a badge marking them incomplete, and cannot be direct messaged until their user info brings a public key | -| **Exclude infrastructure** | Hide infrastructure-role nodes (Router, Router Late, Client Base, and legacy Repeater nodes) and any node that cannot be messaged, whatever its role | -| **Exclude MQTT** | Hide nodes heard only via MQTT internet bridge | -| **Only show ignored Nodes** | Replace the list with the nodes you have ignored. Every other node is hidden while this is on, and a banner appears at the top of the list to take you back | +| Filter | Leírás | +| --------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **Hide offline nodes** | Show only nodes heard within the last 2 hours | +| **Only show direct nodes** | Show only nodes your radio heard directly, with no relay in between | +| **Include unknown** | Show nodes that haven't sent user info yet. **On by default**, so a node heard before its info arrives stays visible; these carry a badge marking them incomplete, and cannot be direct messaged until their user info brings a public key | +| **Exclude infrastructure** | Hide infrastructure-role nodes (Router, Router Late, Client Base, and legacy Repeater nodes) and any node that cannot be messaged, whatever its role | +| **Exclude MQTT** | Hide nodes heard only via MQTT internet bridge | +| **Signed only** | Show only nodes whose signed broadcasts your node has actually heard and verified. Stricter than the icon: a node on 2.8 shows as signed by its firmware version before any signed broadcast arrives, and a contact verified in person on older firmware is not signed at all | +| **Encrypted only** | Show nodes with a matching public key on file, the key an encrypted direct message needs. A node with a key mismatch is excluded | +| **Only show ignored Nodes** | Replace the list with the nodes you have ignored. Every other node is hidden while this is on, and a banner appears at the top of the list to take you back | ### Sort Options @@ -149,6 +157,8 @@ Tap the hop-histogram icon in the node list's app bar to open a bar chart of how Tapping a node opens the detail view with comprehensive information. See [Node Metrics](node-metrics) for full details on metrics and telemetry. +Signal quality is rated against your modem preset. The same SNR can be good on a long-range preset and poor on a faster one. Traceroute and neighbor-info SNR colors use that preset too. RSSI text has its own strength colors; it affects the quality rating only when a noise-floor reading is also available. + The Details card carries the node's short name, role, IDs, last heard time, hops away, uptime, and its SNR and RSSI: ![Node detail section](../../assets/screenshots/nodes_detail_section.png) diff --git a/docs/hu-rHU/user/settings-module-admin.md b/docs/hu-rHU/user/settings-module-admin.md index 2b9ca62add..25a244308d 100644 --- a/docs/hu-rHU/user/settings-module-admin.md +++ b/docs/hu-rHU/user/settings-module-admin.md @@ -2,7 +2,7 @@ title: Settings — Modules & Admin parent: User Guide nav_order: 8 -last_updated: 2026-08-30 +last_updated: 2026-09-16 description: Configure optional feature modules (MQTT, telemetry, canned messages, TAK, and more) and perform device administration. aliases: - modules @@ -333,7 +333,7 @@ appear only when your own node is selected: ### A programról -**Settings → About** carries the app's own identity rather than the radio's: +**Settings → About** carries the app's own identity rather than the node's: Three sections: @@ -341,7 +341,11 @@ Three sections: - **Apps** — opens with **Need Hardware?**, a rotating carousel of popular devices that links out to where to buy one. It also lists the GitHub repository, the running app version, and **Acknowledgements** (see the next section). -- **Project information** — links to the website and to this documentation. +- **Project information** — links to the website, to this documentation, and to the GNU GPL v3 + license the app is released under. + +The footer states the copyright and that the app is free software with no warranty, which you +may redistribute under the same license. ### Visszaigazolások (ACK-ek) diff --git a/docs/hu-rHU/user/settings-radio-user.md b/docs/hu-rHU/user/settings-radio-user.md index 9d10b690d6..17ab5d88bf 100644 --- a/docs/hu-rHU/user/settings-radio-user.md +++ b/docs/hu-rHU/user/settings-radio-user.md @@ -2,7 +2,7 @@ title: Settings — Radio & User parent: User Guide nav_order: 7 -last_updated: 2026-09-04 +last_updated: 2026-09-11 description: Configure your radio hardware, LoRa presets, user profile, position sharing, power management, and security. aliases: - beállítások @@ -51,7 +51,7 @@ The footer appears as soon as you change something. **Discard** throws the chang The status message is saved with the same **Save**, but it never reboots the node — and, like the rest of this screen, it can be edited on a remote node you administer. For your own radio there is a shortcut while it is connected: touch & hold your node in the [node list](nodes.md) and choose -**Update status**. Older firmware and a disconnected radio have no shortcut — the field above is +**Update status**. Older firmware and a disconnected node have no shortcut — the field itself is still the way in. ## Configuration @@ -77,13 +77,13 @@ On **Settings → LoRa**. | Setting | Leírás | Alapértelmezett | | ---------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------- | -| Régió | Regulatory region for frequency bands. You must set this before transmitting | Unset (must configure) | +| Régió | Regulatory region for frequency bands. You must set this before transmitting. On firmware 2.8 or newer, the first region set also creates the node's identity key and gives it a new node number | Unset (must configure) | | Előbeállítások | Speed/range tradeoff | LongFast | | Ugrások száma | Maximum retransmit hops | 3 | | Adásteljesítmény | Transmission power (dBm); 0 = max allowed for region | 0 (region max) | | Frekvencia felülbírálása | Overrides the computed operating frequency outright (MHz). It does not offset the calculated value — leave at 0 unless you know you need a specific frequency | 0 (use calculated) | | Előbeállítás használata | On by default. Turn it off to set Spread Factor, Coding Rate and Bandwidth by hand instead of taking them from the modem preset | On | -| Szórási Faktor | Manual mode only: 7–12. Higher spreads further but slower | From preset | +| Szórási Faktor | Manual mode only: 5–12. Higher spreads further but slower. On SX127x (RF95) radios the firmware does not accept 5 or 6 and uses 11 instead | From preset | | Kódolási ráta | Manual mode only: 5–8. More redundancy costs airtime | From preset | | Sávszélesség | Manual mode only: the channel bandwidth in kHz, typed in directly. On the 2.4 GHz region the app offers a list of the bandwidths your radio supports instead, and a stored value that is not on that list shows as _Unsupported_ and blocks saving until you pick a supported one | From preset | | Frekvencia sáv | Which slot within the region's band to use. 0 derives it from the primary channel name | 0 (automatic) | @@ -111,13 +111,13 @@ The Lite, Narrow, Medium Turbo, and Tiny presets need firmware 2.8 or newer — | Short Slow | ~5 km | 6.25 kbps | −10 dB | Suburban short-range; moderate building density | | Medium Fast | ~5 km | 3.52 kbps | −12.5 dB | Suburban areas; moderate building density | | Medium Slow | ~8 km | 1.95 kbps | −15 dB | Suburban/rural; moderate range with slower speed | -| Long Turbo | ~10 km | 1.34 kbps | −12.5 dB | Similar range to Long Fast but with 500 kHz bandwidth; faster throughput | +| Long Turbo | ~10 km | 1.34 kbps | −17.5 dB | Similar range to Long Fast but with 500 kHz bandwidth; faster throughput | | Long Fast | ~10 km | 1.1 kbps | −17.5 dB | **General use (default)** — balanced range and speed | | Long Moderate | ~20 km | 0.34 kbps | −17.5 dB | Rural with some terrain; occasional use | | Lite Fast | ~5 km | 1.76 kbps | −12.5 dB | EU 866 MHz SRD band (125 kHz BW); comparable to Medium Fast | | Lite Slow | ~10 km | 0.98 kbps | −15 dB | EU 866 MHz SRD band (125 kHz BW); comparable to Long Fast | -| Narrow Fast | ~5 km | 2.28 kbps | −10 dB | EU 868 MHz band (62.5 kHz BW); avoids interference with other devices | -| Narrow Slow | ~10 km | 1.30 kbps | −12.5 dB | EU 868 MHz band (62.5 kHz BW); comparable to Long Fast | +| Narrow Fast | ~5 km | 2.28 kbps | −7.5 dB | EU 868 MHz band (62.5 kHz BW); avoids interference with other devices | +| Narrow Slow | ~10 km | 1.30 kbps | −10 dB | EU 868 MHz band (62.5 kHz BW); comparable to Long Fast | | Medium Turbo | ~5 km | 7.0 kbps | −12.5 dB | Like Medium Fast but with 500 kHz bandwidth; not legal in every region. Needs firmware 2.8 or newer | | Tiny Fast | ~10 km | 0.68 kbps | −7.5 dB | Amateur bands that cap occupied bandwidth; these presets use 15.6 kHz. Needs firmware 2.8 or newer, an SX126x or SX127x radio, and a TCXO of ±5 ppm or better | | Tiny Slow | ~20 km | 0.33 kbps | −10 dB | Same band restrictions as Tiny Fast, longer range. Same firmware, radio, and TCXO requirements | diff --git a/docs/hu-rHU/user/signal-meter.md b/docs/hu-rHU/user/signal-meter.md index c94bd99435..8505c11a5e 100644 --- a/docs/hu-rHU/user/signal-meter.md +++ b/docs/hu-rHU/user/signal-meter.md @@ -2,7 +2,7 @@ title: How the Meshtastic Signal Meter Works parent: User Guide nav_order: 15 -last_updated: 2026-08-30 +last_updated: 2026-09-09 description: How the signal meter rates quality from SNR relative to the LoRa modem preset — spread spectrum, presets, and what the bars really mean. aliases: - signal @@ -32,7 +32,7 @@ Each modem preset has an SNR limit: the lowest SNR at which that preset can stil ## Rating Signal Quality -The app rates signal quality (None, Bad, Fair, or Good) from SNR alone, measured against the active preset's SNR limit. It does not factor in RSSI: without knowing the local noise floor, RSSI alone cannot say whether a signal is decodable. RSSI is still available — on the node detail screen and in the metrics charts. +The app rates signal quality (None, Bad, Fair, or Good) from SNR relative to the active preset's SNR limit. When RSSI and a noise-floor reading are both available, it also rates the difference between them against that limit and uses the worse rating. Otherwise, it uses SNR alone. Because the rating is relative to the preset, the same SNR rates differently on different presets. An SNR of −16 dB rates Good on Long Fast (SNR limit −17.5 dB) but None on Short Fast (SNR limit −7.5 dB). Let `limit` be the active preset's SNR limit: @@ -45,7 +45,7 @@ Because the rating is relative to the preset, the same SNR rates differently on The icon never goes blank, so count the bars carefully: a single bar means None, not a weak but usable link, and Good is a solid wedge rather than a set of bars. A gray three-bar icon labeled Unknown is a different state again — the packet carried no SNR measurement at all, which is not the same as measuring one and finding it too weak. -> ℹ️ **Note:** Traceroute hop colors use fixed thresholds (−7 dB / −15 dB); the per-node signal meter uses the preset-relative rating instead. +> ℹ️ **Note:** Traceroute and neighbor-info SNR colors use the active preset's limits too. RSSI text has separate strength colors: green above −115 dBm, yellow above −120 dBm, orange above −126 dBm, and red at or below −126 dBm. ## Diagnosing Local Interference diff --git a/docs/hu-rHU/user/tak.md b/docs/hu-rHU/user/tak.md index b21ca0c45b..7325f1496b 100644 --- a/docs/hu-rHU/user/tak.md +++ b/docs/hu-rHU/user/tak.md @@ -2,7 +2,7 @@ title: TAK Integration parent: User Guide nav_order: 10 -last_updated: 2026-08-30 +last_updated: 2026-09-11 description: Interoperate with ATAK and WinTAK — CoT position sharing, TAK roles, and plugin setup. aliases: - tak @@ -36,7 +36,7 @@ The TAK module allows Meshtastic nodes to: > ⚠️ **Warning:** The old **Meshtastic ATAK Plugin** is no longer part of this path and cannot > work. It bridged through the cross-process AIDL API, which was removed in app 2.8.0; the mesh > service is now in-process only. Do not install it. Interop today runs over the app's own local -> TAK server plus the Mesh to CoT Converter, both described below, with stock ATAK/iTAK/WinTAK. +> TAK server plus the Mesh to CoT Converter, both described in the following sections, with stock ATAK/iTAK/WinTAK. ### Configuration diff --git a/docs/hu-rHU/user/translate.md b/docs/hu-rHU/user/translate.md index ad2d674fb1..aa56a8df4d 100644 --- a/docs/hu-rHU/user/translate.md +++ b/docs/hu-rHU/user/translate.md @@ -2,7 +2,7 @@ title: Translate the App parent: User Guide nav_order: 17 -last_updated: 2026-08-30 +last_updated: 2026-09-11 description: How the app and its documentation are translated via Crowdin, and guidelines for contributing translations. aliases: - translate @@ -45,7 +45,7 @@ If your language is not yet listed on Crowdin: The Android app uses **Compose Multiplatform resources** for all user-visible strings: -``` +```text core/resources/src/commonMain/composeResources/ ├── values/ ← English (default) │ └── strings.xml @@ -58,7 +58,7 @@ core/resources/src/commonMain/composeResources/ In-app documentation follows a similar pattern under `docs/`: -``` +```text docs/ ├── en/user/ ← English source (default) │ ├── onboarding.md diff --git a/docs/is-rIS/user/desktop.md b/docs/is-rIS/user/desktop.md index efca3c03c9..2ec0d98037 100644 --- a/docs/is-rIS/user/desktop.md +++ b/docs/is-rIS/user/desktop.md @@ -2,7 +2,7 @@ title: Desktop App parent: User Guide nav_order: 14 -last_updated: 2026-08-30 +last_updated: 2026-09-11 description: Install and use the Meshtastic Desktop app on Linux, macOS, and Windows — connections, feature parity, and keyboard shortcuts. aliases: - desktop @@ -58,20 +58,20 @@ Bluetooth Low Energy is supported on desktop via the [Kable](https://github.com/ ## Feature Parity -| Feature | Android | Desktop | Notes | -| ----------------------------------------------------- | ------- | ------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Messaging | ✓ | ✓ | Full parity | -| Node List | ✓ | ✓ | Full parity | -| Map | ✓ | ✓ | Interactive MapLibre map, with base map and overlay pickers and custom tile sources. No offline downloads or local `.mbtiles` archives | -| Map layers (`.kml`/`.kmz`/GeoJSON) | ✓ | ✓ | Same layer store and sheet as Android; imported files draw on the desktop map | -| Site Planner | ✓ | ✓\* | \*Opens in your browser on desktop; the estimate is not drawn on the desktop map | -| Settings | ✓ | ✓ | Full parity | -| Bluetooth (BLE) | ✓ | ✓ | Via Kable on desktop | -| Firmware Update | ✓ | ✓ | In-app USB, BLE, and Wi-Fi (ESP32) update work the same as Android. The USB maintenance flow — nRF52/RP2040 factory erase and bootloader upgrade — is Android-only | -| Notifications | ✓ | ✓ | Native OS notifications | -| Widgets | ✓ | ✗ | Android-only | -| AI Assistant (Chirpy) | ✓\* | ✗ | Google flavor Android only | -| App Functions (system AI) | ✓† | ✗ | Google flavor Android only | +| Feature | Android | Desktop | Notes | +| ----------------------------------------------------- | ------- | ------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Messaging | ✓ | ✓ | Full parity | +| Node List | ✓ | ✓ | Full parity | +| Map | ✓ | ✓ | Interactive MapLibre map, with base map and overlay pickers and custom tile sources. Offline terrain (hillshade and contours) can be downloaded; offline base-map packs and local `.mbtiles` archives cannot | +| Map layers (`.kml`/`.kmz`/GeoJSON) | ✓ | ✓ | Same layer store and sheet as Android; imported files draw on the desktop map | +| Site Planner | ✓ | ✓\* | \*Opens in your browser on desktop; the estimate is not drawn on the desktop map | +| Settings | ✓ | ✓ | Full parity | +| Bluetooth (BLE) | ✓ | ✓ | Via Kable on desktop | +| Firmware Update | ✓ | ✓ | In-app USB, BLE, and Wi-Fi (ESP32) update work the same as Android. The USB maintenance flow — nRF52/RP2040 factory erase and bootloader upgrade — is Android-only | +| Notifications | ✓ | ✓ | Native OS notifications | +| Widgets | ✓ | ✗ | Android-only | +| AI Assistant (Chirpy) | ✓\* | ✗ | Google flavor Android only | +| App Functions (system AI) | ✓† | ✗ | Google flavor Android only | \*Chirpy AI requires Android 14+ on Google flavor builds with supported hardware. @@ -122,7 +122,7 @@ Individual doc pages render with full formatting: ## Building from Source -```bash +```shell git clone https://github.com/meshtastic/Meshtastic-Android.git cd Meshtastic-Android ./gradlew :desktopApp:run @@ -135,7 +135,7 @@ Requirements: ## Known Limitations -- Offline tile downloads and local `.mbtiles` archives are not available on desktop. +- Offline base-map downloads and local `.mbtiles` archives are not available on desktop. Offline terrain is — see [Map & Waypoints](map-and-waypoints). - `.kml`/`.kmz`/GeoJSON layer import works — see [Map & Waypoints](map-and-waypoints#map-layers). Site Planner opens in your browser rather than in the app; to bring its coverage estimate onto the map, click the transmitter pin diff --git a/docs/is-rIS/user/discovery.md b/docs/is-rIS/user/discovery.md index 9ad6cbfbe8..4b9a071eb5 100644 --- a/docs/is-rIS/user/discovery.md +++ b/docs/is-rIS/user/discovery.md @@ -2,7 +2,7 @@ title: Local Mesh Discovery parent: User Guide nav_order: 12 -last_updated: 2026-08-30 +last_updated: 2026-09-11 description: Explore your mesh network — the Local Mesh Discovery scanner, traceroute paths, neighbor maps, and node discovery tools. aliases: - discovery @@ -97,7 +97,7 @@ Mesh Beacon lets nodes invite others to join their mesh. A beaconing node period Configure it under **Settings → Module configuration → Mesh Beacon**. The entry appears only on radios running firmware 2.8.0 or newer. A read-only **Region** row at the top of the screen shows the region the beacon advertises: that region, and the preset, are always the ones the radio itself uses, so a beacon cannot invite anyone onto settings your radio is not running. - **Listen for beacons** — receive invitations broadcast by other nodes. -- **Broadcast a beacon** — periodically advertise this mesh to nearby nodes, with an optional **Beacon message** of up to 100 bytes, a **Broadcast interval** picked from fixed intervals between 1 hour and 72 hours, and an **Offered channel** chosen from your radio's own channels. The offered channel is required, and defaults to your primary channel. +- **Broadcast a beacon** — periodically advertise this mesh to nearby nodes, with an optional **Beacon message** of up to 100 bytes, a **Broadcast interval** picked from fixed intervals between 1 hour and 72 hours, and an **Offered channel** chosen from your radio's own channels. The offered channel is required, and defaults to your primary channel. Over remote admin the picker offers the primary channel only. - **Broadcast targets** — optional extra destinations beyond the offered channel. **Add target** appends a row; each row picks a **Channel** and a **Transmit preset**, and **Remove target** deletes it. With no targets, the beacon goes out on the offered channel alone. Two conditions block beacon setup: diff --git a/docs/is-rIS/user/help-and-docs.md b/docs/is-rIS/user/help-and-docs.md index f306cde7f7..c83898cea3 100644 --- a/docs/is-rIS/user/help-and-docs.md +++ b/docs/is-rIS/user/help-and-docs.md @@ -2,7 +2,7 @@ title: Help & In-App Docs parent: User Guide nav_order: 21 -last_updated: 2026-08-30 +last_updated: 2026-09-11 description: Browse this documentation inside the app, search it, and ask Chirpy — the on-device AI assistant — questions about Meshtastic. aliases: - help @@ -23,7 +23,7 @@ The docs browser lists every user-guide page. Tap a page to read it; images and ### Search -The search box sits at the top of the docs browser. Type in it to filter pages by title and keywords — results update as you type, and the **Clear search** button (✕) empties the box. +Tap the search field at the top of the docs browser to open a full-screen search view. Type to filter pages by title and keywords — results update as you type, under the same field. The back arrow, or the keyboard's search action, closes the view. The field hides while you scroll down the page list and comes back when you scroll up. ![Searching the in-app documentation](../../assets/screenshots/docs-browser_search.png) diff --git a/docs/is-rIS/user/map-and-waypoints.md b/docs/is-rIS/user/map-and-waypoints.md index 4d4203ec28..c162477c3e 100644 --- a/docs/is-rIS/user/map-and-waypoints.md +++ b/docs/is-rIS/user/map-and-waypoints.md @@ -2,7 +2,7 @@ title: Map & Waypoints parent: User Guide nav_order: 6 -last_updated: 2026-09-01 +last_updated: 2026-09-11 description: View node positions on the map, create and share waypoints, manage map layers and Site Planner, and control position sharing and privacy. aliases: - map @@ -44,7 +44,7 @@ The floating toolbar provides quick access to the compass, the map type and laye ### Filtering the Map -Tap the filter button in the floating toolbar to open **Filter map**. **Display** controls what is drawn: **Only Favorites**, **Show Waypoints**, **Show Precision Circles**, and a slider that hides nodes not heard from recently. **Node roles** is a chip per device role, plus **All** to show every role; a selected chip means that role is shown. **Nodes** narrows the set further with **Hide offline nodes**, **Only show direct nodes**, **Exclude MQTT**, **Show ignored nodes**, and **Include unknown**. +Tap the filter button in the floating toolbar to open **Filter map**. **Display** controls what is drawn: **Only Favorites**, **Show Waypoints**, **Show Precision Circles**, and a slider that hides nodes not heard from recently. **Node roles** is a chip per device role, plus **All** to show every role; a selected chip means that role is shown. **Nodes** narrows the set further with **Hide offline nodes**, **Only show direct nodes**, **Signed only**, **Encrypted only**, **Exclude MQTT**, **Show ignored nodes**, and **Include unknown** — the same rules as the node list's [filter toggles](nodes). A dot on the filter button means at least one filter is hiding something — check it before concluding the mesh is quiet. Turning **Show Waypoints** off hides every waypoint, including your own. **Show ignored nodes** adds them to the map rather than showing only them — unlike the node list's **Only show ignored Nodes**. @@ -147,8 +147,9 @@ picker, all three offer the same raster base maps: Overlays can be toggled on top of any base map, from the layers sheet: - **Weather radar** — NOAA NEXRAD reflectivity (US coverage) -- **Hillshade** — terrain relief, on **F-Droid** and **Desktop** only. Useful for understanding why a - link fails, since LoRa range is limited by terrain +- **Hillshade** — terrain relief from an online tile source, on **F-Droid** and **Desktop** only; Google + Play draws hillshade from downloaded offline terrain instead. Useful for understanding why a link + fails, since LoRa range is limited by terrain ### Adding your own tile source @@ -156,7 +157,7 @@ Any XYZ tile endpoint can be added as a base map, on every flavor and on desktop Tile Sources** at the foot of the base map picker and paste a URL template using `{z}`, `{x}` and `{y}` — plus `{s}` if the provider uses rotating subdomains. A national mapping service, for example: -``` +```text https://wmts.geo.admin.ch/1.0.0/ch.swisstopo.pixelkarte-farbe/default/current/3857/{z}/{x}/{y}.jpeg ``` @@ -173,7 +174,18 @@ On **Google Play**, the layers sheet's **Offline Manager** section downloads its — water, roads and administrative boundaries extracted from the public Protomaps basemap dataset, drawn directly as map shapes rather than raster tiles. Frame the area you want, tap **Start Download**, and once it finishes flip **Show on map** to draw it (it's off by default so it never surprises you by covering up the -live map). You can still import a pre-made `.mbtiles` archive here too, as before. **Desktop** has neither. +live map). You can still import a pre-made `.mbtiles` archive here too, as before. **Desktop** has neither, though +the offline terrain described next does work there. + +### Offline terrain + +Hillshade and elevation contours can be downloaded for offline use as well, so a downloaded area still +shows why a link fails when there is no network. The terrain data comes from Mapterhorn and is credited on +the map. On **Google Play**, each downloaded region in the **Offline Manager** list has a **Download Terrain** +button and **Hillshade** and **Contours** switches; terrain attaches to that region and shares its storage limit, and an area +too large for terrain asks you to zoom in or pick a smaller region. On **F-Droid** and **Desktop**, the +layers sheet has an **Offline Terrain** section of its own: frame the area, tap **Start Download**, and the +hillshade and contours draw from the downloaded tiles from then on. ### Going offline @@ -181,8 +193,8 @@ An **Offline** pill appears over the map whenever the device has no network conn builds, if you've imported a local `.mbtiles` archive, the map switches to it automatically the moment the network drops — no toggle to remember — and switches back once you're reconnected, as long as you haven't picked a different base map yourself in between. On **F-Droid**, a downloaded offline area keeps rendering on -its own; the pill is purely informational there. **Desktop** has no offline downloads at all yet, so the pill -is informational there too. +its own; the pill is purely informational there. **Desktop** downloads offline terrain but no base-map +packs, so the pill is informational there too. ## Related Topics diff --git a/docs/is-rIS/user/messages-and-channels.md b/docs/is-rIS/user/messages-and-channels.md index 6b961252b7..3cb2cccf85 100644 --- a/docs/is-rIS/user/messages-and-channels.md +++ b/docs/is-rIS/user/messages-and-channels.md @@ -2,7 +2,7 @@ title: Messages & Channels parent: User Guide nav_order: 3 -last_updated: 2026-08-30 +last_updated: 2026-09-14 description: Send and receive messages, manage channels, configure encryption, search conversations, and use quick chat, reactions, and message actions. aliases: - channels @@ -50,6 +50,21 @@ Key length alone does not change the icon: a 128-bit key and a 256-bit key both Tapping an existing channel opens the same editor, where you can change the name, the PSK, MQTT uplink and downlink, and position precision. Every edit on this screen — adding, editing, deleting, or dragging a channel into a new order — waits on **Send** the same way. +### Archived Channels + +A conversation belongs to the channel it was held on, not to the slot that channel occupied. When a channel leaves your radio — you scan a different channel QR, replace a channel's key, or switch the modem preset — its conversation is **archived** rather than deleted or merged into whatever takes the slot. + +An archived conversation: + +- stays in the **Channels** list under the name the channel had, below the channels currently on your radio, marked with a history icon; +- keeps its full message history, searchable as usual; +- is read-only — you cannot send, reply, or react, because the channel is no longer on the radio to send on, and it is not offered as a share target; +- comes back automatically if you re-add that exact channel, taking its messages back to the live slot. + +Changing the modem preset archives the channel too: the preset is part of a default channel's identity, so **LongFast** and **MediumFast** are different channels even though neither the name nor the key changed. Changing only the region does not archive anything. + +To remove an archived conversation for good, long-press it in the conversation list and delete it. + ## Direct Messages Direct messages (DMs) go to one specific node. When both radios hold each other's public keys, your radio encrypts the message to that node's public key, so no one else on the mesh can read it — not even nodes that share your channel. diff --git a/docs/is-rIS/user/mqtt.md b/docs/is-rIS/user/mqtt.md index c6634979c9..0e6ba39685 100644 --- a/docs/is-rIS/user/mqtt.md +++ b/docs/is-rIS/user/mqtt.md @@ -2,7 +2,7 @@ title: MQTT parent: User Guide nav_order: 11 -last_updated: 2026-08-30 +last_updated: 2026-09-11 description: Bridge your mesh to the internet — MQTT broker setup, encryption layers, and map reporting. aliases: - mqtt @@ -25,7 +25,7 @@ The MQTT module connects your node to an MQTT broker, allowing: ## How It Works -``` +```text [Your Node] → Radio → [Gateway Node with Wi-Fi] → MQTT Broker → [Remote Gateway] → Radio → [Remote Node] ``` diff --git a/docs/is-rIS/user/node-metrics.md b/docs/is-rIS/user/node-metrics.md index 5611082d99..63c15f5dd5 100644 --- a/docs/is-rIS/user/node-metrics.md +++ b/docs/is-rIS/user/node-metrics.md @@ -2,7 +2,7 @@ title: Node Metrics parent: User Guide nav_order: 5 -last_updated: 2026-08-30 +last_updated: 2026-09-09 description: Telemetry dashboards for each mesh node — device health, environment sensors, air quality, signal quality, power, traceroute, and position history. aliases: - metrics @@ -114,7 +114,7 @@ Radio signal quality information: ### Signal Quality Reference -Signal quality is rated from **SNR relative to the active LoRa modem preset's demodulation floor**, not from fixed thresholds — a given SNR means different things on different presets (e.g. −15 dB is fine on LongSlow but unusable on ShortFast). RSSI is shown but is not part of the rating. In the table, _limit_ is the preset's SNR limit. +Signal quality is rated from **SNR relative to the active LoRa modem preset's demodulation floor**, not from fixed thresholds — a given SNR means different things on different presets (e.g. −15 dB is fine on LongSlow but unusable on ShortFast). When RSSI and a noise-floor reading are both available, the app also rates their difference against the preset limit and uses the worse rating. Otherwise, RSSI is display-only. In the table, _limit_ is the preset's SNR limit. | Quality | Criteria | | ------- | ---------------------------------------------------------------- | diff --git a/docs/is-rIS/user/nodes.md b/docs/is-rIS/user/nodes.md index 7f51e176e4..b4be385d56 100644 --- a/docs/is-rIS/user/nodes.md +++ b/docs/is-rIS/user/nodes.md @@ -2,7 +2,7 @@ title: Nodes parent: User Guide nav_order: 4 -last_updated: 2026-09-04 +last_updated: 2026-09-11 description: Browse, filter, and sort mesh nodes — view details, signal quality, roles, and quick actions. aliases: - node-list @@ -74,19 +74,23 @@ Most users should keep the default **Client** role. Consider a different role wh > 💡 **Tip:** The mesh works best when most nodes are **Client** or **Router**. Too many Client Mute nodes reduce mesh resilience; too many Routers in a dense area can cause congestion. A good rule of thumb: one Router per 5–10 Clients in your area. -### Encryption Indicators +### Security indicators -Nodes display encryption status icons next to their name: +Each node carries one security icon beside its name in the node list. Tap it to read what it means, and choose **Show All Meanings** in that dialog for the full legend. The detail screen shows the same state in words, as a **Security** row that opens the same dialog. -| Icon | Meaning | -| ----------- | ------------------------------------------------------------------------------------------------------------------------------------------------- | -| 🔒 Locked | Communication uses PKI (public key infrastructure) — end-to-end encrypted with verified identity | -| 🔓 Unlocked | No public key has been received for this node, so it cannot be direct messaged — use **Request User Info** on the node detail page to ask for one | -| ⚠️ Mismatch | Public key mismatch — the node's key has changed since last seen (investigate before trusting) | +| Icon | Meaning | Shown for | +| --------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------- | +| Person with a shield check (green) | **Verified contact** — you verified this node's key in person by exchanging contact QR codes, so its identity is confirmed. The strongest trust the list shows. Your own connected node carries it too | Any firmware version | +| Nodes icon with a shield check (green) | **Signed node** — this node signs its broadcasts with its identity key, so its identity is consistent over time, but you have not verified it in person. On firmware 2.8 the icon appears from the version alone, before any signed broadcast has been heard | Firmware 2.8 or newer, and any node whose signed broadcast your node has verified | +| 🔒 Closed lock | A public key is on file and matches, so direct messages to this node are encrypted | Firmware before 2.8, or no reported version | +| 🔓 Open lock | No public key has been received for this node, so it cannot be direct messaged — use **Request User Info** on the node detail page to ask for one | Firmware before 2.8, or no reported version | +| ⚠️ Mismatch | **Public key mismatch** — a different key arrived for this node after one was stored. Investigate before trusting | Any firmware version | -> 💡 **Tip:** Direct messages always use PKI, so the radio needs the other node's public key before it can send one. It refuses the send rather than falling back to channel encryption. Keys arrive inside node info, which is why an open lock usually clears itself once that node is heard from properly. If you see a key mismatch warning, the node may have been reset or compromised. +Every node on firmware 2.8 or newer signs its broadcasts, so on that firmware the signed state is the baseline and the locks are not shown. -To clear a mismatch, first confirm through another trusted channel that the key change was intentional — a factory reset causes one. Then touch & hold the node, choose **Remove**, and let the two radios exchange keys again the next time yours hears it. +Direct messages always use public-key encryption, so your node needs the other node's public key before it can send one. It refuses the send rather than falling back to channel encryption. Keys arrive inside node info, which is why an open lock usually clears itself once that node is heard from properly. + +A mismatch never replaces the key you already hold. The app keeps the first key it recorded and refuses the new one, as the firmware does, so a stray or hostile node info cannot silently break encrypted messaging to a contact. To clear a mismatch, first confirm through another trusted channel that the key change was intentional — a factory reset causes one. Then touch & hold the node, choose **Remove**, and let the two nodes exchange keys again the next time yours hears it. ## Quick Actions @@ -108,7 +112,9 @@ radio is connected and running firmware 2.8 or newer — see ## Sharing a Contact -On a node's detail screen, tap **Share Contact** to produce a link and a QR code for that node. From the same dialog, **Write to NFC tag** saves the link to a writable NFC tag that anyone can tap to open. +On a node's detail screen, tap **Share Contact** to produce a link and a QR code for that node. From the same dialog, **Share link** opens the Android share sheet (on desktop it copies the link instead), **Write to NFC tag** saves it to a writable NFC tag, and **Copy** puts it on the clipboard. While that dialog is open and in front of you, the phone also offers the same link to any NFC reader, so someone can take the contact by tapping their phone against yours with no tag involved. + +Sharing your own contact this way marks it as verified in person, so whoever imports it sees the **Verified contact** icon rather than the signed one. Relaying someone else's contact passes on only what your app had already recorded about them. To add someone else's contact, use the import button on the node list and choose **Scan Shared Contact QR Code**, **Scan Shared Contact NFC**, or **Input Shared Contact URL**. The app asks you to confirm with **Import Shared Contact?**, and warns you when the contact is one you already have. @@ -120,14 +126,16 @@ Type in the search field to filter nodes by name or short name. The filter updat ### Filter Toggles -| Filter | Lýsing | -| --------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| **Hide offline nodes** | Show only nodes heard within the last 2 hours | -| **Only show direct nodes** | Show only nodes your radio heard directly, with no relay in between | -| **Include unknown** | Show nodes that haven't sent user info yet. **On by default**, so a node heard before its info arrives stays visible; these carry a badge marking them incomplete, and cannot be direct messaged until their user info brings a public key | -| **Exclude infrastructure** | Hide infrastructure-role nodes (Router, Router Late, Client Base, and legacy Repeater nodes) and any node that cannot be messaged, whatever its role | -| **Exclude MQTT** | Hide nodes heard only via MQTT internet bridge | -| **Only show ignored Nodes** | Replace the list with the nodes you have ignored. Every other node is hidden while this is on, and a banner appears at the top of the list to take you back | +| Filter | Lýsing | +| --------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **Hide offline nodes** | Show only nodes heard within the last 2 hours | +| **Only show direct nodes** | Show only nodes your radio heard directly, with no relay in between | +| **Include unknown** | Show nodes that haven't sent user info yet. **On by default**, so a node heard before its info arrives stays visible; these carry a badge marking them incomplete, and cannot be direct messaged until their user info brings a public key | +| **Exclude infrastructure** | Hide infrastructure-role nodes (Router, Router Late, Client Base, and legacy Repeater nodes) and any node that cannot be messaged, whatever its role | +| **Exclude MQTT** | Hide nodes heard only via MQTT internet bridge | +| **Signed only** | Show only nodes whose signed broadcasts your node has actually heard and verified. Stricter than the icon: a node on 2.8 shows as signed by its firmware version before any signed broadcast arrives, and a contact verified in person on older firmware is not signed at all | +| **Encrypted only** | Show nodes with a matching public key on file, the key an encrypted direct message needs. A node with a key mismatch is excluded | +| **Only show ignored Nodes** | Replace the list with the nodes you have ignored. Every other node is hidden while this is on, and a banner appears at the top of the list to take you back | ### Sort Options @@ -149,6 +157,8 @@ Tap the hop-histogram icon in the node list's app bar to open a bar chart of how Tapping a node opens the detail view with comprehensive information. See [Node Metrics](node-metrics) for full details on metrics and telemetry. +Signal quality is rated against your modem preset. The same SNR can be good on a long-range preset and poor on a faster one. Traceroute and neighbor-info SNR colors use that preset too. RSSI text has its own strength colors; it affects the quality rating only when a noise-floor reading is also available. + The Details card carries the node's short name, role, IDs, last heard time, hops away, uptime, and its SNR and RSSI: ![Node detail section](../../assets/screenshots/nodes_detail_section.png) diff --git a/docs/is-rIS/user/settings-module-admin.md b/docs/is-rIS/user/settings-module-admin.md index 6f4716b37e..4bf88f9a61 100644 --- a/docs/is-rIS/user/settings-module-admin.md +++ b/docs/is-rIS/user/settings-module-admin.md @@ -2,7 +2,7 @@ title: Settings — Modules & Admin parent: User Guide nav_order: 8 -last_updated: 2026-08-30 +last_updated: 2026-09-16 description: Configure optional feature modules (MQTT, telemetry, canned messages, TAK, and more) and perform device administration. aliases: - modules @@ -333,7 +333,7 @@ appear only when your own node is selected: ### Um smáforrit -**Settings → About** carries the app's own identity rather than the radio's: +**Settings → About** carries the app's own identity rather than the node's: Three sections: @@ -341,7 +341,11 @@ Three sections: - **Apps** — opens with **Need Hardware?**, a rotating carousel of popular devices that links out to where to buy one. It also lists the GitHub repository, the running app version, and **Acknowledgements** (see the next section). -- **Project information** — links to the website and to this documentation. +- **Project information** — links to the website, to this documentation, and to the GNU GPL v3 + license the app is released under. + +The footer states the copyright and that the app is free software with no warranty, which you +may redistribute under the same license. ### Acknowledgements diff --git a/docs/is-rIS/user/settings-radio-user.md b/docs/is-rIS/user/settings-radio-user.md index cb328f8dc8..5bd597ffe2 100644 --- a/docs/is-rIS/user/settings-radio-user.md +++ b/docs/is-rIS/user/settings-radio-user.md @@ -2,7 +2,7 @@ title: Settings — Radio & User parent: User Guide nav_order: 7 -last_updated: 2026-09-04 +last_updated: 2026-09-11 description: Configure your radio hardware, LoRa presets, user profile, position sharing, power management, and security. aliases: - settings @@ -51,7 +51,7 @@ The footer appears as soon as you change something. **Discard** throws the chang The status message is saved with the same **Save**, but it never reboots the node — and, like the rest of this screen, it can be edited on a remote node you administer. For your own radio there is a shortcut while it is connected: touch & hold your node in the [node list](nodes.md) and choose -**Update status**. Older firmware and a disconnected radio have no shortcut — the field above is +**Update status**. Older firmware and a disconnected node have no shortcut — the field itself is still the way in. ## Configuration @@ -77,13 +77,13 @@ On **Settings → LoRa**. | Setting | Lýsing | Default | | ------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------- | -| Svæði | Regulatory region for frequency bands. You must set this before transmitting | Unset (must configure) | +| Svæði | Regulatory region for frequency bands. You must set this before transmitting. On firmware 2.8 or newer, the first region set also creates the node's identity key and gives it a new node number | Unset (must configure) | | Presets | Speed/range tradeoff | LongFast | | Number of Hops | Maximum retransmit hops | 3 | | Transmit Power | Transmission power (dBm); 0 = max allowed for region | 0 (region max) | | Frequency Override | Overrides the computed operating frequency outright (MHz). It does not offset the calculated value — leave at 0 unless you know you need a specific frequency | 0 (use calculated) | | Use Preset | On by default. Turn it off to set Spread Factor, Coding Rate and Bandwidth by hand instead of taking them from the modem preset | On | -| Spread Factor | Manual mode only: 7–12. Higher spreads further but slower | From preset | +| Spread Factor | Manual mode only: 5–12. Higher spreads further but slower. On SX127x (RF95) radios the firmware does not accept 5 or 6 and uses 11 instead | From preset | | Coding Rate | Manual mode only: 5–8. More redundancy costs airtime | From preset | | Bandwidth | Manual mode only: the channel bandwidth in kHz, typed in directly. On the 2.4 GHz region the app offers a list of the bandwidths your radio supports instead, and a stored value that is not on that list shows as _Unsupported_ and blocks saving until you pick a supported one | From preset | | Frequency Slot | Which slot within the region's band to use. 0 derives it from the primary channel name | 0 (automatic) | @@ -111,13 +111,13 @@ The Lite, Narrow, Medium Turbo, and Tiny presets need firmware 2.8 or newer — | Short Slow | ~5 km | 6.25 kbps | −10 dB | Suburban short-range; moderate building density | | Medium Fast | ~5 km | 3.52 kbps | −12.5 dB | Suburban areas; moderate building density | | Medium Slow | ~8 km | 1.95 kbps | −15 dB | Suburban/rural; moderate range with slower speed | -| Long Turbo | ~10 km | 1.34 kbps | −12.5 dB | Similar range to Long Fast but with 500 kHz bandwidth; faster throughput | +| Long Turbo | ~10 km | 1.34 kbps | −17.5 dB | Similar range to Long Fast but with 500 kHz bandwidth; faster throughput | | Long Fast | ~10 km | 1.1 kbps | −17.5 dB | **General use (default)** — balanced range and speed | | Long Moderate | ~20 km | 0.34 kbps | −17.5 dB | Rural with some terrain; occasional use | | Lite Fast | ~5 km | 1.76 kbps | −12.5 dB | EU 866 MHz SRD band (125 kHz BW); comparable to Medium Fast | | Lite Slow | ~10 km | 0.98 kbps | −15 dB | EU 866 MHz SRD band (125 kHz BW); comparable to Long Fast | -| Narrow Fast | ~5 km | 2.28 kbps | −10 dB | EU 868 MHz band (62.5 kHz BW); avoids interference with other devices | -| Narrow Slow | ~10 km | 1.30 kbps | −12.5 dB | EU 868 MHz band (62.5 kHz BW); comparable to Long Fast | +| Narrow Fast | ~5 km | 2.28 kbps | −7.5 dB | EU 868 MHz band (62.5 kHz BW); avoids interference with other devices | +| Narrow Slow | ~10 km | 1.30 kbps | −10 dB | EU 868 MHz band (62.5 kHz BW); comparable to Long Fast | | Medium Turbo | ~5 km | 7.0 kbps | −12.5 dB | Like Medium Fast but with 500 kHz bandwidth; not legal in every region. Needs firmware 2.8 or newer | | Tiny Fast | ~10 km | 0.68 kbps | −7.5 dB | Amateur bands that cap occupied bandwidth; these presets use 15.6 kHz. Needs firmware 2.8 or newer, an SX126x or SX127x radio, and a TCXO of ±5 ppm or better | | Tiny Slow | ~20 km | 0.33 kbps | −10 dB | Same band restrictions as Tiny Fast, longer range. Same firmware, radio, and TCXO requirements | diff --git a/docs/is-rIS/user/signal-meter.md b/docs/is-rIS/user/signal-meter.md index c4e7c190bd..12d6aaade0 100644 --- a/docs/is-rIS/user/signal-meter.md +++ b/docs/is-rIS/user/signal-meter.md @@ -2,7 +2,7 @@ title: How the Meshtastic Signal Meter Works parent: User Guide nav_order: 15 -last_updated: 2026-08-30 +last_updated: 2026-09-09 description: How the signal meter rates quality from SNR relative to the LoRa modem preset — spread spectrum, presets, and what the bars really mean. aliases: - signal @@ -32,7 +32,7 @@ Each modem preset has an SNR limit: the lowest SNR at which that preset can stil ## Rating Signal Quality -The app rates signal quality (None, Bad, Fair, or Good) from SNR alone, measured against the active preset's SNR limit. It does not factor in RSSI: without knowing the local noise floor, RSSI alone cannot say whether a signal is decodable. RSSI is still available — on the node detail screen and in the metrics charts. +The app rates signal quality (None, Bad, Fair, or Good) from SNR relative to the active preset's SNR limit. When RSSI and a noise-floor reading are both available, it also rates the difference between them against that limit and uses the worse rating. Otherwise, it uses SNR alone. Because the rating is relative to the preset, the same SNR rates differently on different presets. An SNR of −16 dB rates Good on Long Fast (SNR limit −17.5 dB) but None on Short Fast (SNR limit −7.5 dB). Let `limit` be the active preset's SNR limit: @@ -45,7 +45,7 @@ Because the rating is relative to the preset, the same SNR rates differently on The icon never goes blank, so count the bars carefully: a single bar means None, not a weak but usable link, and Good is a solid wedge rather than a set of bars. A gray three-bar icon labeled Unknown is a different state again — the packet carried no SNR measurement at all, which is not the same as measuring one and finding it too weak. -> ℹ️ **Note:** Traceroute hop colors use fixed thresholds (−7 dB / −15 dB); the per-node signal meter uses the preset-relative rating instead. +> ℹ️ **Note:** Traceroute and neighbor-info SNR colors use the active preset's limits too. RSSI text has separate strength colors: green above −115 dBm, yellow above −120 dBm, orange above −126 dBm, and red at or below −126 dBm. ## Diagnosing Local Interference diff --git a/docs/is-rIS/user/tak.md b/docs/is-rIS/user/tak.md index 05809873af..f3c0aef2bf 100644 --- a/docs/is-rIS/user/tak.md +++ b/docs/is-rIS/user/tak.md @@ -2,7 +2,7 @@ title: TAK Integration parent: User Guide nav_order: 10 -last_updated: 2026-08-30 +last_updated: 2026-09-11 description: Interoperate with ATAK and WinTAK — CoT position sharing, TAK roles, and plugin setup. aliases: - tak @@ -36,7 +36,7 @@ The TAK module allows Meshtastic nodes to: > ⚠️ **Warning:** The old **Meshtastic ATAK Plugin** is no longer part of this path and cannot > work. It bridged through the cross-process AIDL API, which was removed in app 2.8.0; the mesh > service is now in-process only. Do not install it. Interop today runs over the app's own local -> TAK server plus the Mesh to CoT Converter, both described below, with stock ATAK/iTAK/WinTAK. +> TAK server plus the Mesh to CoT Converter, both described in the following sections, with stock ATAK/iTAK/WinTAK. ### Configuration diff --git a/docs/is-rIS/user/translate.md b/docs/is-rIS/user/translate.md index 67d69e2d96..cdc8b6d87f 100644 --- a/docs/is-rIS/user/translate.md +++ b/docs/is-rIS/user/translate.md @@ -2,7 +2,7 @@ title: Translate the App parent: User Guide nav_order: 17 -last_updated: 2026-08-30 +last_updated: 2026-09-11 description: How the app and its documentation are translated via Crowdin, and guidelines for contributing translations. aliases: - translate @@ -45,7 +45,7 @@ If your language is not yet listed on Crowdin: The Android app uses **Compose Multiplatform resources** for all user-visible strings: -``` +```text core/resources/src/commonMain/composeResources/ ├── values/ ← English (default) │ └── strings.xml @@ -58,7 +58,7 @@ core/resources/src/commonMain/composeResources/ In-app documentation follows a similar pattern under `docs/`: -``` +```text docs/ ├── en/user/ ← English source (default) │ ├── onboarding.md diff --git a/docs/it-rIT/user/desktop.md b/docs/it-rIT/user/desktop.md index 623945a6d3..2abaa5f6bd 100644 --- a/docs/it-rIT/user/desktop.md +++ b/docs/it-rIT/user/desktop.md @@ -2,7 +2,7 @@ title: Desktop App parent: User Guide nav_order: 14 -last_updated: 2026-08-30 +last_updated: 2026-09-11 description: Install and use the Meshtastic Desktop app on Linux, macOS, and Windows — connections, feature parity, and keyboard shortcuts. aliases: - desktop @@ -58,20 +58,20 @@ Bluetooth Low Energy is supported on desktop via the [Kable](https://github.com/ ## Feature Parity -| Feature | Android | Desktop | Note | -| ----------------------------------------------------- | ------- | ------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Messaging | ✓ | ✓ | Full parity | -| Node List | ✓ | ✓ | Full parity | -| Mappa | ✓ | ✓ | Interactive MapLibre map, with base map and overlay pickers and custom tile sources. No offline downloads or local `.mbtiles` archives | -| Map layers (`.kml`/`.kmz`/GeoJSON) | ✓ | ✓ | Same layer store and sheet as Android; imported files draw on the desktop map | -| Site Planner | ✓ | ✓\* | \*Opens in your browser on desktop; the estimate is not drawn on the desktop map | -| Impostazioni | ✓ | ✓ | Full parity | -| Bluetooth (BLE) | ✓ | ✓ | Via Kable on desktop | -| Aggiornamento Firmware | ✓ | ✓ | In-app USB, BLE, and Wi-Fi (ESP32) update work the same as Android. The USB maintenance flow — nRF52/RP2040 factory erase and bootloader upgrade — is Android-only | -| Notifications | ✓ | ✓ | Native OS notifications | -| Widgets | ✓ | ✗ | Android-only | -| AI Assistant (Chirpy) | ✓\* | ✗ | Google flavor Android only | -| App Functions (system AI) | ✓† | ✗ | Google flavor Android only | +| Feature | Android | Desktop | Note | +| ----------------------------------------------------- | ------- | ------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Messaging | ✓ | ✓ | Full parity | +| Node List | ✓ | ✓ | Full parity | +| Mappa | ✓ | ✓ | Interactive MapLibre map, with base map and overlay pickers and custom tile sources. Offline terrain (hillshade and contours) can be downloaded; offline base-map packs and local `.mbtiles` archives cannot | +| Map layers (`.kml`/`.kmz`/GeoJSON) | ✓ | ✓ | Same layer store and sheet as Android; imported files draw on the desktop map | +| Site Planner | ✓ | ✓\* | \*Opens in your browser on desktop; the estimate is not drawn on the desktop map | +| Impostazioni | ✓ | ✓ | Full parity | +| Bluetooth (BLE) | ✓ | ✓ | Via Kable on desktop | +| Aggiornamento Firmware | ✓ | ✓ | In-app USB, BLE, and Wi-Fi (ESP32) update work the same as Android. The USB maintenance flow — nRF52/RP2040 factory erase and bootloader upgrade — is Android-only | +| Notifications | ✓ | ✓ | Native OS notifications | +| Widgets | ✓ | ✗ | Android-only | +| AI Assistant (Chirpy) | ✓\* | ✗ | Google flavor Android only | +| App Functions (system AI) | ✓† | ✗ | Google flavor Android only | \*Chirpy AI requires Android 14+ on Google flavor builds with supported hardware. @@ -122,7 +122,7 @@ Individual doc pages render with full formatting: ## Building from Source -```bash +```shell git clone https://github.com/meshtastic/Meshtastic-Android.git cd Meshtastic-Android ./gradlew :desktopApp:run @@ -135,7 +135,7 @@ Requisiti: ## Limitazioni conosciute -- Offline tile downloads and local `.mbtiles` archives are not available on desktop. +- Offline base-map downloads and local `.mbtiles` archives are not available on desktop. Offline terrain is — see [Map & Waypoints](map-and-waypoints). - `.kml`/`.kmz`/GeoJSON layer import works — see [Map & Waypoints](map-and-waypoints#map-layers). Site Planner opens in your browser rather than in the app; to bring its coverage estimate onto the map, click the transmitter pin diff --git a/docs/it-rIT/user/discovery.md b/docs/it-rIT/user/discovery.md index 1ba49554aa..2940c1e849 100644 --- a/docs/it-rIT/user/discovery.md +++ b/docs/it-rIT/user/discovery.md @@ -2,7 +2,7 @@ title: Discovery della mesh locale parent: User Guide nav_order: 12 -last_updated: 2026-08-30 +last_updated: 2026-09-11 description: Explore your mesh network — the Local Mesh Discovery scanner, traceroute paths, neighbor maps, and node discovery tools. aliases: - discovery @@ -97,7 +97,7 @@ Mesh Beacon lets nodes invite others to join their mesh. A beaconing node period Configure it under **Settings → Module configuration → Mesh Beacon**. The entry appears only on radios running firmware 2.8.0 or newer. A read-only **Region** row at the top of the screen shows the region the beacon advertises: that region, and the preset, are always the ones the radio itself uses, so a beacon cannot invite anyone onto settings your radio is not running. - **Listen for beacons** — receive invitations broadcast by other nodes. -- **Broadcast a beacon** — periodically advertise this mesh to nearby nodes, with an optional **Beacon message** of up to 100 bytes, a **Broadcast interval** picked from fixed intervals between 1 hour and 72 hours, and an **Offered channel** chosen from your radio's own channels. The offered channel is required, and defaults to your primary channel. +- **Broadcast a beacon** — periodically advertise this mesh to nearby nodes, with an optional **Beacon message** of up to 100 bytes, a **Broadcast interval** picked from fixed intervals between 1 hour and 72 hours, and an **Offered channel** chosen from your radio's own channels. The offered channel is required, and defaults to your primary channel. Over remote admin the picker offers the primary channel only. - **Broadcast targets** — optional extra destinations beyond the offered channel. **Add target** appends a row; each row picks a **Channel** and a **Transmit preset**, and **Remove target** deletes it. With no targets, the beacon goes out on the offered channel alone. Two conditions block beacon setup: diff --git a/docs/it-rIT/user/help-and-docs.md b/docs/it-rIT/user/help-and-docs.md index f306cde7f7..c83898cea3 100644 --- a/docs/it-rIT/user/help-and-docs.md +++ b/docs/it-rIT/user/help-and-docs.md @@ -2,7 +2,7 @@ title: Help & In-App Docs parent: User Guide nav_order: 21 -last_updated: 2026-08-30 +last_updated: 2026-09-11 description: Browse this documentation inside the app, search it, and ask Chirpy — the on-device AI assistant — questions about Meshtastic. aliases: - help @@ -23,7 +23,7 @@ The docs browser lists every user-guide page. Tap a page to read it; images and ### Search -The search box sits at the top of the docs browser. Type in it to filter pages by title and keywords — results update as you type, and the **Clear search** button (✕) empties the box. +Tap the search field at the top of the docs browser to open a full-screen search view. Type to filter pages by title and keywords — results update as you type, under the same field. The back arrow, or the keyboard's search action, closes the view. The field hides while you scroll down the page list and comes back when you scroll up. ![Searching the in-app documentation](../../assets/screenshots/docs-browser_search.png) diff --git a/docs/it-rIT/user/map-and-waypoints.md b/docs/it-rIT/user/map-and-waypoints.md index dbaf285225..b221eb35d5 100644 --- a/docs/it-rIT/user/map-and-waypoints.md +++ b/docs/it-rIT/user/map-and-waypoints.md @@ -2,7 +2,7 @@ title: Map & Waypoints parent: User Guide nav_order: 6 -last_updated: 2026-09-01 +last_updated: 2026-09-11 description: View node positions on the map, create and share waypoints, manage map layers and Site Planner, and control position sharing and privacy. aliases: - map @@ -44,7 +44,7 @@ The floating toolbar provides quick access to the compass, the map type and laye ### Filtering the Map -Tap the filter button in the floating toolbar to open **Filter map**. **Display** controls what is drawn: **Only Favorites**, **Show Waypoints**, **Show Precision Circles**, and a slider that hides nodes not heard from recently. **Node roles** is a chip per device role, plus **All** to show every role; a selected chip means that role is shown. **Nodes** narrows the set further with **Hide offline nodes**, **Only show direct nodes**, **Exclude MQTT**, **Show ignored nodes**, and **Include unknown**. +Tap the filter button in the floating toolbar to open **Filter map**. **Display** controls what is drawn: **Only Favorites**, **Show Waypoints**, **Show Precision Circles**, and a slider that hides nodes not heard from recently. **Node roles** is a chip per device role, plus **All** to show every role; a selected chip means that role is shown. **Nodes** narrows the set further with **Hide offline nodes**, **Only show direct nodes**, **Signed only**, **Encrypted only**, **Exclude MQTT**, **Show ignored nodes**, and **Include unknown** — the same rules as the node list's [filter toggles](nodes). A dot on the filter button means at least one filter is hiding something — check it before concluding the mesh is quiet. Turning **Show Waypoints** off hides every waypoint, including your own. **Show ignored nodes** adds them to the map rather than showing only them — unlike the node list's **Only show ignored Nodes**. @@ -147,8 +147,9 @@ picker, all three offer the same raster base maps: Overlays can be toggled on top of any base map, from the layers sheet: - **Weather radar** — NOAA NEXRAD reflectivity (US coverage) -- **Hillshade** — terrain relief, on **F-Droid** and **Desktop** only. Useful for understanding why a - link fails, since LoRa range is limited by terrain +- **Hillshade** — terrain relief from an online tile source, on **F-Droid** and **Desktop** only; Google + Play draws hillshade from downloaded offline terrain instead. Useful for understanding why a link + fails, since LoRa range is limited by terrain ### Adding your own tile source @@ -156,7 +157,7 @@ Any XYZ tile endpoint can be added as a base map, on every flavor and on desktop Tile Sources** at the foot of the base map picker and paste a URL template using `{z}`, `{x}` and `{y}` — plus `{s}` if the provider uses rotating subdomains. A national mapping service, for example: -``` +```text https://wmts.geo.admin.ch/1.0.0/ch.swisstopo.pixelkarte-farbe/default/current/3857/{z}/{x}/{y}.jpeg ``` @@ -173,7 +174,18 @@ On **Google Play**, the layers sheet's **Offline Manager** section downloads its — water, roads and administrative boundaries extracted from the public Protomaps basemap dataset, drawn directly as map shapes rather than raster tiles. Frame the area you want, tap **Start Download**, and once it finishes flip **Show on map** to draw it (it's off by default so it never surprises you by covering up the -live map). You can still import a pre-made `.mbtiles` archive here too, as before. **Desktop** has neither. +live map). You can still import a pre-made `.mbtiles` archive here too, as before. **Desktop** has neither, though +the offline terrain described next does work there. + +### Offline terrain + +Hillshade and elevation contours can be downloaded for offline use as well, so a downloaded area still +shows why a link fails when there is no network. The terrain data comes from Mapterhorn and is credited on +the map. On **Google Play**, each downloaded region in the **Offline Manager** list has a **Download Terrain** +button and **Hillshade** and **Contours** switches; terrain attaches to that region and shares its storage limit, and an area +too large for terrain asks you to zoom in or pick a smaller region. On **F-Droid** and **Desktop**, the +layers sheet has an **Offline Terrain** section of its own: frame the area, tap **Start Download**, and the +hillshade and contours draw from the downloaded tiles from then on. ### Going offline @@ -181,8 +193,8 @@ An **Offline** pill appears over the map whenever the device has no network conn builds, if you've imported a local `.mbtiles` archive, the map switches to it automatically the moment the network drops — no toggle to remember — and switches back once you're reconnected, as long as you haven't picked a different base map yourself in between. On **F-Droid**, a downloaded offline area keeps rendering on -its own; the pill is purely informational there. **Desktop** has no offline downloads at all yet, so the pill -is informational there too. +its own; the pill is purely informational there. **Desktop** downloads offline terrain but no base-map +packs, so the pill is informational there too. ## Related Topics diff --git a/docs/it-rIT/user/messages-and-channels.md b/docs/it-rIT/user/messages-and-channels.md index 8bcfb921ff..f2c8d0d200 100644 --- a/docs/it-rIT/user/messages-and-channels.md +++ b/docs/it-rIT/user/messages-and-channels.md @@ -2,7 +2,7 @@ title: Messages & Channels parent: User Guide nav_order: 3 -last_updated: 2026-08-30 +last_updated: 2026-09-14 description: Send and receive messages, manage channels, configure encryption, search conversations, and use quick chat, reactions, and message actions. aliases: - channels @@ -50,6 +50,21 @@ Key length alone does not change the icon: a 128-bit key and a 256-bit key both Tapping an existing channel opens the same editor, where you can change the name, the PSK, MQTT uplink and downlink, and position precision. Every edit on this screen — adding, editing, deleting, or dragging a channel into a new order — waits on **Send** the same way. +### Archived Channels + +A conversation belongs to the channel it was held on, not to the slot that channel occupied. When a channel leaves your radio — you scan a different channel QR, replace a channel's key, or switch the modem preset — its conversation is **archived** rather than deleted or merged into whatever takes the slot. + +An archived conversation: + +- stays in the **Channels** list under the name the channel had, below the channels currently on your radio, marked with a history icon; +- keeps its full message history, searchable as usual; +- is read-only — you cannot send, reply, or react, because the channel is no longer on the radio to send on, and it is not offered as a share target; +- comes back automatically if you re-add that exact channel, taking its messages back to the live slot. + +Changing the modem preset archives the channel too: the preset is part of a default channel's identity, so **LongFast** and **MediumFast** are different channels even though neither the name nor the key changed. Changing only the region does not archive anything. + +To remove an archived conversation for good, long-press it in the conversation list and delete it. + ## Messaggi Diretti Direct messages (DMs) go to one specific node. When both radios hold each other's public keys, your radio encrypts the message to that node's public key, so no one else on the mesh can read it — not even nodes that share your channel. diff --git a/docs/it-rIT/user/mqtt.md b/docs/it-rIT/user/mqtt.md index 13bfda3525..3145e19feb 100644 --- a/docs/it-rIT/user/mqtt.md +++ b/docs/it-rIT/user/mqtt.md @@ -2,7 +2,7 @@ title: MQTT parent: User Guide nav_order: 11 -last_updated: 2026-08-30 +last_updated: 2026-09-11 description: Bridge your mesh to the internet — MQTT broker setup, encryption layers, and map reporting. aliases: - mqtt @@ -25,7 +25,7 @@ The MQTT module connects your node to an MQTT broker, allowing: ## Come funziona -``` +```text [Your Node] → Radio → [Gateway Node with Wi-Fi] → MQTT Broker → [Remote Gateway] → Radio → [Remote Node] ``` diff --git a/docs/it-rIT/user/node-metrics.md b/docs/it-rIT/user/node-metrics.md index 04c5f02aa0..d5d6668c56 100644 --- a/docs/it-rIT/user/node-metrics.md +++ b/docs/it-rIT/user/node-metrics.md @@ -2,7 +2,7 @@ title: Node Metrics parent: User Guide nav_order: 5 -last_updated: 2026-08-30 +last_updated: 2026-09-09 description: Telemetry dashboards for each mesh node — device health, environment sensors, air quality, signal quality, power, traceroute, and position history. aliases: - metrics @@ -114,7 +114,7 @@ Radio signal quality information: ### Signal Quality Reference -Signal quality is rated from **SNR relative to the active LoRa modem preset's demodulation floor**, not from fixed thresholds — a given SNR means different things on different presets (e.g. −15 dB is fine on LongSlow but unusable on ShortFast). RSSI is shown but is not part of the rating. In the table, _limit_ is the preset's SNR limit. +Signal quality is rated from **SNR relative to the active LoRa modem preset's demodulation floor**, not from fixed thresholds — a given SNR means different things on different presets (e.g. −15 dB is fine on LongSlow but unusable on ShortFast). When RSSI and a noise-floor reading are both available, the app also rates their difference against the preset limit and uses the worse rating. Otherwise, RSSI is display-only. In the table, _limit_ is the preset's SNR limit. | Quality | Criteria | | -------- | ---------------------------------------------------------------- | diff --git a/docs/it-rIT/user/nodes.md b/docs/it-rIT/user/nodes.md index 9c84b412d5..fa03c91053 100644 --- a/docs/it-rIT/user/nodes.md +++ b/docs/it-rIT/user/nodes.md @@ -2,7 +2,7 @@ title: Nodi parent: User Guide nav_order: 4 -last_updated: 2026-09-04 +last_updated: 2026-09-11 description: Browse, filter, and sort mesh nodes — view details, signal quality, roles, and quick actions. aliases: - node-list @@ -74,19 +74,23 @@ Most users should keep the default **Client** role. Consider a different role wh > 💡 **Tip:** The mesh works best when most nodes are **Client** or **Router**. Too many Client Mute nodes reduce mesh resilience; too many Routers in a dense area can cause congestion. A good rule of thumb: one Router per 5–10 Clients in your area. -### Encryption Indicators +### Security indicators -Nodes display encryption status icons next to their name: +Each node carries one security icon beside its name in the node list. Tap it to read what it means, and choose **Show All Meanings** in that dialog for the full legend. The detail screen shows the same state in words, as a **Security** row that opens the same dialog. -| Icon | Meaning | -| ----------- | ------------------------------------------------------------------------------------------------------------------------------------------------- | -| 🔒 Locked | Communication uses PKI (public key infrastructure) — end-to-end encrypted with verified identity | -| 🔓 Unlocked | No public key has been received for this node, so it cannot be direct messaged — use **Request User Info** on the node detail page to ask for one | -| ⚠️ Mismatch | Public key mismatch — the node's key has changed since last seen (investigate before trusting) | +| Icon | Meaning | Shown for | +| --------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------- | +| Person with a shield check (green) | **Verified contact** — you verified this node's key in person by exchanging contact QR codes, so its identity is confirmed. The strongest trust the list shows. Your own connected node carries it too | Any firmware version | +| Nodes icon with a shield check (green) | **Signed node** — this node signs its broadcasts with its identity key, so its identity is consistent over time, but you have not verified it in person. On firmware 2.8 the icon appears from the version alone, before any signed broadcast has been heard | Firmware 2.8 or newer, and any node whose signed broadcast your node has verified | +| 🔒 Closed lock | A public key is on file and matches, so direct messages to this node are encrypted | Firmware before 2.8, or no reported version | +| 🔓 Open lock | No public key has been received for this node, so it cannot be direct messaged — use **Request User Info** on the node detail page to ask for one | Firmware before 2.8, or no reported version | +| ⚠️ Mismatch | **Public key mismatch** — a different key arrived for this node after one was stored. Investigate before trusting | Any firmware version | -> 💡 **Tip:** Direct messages always use PKI, so the radio needs the other node's public key before it can send one. It refuses the send rather than falling back to channel encryption. Keys arrive inside node info, which is why an open lock usually clears itself once that node is heard from properly. If you see a key mismatch warning, the node may have been reset or compromised. +Every node on firmware 2.8 or newer signs its broadcasts, so on that firmware the signed state is the baseline and the locks are not shown. -To clear a mismatch, first confirm through another trusted channel that the key change was intentional — a factory reset causes one. Then touch & hold the node, choose **Remove**, and let the two radios exchange keys again the next time yours hears it. +Direct messages always use public-key encryption, so your node needs the other node's public key before it can send one. It refuses the send rather than falling back to channel encryption. Keys arrive inside node info, which is why an open lock usually clears itself once that node is heard from properly. + +A mismatch never replaces the key you already hold. The app keeps the first key it recorded and refuses the new one, as the firmware does, so a stray or hostile node info cannot silently break encrypted messaging to a contact. To clear a mismatch, first confirm through another trusted channel that the key change was intentional — a factory reset causes one. Then touch & hold the node, choose **Remove**, and let the two nodes exchange keys again the next time yours hears it. ## Quick Actions @@ -108,7 +112,9 @@ radio is connected and running firmware 2.8 or newer — see ## Sharing a Contact -On a node's detail screen, tap **Share Contact** to produce a link and a QR code for that node. From the same dialog, **Write to NFC tag** saves the link to a writable NFC tag that anyone can tap to open. +On a node's detail screen, tap **Share Contact** to produce a link and a QR code for that node. From the same dialog, **Share link** opens the Android share sheet (on desktop it copies the link instead), **Write to NFC tag** saves it to a writable NFC tag, and **Copy** puts it on the clipboard. While that dialog is open and in front of you, the phone also offers the same link to any NFC reader, so someone can take the contact by tapping their phone against yours with no tag involved. + +Sharing your own contact this way marks it as verified in person, so whoever imports it sees the **Verified contact** icon rather than the signed one. Relaying someone else's contact passes on only what your app had already recorded about them. To add someone else's contact, use the import button on the node list and choose **Scan Shared Contact QR Code**, **Scan Shared Contact NFC**, or **Input Shared Contact URL**. The app asks you to confirm with **Import Shared Contact?**, and warns you when the contact is one you already have. @@ -120,14 +126,16 @@ Type in the search field to filter nodes by name or short name. The filter updat ### Filter Toggles -| Filtro | Descrizione | -| --------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| **Hide offline nodes** | Show only nodes heard within the last 2 hours | -| **Only show direct nodes** | Show only nodes your radio heard directly, with no relay in between | -| **Include unknown** | Show nodes that haven't sent user info yet. **On by default**, so a node heard before its info arrives stays visible; these carry a badge marking them incomplete, and cannot be direct messaged until their user info brings a public key | -| **Exclude infrastructure** | Hide infrastructure-role nodes (Router, Router Late, Client Base, and legacy Repeater nodes) and any node that cannot be messaged, whatever its role | -| **Exclude MQTT** | Hide nodes heard only via MQTT internet bridge | -| **Only show ignored Nodes** | Replace the list with the nodes you have ignored. Every other node is hidden while this is on, and a banner appears at the top of the list to take you back | +| Filtro | Descrizione | +| --------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **Hide offline nodes** | Show only nodes heard within the last 2 hours | +| **Only show direct nodes** | Show only nodes your radio heard directly, with no relay in between | +| **Include unknown** | Show nodes that haven't sent user info yet. **On by default**, so a node heard before its info arrives stays visible; these carry a badge marking them incomplete, and cannot be direct messaged until their user info brings a public key | +| **Exclude infrastructure** | Hide infrastructure-role nodes (Router, Router Late, Client Base, and legacy Repeater nodes) and any node that cannot be messaged, whatever its role | +| **Exclude MQTT** | Hide nodes heard only via MQTT internet bridge | +| **Signed only** | Show only nodes whose signed broadcasts your node has actually heard and verified. Stricter than the icon: a node on 2.8 shows as signed by its firmware version before any signed broadcast arrives, and a contact verified in person on older firmware is not signed at all | +| **Encrypted only** | Show nodes with a matching public key on file, the key an encrypted direct message needs. A node with a key mismatch is excluded | +| **Only show ignored Nodes** | Replace the list with the nodes you have ignored. Every other node is hidden while this is on, and a banner appears at the top of the list to take you back | ### Sort Options @@ -149,6 +157,8 @@ Tap the hop-histogram icon in the node list's app bar to open a bar chart of how Tapping a node opens the detail view with comprehensive information. See [Node Metrics](node-metrics) for full details on metrics and telemetry. +Signal quality is rated against your modem preset. The same SNR can be good on a long-range preset and poor on a faster one. Traceroute and neighbor-info SNR colors use that preset too. RSSI text has its own strength colors; it affects the quality rating only when a noise-floor reading is also available. + The Details card carries the node's short name, role, IDs, last heard time, hops away, uptime, and its SNR and RSSI: ![Node detail section](../../assets/screenshots/nodes_detail_section.png) diff --git a/docs/it-rIT/user/settings-module-admin.md b/docs/it-rIT/user/settings-module-admin.md index df8ab11ddd..32ff37ab30 100644 --- a/docs/it-rIT/user/settings-module-admin.md +++ b/docs/it-rIT/user/settings-module-admin.md @@ -2,7 +2,7 @@ title: Settings — Modules & Admin parent: User Guide nav_order: 8 -last_updated: 2026-08-30 +last_updated: 2026-09-16 description: Configure optional feature modules (MQTT, telemetry, canned messages, TAK, and more) and perform device administration. aliases: - modules @@ -333,7 +333,7 @@ appear only when your own node is selected: ### Informazioni -**Settings → About** carries the app's own identity rather than the radio's: +**Settings → About** carries the app's own identity rather than the node's: Three sections: @@ -341,7 +341,11 @@ Three sections: - **Apps** — opens with **Need Hardware?**, a rotating carousel of popular devices that links out to where to buy one. It also lists the GitHub repository, the running app version, and **Acknowledgements** (see the next section). -- **Project information** — links to the website and to this documentation. +- **Project information** — links to the website, to this documentation, and to the GNU GPL v3 + license the app is released under. + +The footer states the copyright and that the app is free software with no warranty, which you +may redistribute under the same license. ### Ringraziamenti diff --git a/docs/it-rIT/user/settings-radio-user.md b/docs/it-rIT/user/settings-radio-user.md index 344ecdc8e4..81dae9a4fe 100644 --- a/docs/it-rIT/user/settings-radio-user.md +++ b/docs/it-rIT/user/settings-radio-user.md @@ -2,7 +2,7 @@ title: Settings — Radio & User parent: User Guide nav_order: 7 -last_updated: 2026-09-04 +last_updated: 2026-09-11 description: Configure your radio hardware, LoRa presets, user profile, position sharing, power management, and security. aliases: - impostazioni @@ -51,7 +51,7 @@ The footer appears as soon as you change something. **Discard** throws the chang The status message is saved with the same **Save**, but it never reboots the node — and, like the rest of this screen, it can be edited on a remote node you administer. For your own radio there is a shortcut while it is connected: touch & hold your node in the [node list](nodes.md) and choose -**Update status**. Older firmware and a disconnected radio have no shortcut — the field above is +**Update status**. Older firmware and a disconnected node have no shortcut — the field itself is still the way in. ## Configurazione @@ -77,13 +77,13 @@ On **Settings → LoRa**. | Impostazione | Descrizione | Predefinito | | ------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------- | -| Regione | Regulatory region for frequency bands. You must set this before transmitting | Unset (must configure) | +| Regione | Regulatory region for frequency bands. You must set this before transmitting. On firmware 2.8 or newer, the first region set also creates the node's identity key and gives it a new node number | Unset (must configure) | | Preset | Speed/range tradeoff | LongFast | | Numero di Hop | Maximum retransmit hops | 3 | | Potenza di Trasmissione | Transmission power (dBm); 0 = max allowed for region | 0 (region max) | | Sovrascrivi Frequenza | Overrides the computed operating frequency outright (MHz). It does not offset the calculated value — leave at 0 unless you know you need a specific frequency | 0 (use calculated) | | Usa Preset | On by default. Turn it off to set Spread Factor, Coding Rate and Bandwidth by hand instead of taking them from the modem preset | On | -| Spread Factor | Manual mode only: 7–12. Higher spreads further but slower | From preset | +| Spread Factor | Manual mode only: 5–12. Higher spreads further but slower. On SX127x (RF95) radios the firmware does not accept 5 or 6 and uses 11 instead | From preset | | Coding Rate | Manual mode only: 5–8. More redundancy costs airtime | From preset | | Larghezza di banda | Manual mode only: the channel bandwidth in kHz, typed in directly. On the 2.4 GHz region the app offers a list of the bandwidths your radio supports instead, and a stored value that is not on that list shows as _Unsupported_ and blocks saving until you pick a supported one | From preset | | Slot di Frequenza | Which slot within the region's band to use. 0 derives it from the primary channel name | 0 (automatic) | @@ -111,13 +111,13 @@ The Lite, Narrow, Medium Turbo, and Tiny presets need firmware 2.8 or newer — | Short Slow | ~5 km | 6,25 kbps | −10 dB | Suburban short-range; moderate building density | | Medium Fast | ~5 km | 3,52 kbps | −12.5 dB | Suburban areas; moderate building density | | Medium Slow | ~8 km | 1,95 kbps | −15 dB | Suburban/rural; moderate range with slower speed | -| Long Turbo | ~10 km | 1.34 kbps | −12.5 dB | Similar range to Long Fast but with 500 kHz bandwidth; faster throughput | +| Long Turbo | ~10 km | 1.34 kbps | −17.5 dB | Similar range to Long Fast but with 500 kHz bandwidth; faster throughput | | Long Fast | ~10 km | 1.1 kbps | −17.5 dB | **General use (default)** — balanced range and speed | | Long Moderate | ~20 km | 0,34 kbps | −17.5 dB | Rural with some terrain; occasional use | | Lite Fast | ~5 km | 1.76 kbps | −12.5 dB | EU 866 MHz SRD band (125 kHz BW); comparable to Medium Fast | | Lite Slow | ~10 km | 0.98 kbps | −15 dB | EU 866 MHz SRD band (125 kHz BW); comparable to Long Fast | -| Narrow Fast | ~5 km | 2.28 kbps | −10 dB | EU 868 MHz band (62.5 kHz BW); avoids interference with other devices | -| Narrow Slow | ~10 km | 1.30 kbps | −12.5 dB | EU 868 MHz band (62.5 kHz BW); comparable to Long Fast | +| Narrow Fast | ~5 km | 2.28 kbps | −7.5 dB | EU 868 MHz band (62.5 kHz BW); avoids interference with other devices | +| Narrow Slow | ~10 km | 1.30 kbps | −10 dB | EU 868 MHz band (62.5 kHz BW); comparable to Long Fast | | Medium Turbo | ~5 km | 7.0 kbps | −12.5 dB | Like Medium Fast but with 500 kHz bandwidth; not legal in every region. Needs firmware 2.8 or newer | | Tiny Fast | ~10 km | 0.68 kbps | −7.5 dB | Amateur bands that cap occupied bandwidth; these presets use 15.6 kHz. Needs firmware 2.8 or newer, an SX126x or SX127x radio, and a TCXO of ±5 ppm or better | | Tiny Slow | ~20 km | 0.33 kbps | −10 dB | Same band restrictions as Tiny Fast, longer range. Same firmware, radio, and TCXO requirements | diff --git a/docs/it-rIT/user/signal-meter.md b/docs/it-rIT/user/signal-meter.md index 42093c4958..b7b8a20c72 100644 --- a/docs/it-rIT/user/signal-meter.md +++ b/docs/it-rIT/user/signal-meter.md @@ -2,7 +2,7 @@ title: How the Meshtastic Signal Meter Works parent: User Guide nav_order: 15 -last_updated: 2026-08-30 +last_updated: 2026-09-09 description: How the signal meter rates quality from SNR relative to the LoRa modem preset — spread spectrum, presets, and what the bars really mean. aliases: - signal @@ -32,7 +32,7 @@ Each modem preset has an SNR limit: the lowest SNR at which that preset can stil ## Rating Signal Quality -The app rates signal quality (None, Bad, Fair, or Good) from SNR alone, measured against the active preset's SNR limit. It does not factor in RSSI: without knowing the local noise floor, RSSI alone cannot say whether a signal is decodable. RSSI is still available — on the node detail screen and in the metrics charts. +The app rates signal quality (None, Bad, Fair, or Good) from SNR relative to the active preset's SNR limit. When RSSI and a noise-floor reading are both available, it also rates the difference between them against that limit and uses the worse rating. Otherwise, it uses SNR alone. Because the rating is relative to the preset, the same SNR rates differently on different presets. An SNR of −16 dB rates Good on Long Fast (SNR limit −17.5 dB) but None on Short Fast (SNR limit −7.5 dB). Let `limit` be the active preset's SNR limit: @@ -45,7 +45,7 @@ Because the rating is relative to the preset, the same SNR rates differently on The icon never goes blank, so count the bars carefully: a single bar means None, not a weak but usable link, and Good is a solid wedge rather than a set of bars. A gray three-bar icon labeled Unknown is a different state again — the packet carried no SNR measurement at all, which is not the same as measuring one and finding it too weak. -> ℹ️ **Note:** Traceroute hop colors use fixed thresholds (−7 dB / −15 dB); the per-node signal meter uses the preset-relative rating instead. +> ℹ️ **Note:** Traceroute and neighbor-info SNR colors use the active preset's limits too. RSSI text has separate strength colors: green above −115 dBm, yellow above −120 dBm, orange above −126 dBm, and red at or below −126 dBm. ## Diagnosing Local Interference diff --git a/docs/it-rIT/user/tak.md b/docs/it-rIT/user/tak.md index 9d75a4c20a..f1fbcf4688 100644 --- a/docs/it-rIT/user/tak.md +++ b/docs/it-rIT/user/tak.md @@ -2,7 +2,7 @@ title: TAK Integration parent: User Guide nav_order: 10 -last_updated: 2026-08-30 +last_updated: 2026-09-11 description: Interoperate with ATAK and WinTAK — CoT position sharing, TAK roles, and plugin setup. aliases: - tak @@ -36,7 +36,7 @@ The TAK module allows Meshtastic nodes to: > ⚠️ **Warning:** The old **Meshtastic ATAK Plugin** is no longer part of this path and cannot > work. It bridged through the cross-process AIDL API, which was removed in app 2.8.0; the mesh > service is now in-process only. Do not install it. Interop today runs over the app's own local -> TAK server plus the Mesh to CoT Converter, both described below, with stock ATAK/iTAK/WinTAK. +> TAK server plus the Mesh to CoT Converter, both described in the following sections, with stock ATAK/iTAK/WinTAK. ### Configurazione diff --git a/docs/it-rIT/user/translate.md b/docs/it-rIT/user/translate.md index 2e153493e4..0404022919 100644 --- a/docs/it-rIT/user/translate.md +++ b/docs/it-rIT/user/translate.md @@ -2,7 +2,7 @@ title: Translate the App parent: User Guide nav_order: 17 -last_updated: 2026-08-30 +last_updated: 2026-09-11 description: How the app and its documentation are translated via Crowdin, and guidelines for contributing translations. aliases: - translate @@ -45,7 +45,7 @@ If your language is not yet listed on Crowdin: The Android app uses **Compose Multiplatform resources** for all user-visible strings: -``` +```text core/resources/src/commonMain/composeResources/ ├── values/ ← English (default) │ └── strings.xml @@ -58,7 +58,7 @@ core/resources/src/commonMain/composeResources/ In-app documentation follows a similar pattern under `docs/`: -``` +```text docs/ ├── en/user/ ← English source (default) │ ├── onboarding.md diff --git a/docs/iw-rIL/user/desktop.md b/docs/iw-rIL/user/desktop.md index 04f7d3980b..4c4af4607f 100644 --- a/docs/iw-rIL/user/desktop.md +++ b/docs/iw-rIL/user/desktop.md @@ -2,7 +2,7 @@ title: Desktop App parent: User Guide nav_order: 14 -last_updated: 2026-08-30 +last_updated: 2026-09-11 description: Install and use the Meshtastic Desktop app on Linux, macOS, and Windows — connections, feature parity, and keyboard shortcuts. aliases: - desktop @@ -58,20 +58,20 @@ Bluetooth Low Energy is supported on desktop via the [Kable](https://github.com/ ## Feature Parity -| Feature | Android | Desktop | Notes | -| ----------------------------------------------------- | ------- | ------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Messaging | ✓ | ✓ | Full parity | -| Node List | ✓ | ✓ | Full parity | -| מפה | ✓ | ✓ | Interactive MapLibre map, with base map and overlay pickers and custom tile sources. No offline downloads or local `.mbtiles` archives | -| Map layers (`.kml`/`.kmz`/GeoJSON) | ✓ | ✓ | Same layer store and sheet as Android; imported files draw on the desktop map | -| Site Planner | ✓ | ✓\* | \*Opens in your browser on desktop; the estimate is not drawn on the desktop map | -| הגדרות | ✓ | ✓ | Full parity | -| Bluetooth (BLE) | ✓ | ✓ | Via Kable on desktop | -| Firmware Update | ✓ | ✓ | In-app USB, BLE, and Wi-Fi (ESP32) update work the same as Android. The USB maintenance flow — nRF52/RP2040 factory erase and bootloader upgrade — is Android-only | -| Notifications | ✓ | ✓ | Native OS notifications | -| Widgets | ✓ | ✗ | Android-only | -| AI Assistant (Chirpy) | ✓\* | ✗ | Google flavor Android only | -| App Functions (system AI) | ✓† | ✗ | Google flavor Android only | +| Feature | Android | Desktop | Notes | +| ----------------------------------------------------- | ------- | ------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Messaging | ✓ | ✓ | Full parity | +| Node List | ✓ | ✓ | Full parity | +| מפה | ✓ | ✓ | Interactive MapLibre map, with base map and overlay pickers and custom tile sources. Offline terrain (hillshade and contours) can be downloaded; offline base-map packs and local `.mbtiles` archives cannot | +| Map layers (`.kml`/`.kmz`/GeoJSON) | ✓ | ✓ | Same layer store and sheet as Android; imported files draw on the desktop map | +| Site Planner | ✓ | ✓\* | \*Opens in your browser on desktop; the estimate is not drawn on the desktop map | +| הגדרות | ✓ | ✓ | Full parity | +| Bluetooth (BLE) | ✓ | ✓ | Via Kable on desktop | +| Firmware Update | ✓ | ✓ | In-app USB, BLE, and Wi-Fi (ESP32) update work the same as Android. The USB maintenance flow — nRF52/RP2040 factory erase and bootloader upgrade — is Android-only | +| Notifications | ✓ | ✓ | Native OS notifications | +| Widgets | ✓ | ✗ | Android-only | +| AI Assistant (Chirpy) | ✓\* | ✗ | Google flavor Android only | +| App Functions (system AI) | ✓† | ✗ | Google flavor Android only | \*Chirpy AI requires Android 14+ on Google flavor builds with supported hardware. @@ -122,7 +122,7 @@ Individual doc pages render with full formatting: ## Building from Source -```bash +```shell git clone https://github.com/meshtastic/Meshtastic-Android.git cd Meshtastic-Android ./gradlew :desktopApp:run @@ -135,7 +135,7 @@ Requirements: ## Known Limitations -- Offline tile downloads and local `.mbtiles` archives are not available on desktop. +- Offline base-map downloads and local `.mbtiles` archives are not available on desktop. Offline terrain is — see [Map & Waypoints](map-and-waypoints). - `.kml`/`.kmz`/GeoJSON layer import works — see [Map & Waypoints](map-and-waypoints#map-layers). Site Planner opens in your browser rather than in the app; to bring its coverage estimate onto the map, click the transmitter pin diff --git a/docs/iw-rIL/user/discovery.md b/docs/iw-rIL/user/discovery.md index 0df9df6c2f..4e4cb8af8b 100644 --- a/docs/iw-rIL/user/discovery.md +++ b/docs/iw-rIL/user/discovery.md @@ -2,7 +2,7 @@ title: Local Mesh Discovery parent: User Guide nav_order: 12 -last_updated: 2026-08-30 +last_updated: 2026-09-11 description: Explore your mesh network — the Local Mesh Discovery scanner, traceroute paths, neighbor maps, and node discovery tools. aliases: - discovery @@ -97,7 +97,7 @@ Mesh Beacon lets nodes invite others to join their mesh. A beaconing node period Configure it under **Settings → Module configuration → Mesh Beacon**. The entry appears only on radios running firmware 2.8.0 or newer. A read-only **Region** row at the top of the screen shows the region the beacon advertises: that region, and the preset, are always the ones the radio itself uses, so a beacon cannot invite anyone onto settings your radio is not running. - **Listen for beacons** — receive invitations broadcast by other nodes. -- **Broadcast a beacon** — periodically advertise this mesh to nearby nodes, with an optional **Beacon message** of up to 100 bytes, a **Broadcast interval** picked from fixed intervals between 1 hour and 72 hours, and an **Offered channel** chosen from your radio's own channels. The offered channel is required, and defaults to your primary channel. +- **Broadcast a beacon** — periodically advertise this mesh to nearby nodes, with an optional **Beacon message** of up to 100 bytes, a **Broadcast interval** picked from fixed intervals between 1 hour and 72 hours, and an **Offered channel** chosen from your radio's own channels. The offered channel is required, and defaults to your primary channel. Over remote admin the picker offers the primary channel only. - **Broadcast targets** — optional extra destinations beyond the offered channel. **Add target** appends a row; each row picks a **Channel** and a **Transmit preset**, and **Remove target** deletes it. With no targets, the beacon goes out on the offered channel alone. Two conditions block beacon setup: diff --git a/docs/iw-rIL/user/help-and-docs.md b/docs/iw-rIL/user/help-and-docs.md index f306cde7f7..c83898cea3 100644 --- a/docs/iw-rIL/user/help-and-docs.md +++ b/docs/iw-rIL/user/help-and-docs.md @@ -2,7 +2,7 @@ title: Help & In-App Docs parent: User Guide nav_order: 21 -last_updated: 2026-08-30 +last_updated: 2026-09-11 description: Browse this documentation inside the app, search it, and ask Chirpy — the on-device AI assistant — questions about Meshtastic. aliases: - help @@ -23,7 +23,7 @@ The docs browser lists every user-guide page. Tap a page to read it; images and ### Search -The search box sits at the top of the docs browser. Type in it to filter pages by title and keywords — results update as you type, and the **Clear search** button (✕) empties the box. +Tap the search field at the top of the docs browser to open a full-screen search view. Type to filter pages by title and keywords — results update as you type, under the same field. The back arrow, or the keyboard's search action, closes the view. The field hides while you scroll down the page list and comes back when you scroll up. ![Searching the in-app documentation](../../assets/screenshots/docs-browser_search.png) diff --git a/docs/iw-rIL/user/map-and-waypoints.md b/docs/iw-rIL/user/map-and-waypoints.md index bc3fb873af..4c735ebc51 100644 --- a/docs/iw-rIL/user/map-and-waypoints.md +++ b/docs/iw-rIL/user/map-and-waypoints.md @@ -2,7 +2,7 @@ title: Map & Waypoints parent: User Guide nav_order: 6 -last_updated: 2026-09-01 +last_updated: 2026-09-11 description: View node positions on the map, create and share waypoints, manage map layers and Site Planner, and control position sharing and privacy. aliases: - map @@ -44,7 +44,7 @@ The floating toolbar provides quick access to the compass, the map type and laye ### Filtering the Map -Tap the filter button in the floating toolbar to open **Filter map**. **Display** controls what is drawn: **Only Favorites**, **Show Waypoints**, **Show Precision Circles**, and a slider that hides nodes not heard from recently. **Node roles** is a chip per device role, plus **All** to show every role; a selected chip means that role is shown. **Nodes** narrows the set further with **Hide offline nodes**, **Only show direct nodes**, **Exclude MQTT**, **Show ignored nodes**, and **Include unknown**. +Tap the filter button in the floating toolbar to open **Filter map**. **Display** controls what is drawn: **Only Favorites**, **Show Waypoints**, **Show Precision Circles**, and a slider that hides nodes not heard from recently. **Node roles** is a chip per device role, plus **All** to show every role; a selected chip means that role is shown. **Nodes** narrows the set further with **Hide offline nodes**, **Only show direct nodes**, **Signed only**, **Encrypted only**, **Exclude MQTT**, **Show ignored nodes**, and **Include unknown** — the same rules as the node list's [filter toggles](nodes). A dot on the filter button means at least one filter is hiding something — check it before concluding the mesh is quiet. Turning **Show Waypoints** off hides every waypoint, including your own. **Show ignored nodes** adds them to the map rather than showing only them — unlike the node list's **Only show ignored Nodes**. @@ -147,8 +147,9 @@ picker, all three offer the same raster base maps: Overlays can be toggled on top of any base map, from the layers sheet: - **Weather radar** — NOAA NEXRAD reflectivity (US coverage) -- **Hillshade** — terrain relief, on **F-Droid** and **Desktop** only. Useful for understanding why a - link fails, since LoRa range is limited by terrain +- **Hillshade** — terrain relief from an online tile source, on **F-Droid** and **Desktop** only; Google + Play draws hillshade from downloaded offline terrain instead. Useful for understanding why a link + fails, since LoRa range is limited by terrain ### Adding your own tile source @@ -156,7 +157,7 @@ Any XYZ tile endpoint can be added as a base map, on every flavor and on desktop Tile Sources** at the foot of the base map picker and paste a URL template using `{z}`, `{x}` and `{y}` — plus `{s}` if the provider uses rotating subdomains. A national mapping service, for example: -``` +```text https://wmts.geo.admin.ch/1.0.0/ch.swisstopo.pixelkarte-farbe/default/current/3857/{z}/{x}/{y}.jpeg ``` @@ -173,7 +174,18 @@ On **Google Play**, the layers sheet's **Offline Manager** section downloads its — water, roads and administrative boundaries extracted from the public Protomaps basemap dataset, drawn directly as map shapes rather than raster tiles. Frame the area you want, tap **Start Download**, and once it finishes flip **Show on map** to draw it (it's off by default so it never surprises you by covering up the -live map). You can still import a pre-made `.mbtiles` archive here too, as before. **Desktop** has neither. +live map). You can still import a pre-made `.mbtiles` archive here too, as before. **Desktop** has neither, though +the offline terrain described next does work there. + +### Offline terrain + +Hillshade and elevation contours can be downloaded for offline use as well, so a downloaded area still +shows why a link fails when there is no network. The terrain data comes from Mapterhorn and is credited on +the map. On **Google Play**, each downloaded region in the **Offline Manager** list has a **Download Terrain** +button and **Hillshade** and **Contours** switches; terrain attaches to that region and shares its storage limit, and an area +too large for terrain asks you to zoom in or pick a smaller region. On **F-Droid** and **Desktop**, the +layers sheet has an **Offline Terrain** section of its own: frame the area, tap **Start Download**, and the +hillshade and contours draw from the downloaded tiles from then on. ### Going offline @@ -181,8 +193,8 @@ An **Offline** pill appears over the map whenever the device has no network conn builds, if you've imported a local `.mbtiles` archive, the map switches to it automatically the moment the network drops — no toggle to remember — and switches back once you're reconnected, as long as you haven't picked a different base map yourself in between. On **F-Droid**, a downloaded offline area keeps rendering on -its own; the pill is purely informational there. **Desktop** has no offline downloads at all yet, so the pill -is informational there too. +its own; the pill is purely informational there. **Desktop** downloads offline terrain but no base-map +packs, so the pill is informational there too. ## Related Topics diff --git a/docs/iw-rIL/user/messages-and-channels.md b/docs/iw-rIL/user/messages-and-channels.md index 9257b70c60..c68ba11050 100644 --- a/docs/iw-rIL/user/messages-and-channels.md +++ b/docs/iw-rIL/user/messages-and-channels.md @@ -2,7 +2,7 @@ title: Messages & Channels parent: User Guide nav_order: 3 -last_updated: 2026-08-30 +last_updated: 2026-09-14 description: Send and receive messages, manage channels, configure encryption, search conversations, and use quick chat, reactions, and message actions. aliases: - channels @@ -50,6 +50,21 @@ Key length alone does not change the icon: a 128-bit key and a 256-bit key both Tapping an existing channel opens the same editor, where you can change the name, the PSK, MQTT uplink and downlink, and position precision. Every edit on this screen — adding, editing, deleting, or dragging a channel into a new order — waits on **Send** the same way. +### Archived Channels + +A conversation belongs to the channel it was held on, not to the slot that channel occupied. When a channel leaves your radio — you scan a different channel QR, replace a channel's key, or switch the modem preset — its conversation is **archived** rather than deleted or merged into whatever takes the slot. + +An archived conversation: + +- stays in the **Channels** list under the name the channel had, below the channels currently on your radio, marked with a history icon; +- keeps its full message history, searchable as usual; +- is read-only — you cannot send, reply, or react, because the channel is no longer on the radio to send on, and it is not offered as a share target; +- comes back automatically if you re-add that exact channel, taking its messages back to the live slot. + +Changing the modem preset archives the channel too: the preset is part of a default channel's identity, so **LongFast** and **MediumFast** are different channels even though neither the name nor the key changed. Changing only the region does not archive anything. + +To remove an archived conversation for good, long-press it in the conversation list and delete it. + ## Direct Messages Direct messages (DMs) go to one specific node. When both radios hold each other's public keys, your radio encrypts the message to that node's public key, so no one else on the mesh can read it — not even nodes that share your channel. diff --git a/docs/iw-rIL/user/mqtt.md b/docs/iw-rIL/user/mqtt.md index f460ff4274..78816530eb 100644 --- a/docs/iw-rIL/user/mqtt.md +++ b/docs/iw-rIL/user/mqtt.md @@ -2,7 +2,7 @@ title: MQTT parent: User Guide nav_order: 11 -last_updated: 2026-08-30 +last_updated: 2026-09-11 description: Bridge your mesh to the internet — MQTT broker setup, encryption layers, and map reporting. aliases: - mqtt @@ -25,7 +25,7 @@ The MQTT module connects your node to an MQTT broker, allowing: ## How It Works -``` +```text [Your Node] → Radio → [Gateway Node with Wi-Fi] → MQTT Broker → [Remote Gateway] → Radio → [Remote Node] ``` diff --git a/docs/iw-rIL/user/node-metrics.md b/docs/iw-rIL/user/node-metrics.md index 19d45b76d4..9bd2925f69 100644 --- a/docs/iw-rIL/user/node-metrics.md +++ b/docs/iw-rIL/user/node-metrics.md @@ -2,7 +2,7 @@ title: Node Metrics parent: User Guide nav_order: 5 -last_updated: 2026-08-30 +last_updated: 2026-09-09 description: Telemetry dashboards for each mesh node — device health, environment sensors, air quality, signal quality, power, traceroute, and position history. aliases: - metrics @@ -114,7 +114,7 @@ Radio signal quality information: ### Signal Quality Reference -Signal quality is rated from **SNR relative to the active LoRa modem preset's demodulation floor**, not from fixed thresholds — a given SNR means different things on different presets (e.g. −15 dB is fine on LongSlow but unusable on ShortFast). RSSI is shown but is not part of the rating. In the table, _limit_ is the preset's SNR limit. +Signal quality is rated from **SNR relative to the active LoRa modem preset's demodulation floor**, not from fixed thresholds — a given SNR means different things on different presets (e.g. −15 dB is fine on LongSlow but unusable on ShortFast). When RSSI and a noise-floor reading are both available, the app also rates their difference against the preset limit and uses the worse rating. Otherwise, RSSI is display-only. In the table, _limit_ is the preset's SNR limit. | Quality | Criteria | | ------- | ---------------------------------------------------------------- | diff --git a/docs/iw-rIL/user/nodes.md b/docs/iw-rIL/user/nodes.md index a91cb8f1d6..dde88a46b0 100644 --- a/docs/iw-rIL/user/nodes.md +++ b/docs/iw-rIL/user/nodes.md @@ -2,7 +2,7 @@ title: Nodes parent: User Guide nav_order: 4 -last_updated: 2026-09-04 +last_updated: 2026-09-11 description: Browse, filter, and sort mesh nodes — view details, signal quality, roles, and quick actions. aliases: - node-list @@ -74,19 +74,23 @@ Most users should keep the default **Client** role. Consider a different role wh > 💡 **Tip:** The mesh works best when most nodes are **Client** or **Router**. Too many Client Mute nodes reduce mesh resilience; too many Routers in a dense area can cause congestion. A good rule of thumb: one Router per 5–10 Clients in your area. -### Encryption Indicators +### Security indicators -Nodes display encryption status icons next to their name: +Each node carries one security icon beside its name in the node list. Tap it to read what it means, and choose **Show All Meanings** in that dialog for the full legend. The detail screen shows the same state in words, as a **Security** row that opens the same dialog. -| Icon | Meaning | -| ----------- | ------------------------------------------------------------------------------------------------------------------------------------------------- | -| 🔒 Locked | Communication uses PKI (public key infrastructure) — end-to-end encrypted with verified identity | -| 🔓 Unlocked | No public key has been received for this node, so it cannot be direct messaged — use **Request User Info** on the node detail page to ask for one | -| ⚠️ Mismatch | Public key mismatch — the node's key has changed since last seen (investigate before trusting) | +| Icon | Meaning | Shown for | +| --------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------- | +| Person with a shield check (green) | **Verified contact** — you verified this node's key in person by exchanging contact QR codes, so its identity is confirmed. The strongest trust the list shows. Your own connected node carries it too | Any firmware version | +| Nodes icon with a shield check (green) | **Signed node** — this node signs its broadcasts with its identity key, so its identity is consistent over time, but you have not verified it in person. On firmware 2.8 the icon appears from the version alone, before any signed broadcast has been heard | Firmware 2.8 or newer, and any node whose signed broadcast your node has verified | +| 🔒 Closed lock | A public key is on file and matches, so direct messages to this node are encrypted | Firmware before 2.8, or no reported version | +| 🔓 Open lock | No public key has been received for this node, so it cannot be direct messaged — use **Request User Info** on the node detail page to ask for one | Firmware before 2.8, or no reported version | +| ⚠️ Mismatch | **Public key mismatch** — a different key arrived for this node after one was stored. Investigate before trusting | Any firmware version | -> 💡 **Tip:** Direct messages always use PKI, so the radio needs the other node's public key before it can send one. It refuses the send rather than falling back to channel encryption. Keys arrive inside node info, which is why an open lock usually clears itself once that node is heard from properly. If you see a key mismatch warning, the node may have been reset or compromised. +Every node on firmware 2.8 or newer signs its broadcasts, so on that firmware the signed state is the baseline and the locks are not shown. -To clear a mismatch, first confirm through another trusted channel that the key change was intentional — a factory reset causes one. Then touch & hold the node, choose **Remove**, and let the two radios exchange keys again the next time yours hears it. +Direct messages always use public-key encryption, so your node needs the other node's public key before it can send one. It refuses the send rather than falling back to channel encryption. Keys arrive inside node info, which is why an open lock usually clears itself once that node is heard from properly. + +A mismatch never replaces the key you already hold. The app keeps the first key it recorded and refuses the new one, as the firmware does, so a stray or hostile node info cannot silently break encrypted messaging to a contact. To clear a mismatch, first confirm through another trusted channel that the key change was intentional — a factory reset causes one. Then touch & hold the node, choose **Remove**, and let the two nodes exchange keys again the next time yours hears it. ## Quick Actions @@ -108,7 +112,9 @@ radio is connected and running firmware 2.8 or newer — see ## Sharing a Contact -On a node's detail screen, tap **Share Contact** to produce a link and a QR code for that node. From the same dialog, **Write to NFC tag** saves the link to a writable NFC tag that anyone can tap to open. +On a node's detail screen, tap **Share Contact** to produce a link and a QR code for that node. From the same dialog, **Share link** opens the Android share sheet (on desktop it copies the link instead), **Write to NFC tag** saves it to a writable NFC tag, and **Copy** puts it on the clipboard. While that dialog is open and in front of you, the phone also offers the same link to any NFC reader, so someone can take the contact by tapping their phone against yours with no tag involved. + +Sharing your own contact this way marks it as verified in person, so whoever imports it sees the **Verified contact** icon rather than the signed one. Relaying someone else's contact passes on only what your app had already recorded about them. To add someone else's contact, use the import button on the node list and choose **Scan Shared Contact QR Code**, **Scan Shared Contact NFC**, or **Input Shared Contact URL**. The app asks you to confirm with **Import Shared Contact?**, and warns you when the contact is one you already have. @@ -120,14 +126,16 @@ Type in the search field to filter nodes by name or short name. The filter updat ### Filter Toggles -| פילטר | תיאור | -| --------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| **Hide offline nodes** | Show only nodes heard within the last 2 hours | -| **Only show direct nodes** | Show only nodes your radio heard directly, with no relay in between | -| **Include unknown** | Show nodes that haven't sent user info yet. **On by default**, so a node heard before its info arrives stays visible; these carry a badge marking them incomplete, and cannot be direct messaged until their user info brings a public key | -| **Exclude infrastructure** | Hide infrastructure-role nodes (Router, Router Late, Client Base, and legacy Repeater nodes) and any node that cannot be messaged, whatever its role | -| **Exclude MQTT** | Hide nodes heard only via MQTT internet bridge | -| **Only show ignored Nodes** | Replace the list with the nodes you have ignored. Every other node is hidden while this is on, and a banner appears at the top of the list to take you back | +| פילטר | תיאור | +| --------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **Hide offline nodes** | Show only nodes heard within the last 2 hours | +| **Only show direct nodes** | Show only nodes your radio heard directly, with no relay in between | +| **Include unknown** | Show nodes that haven't sent user info yet. **On by default**, so a node heard before its info arrives stays visible; these carry a badge marking them incomplete, and cannot be direct messaged until their user info brings a public key | +| **Exclude infrastructure** | Hide infrastructure-role nodes (Router, Router Late, Client Base, and legacy Repeater nodes) and any node that cannot be messaged, whatever its role | +| **Exclude MQTT** | Hide nodes heard only via MQTT internet bridge | +| **Signed only** | Show only nodes whose signed broadcasts your node has actually heard and verified. Stricter than the icon: a node on 2.8 shows as signed by its firmware version before any signed broadcast arrives, and a contact verified in person on older firmware is not signed at all | +| **Encrypted only** | Show nodes with a matching public key on file, the key an encrypted direct message needs. A node with a key mismatch is excluded | +| **Only show ignored Nodes** | Replace the list with the nodes you have ignored. Every other node is hidden while this is on, and a banner appears at the top of the list to take you back | ### Sort Options @@ -149,6 +157,8 @@ Tap the hop-histogram icon in the node list's app bar to open a bar chart of how Tapping a node opens the detail view with comprehensive information. See [Node Metrics](node-metrics) for full details on metrics and telemetry. +Signal quality is rated against your modem preset. The same SNR can be good on a long-range preset and poor on a faster one. Traceroute and neighbor-info SNR colors use that preset too. RSSI text has its own strength colors; it affects the quality rating only when a noise-floor reading is also available. + The Details card carries the node's short name, role, IDs, last heard time, hops away, uptime, and its SNR and RSSI: ![Node detail section](../../assets/screenshots/nodes_detail_section.png) diff --git a/docs/iw-rIL/user/settings-module-admin.md b/docs/iw-rIL/user/settings-module-admin.md index d27ee13046..c0f5f7d062 100644 --- a/docs/iw-rIL/user/settings-module-admin.md +++ b/docs/iw-rIL/user/settings-module-admin.md @@ -2,7 +2,7 @@ title: Settings — Modules & Admin parent: User Guide nav_order: 8 -last_updated: 2026-08-30 +last_updated: 2026-09-16 description: Configure optional feature modules (MQTT, telemetry, canned messages, TAK, and more) and perform device administration. aliases: - modules @@ -333,7 +333,7 @@ appear only when your own node is selected: ### אודות -**Settings → About** carries the app's own identity rather than the radio's: +**Settings → About** carries the app's own identity rather than the node's: Three sections: @@ -341,7 +341,11 @@ Three sections: - **Apps** — opens with **Need Hardware?**, a rotating carousel of popular devices that links out to where to buy one. It also lists the GitHub repository, the running app version, and **Acknowledgements** (see the next section). -- **Project information** — links to the website and to this documentation. +- **Project information** — links to the website, to this documentation, and to the GNU GPL v3 + license the app is released under. + +The footer states the copyright and that the app is free software with no warranty, which you +may redistribute under the same license. ### Acknowledgements diff --git a/docs/iw-rIL/user/settings-radio-user.md b/docs/iw-rIL/user/settings-radio-user.md index 5100f7d58d..c650b57100 100644 --- a/docs/iw-rIL/user/settings-radio-user.md +++ b/docs/iw-rIL/user/settings-radio-user.md @@ -2,7 +2,7 @@ title: Settings — Radio & User parent: User Guide nav_order: 7 -last_updated: 2026-09-04 +last_updated: 2026-09-11 description: Configure your radio hardware, LoRa presets, user profile, position sharing, power management, and security. aliases: - הגדרות @@ -51,7 +51,7 @@ The footer appears as soon as you change something. **Discard** throws the chang The status message is saved with the same **Save**, but it never reboots the node — and, like the rest of this screen, it can be edited on a remote node you administer. For your own radio there is a shortcut while it is connected: touch & hold your node in the [node list](nodes.md) and choose -**Update status**. Older firmware and a disconnected radio have no shortcut — the field above is +**Update status**. Older firmware and a disconnected node have no shortcut — the field itself is still the way in. ## Configuration @@ -77,13 +77,13 @@ On **Settings → LoRa**. | Setting | תיאור | Default | | ------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------- | -| אזור | Regulatory region for frequency bands. You must set this before transmitting | Unset (must configure) | +| אזור | Regulatory region for frequency bands. You must set this before transmitting. On firmware 2.8 or newer, the first region set also creates the node's identity key and gives it a new node number | Unset (must configure) | | Presets | Speed/range tradeoff | LongFast | | Number of Hops | Maximum retransmit hops | 3 | | Transmit Power | Transmission power (dBm); 0 = max allowed for region | 0 (region max) | | Frequency Override | Overrides the computed operating frequency outright (MHz). It does not offset the calculated value — leave at 0 unless you know you need a specific frequency | 0 (use calculated) | | Use Preset | On by default. Turn it off to set Spread Factor, Coding Rate and Bandwidth by hand instead of taking them from the modem preset | On | -| Spread Factor | Manual mode only: 7–12. Higher spreads further but slower | From preset | +| Spread Factor | Manual mode only: 5–12. Higher spreads further but slower. On SX127x (RF95) radios the firmware does not accept 5 or 6 and uses 11 instead | From preset | | Coding Rate | Manual mode only: 5–8. More redundancy costs airtime | From preset | | Bandwidth | Manual mode only: the channel bandwidth in kHz, typed in directly. On the 2.4 GHz region the app offers a list of the bandwidths your radio supports instead, and a stored value that is not on that list shows as _Unsupported_ and blocks saving until you pick a supported one | From preset | | Frequency Slot | Which slot within the region's band to use. 0 derives it from the primary channel name | 0 (automatic) | @@ -111,13 +111,13 @@ The Lite, Narrow, Medium Turbo, and Tiny presets need firmware 2.8 or newer — | Short Slow | ~5 km | 6.25 kbps | −10 dB | Suburban short-range; moderate building density | | Medium Fast | ~5 km | 3.52 kbps | −12.5 dB | Suburban areas; moderate building density | | Medium Slow | ~8 km | 1.95 kbps | −15 dB | Suburban/rural; moderate range with slower speed | -| Long Turbo | ~10 km | 1.34 kbps | −12.5 dB | Similar range to Long Fast but with 500 kHz bandwidth; faster throughput | +| Long Turbo | ~10 km | 1.34 kbps | −17.5 dB | Similar range to Long Fast but with 500 kHz bandwidth; faster throughput | | Long Fast | ~10 km | 1.1 kbps | −17.5 dB | **General use (default)** — balanced range and speed | | Long Moderate | ~20 km | 0.34 kbps | −17.5 dB | Rural with some terrain; occasional use | | Lite Fast | ~5 km | 1.76 kbps | −12.5 dB | EU 866 MHz SRD band (125 kHz BW); comparable to Medium Fast | | Lite Slow | ~10 km | 0.98 kbps | −15 dB | EU 866 MHz SRD band (125 kHz BW); comparable to Long Fast | -| Narrow Fast | ~5 km | 2.28 kbps | −10 dB | EU 868 MHz band (62.5 kHz BW); avoids interference with other devices | -| Narrow Slow | ~10 km | 1.30 kbps | −12.5 dB | EU 868 MHz band (62.5 kHz BW); comparable to Long Fast | +| Narrow Fast | ~5 km | 2.28 kbps | −7.5 dB | EU 868 MHz band (62.5 kHz BW); avoids interference with other devices | +| Narrow Slow | ~10 km | 1.30 kbps | −10 dB | EU 868 MHz band (62.5 kHz BW); comparable to Long Fast | | Medium Turbo | ~5 km | 7.0 kbps | −12.5 dB | Like Medium Fast but with 500 kHz bandwidth; not legal in every region. Needs firmware 2.8 or newer | | Tiny Fast | ~10 km | 0.68 kbps | −7.5 dB | Amateur bands that cap occupied bandwidth; these presets use 15.6 kHz. Needs firmware 2.8 or newer, an SX126x or SX127x radio, and a TCXO of ±5 ppm or better | | Tiny Slow | ~20 km | 0.33 kbps | −10 dB | Same band restrictions as Tiny Fast, longer range. Same firmware, radio, and TCXO requirements | diff --git a/docs/iw-rIL/user/signal-meter.md b/docs/iw-rIL/user/signal-meter.md index c4e7c190bd..12d6aaade0 100644 --- a/docs/iw-rIL/user/signal-meter.md +++ b/docs/iw-rIL/user/signal-meter.md @@ -2,7 +2,7 @@ title: How the Meshtastic Signal Meter Works parent: User Guide nav_order: 15 -last_updated: 2026-08-30 +last_updated: 2026-09-09 description: How the signal meter rates quality from SNR relative to the LoRa modem preset — spread spectrum, presets, and what the bars really mean. aliases: - signal @@ -32,7 +32,7 @@ Each modem preset has an SNR limit: the lowest SNR at which that preset can stil ## Rating Signal Quality -The app rates signal quality (None, Bad, Fair, or Good) from SNR alone, measured against the active preset's SNR limit. It does not factor in RSSI: without knowing the local noise floor, RSSI alone cannot say whether a signal is decodable. RSSI is still available — on the node detail screen and in the metrics charts. +The app rates signal quality (None, Bad, Fair, or Good) from SNR relative to the active preset's SNR limit. When RSSI and a noise-floor reading are both available, it also rates the difference between them against that limit and uses the worse rating. Otherwise, it uses SNR alone. Because the rating is relative to the preset, the same SNR rates differently on different presets. An SNR of −16 dB rates Good on Long Fast (SNR limit −17.5 dB) but None on Short Fast (SNR limit −7.5 dB). Let `limit` be the active preset's SNR limit: @@ -45,7 +45,7 @@ Because the rating is relative to the preset, the same SNR rates differently on The icon never goes blank, so count the bars carefully: a single bar means None, not a weak but usable link, and Good is a solid wedge rather than a set of bars. A gray three-bar icon labeled Unknown is a different state again — the packet carried no SNR measurement at all, which is not the same as measuring one and finding it too weak. -> ℹ️ **Note:** Traceroute hop colors use fixed thresholds (−7 dB / −15 dB); the per-node signal meter uses the preset-relative rating instead. +> ℹ️ **Note:** Traceroute and neighbor-info SNR colors use the active preset's limits too. RSSI text has separate strength colors: green above −115 dBm, yellow above −120 dBm, orange above −126 dBm, and red at or below −126 dBm. ## Diagnosing Local Interference diff --git a/docs/iw-rIL/user/tak.md b/docs/iw-rIL/user/tak.md index 94b188f759..62f5ec7a71 100644 --- a/docs/iw-rIL/user/tak.md +++ b/docs/iw-rIL/user/tak.md @@ -2,7 +2,7 @@ title: TAK Integration parent: User Guide nav_order: 10 -last_updated: 2026-08-30 +last_updated: 2026-09-11 description: Interoperate with ATAK and WinTAK — CoT position sharing, TAK roles, and plugin setup. aliases: - tak @@ -36,7 +36,7 @@ The TAK module allows Meshtastic nodes to: > ⚠️ **Warning:** The old **Meshtastic ATAK Plugin** is no longer part of this path and cannot > work. It bridged through the cross-process AIDL API, which was removed in app 2.8.0; the mesh > service is now in-process only. Do not install it. Interop today runs over the app's own local -> TAK server plus the Mesh to CoT Converter, both described below, with stock ATAK/iTAK/WinTAK. +> TAK server plus the Mesh to CoT Converter, both described in the following sections, with stock ATAK/iTAK/WinTAK. ### Configuration diff --git a/docs/iw-rIL/user/translate.md b/docs/iw-rIL/user/translate.md index 67d69e2d96..cdc8b6d87f 100644 --- a/docs/iw-rIL/user/translate.md +++ b/docs/iw-rIL/user/translate.md @@ -2,7 +2,7 @@ title: Translate the App parent: User Guide nav_order: 17 -last_updated: 2026-08-30 +last_updated: 2026-09-11 description: How the app and its documentation are translated via Crowdin, and guidelines for contributing translations. aliases: - translate @@ -45,7 +45,7 @@ If your language is not yet listed on Crowdin: The Android app uses **Compose Multiplatform resources** for all user-visible strings: -``` +```text core/resources/src/commonMain/composeResources/ ├── values/ ← English (default) │ └── strings.xml @@ -58,7 +58,7 @@ core/resources/src/commonMain/composeResources/ In-app documentation follows a similar pattern under `docs/`: -``` +```text docs/ ├── en/user/ ← English source (default) │ ├── onboarding.md diff --git a/docs/ja-rJP/user/desktop.md b/docs/ja-rJP/user/desktop.md index b6353e1a53..ecc4fb087a 100644 --- a/docs/ja-rJP/user/desktop.md +++ b/docs/ja-rJP/user/desktop.md @@ -2,7 +2,7 @@ title: デスクトップアプリ parent: User Guide nav_order: 14 -last_updated: 2026-08-30 +last_updated: 2026-09-11 description: Linux、macOS、Windows で Meshtastic デスクトップアプリをインストールして使う方法。接続、機能の対応状況、キーボードショートカットを説明します。 aliases: - desktop @@ -58,20 +58,20 @@ Bluetooth Low Energy is supported on desktop via the [Kable](https://github.com/ ## 機能の対応状況 -| 機能 | Android | デスクトップ | 備考 | -| ----------------------------------------------------- | ------- | ------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| メッセージング | ✓ | ✓ | 完全対応 | -| ノードリスト | ✓ | ✓ | 完全対応 | -| マップ | ✓ | ✓ | Interactive MapLibre map, with base map and overlay pickers and custom tile sources. No offline downloads or local `.mbtiles` archives | -| Map layers (`.kml`/`.kmz`/GeoJSON) | ✓ | ✓ | Same layer store and sheet as Android; imported files draw on the desktop map | -| サイトプランナー | ✓ | ✓\* | \*Opens in your browser on desktop; the estimate is not drawn on the desktop map | -| 設定 | ✓ | ✓ | 完全対応 | -| Bluetooth(BLE) | ✓ | ✓ | デスクトップでは Kable 経由 | -| ファームウェアの更新 | ✓ | ✓ | In-app USB, BLE, and Wi-Fi (ESP32) update work the same as Android. The USB maintenance flow — nRF52/RP2040 factory erase and bootloader upgrade — is Android-only | -| 通知 | ✓ | ✓ | OS ネイティブの通知 | -| ウィジェット | ✓ | ✗ | Android のみ | -| AI アシスタント(Chirpy) | ✓\* | ✗ | Google 版の Android のみ | -| アプリ機能(システム AI) | ✓† | ✗ | Google 版の Android のみ | +| 機能 | Android | デスクトップ | 備考 | +| ----------------------------------------------------- | ------- | ------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| メッセージング | ✓ | ✓ | 完全対応 | +| ノードリスト | ✓ | ✓ | 完全対応 | +| マップ | ✓ | ✓ | Interactive MapLibre map, with base map and overlay pickers and custom tile sources. Offline terrain (hillshade and contours) can be downloaded; offline base-map packs and local `.mbtiles` archives cannot | +| Map layers (`.kml`/`.kmz`/GeoJSON) | ✓ | ✓ | Same layer store and sheet as Android; imported files draw on the desktop map | +| サイトプランナー | ✓ | ✓\* | \*Opens in your browser on desktop; the estimate is not drawn on the desktop map | +| 設定 | ✓ | ✓ | 完全対応 | +| Bluetooth(BLE) | ✓ | ✓ | デスクトップでは Kable 経由 | +| ファームウェアの更新 | ✓ | ✓ | In-app USB, BLE, and Wi-Fi (ESP32) update work the same as Android. The USB maintenance flow — nRF52/RP2040 factory erase and bootloader upgrade — is Android-only | +| 通知 | ✓ | ✓ | OS ネイティブの通知 | +| ウィジェット | ✓ | ✗ | Android のみ | +| AI アシスタント(Chirpy) | ✓\* | ✗ | Google 版の Android のみ | +| アプリ機能(システム AI) | ✓† | ✗ | Google 版の Android のみ | \*Chirpy AI には、対応ハードウェアを備えた Google 版ビルドで Android 14 以降が必要です。 @@ -122,7 +122,7 @@ The desktop app includes a built-in documentation browser for quick access to he ## ソースからビルドする -```bash +```shell git clone https://github.com/meshtastic/Meshtastic-Android.git cd Meshtastic-Android ./gradlew :desktopApp:run @@ -135,7 +135,7 @@ cd Meshtastic-Android ## 既知の制限 -- Offline tile downloads and local `.mbtiles` archives are not available on desktop. +- Offline base-map downloads and local `.mbtiles` archives are not available on desktop. Offline terrain is — see [Map & Waypoints](map-and-waypoints). - `.kml`/`.kmz`/GeoJSON layer import works — see [Map & Waypoints](map-and-waypoints#map-layers). Site Planner opens in your browser rather than in the app; to bring its coverage estimate onto the map, click the transmitter pin diff --git a/docs/ja-rJP/user/discovery.md b/docs/ja-rJP/user/discovery.md index 3d16e54bde..b8091b7817 100644 --- a/docs/ja-rJP/user/discovery.md +++ b/docs/ja-rJP/user/discovery.md @@ -2,7 +2,7 @@ title: ローカルメッシュ探索 parent: User Guide nav_order: 12 -last_updated: 2026-08-30 +last_updated: 2026-09-11 description: メッシュネットワークを探索します。ローカルメッシュ探索スキャナー、ルート追跡の経路、隣接ノードのマップ、ノード探索ツールを説明します。 aliases: - discovery @@ -97,7 +97,7 @@ If a scan is interrupted — the app is closed, or the radio goes away — the a Configure it under **Settings → Module configuration → Mesh Beacon**. The entry appears only on radios running firmware 2.8.0 or newer. A read-only **Region** row at the top of the screen shows the region the beacon advertises: that region, and the preset, are always the ones the radio itself uses, so a beacon cannot invite anyone onto settings your radio is not running. - **ビーコンを受信**:他のノードがブロードキャストする招待を受け取ります。 -- **Broadcast a beacon** — periodically advertise this mesh to nearby nodes, with an optional **Beacon message** of up to 100 bytes, a **Broadcast interval** picked from fixed intervals between 1 hour and 72 hours, and an **Offered channel** chosen from your radio's own channels. The offered channel is required, and defaults to your primary channel. +- **Broadcast a beacon** — periodically advertise this mesh to nearby nodes, with an optional **Beacon message** of up to 100 bytes, a **Broadcast interval** picked from fixed intervals between 1 hour and 72 hours, and an **Offered channel** chosen from your radio's own channels. The offered channel is required, and defaults to your primary channel. Over remote admin the picker offers the primary channel only. - **Broadcast targets** — optional extra destinations beyond the offered channel. **Add target** appends a row; each row picks a **Channel** and a **Transmit preset**, and **Remove target** deletes it. With no targets, the beacon goes out on the offered channel alone. Two conditions block beacon setup: diff --git a/docs/ja-rJP/user/help-and-docs.md b/docs/ja-rJP/user/help-and-docs.md index e7c2817829..b36917c428 100644 --- a/docs/ja-rJP/user/help-and-docs.md +++ b/docs/ja-rJP/user/help-and-docs.md @@ -2,7 +2,7 @@ title: ヘルプとアプリ内ドキュメント parent: User Guide nav_order: 21 -last_updated: 2026-08-30 +last_updated: 2026-09-11 description: このドキュメントをアプリ内で閲覧・検索し、オンデバイスの AI アシスタント Chirpy に Meshtastic について質問できます。 aliases: - help @@ -23,7 +23,7 @@ aliases: ### 検索 -The search box sits at the top of the docs browser. Type in it to filter pages by title and keywords — results update as you type, and the **Clear search** button (✕) empties the box. +Tap the search field at the top of the docs browser to open a full-screen search view. Type to filter pages by title and keywords — results update as you type, under the same field. The back arrow, or the keyboard's search action, closes the view. The field hides while you scroll down the page list and comes back when you scroll up. ![アプリ内ドキュメントの検索](../../assets/screenshots/docs-browser_search.png) diff --git a/docs/ja-rJP/user/map-and-waypoints.md b/docs/ja-rJP/user/map-and-waypoints.md index 208d8234fc..da5ce2408a 100644 --- a/docs/ja-rJP/user/map-and-waypoints.md +++ b/docs/ja-rJP/user/map-and-waypoints.md @@ -2,7 +2,7 @@ title: マップとウェイポイント parent: User Guide nav_order: 6 -last_updated: 2026-09-01 +last_updated: 2026-09-11 description: マップ上でノードの位置を確認し、ウェイポイントの作成・共有、マップレイヤーとサイトプランナーの管理、位置共有とプライバシーの制御を行います。 aliases: - map @@ -44,7 +44,7 @@ The floating toolbar provides quick access to the compass, the map type and laye ### Filtering the Map -Tap the filter button in the floating toolbar to open **Filter map**. **Display** controls what is drawn: **Only Favorites**, **Show Waypoints**, **Show Precision Circles**, and a slider that hides nodes not heard from recently. **Node roles** is a chip per device role, plus **All** to show every role; a selected chip means that role is shown. **Nodes** narrows the set further with **Hide offline nodes**, **Only show direct nodes**, **Exclude MQTT**, **Show ignored nodes**, and **Include unknown**. +Tap the filter button in the floating toolbar to open **Filter map**. **Display** controls what is drawn: **Only Favorites**, **Show Waypoints**, **Show Precision Circles**, and a slider that hides nodes not heard from recently. **Node roles** is a chip per device role, plus **All** to show every role; a selected chip means that role is shown. **Nodes** narrows the set further with **Hide offline nodes**, **Only show direct nodes**, **Signed only**, **Encrypted only**, **Exclude MQTT**, **Show ignored nodes**, and **Include unknown** — the same rules as the node list's [filter toggles](nodes). A dot on the filter button means at least one filter is hiding something — check it before concluding the mesh is quiet. Turning **Show Waypoints** off hides every waypoint, including your own. **Show ignored nodes** adds them to the map rather than showing only them — unlike the node list's **Only show ignored Nodes**. @@ -147,8 +147,9 @@ picker, all three offer the same raster base maps: Overlays can be toggled on top of any base map, from the layers sheet: - **Weather radar** — NOAA NEXRAD reflectivity (US coverage) -- **Hillshade** — terrain relief, on **F-Droid** and **Desktop** only. Useful for understanding why a - link fails, since LoRa range is limited by terrain +- **Hillshade** — terrain relief from an online tile source, on **F-Droid** and **Desktop** only; Google + Play draws hillshade from downloaded offline terrain instead. Useful for understanding why a link + fails, since LoRa range is limited by terrain ### Adding your own tile source @@ -156,7 +157,7 @@ Any XYZ tile endpoint can be added as a base map, on every flavor and on desktop Tile Sources** at the foot of the base map picker and paste a URL template using `{z}`, `{x}` and `{y}` — plus `{s}` if the provider uses rotating subdomains. A national mapping service, for example: -``` +```text https://wmts.geo.admin.ch/1.0.0/ch.swisstopo.pixelkarte-farbe/default/current/3857/{z}/{x}/{y}.jpeg ``` @@ -173,7 +174,18 @@ On **Google Play**, the layers sheet's **Offline Manager** section downloads its — water, roads and administrative boundaries extracted from the public Protomaps basemap dataset, drawn directly as map shapes rather than raster tiles. Frame the area you want, tap **Start Download**, and once it finishes flip **Show on map** to draw it (it's off by default so it never surprises you by covering up the -live map). You can still import a pre-made `.mbtiles` archive here too, as before. **Desktop** has neither. +live map). You can still import a pre-made `.mbtiles` archive here too, as before. **Desktop** has neither, though +the offline terrain described next does work there. + +### Offline terrain + +Hillshade and elevation contours can be downloaded for offline use as well, so a downloaded area still +shows why a link fails when there is no network. The terrain data comes from Mapterhorn and is credited on +the map. On **Google Play**, each downloaded region in the **Offline Manager** list has a **Download Terrain** +button and **Hillshade** and **Contours** switches; terrain attaches to that region and shares its storage limit, and an area +too large for terrain asks you to zoom in or pick a smaller region. On **F-Droid** and **Desktop**, the +layers sheet has an **Offline Terrain** section of its own: frame the area, tap **Start Download**, and the +hillshade and contours draw from the downloaded tiles from then on. ### Going offline @@ -181,8 +193,8 @@ An **Offline** pill appears over the map whenever the device has no network conn builds, if you've imported a local `.mbtiles` archive, the map switches to it automatically the moment the network drops — no toggle to remember — and switches back once you're reconnected, as long as you haven't picked a different base map yourself in between. On **F-Droid**, a downloaded offline area keeps rendering on -its own; the pill is purely informational there. **Desktop** has no offline downloads at all yet, so the pill -is informational there too. +its own; the pill is purely informational there. **Desktop** downloads offline terrain but no base-map +packs, so the pill is informational there too. ## 関連トピック diff --git a/docs/ja-rJP/user/messages-and-channels.md b/docs/ja-rJP/user/messages-and-channels.md index f367d8e9bb..c79960a717 100644 --- a/docs/ja-rJP/user/messages-and-channels.md +++ b/docs/ja-rJP/user/messages-and-channels.md @@ -2,7 +2,7 @@ title: メッセージとチャンネル parent: User Guide nav_order: 3 -last_updated: 2026-08-30 +last_updated: 2026-09-14 description: メッセージの送受信、チャンネルの管理、暗号化の設定、会話の検索、クイックチャット・リアクション・メッセージ操作の使い方を説明します。 aliases: - channels @@ -50,6 +50,21 @@ Key length alone does not change the icon: a 128-bit key and a 256-bit key both Tapping an existing channel opens the same editor, where you can change the name, the PSK, MQTT uplink and downlink, and position precision. Every edit on this screen — adding, editing, deleting, or dragging a channel into a new order — waits on **Send** the same way. +### Archived Channels + +A conversation belongs to the channel it was held on, not to the slot that channel occupied. When a channel leaves your radio — you scan a different channel QR, replace a channel's key, or switch the modem preset — its conversation is **archived** rather than deleted or merged into whatever takes the slot. + +An archived conversation: + +- stays in the **Channels** list under the name the channel had, below the channels currently on your radio, marked with a history icon; +- keeps its full message history, searchable as usual; +- is read-only — you cannot send, reply, or react, because the channel is no longer on the radio to send on, and it is not offered as a share target; +- comes back automatically if you re-add that exact channel, taking its messages back to the live slot. + +Changing the modem preset archives the channel too: the preset is part of a default channel's identity, so **LongFast** and **MediumFast** are different channels even though neither the name nor the key changed. Changing only the region does not archive anything. + +To remove an archived conversation for good, long-press it in the conversation list and delete it. + ## ダイレクトメッセージ Direct messages (DMs) go to one specific node. When both radios hold each other's public keys, your radio encrypts the message to that node's public key, so no one else on the mesh can read it — not even nodes that share your channel. diff --git a/docs/ja-rJP/user/mqtt.md b/docs/ja-rJP/user/mqtt.md index 5acac33a81..e482c5f93e 100644 --- a/docs/ja-rJP/user/mqtt.md +++ b/docs/ja-rJP/user/mqtt.md @@ -2,7 +2,7 @@ title: MQTT parent: User Guide nav_order: 11 -last_updated: 2026-08-30 +last_updated: 2026-09-11 description: メッシュをインターネットに橋渡しします。MQTT サーバーの設定、暗号化の各レイヤー、マップ報告について説明します。 aliases: - mqtt @@ -25,7 +25,7 @@ MQTT モジュールは、ノードを MQTT サーバーに接続し、次のこ ## 仕組み -``` +```text [Your Node] → Radio → [Gateway Node with Wi-Fi] → MQTT Broker → [Remote Gateway] → Radio → [Remote Node] ``` diff --git a/docs/ja-rJP/user/node-metrics.md b/docs/ja-rJP/user/node-metrics.md index df16eb52f8..db06b308eb 100644 --- a/docs/ja-rJP/user/node-metrics.md +++ b/docs/ja-rJP/user/node-metrics.md @@ -2,7 +2,7 @@ title: ノードメトリクス parent: User Guide nav_order: 5 -last_updated: 2026-08-30 +last_updated: 2026-09-09 description: 各メッシュノードのテレメトリダッシュボード。デバイスの状態、環境センサー、大気質、信号品質、電力、ルート追跡、位置履歴を表示します。 aliases: - metrics @@ -114,7 +114,7 @@ CO₂ readings are color-coded by severity so you can read air quality at a glan ### 信号品質の目安 -信号品質は、固定のしきい値ではなく、**現在の LoRa モデムプリセットの復調限界に対する SNR** で評価されます。同じ SNR でもプリセットによって意味が異なります(例:−15 dB は LongSlow では問題ありませんが、ShortFast では使い物になりません)。 RSSI は表示されますが、評価には含まれません。 In the table, _limit_ is the preset's SNR limit. +信号品質は、固定のしきい値ではなく、**現在の LoRa モデムプリセットの復調限界に対する SNR** で評価されます。同じ SNR でもプリセットによって意味が異なります(例:−15 dB は LongSlow では問題ありませんが、ShortFast では使い物になりません)。 When RSSI and a noise-floor reading are both available, the app also rates their difference against the preset limit and uses the worse rating. Otherwise, RSSI is display-only. In the table, _limit_ is the preset's SNR limit. | 品質 | 基準 | | -- | ----------------------------------------------------- | diff --git a/docs/ja-rJP/user/nodes.md b/docs/ja-rJP/user/nodes.md index 9aa5bfcc80..0a711a8fa9 100644 --- a/docs/ja-rJP/user/nodes.md +++ b/docs/ja-rJP/user/nodes.md @@ -2,7 +2,7 @@ title: ノード parent: User Guide nav_order: 4 -last_updated: 2026-09-04 +last_updated: 2026-09-11 description: メッシュノードの閲覧・絞り込み・並べ替え。詳細、信号品質、役割、クイック操作を確認できます。 aliases: - node-list @@ -74,19 +74,23 @@ There is no separate "away" tier. > 💡 **ヒント:** メッシュは、ほとんどのノードが**クライアント**または**ルーター**のときに最も良く機能します。 Too many Client Mute nodes reduce mesh resilience; too many Routers in a dense area can cause congestion. 目安としては、地域内のクライアント 5〜10 台につきルーター 1 台です。 -### 暗号化の表示 +### Security indicators -ノードは、名前の横に暗号化ステータスのアイコンを表示します: +Each node carries one security icon beside its name in the node list. Tap it to read what it means, and choose **Show All Meanings** in that dialog for the full legend. The detail screen shows the same state in words, as a **Security** row that opens the same dialog. -| アイコン | 意味 | -| -------- | ------------------------------------------------------------------------------------------------------------------------------------------------- | -| 🔒 ロック | 通信は PKI(公開鍵基盤)を使用します。本人性が検証された、エンドツーエンドの暗号化です | -| 🔓 ロック解除 | No public key has been received for this node, so it cannot be direct messaged — use **Request User Info** on the node detail page to ask for one | -| ⚠️ 不一致 | 公開鍵の不一致。前回確認時からノードの鍵が変わっています(信頼する前に調べてください) | +| アイコン | 意味 | Shown for | +| --------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------- | +| Person with a shield check (green) | **Verified contact** — you verified this node's key in person by exchanging contact QR codes, so its identity is confirmed. The strongest trust the list shows. Your own connected node carries it too | Any firmware version | +| Nodes icon with a shield check (green) | **Signed node** — this node signs its broadcasts with its identity key, so its identity is consistent over time, but you have not verified it in person. On firmware 2.8 the icon appears from the version alone, before any signed broadcast has been heard | Firmware 2.8 or newer, and any node whose signed broadcast your node has verified | +| 🔒 Closed lock | A public key is on file and matches, so direct messages to this node are encrypted | Firmware before 2.8, or no reported version | +| 🔓 Open lock | No public key has been received for this node, so it cannot be direct messaged — use **Request User Info** on the node detail page to ask for one | Firmware before 2.8, or no reported version | +| ⚠️ 不一致 | **Public key mismatch** — a different key arrived for this node after one was stored. Investigate before trusting | Any firmware version | -> 💡 **Tip:** Direct messages always use PKI, so the radio needs the other node's public key before it can send one. It refuses the send rather than falling back to channel encryption. Keys arrive inside node info, which is why an open lock usually clears itself once that node is heard from properly. 鍵の不一致の警告が表示された場合、そのノードはリセットされたか、侵害された可能性があります。 +Every node on firmware 2.8 or newer signs its broadcasts, so on that firmware the signed state is the baseline and the locks are not shown. -To clear a mismatch, first confirm through another trusted channel that the key change was intentional — a factory reset causes one. Then touch & hold the node, choose **Remove**, and let the two radios exchange keys again the next time yours hears it. +Direct messages always use public-key encryption, so your node needs the other node's public key before it can send one. It refuses the send rather than falling back to channel encryption. Keys arrive inside node info, which is why an open lock usually clears itself once that node is heard from properly. + +A mismatch never replaces the key you already hold. The app keeps the first key it recorded and refuses the new one, as the firmware does, so a stray or hostile node info cannot silently break encrypted messaging to a contact. To clear a mismatch, first confirm through another trusted channel that the key change was intentional — a factory reset causes one. Then touch & hold the node, choose **Remove**, and let the two nodes exchange keys again the next time yours hears it. ## クイック操作 @@ -108,7 +112,9 @@ radio is connected and running firmware 2.8 or newer — see ## Sharing a Contact -On a node's detail screen, tap **Share Contact** to produce a link and a QR code for that node. From the same dialog, **Write to NFC tag** saves the link to a writable NFC tag that anyone can tap to open. +On a node's detail screen, tap **Share Contact** to produce a link and a QR code for that node. From the same dialog, **Share link** opens the Android share sheet (on desktop it copies the link instead), **Write to NFC tag** saves it to a writable NFC tag, and **Copy** puts it on the clipboard. While that dialog is open and in front of you, the phone also offers the same link to any NFC reader, so someone can take the contact by tapping their phone against yours with no tag involved. + +Sharing your own contact this way marks it as verified in person, so whoever imports it sees the **Verified contact** icon rather than the signed one. Relaying someone else's contact passes on only what your app had already recorded about them. To add someone else's contact, use the import button on the node list and choose **Scan Shared Contact QR Code**, **Scan Shared Contact NFC**, or **Input Shared Contact URL**. The app asks you to confirm with **Import Shared Contact?**, and warns you when the contact is one you already have. @@ -120,14 +126,16 @@ To add someone else's contact, use the import button on the node list and choose ### 絞り込みトグル -| 絞り込み | 説明 | -| --------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| **Hide offline nodes** | 過去 2 時間以内に受信したノードのみを表示します | -| **Only show direct nodes** | Show only nodes your radio heard directly, with no relay in between | -| **不明なノードを含む** | Show nodes that haven't sent user info yet. **On by default**, so a node heard before its info arrives stays visible; these carry a badge marking them incomplete, and cannot be direct messaged until their user info brings a public key | -| **インフラを除外** | Hide infrastructure-role nodes (Router, Router Late, Client Base, and legacy Repeater nodes) and any node that cannot be messaged, whatever its role | -| **MQTT を除外** | MQTT のインターネットブリッジ経由でのみ受信したノードを非表示にします | -| **Only show ignored Nodes** | Replace the list with the nodes you have ignored. Every other node is hidden while this is on, and a banner appears at the top of the list to take you back | +| 絞り込み | 説明 | +| --------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **Hide offline nodes** | 過去 2 時間以内に受信したノードのみを表示します | +| **Only show direct nodes** | Show only nodes your radio heard directly, with no relay in between | +| **不明なノードを含む** | Show nodes that haven't sent user info yet. **On by default**, so a node heard before its info arrives stays visible; these carry a badge marking them incomplete, and cannot be direct messaged until their user info brings a public key | +| **インフラを除外** | Hide infrastructure-role nodes (Router, Router Late, Client Base, and legacy Repeater nodes) and any node that cannot be messaged, whatever its role | +| **MQTT を除外** | MQTT のインターネットブリッジ経由でのみ受信したノードを非表示にします | +| **Signed only** | Show only nodes whose signed broadcasts your node has actually heard and verified. Stricter than the icon: a node on 2.8 shows as signed by its firmware version before any signed broadcast arrives, and a contact verified in person on older firmware is not signed at all | +| **Encrypted only** | Show nodes with a matching public key on file, the key an encrypted direct message needs. A node with a key mismatch is excluded | +| **Only show ignored Nodes** | Replace the list with the nodes you have ignored. Every other node is hidden while this is on, and a banner appears at the top of the list to take you back | ### 並べ替えオプション @@ -149,6 +157,8 @@ To add someone else's contact, use the import button on the node list and choose ノードをタップすると、詳しい情報を含む詳細ビューが開きます。 メトリクスとテレメトリの詳細については、[ノードメトリクス](node-metrics) を参照してください。 +Signal quality is rated against your modem preset. The same SNR can be good on a long-range preset and poor on a faster one. Traceroute and neighbor-info SNR colors use that preset too. RSSI text has its own strength colors; it affects the quality rating only when a noise-floor reading is also available. + The Details card carries the node's short name, role, IDs, last heard time, hops away, uptime, and its SNR and RSSI: ![ノードの詳細セクション](../../assets/screenshots/nodes_detail_section.png) diff --git a/docs/ja-rJP/user/settings-module-admin.md b/docs/ja-rJP/user/settings-module-admin.md index 4f2fbc13aa..5cd055c33a 100644 --- a/docs/ja-rJP/user/settings-module-admin.md +++ b/docs/ja-rJP/user/settings-module-admin.md @@ -2,7 +2,7 @@ title: 設定:モジュールと管理 parent: User Guide nav_order: 8 -last_updated: 2026-08-30 +last_updated: 2026-09-16 description: オプションの機能モジュール(MQTT、テレメトリ、定型メッセージ、TAK など)を設定し、デバイスの管理を行います。 aliases: - modules @@ -333,7 +333,7 @@ appear only when your own node is selected: ### 概要 -**Settings → About** carries the app's own identity rather than the radio's: +**Settings → About** carries the app's own identity rather than the node's: Three sections: @@ -341,7 +341,11 @@ Three sections: - **Apps** — opens with **Need Hardware?**, a rotating carousel of popular devices that links out to where to buy one. It also lists the GitHub repository, the running app version, and **Acknowledgements** (see the next section). -- **Project information** — links to the website and to this documentation. +- **Project information** — links to the website, to this documentation, and to the GNU GPL v3 + license the app is released under. + +The footer states the copyright and that the app is free software with no warranty, which you +may redistribute under the same license. ### 謝辞 diff --git a/docs/ja-rJP/user/settings-radio-user.md b/docs/ja-rJP/user/settings-radio-user.md index b0b5666c36..5459434655 100644 --- a/docs/ja-rJP/user/settings-radio-user.md +++ b/docs/ja-rJP/user/settings-radio-user.md @@ -2,7 +2,7 @@ title: 設定:無線機とユーザー parent: User Guide nav_order: 7 -last_updated: 2026-09-04 +last_updated: 2026-09-11 description: 無線機のハードウェア、LoRa プリセット、ユーザープロファイル、位置共有、電源管理、セキュリティを設定します。 aliases: - 設定 @@ -51,7 +51,7 @@ The footer appears as soon as you change something. **Discard** throws the chang The status message is saved with the same **Save**, but it never reboots the node — and, like the rest of this screen, it can be edited on a remote node you administer. For your own radio there is a shortcut while it is connected: touch & hold your node in the [node list](nodes.md) and choose -**Update status**. Older firmware and a disconnected radio have no shortcut — the field above is +**Update status**. Older firmware and a disconnected node have no shortcut — the field itself is still the way in. ## 設定 @@ -77,13 +77,13 @@ On **Settings → LoRa**. | 設定項目 | 説明 | デフォルト | | ------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------- | -| リージョン | Regulatory region for frequency bands. You must set this before transmitting | 未設定(要設定) | +| リージョン | Regulatory region for frequency bands. You must set this before transmitting. On firmware 2.8 or newer, the first region set also creates the node's identity key and gives it a new node number | 未設定(要設定) | | プリセット | 速度と距離のトレードオフ | LongFast | | ホップ数 | 再送信の最大ホップ数 | 3 | | 送信出力 | 送信出力(dBm)。0 = リージョンで許可された最大値 | 0(リージョン最大) | | 周波数の上書き | Overrides the computed operating frequency outright (MHz). It does not offset the calculated value — leave at 0 unless you know you need a specific frequency | 0 (use calculated) | | プリセットを使用 | On by default. Turn it off to set Spread Factor, Coding Rate and Bandwidth by hand instead of taking them from the modem preset | On | -| 拡散率 | Manual mode only: 7–12. Higher spreads further but slower | From preset | +| 拡散率 | Manual mode only: 5–12. Higher spreads further but slower. On SX127x (RF95) radios the firmware does not accept 5 or 6 and uses 11 instead | From preset | | 符号化レート | Manual mode only: 5–8. More redundancy costs airtime | From preset | | 帯域 | Manual mode only: the channel bandwidth in kHz, typed in directly. On the 2.4 GHz region the app offers a list of the bandwidths your radio supports instead, and a stored value that is not on that list shows as _Unsupported_ and blocks saving until you pick a supported one | From preset | | 周波数スロット | Which slot within the region's band to use. 0 derives it from the primary channel name | 0 (automatic) | @@ -111,13 +111,13 @@ The Lite, Narrow, Medium Turbo, and Tiny presets need firmware 2.8 or newer — | Short Slow | ~5 km | 6.25 kbps | −10 dB | 郊外の短距離。中程度の建物密度 | | Medium Fast | ~5 km | 3.52 kbps | −12.5 dB | 郊外エリア。中程度の建物密度 | | Medium Slow | ~8 km | 1.95 kbps | −15 dB | 郊外・地方。中程度の距離で低速 | -| Long Turbo | ~10 km | 1.34 kbps | −12.5 dB | Long Fast と同程度の距離だが 500 kHz 帯域幅。スループットが速い | +| Long Turbo | ~10 km | 1.34 kbps | −17.5 dB | Long Fast と同程度の距離だが 500 kHz 帯域幅。スループットが速い | | Long Fast | ~10 km | 1.1 kbps | −17.5 dB | **一般用途(デフォルト)**:距離と速度のバランスが良い | | Long Moderate | ~20 km | 0.34 kbps | −17.5 dB | 起伏のある地方。ときどき使う用途 | | Lite Fast | ~5 km | 1.76 kbps | −12.5 dB | EU 866 MHz SRD 帯(125 kHz 帯域幅)。Medium Fast に相当 | | Lite Slow | ~10 km | 0.98 kbps | −15 dB | EU 866 MHz SRD 帯(125 kHz 帯域幅)。Long Fast に相当 | -| Narrow Fast | ~5 km | 2.28 kbps | −10 dB | EU 868 MHz 帯(62.5 kHz 帯域幅)。他のデバイスとの干渉を避ける | -| Narrow Slow | ~10 km | 1.30 kbps | −12.5 dB | EU 868 MHz 帯(62.5 kHz 帯域幅)。Long Fast に相当 | +| Narrow Fast | ~5 km | 2.28 kbps | −7.5 dB | EU 868 MHz 帯(62.5 kHz 帯域幅)。他のデバイスとの干渉を避ける | +| Narrow Slow | ~10 km | 1.30 kbps | −10 dB | EU 868 MHz 帯(62.5 kHz 帯域幅)。Long Fast に相当 | | Medium Turbo | ~5 km | 7.0 kbps | −12.5 dB | Like Medium Fast but with 500 kHz bandwidth; not legal in every region. Needs firmware 2.8 or newer | | Tiny Fast | ~10 km | 0.68 kbps | −7.5 dB | Amateur bands that cap occupied bandwidth; these presets use 15.6 kHz. Needs firmware 2.8 or newer, an SX126x or SX127x radio, and a TCXO of ±5 ppm or better | | Tiny Slow | ~20 km | 0.33 kbps | −10 dB | Same band restrictions as Tiny Fast, longer range. Same firmware, radio, and TCXO requirements | diff --git a/docs/ja-rJP/user/signal-meter.md b/docs/ja-rJP/user/signal-meter.md index 33c773d3ad..a1c88df451 100644 --- a/docs/ja-rJP/user/signal-meter.md +++ b/docs/ja-rJP/user/signal-meter.md @@ -2,7 +2,7 @@ title: Meshtastic の信号メーターの仕組み parent: User Guide nav_order: 15 -last_updated: 2026-08-30 +last_updated: 2026-09-09 description: 信号メーターが、LoRa モデムプリセットに対する SNR から品質をどう評価するかを説明します。スペクトラム拡散、プリセット、バーが実際に意味するもの。 aliases: - signal @@ -32,7 +32,7 @@ Each modem preset has an SNR limit: the lowest SNR at which that preset can stil ## Rating Signal Quality -The app rates signal quality (None, Bad, Fair, or Good) from SNR alone, measured against the active preset's SNR limit. It does not factor in RSSI: without knowing the local noise floor, RSSI alone cannot say whether a signal is decodable. RSSI is still available — on the node detail screen and in the metrics charts. +The app rates signal quality (None, Bad, Fair, or Good) from SNR relative to the active preset's SNR limit. When RSSI and a noise-floor reading are both available, it also rates the difference between them against that limit and uses the worse rating. Otherwise, it uses SNR alone. Because the rating is relative to the preset, the same SNR rates differently on different presets. An SNR of −16 dB rates Good on Long Fast (SNR limit −17.5 dB) but None on Short Fast (SNR limit −7.5 dB). Let `limit` be the active preset's SNR limit: @@ -45,7 +45,7 @@ Because the rating is relative to the preset, the same SNR rates differently on The icon never goes blank, so count the bars carefully: a single bar means None, not a weak but usable link, and Good is a solid wedge rather than a set of bars. A gray three-bar icon labeled Unknown is a different state again — the packet carried no SNR measurement at all, which is not the same as measuring one and finding it too weak. -> ℹ️ **Note:** Traceroute hop colors use fixed thresholds (−7 dB / −15 dB); the per-node signal meter uses the preset-relative rating instead. +> ℹ️ **Note:** Traceroute and neighbor-info SNR colors use the active preset's limits too. RSSI text has separate strength colors: green above −115 dBm, yellow above −120 dBm, orange above −126 dBm, and red at or below −126 dBm. ## Diagnosing Local Interference diff --git a/docs/ja-rJP/user/tak.md b/docs/ja-rJP/user/tak.md index a17d18bc0b..5420a75aa0 100644 --- a/docs/ja-rJP/user/tak.md +++ b/docs/ja-rJP/user/tak.md @@ -2,7 +2,7 @@ title: TAK 連携 parent: User Guide nav_order: 10 -last_updated: 2026-08-30 +last_updated: 2026-09-11 description: ATAK および WinTAK と相互運用します。CoT による位置共有、TAK の役割、プラグインの設定を説明します。 aliases: - tak @@ -36,7 +36,7 @@ TAK モジュールを使うと、Meshtastic のノードは次のことがで > ⚠️ **Warning:** The old **Meshtastic ATAK Plugin** is no longer part of this path and cannot > work. It bridged through the cross-process AIDL API, which was removed in app 2.8.0; the mesh > service is now in-process only. Do not install it. Interop today runs over the app's own local -> TAK server plus the Mesh to CoT Converter, both described below, with stock ATAK/iTAK/WinTAK. +> TAK server plus the Mesh to CoT Converter, both described in the following sections, with stock ATAK/iTAK/WinTAK. ### 設定 diff --git a/docs/ja-rJP/user/translate.md b/docs/ja-rJP/user/translate.md index 79b9c13d5d..15ba62c543 100644 --- a/docs/ja-rJP/user/translate.md +++ b/docs/ja-rJP/user/translate.md @@ -2,7 +2,7 @@ title: アプリを翻訳する parent: User Guide nav_order: 17 -last_updated: 2026-08-30 +last_updated: 2026-09-11 description: アプリとそのドキュメントが Crowdin を通じてどう翻訳されるか、および翻訳に貢献するためのガイドラインを説明します。 aliases: - translate @@ -45,7 +45,7 @@ The app and its in-app docs are translated on Crowdin — this page shows how to Android アプリは、ユーザーに表示されるすべての文字列に **Compose Multiplatform リソース**を使用しています: -``` +```text core/resources/src/commonMain/composeResources/ ├── values/ ← 英語(デフォルト) │ └── strings.xml @@ -58,7 +58,7 @@ core/resources/src/commonMain/composeResources/ アプリ内ドキュメントも、`docs/` の下で同様のパターンに従います: -``` +```text docs/ ├── en/user/ ← 英語ソース(デフォルト) │ ├── onboarding.md diff --git a/docs/ko-rKR/user/desktop.md b/docs/ko-rKR/user/desktop.md index 92d64f2cdb..fac8e481b1 100644 --- a/docs/ko-rKR/user/desktop.md +++ b/docs/ko-rKR/user/desktop.md @@ -2,7 +2,7 @@ title: Desktop App parent: User Guide nav_order: 14 -last_updated: 2026-08-30 +last_updated: 2026-09-11 description: Install and use the Meshtastic Desktop app on Linux, macOS, and Windows — connections, feature parity, and keyboard shortcuts. aliases: - desktop @@ -58,20 +58,20 @@ Bluetooth Low Energy is supported on desktop via the [Kable](https://github.com/ ## Feature Parity -| Feature | Android | Desktop | Notes | -| ----------------------------------------------------- | ------- | ------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Messaging | ✓ | ✓ | Full parity | -| Node List | ✓ | ✓ | Full parity | -| 지도 | ✓ | ✓ | Interactive MapLibre map, with base map and overlay pickers and custom tile sources. No offline downloads or local `.mbtiles` archives | -| Map layers (`.kml`/`.kmz`/GeoJSON) | ✓ | ✓ | Same layer store and sheet as Android; imported files draw on the desktop map | -| Site Planner | ✓ | ✓\* | \*Opens in your browser on desktop; the estimate is not drawn on the desktop map | -| 설정 | ✓ | ✓ | Full parity | -| Bluetooth (BLE) | ✓ | ✓ | Via Kable on desktop | -| Firmware Update | ✓ | ✓ | In-app USB, BLE, and Wi-Fi (ESP32) update work the same as Android. The USB maintenance flow — nRF52/RP2040 factory erase and bootloader upgrade — is Android-only | -| Notifications | ✓ | ✓ | Native OS notifications | -| Widgets | ✓ | ✗ | Android-only | -| AI Assistant (Chirpy) | ✓\* | ✗ | Google flavor Android only | -| App Functions (system AI) | ✓† | ✗ | Google flavor Android only | +| Feature | Android | Desktop | Notes | +| ----------------------------------------------------- | ------- | ------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Messaging | ✓ | ✓ | Full parity | +| Node List | ✓ | ✓ | Full parity | +| 지도 | ✓ | ✓ | Interactive MapLibre map, with base map and overlay pickers and custom tile sources. Offline terrain (hillshade and contours) can be downloaded; offline base-map packs and local `.mbtiles` archives cannot | +| Map layers (`.kml`/`.kmz`/GeoJSON) | ✓ | ✓ | Same layer store and sheet as Android; imported files draw on the desktop map | +| Site Planner | ✓ | ✓\* | \*Opens in your browser on desktop; the estimate is not drawn on the desktop map | +| 설정 | ✓ | ✓ | Full parity | +| Bluetooth (BLE) | ✓ | ✓ | Via Kable on desktop | +| Firmware Update | ✓ | ✓ | In-app USB, BLE, and Wi-Fi (ESP32) update work the same as Android. The USB maintenance flow — nRF52/RP2040 factory erase and bootloader upgrade — is Android-only | +| Notifications | ✓ | ✓ | Native OS notifications | +| Widgets | ✓ | ✗ | Android-only | +| AI Assistant (Chirpy) | ✓\* | ✗ | Google flavor Android only | +| App Functions (system AI) | ✓† | ✗ | Google flavor Android only | \*Chirpy AI requires Android 14+ on Google flavor builds with supported hardware. @@ -122,7 +122,7 @@ Individual doc pages render with full formatting: ## Building from Source -```bash +```shell git clone https://github.com/meshtastic/Meshtastic-Android.git cd Meshtastic-Android ./gradlew :desktopApp:run @@ -135,7 +135,7 @@ Requirements: ## Known Limitations -- Offline tile downloads and local `.mbtiles` archives are not available on desktop. +- Offline base-map downloads and local `.mbtiles` archives are not available on desktop. Offline terrain is — see [Map & Waypoints](map-and-waypoints). - `.kml`/`.kmz`/GeoJSON layer import works — see [Map & Waypoints](map-and-waypoints#map-layers). Site Planner opens in your browser rather than in the app; to bring its coverage estimate onto the map, click the transmitter pin diff --git a/docs/ko-rKR/user/discovery.md b/docs/ko-rKR/user/discovery.md index e309e0849a..f4299eaace 100644 --- a/docs/ko-rKR/user/discovery.md +++ b/docs/ko-rKR/user/discovery.md @@ -2,7 +2,7 @@ title: Local Mesh Discovery parent: User Guide nav_order: 12 -last_updated: 2026-08-30 +last_updated: 2026-09-11 description: Explore your mesh network — the Local Mesh Discovery scanner, traceroute paths, neighbor maps, and node discovery tools. aliases: - discovery @@ -97,7 +97,7 @@ Mesh Beacon lets nodes invite others to join their mesh. A beaconing node period Configure it under **Settings → Module configuration → Mesh Beacon**. The entry appears only on radios running firmware 2.8.0 or newer. A read-only **Region** row at the top of the screen shows the region the beacon advertises: that region, and the preset, are always the ones the radio itself uses, so a beacon cannot invite anyone onto settings your radio is not running. - **Listen for beacons** — receive invitations broadcast by other nodes. -- **Broadcast a beacon** — periodically advertise this mesh to nearby nodes, with an optional **Beacon message** of up to 100 bytes, a **Broadcast interval** picked from fixed intervals between 1 hour and 72 hours, and an **Offered channel** chosen from your radio's own channels. The offered channel is required, and defaults to your primary channel. +- **Broadcast a beacon** — periodically advertise this mesh to nearby nodes, with an optional **Beacon message** of up to 100 bytes, a **Broadcast interval** picked from fixed intervals between 1 hour and 72 hours, and an **Offered channel** chosen from your radio's own channels. The offered channel is required, and defaults to your primary channel. Over remote admin the picker offers the primary channel only. - **Broadcast targets** — optional extra destinations beyond the offered channel. **Add target** appends a row; each row picks a **Channel** and a **Transmit preset**, and **Remove target** deletes it. With no targets, the beacon goes out on the offered channel alone. Two conditions block beacon setup: diff --git a/docs/ko-rKR/user/help-and-docs.md b/docs/ko-rKR/user/help-and-docs.md index f306cde7f7..c83898cea3 100644 --- a/docs/ko-rKR/user/help-and-docs.md +++ b/docs/ko-rKR/user/help-and-docs.md @@ -2,7 +2,7 @@ title: Help & In-App Docs parent: User Guide nav_order: 21 -last_updated: 2026-08-30 +last_updated: 2026-09-11 description: Browse this documentation inside the app, search it, and ask Chirpy — the on-device AI assistant — questions about Meshtastic. aliases: - help @@ -23,7 +23,7 @@ The docs browser lists every user-guide page. Tap a page to read it; images and ### Search -The search box sits at the top of the docs browser. Type in it to filter pages by title and keywords — results update as you type, and the **Clear search** button (✕) empties the box. +Tap the search field at the top of the docs browser to open a full-screen search view. Type to filter pages by title and keywords — results update as you type, under the same field. The back arrow, or the keyboard's search action, closes the view. The field hides while you scroll down the page list and comes back when you scroll up. ![Searching the in-app documentation](../../assets/screenshots/docs-browser_search.png) diff --git a/docs/ko-rKR/user/map-and-waypoints.md b/docs/ko-rKR/user/map-and-waypoints.md index 0d72dcdc74..d5e1bba45c 100644 --- a/docs/ko-rKR/user/map-and-waypoints.md +++ b/docs/ko-rKR/user/map-and-waypoints.md @@ -2,7 +2,7 @@ title: Map & Waypoints parent: User Guide nav_order: 6 -last_updated: 2026-09-01 +last_updated: 2026-09-11 description: View node positions on the map, create and share waypoints, manage map layers and Site Planner, and control position sharing and privacy. aliases: - map @@ -44,7 +44,7 @@ The floating toolbar provides quick access to the compass, the map type and laye ### Filtering the Map -Tap the filter button in the floating toolbar to open **Filter map**. **Display** controls what is drawn: **Only Favorites**, **Show Waypoints**, **Show Precision Circles**, and a slider that hides nodes not heard from recently. **Node roles** is a chip per device role, plus **All** to show every role; a selected chip means that role is shown. **Nodes** narrows the set further with **Hide offline nodes**, **Only show direct nodes**, **Exclude MQTT**, **Show ignored nodes**, and **Include unknown**. +Tap the filter button in the floating toolbar to open **Filter map**. **Display** controls what is drawn: **Only Favorites**, **Show Waypoints**, **Show Precision Circles**, and a slider that hides nodes not heard from recently. **Node roles** is a chip per device role, plus **All** to show every role; a selected chip means that role is shown. **Nodes** narrows the set further with **Hide offline nodes**, **Only show direct nodes**, **Signed only**, **Encrypted only**, **Exclude MQTT**, **Show ignored nodes**, and **Include unknown** — the same rules as the node list's [filter toggles](nodes). A dot on the filter button means at least one filter is hiding something — check it before concluding the mesh is quiet. Turning **Show Waypoints** off hides every waypoint, including your own. **Show ignored nodes** adds them to the map rather than showing only them — unlike the node list's **Only show ignored Nodes**. @@ -147,8 +147,9 @@ picker, all three offer the same raster base maps: Overlays can be toggled on top of any base map, from the layers sheet: - **Weather radar** — NOAA NEXRAD reflectivity (US coverage) -- **Hillshade** — terrain relief, on **F-Droid** and **Desktop** only. Useful for understanding why a - link fails, since LoRa range is limited by terrain +- **Hillshade** — terrain relief from an online tile source, on **F-Droid** and **Desktop** only; Google + Play draws hillshade from downloaded offline terrain instead. Useful for understanding why a link + fails, since LoRa range is limited by terrain ### Adding your own tile source @@ -156,7 +157,7 @@ Any XYZ tile endpoint can be added as a base map, on every flavor and on desktop Tile Sources** at the foot of the base map picker and paste a URL template using `{z}`, `{x}` and `{y}` — plus `{s}` if the provider uses rotating subdomains. A national mapping service, for example: -``` +```text https://wmts.geo.admin.ch/1.0.0/ch.swisstopo.pixelkarte-farbe/default/current/3857/{z}/{x}/{y}.jpeg ``` @@ -173,7 +174,18 @@ On **Google Play**, the layers sheet's **Offline Manager** section downloads its — water, roads and administrative boundaries extracted from the public Protomaps basemap dataset, drawn directly as map shapes rather than raster tiles. Frame the area you want, tap **Start Download**, and once it finishes flip **Show on map** to draw it (it's off by default so it never surprises you by covering up the -live map). You can still import a pre-made `.mbtiles` archive here too, as before. **Desktop** has neither. +live map). You can still import a pre-made `.mbtiles` archive here too, as before. **Desktop** has neither, though +the offline terrain described next does work there. + +### Offline terrain + +Hillshade and elevation contours can be downloaded for offline use as well, so a downloaded area still +shows why a link fails when there is no network. The terrain data comes from Mapterhorn and is credited on +the map. On **Google Play**, each downloaded region in the **Offline Manager** list has a **Download Terrain** +button and **Hillshade** and **Contours** switches; terrain attaches to that region and shares its storage limit, and an area +too large for terrain asks you to zoom in or pick a smaller region. On **F-Droid** and **Desktop**, the +layers sheet has an **Offline Terrain** section of its own: frame the area, tap **Start Download**, and the +hillshade and contours draw from the downloaded tiles from then on. ### Going offline @@ -181,8 +193,8 @@ An **Offline** pill appears over the map whenever the device has no network conn builds, if you've imported a local `.mbtiles` archive, the map switches to it automatically the moment the network drops — no toggle to remember — and switches back once you're reconnected, as long as you haven't picked a different base map yourself in between. On **F-Droid**, a downloaded offline area keeps rendering on -its own; the pill is purely informational there. **Desktop** has no offline downloads at all yet, so the pill -is informational there too. +its own; the pill is purely informational there. **Desktop** downloads offline terrain but no base-map +packs, so the pill is informational there too. ## Related Topics diff --git a/docs/ko-rKR/user/messages-and-channels.md b/docs/ko-rKR/user/messages-and-channels.md index 88eeae7e1d..53da3f7c92 100644 --- a/docs/ko-rKR/user/messages-and-channels.md +++ b/docs/ko-rKR/user/messages-and-channels.md @@ -2,7 +2,7 @@ title: Messages & Channels parent: User Guide nav_order: 3 -last_updated: 2026-08-30 +last_updated: 2026-09-14 description: Send and receive messages, manage channels, configure encryption, search conversations, and use quick chat, reactions, and message actions. aliases: - channels @@ -50,6 +50,21 @@ Key length alone does not change the icon: a 128-bit key and a 256-bit key both Tapping an existing channel opens the same editor, where you can change the name, the PSK, MQTT uplink and downlink, and position precision. Every edit on this screen — adding, editing, deleting, or dragging a channel into a new order — waits on **Send** the same way. +### Archived Channels + +A conversation belongs to the channel it was held on, not to the slot that channel occupied. When a channel leaves your radio — you scan a different channel QR, replace a channel's key, or switch the modem preset — its conversation is **archived** rather than deleted or merged into whatever takes the slot. + +An archived conversation: + +- stays in the **Channels** list under the name the channel had, below the channels currently on your radio, marked with a history icon; +- keeps its full message history, searchable as usual; +- is read-only — you cannot send, reply, or react, because the channel is no longer on the radio to send on, and it is not offered as a share target; +- comes back automatically if you re-add that exact channel, taking its messages back to the live slot. + +Changing the modem preset archives the channel too: the preset is part of a default channel's identity, so **LongFast** and **MediumFast** are different channels even though neither the name nor the key changed. Changing only the region does not archive anything. + +To remove an archived conversation for good, long-press it in the conversation list and delete it. + ## Direct Messages Direct messages (DMs) go to one specific node. When both radios hold each other's public keys, your radio encrypts the message to that node's public key, so no one else on the mesh can read it — not even nodes that share your channel. diff --git a/docs/ko-rKR/user/mqtt.md b/docs/ko-rKR/user/mqtt.md index fffb00ed95..23e8471fd3 100644 --- a/docs/ko-rKR/user/mqtt.md +++ b/docs/ko-rKR/user/mqtt.md @@ -2,7 +2,7 @@ title: MQTT parent: User Guide nav_order: 11 -last_updated: 2026-08-30 +last_updated: 2026-09-11 description: Bridge your mesh to the internet — MQTT broker setup, encryption layers, and map reporting. aliases: - mqtt @@ -25,7 +25,7 @@ The MQTT module connects your node to an MQTT broker, allowing: ## How It Works -``` +```text [Your Node] → Radio → [Gateway Node with Wi-Fi] → MQTT Broker → [Remote Gateway] → Radio → [Remote Node] ``` diff --git a/docs/ko-rKR/user/node-metrics.md b/docs/ko-rKR/user/node-metrics.md index 58dc0a3800..215f871d42 100644 --- a/docs/ko-rKR/user/node-metrics.md +++ b/docs/ko-rKR/user/node-metrics.md @@ -2,7 +2,7 @@ title: Node Metrics parent: User Guide nav_order: 5 -last_updated: 2026-08-30 +last_updated: 2026-09-09 description: Telemetry dashboards for each mesh node — device health, environment sensors, air quality, signal quality, power, traceroute, and position history. aliases: - metrics @@ -114,7 +114,7 @@ Radio signal quality information: ### Signal Quality Reference -Signal quality is rated from **SNR relative to the active LoRa modem preset's demodulation floor**, not from fixed thresholds — a given SNR means different things on different presets (e.g. −15 dB is fine on LongSlow but unusable on ShortFast). RSSI is shown but is not part of the rating. In the table, _limit_ is the preset's SNR limit. +Signal quality is rated from **SNR relative to the active LoRa modem preset's demodulation floor**, not from fixed thresholds — a given SNR means different things on different presets (e.g. −15 dB is fine on LongSlow but unusable on ShortFast). When RSSI and a noise-floor reading are both available, the app also rates their difference against the preset limit and uses the worse rating. Otherwise, RSSI is display-only. In the table, _limit_ is the preset's SNR limit. | Quality | Criteria | | ------- | ---------------------------------------------------------------- | diff --git a/docs/ko-rKR/user/nodes.md b/docs/ko-rKR/user/nodes.md index 2aacf5faab..7fb1f2eac7 100644 --- a/docs/ko-rKR/user/nodes.md +++ b/docs/ko-rKR/user/nodes.md @@ -2,7 +2,7 @@ title: 노드 parent: User Guide nav_order: 4 -last_updated: 2026-09-04 +last_updated: 2026-09-11 description: Browse, filter, and sort mesh nodes — view details, signal quality, roles, and quick actions. aliases: - node-list @@ -74,19 +74,23 @@ Most users should keep the default **Client** role. Consider a different role wh > 💡 **Tip:** The mesh works best when most nodes are **Client** or **Router**. Too many Client Mute nodes reduce mesh resilience; too many Routers in a dense area can cause congestion. A good rule of thumb: one Router per 5–10 Clients in your area. -### Encryption Indicators +### Security indicators -Nodes display encryption status icons next to their name: +Each node carries one security icon beside its name in the node list. Tap it to read what it means, and choose **Show All Meanings** in that dialog for the full legend. The detail screen shows the same state in words, as a **Security** row that opens the same dialog. -| Icon | Meaning | -| ----------- | ------------------------------------------------------------------------------------------------------------------------------------------------- | -| 🔒 Locked | Communication uses PKI (public key infrastructure) — end-to-end encrypted with verified identity | -| 🔓 Unlocked | No public key has been received for this node, so it cannot be direct messaged — use **Request User Info** on the node detail page to ask for one | -| ⚠️ Mismatch | Public key mismatch — the node's key has changed since last seen (investigate before trusting) | +| Icon | Meaning | Shown for | +| --------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------- | +| Person with a shield check (green) | **Verified contact** — you verified this node's key in person by exchanging contact QR codes, so its identity is confirmed. The strongest trust the list shows. Your own connected node carries it too | Any firmware version | +| Nodes icon with a shield check (green) | **Signed node** — this node signs its broadcasts with its identity key, so its identity is consistent over time, but you have not verified it in person. On firmware 2.8 the icon appears from the version alone, before any signed broadcast has been heard | Firmware 2.8 or newer, and any node whose signed broadcast your node has verified | +| 🔒 Closed lock | A public key is on file and matches, so direct messages to this node are encrypted | Firmware before 2.8, or no reported version | +| 🔓 Open lock | No public key has been received for this node, so it cannot be direct messaged — use **Request User Info** on the node detail page to ask for one | Firmware before 2.8, or no reported version | +| ⚠️ Mismatch | **Public key mismatch** — a different key arrived for this node after one was stored. Investigate before trusting | Any firmware version | -> 💡 **Tip:** Direct messages always use PKI, so the radio needs the other node's public key before it can send one. It refuses the send rather than falling back to channel encryption. Keys arrive inside node info, which is why an open lock usually clears itself once that node is heard from properly. If you see a key mismatch warning, the node may have been reset or compromised. +Every node on firmware 2.8 or newer signs its broadcasts, so on that firmware the signed state is the baseline and the locks are not shown. -To clear a mismatch, first confirm through another trusted channel that the key change was intentional — a factory reset causes one. Then touch & hold the node, choose **Remove**, and let the two radios exchange keys again the next time yours hears it. +Direct messages always use public-key encryption, so your node needs the other node's public key before it can send one. It refuses the send rather than falling back to channel encryption. Keys arrive inside node info, which is why an open lock usually clears itself once that node is heard from properly. + +A mismatch never replaces the key you already hold. The app keeps the first key it recorded and refuses the new one, as the firmware does, so a stray or hostile node info cannot silently break encrypted messaging to a contact. To clear a mismatch, first confirm through another trusted channel that the key change was intentional — a factory reset causes one. Then touch & hold the node, choose **Remove**, and let the two nodes exchange keys again the next time yours hears it. ## Quick Actions @@ -108,7 +112,9 @@ radio is connected and running firmware 2.8 or newer — see ## Sharing a Contact -On a node's detail screen, tap **Share Contact** to produce a link and a QR code for that node. From the same dialog, **Write to NFC tag** saves the link to a writable NFC tag that anyone can tap to open. +On a node's detail screen, tap **Share Contact** to produce a link and a QR code for that node. From the same dialog, **Share link** opens the Android share sheet (on desktop it copies the link instead), **Write to NFC tag** saves it to a writable NFC tag, and **Copy** puts it on the clipboard. While that dialog is open and in front of you, the phone also offers the same link to any NFC reader, so someone can take the contact by tapping their phone against yours with no tag involved. + +Sharing your own contact this way marks it as verified in person, so whoever imports it sees the **Verified contact** icon rather than the signed one. Relaying someone else's contact passes on only what your app had already recorded about them. To add someone else's contact, use the import button on the node list and choose **Scan Shared Contact QR Code**, **Scan Shared Contact NFC**, or **Input Shared Contact URL**. The app asks you to confirm with **Import Shared Contact?**, and warns you when the contact is one you already have. @@ -120,14 +126,16 @@ Type in the search field to filter nodes by name or short name. The filter updat ### Filter Toggles -| 필터 | 설명 | -| --------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| **Hide offline nodes** | Show only nodes heard within the last 2 hours | -| **Only show direct nodes** | Show only nodes your radio heard directly, with no relay in between | -| **Include unknown** | Show nodes that haven't sent user info yet. **On by default**, so a node heard before its info arrives stays visible; these carry a badge marking them incomplete, and cannot be direct messaged until their user info brings a public key | -| **Exclude infrastructure** | Hide infrastructure-role nodes (Router, Router Late, Client Base, and legacy Repeater nodes) and any node that cannot be messaged, whatever its role | -| **Exclude MQTT** | Hide nodes heard only via MQTT internet bridge | -| **Only show ignored Nodes** | Replace the list with the nodes you have ignored. Every other node is hidden while this is on, and a banner appears at the top of the list to take you back | +| 필터 | 설명 | +| --------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **Hide offline nodes** | Show only nodes heard within the last 2 hours | +| **Only show direct nodes** | Show only nodes your radio heard directly, with no relay in between | +| **Include unknown** | Show nodes that haven't sent user info yet. **On by default**, so a node heard before its info arrives stays visible; these carry a badge marking them incomplete, and cannot be direct messaged until their user info brings a public key | +| **Exclude infrastructure** | Hide infrastructure-role nodes (Router, Router Late, Client Base, and legacy Repeater nodes) and any node that cannot be messaged, whatever its role | +| **Exclude MQTT** | Hide nodes heard only via MQTT internet bridge | +| **Signed only** | Show only nodes whose signed broadcasts your node has actually heard and verified. Stricter than the icon: a node on 2.8 shows as signed by its firmware version before any signed broadcast arrives, and a contact verified in person on older firmware is not signed at all | +| **Encrypted only** | Show nodes with a matching public key on file, the key an encrypted direct message needs. A node with a key mismatch is excluded | +| **Only show ignored Nodes** | Replace the list with the nodes you have ignored. Every other node is hidden while this is on, and a banner appears at the top of the list to take you back | ### Sort Options @@ -149,6 +157,8 @@ Tap the hop-histogram icon in the node list's app bar to open a bar chart of how Tapping a node opens the detail view with comprehensive information. See [Node Metrics](node-metrics) for full details on metrics and telemetry. +Signal quality is rated against your modem preset. The same SNR can be good on a long-range preset and poor on a faster one. Traceroute and neighbor-info SNR colors use that preset too. RSSI text has its own strength colors; it affects the quality rating only when a noise-floor reading is also available. + The Details card carries the node's short name, role, IDs, last heard time, hops away, uptime, and its SNR and RSSI: ![Node detail section](../../assets/screenshots/nodes_detail_section.png) diff --git a/docs/ko-rKR/user/settings-module-admin.md b/docs/ko-rKR/user/settings-module-admin.md index eea2749b82..51d8af2089 100644 --- a/docs/ko-rKR/user/settings-module-admin.md +++ b/docs/ko-rKR/user/settings-module-admin.md @@ -2,7 +2,7 @@ title: Settings — Modules & Admin parent: User Guide nav_order: 8 -last_updated: 2026-08-30 +last_updated: 2026-09-16 description: Configure optional feature modules (MQTT, telemetry, canned messages, TAK, and more) and perform device administration. aliases: - modules @@ -333,7 +333,7 @@ appear only when your own node is selected: ### 앱에 대하여 -**Settings → About** carries the app's own identity rather than the radio's: +**Settings → About** carries the app's own identity rather than the node's: Three sections: @@ -341,7 +341,11 @@ Three sections: - **Apps** — opens with **Need Hardware?**, a rotating carousel of popular devices that links out to where to buy one. It also lists the GitHub repository, the running app version, and **Acknowledgements** (see the next section). -- **Project information** — links to the website and to this documentation. +- **Project information** — links to the website, to this documentation, and to the GNU GPL v3 + license the app is released under. + +The footer states the copyright and that the app is free software with no warranty, which you +may redistribute under the same license. ### Acknowledgements diff --git a/docs/ko-rKR/user/settings-radio-user.md b/docs/ko-rKR/user/settings-radio-user.md index f1f4b666bf..ba9fb08496 100644 --- a/docs/ko-rKR/user/settings-radio-user.md +++ b/docs/ko-rKR/user/settings-radio-user.md @@ -2,7 +2,7 @@ title: Settings — Radio & User parent: User Guide nav_order: 7 -last_updated: 2026-09-04 +last_updated: 2026-09-11 description: Configure your radio hardware, LoRa presets, user profile, position sharing, power management, and security. aliases: - 설정 @@ -51,7 +51,7 @@ The footer appears as soon as you change something. **Discard** throws the chang The status message is saved with the same **Save**, but it never reboots the node — and, like the rest of this screen, it can be edited on a remote node you administer. For your own radio there is a shortcut while it is connected: touch & hold your node in the [node list](nodes.md) and choose -**Update status**. Older firmware and a disconnected radio have no shortcut — the field above is +**Update status**. Older firmware and a disconnected node have no shortcut — the field itself is still the way in. ## 설정 @@ -77,13 +77,13 @@ On **Settings → LoRa**. | Setting | 설명 | 기본값 | | ------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------- | -| 지역 | Regulatory region for frequency bands. You must set this before transmitting | Unset (must configure) | +| 지역 | Regulatory region for frequency bands. You must set this before transmitting. On firmware 2.8 or newer, the first region set also creates the node's identity key and gives it a new node number | Unset (must configure) | | Presets | Speed/range tradeoff | LongFast | | Number of Hops | Maximum retransmit hops | 3 | | 전송 출력 | Transmission power (dBm); 0 = max allowed for region | 0 (region max) | | Frequency Override | Overrides the computed operating frequency outright (MHz). It does not offset the calculated value — leave at 0 unless you know you need a specific frequency | 0 (use calculated) | | 프리셋 사용 | On by default. Turn it off to set Spread Factor, Coding Rate and Bandwidth by hand instead of taking them from the modem preset | On | -| Spread Factor | Manual mode only: 7–12. Higher spreads further but slower | From preset | +| Spread Factor | Manual mode only: 5–12. Higher spreads further but slower. On SX127x (RF95) radios the firmware does not accept 5 or 6 and uses 11 instead | From preset | | Coding rate | Manual mode only: 5–8. More redundancy costs airtime | From preset | | 대역폭 | Manual mode only: the channel bandwidth in kHz, typed in directly. On the 2.4 GHz region the app offers a list of the bandwidths your radio supports instead, and a stored value that is not on that list shows as _Unsupported_ and blocks saving until you pick a supported one | From preset | | 주파수 슬롯 | Which slot within the region's band to use. 0 derives it from the primary channel name | 0 (automatic) | @@ -111,13 +111,13 @@ The Lite, Narrow, Medium Turbo, and Tiny presets need firmware 2.8 or newer — | Short Slow | ~5 km | 6.25 kbps | −10 dB | Suburban short-range; moderate building density | | Medium Fast | ~5 km | 3.52 kbps | −12.5 dB | Suburban areas; moderate building density | | Medium Slow | ~8 km | 1.95 kbps | −15 dB | Suburban/rural; moderate range with slower speed | -| Long Turbo | ~10 km | 1.34 kbps | −12.5 dB | Similar range to Long Fast but with 500 kHz bandwidth; faster throughput | +| Long Turbo | ~10 km | 1.34 kbps | −17.5 dB | Similar range to Long Fast but with 500 kHz bandwidth; faster throughput | | Long Fast | ~10 km | 1.1 kbps | −17.5 dB | **General use (default)** — balanced range and speed | | Long Moderate | ~20 km | 0.34 kbps | −17.5 dB | Rural with some terrain; occasional use | | Lite Fast | ~5 km | 1.76 kbps | −12.5 dB | EU 866 MHz SRD band (125 kHz BW); comparable to Medium Fast | | Lite Slow | ~10 km | 0.98 kbps | −15 dB | EU 866 MHz SRD band (125 kHz BW); comparable to Long Fast | -| Narrow Fast | ~5 km | 2.28 kbps | −10 dB | EU 868 MHz band (62.5 kHz BW); avoids interference with other devices | -| Narrow Slow | ~10 km | 1.30 kbps | −12.5 dB | EU 868 MHz band (62.5 kHz BW); comparable to Long Fast | +| Narrow Fast | ~5 km | 2.28 kbps | −7.5 dB | EU 868 MHz band (62.5 kHz BW); avoids interference with other devices | +| Narrow Slow | ~10 km | 1.30 kbps | −10 dB | EU 868 MHz band (62.5 kHz BW); comparable to Long Fast | | Medium Turbo | ~5 km | 7.0 kbps | −12.5 dB | Like Medium Fast but with 500 kHz bandwidth; not legal in every region. Needs firmware 2.8 or newer | | Tiny Fast | ~10 km | 0.68 kbps | −7.5 dB | Amateur bands that cap occupied bandwidth; these presets use 15.6 kHz. Needs firmware 2.8 or newer, an SX126x or SX127x radio, and a TCXO of ±5 ppm or better | | Tiny Slow | ~20 km | 0.33 kbps | −10 dB | Same band restrictions as Tiny Fast, longer range. Same firmware, radio, and TCXO requirements | diff --git a/docs/ko-rKR/user/signal-meter.md b/docs/ko-rKR/user/signal-meter.md index d3ac9d6d03..8a72c88336 100644 --- a/docs/ko-rKR/user/signal-meter.md +++ b/docs/ko-rKR/user/signal-meter.md @@ -2,7 +2,7 @@ title: How the Meshtastic Signal Meter Works parent: User Guide nav_order: 15 -last_updated: 2026-08-30 +last_updated: 2026-09-09 description: How the signal meter rates quality from SNR relative to the LoRa modem preset — spread spectrum, presets, and what the bars really mean. aliases: - signal @@ -32,7 +32,7 @@ Each modem preset has an SNR limit: the lowest SNR at which that preset can stil ## Rating Signal Quality -The app rates signal quality (None, Bad, Fair, or Good) from SNR alone, measured against the active preset's SNR limit. It does not factor in RSSI: without knowing the local noise floor, RSSI alone cannot say whether a signal is decodable. RSSI is still available — on the node detail screen and in the metrics charts. +The app rates signal quality (None, Bad, Fair, or Good) from SNR relative to the active preset's SNR limit. When RSSI and a noise-floor reading are both available, it also rates the difference between them against that limit and uses the worse rating. Otherwise, it uses SNR alone. Because the rating is relative to the preset, the same SNR rates differently on different presets. An SNR of −16 dB rates Good on Long Fast (SNR limit −17.5 dB) but None on Short Fast (SNR limit −7.5 dB). Let `limit` be the active preset's SNR limit: @@ -45,7 +45,7 @@ Because the rating is relative to the preset, the same SNR rates differently on The icon never goes blank, so count the bars carefully: a single bar means None, not a weak but usable link, and Good is a solid wedge rather than a set of bars. A gray three-bar icon labeled Unknown is a different state again — the packet carried no SNR measurement at all, which is not the same as measuring one and finding it too weak. -> ℹ️ **Note:** Traceroute hop colors use fixed thresholds (−7 dB / −15 dB); the per-node signal meter uses the preset-relative rating instead. +> ℹ️ **Note:** Traceroute and neighbor-info SNR colors use the active preset's limits too. RSSI text has separate strength colors: green above −115 dBm, yellow above −120 dBm, orange above −126 dBm, and red at or below −126 dBm. ## Diagnosing Local Interference diff --git a/docs/ko-rKR/user/tak.md b/docs/ko-rKR/user/tak.md index e483d028bf..e487764b60 100644 --- a/docs/ko-rKR/user/tak.md +++ b/docs/ko-rKR/user/tak.md @@ -2,7 +2,7 @@ title: TAK Integration parent: User Guide nav_order: 10 -last_updated: 2026-08-30 +last_updated: 2026-09-11 description: Interoperate with ATAK and WinTAK — CoT position sharing, TAK roles, and plugin setup. aliases: - tak @@ -36,7 +36,7 @@ The TAK module allows Meshtastic nodes to: > ⚠️ **Warning:** The old **Meshtastic ATAK Plugin** is no longer part of this path and cannot > work. It bridged through the cross-process AIDL API, which was removed in app 2.8.0; the mesh > service is now in-process only. Do not install it. Interop today runs over the app's own local -> TAK server plus the Mesh to CoT Converter, both described below, with stock ATAK/iTAK/WinTAK. +> TAK server plus the Mesh to CoT Converter, both described in the following sections, with stock ATAK/iTAK/WinTAK. ### 설정 diff --git a/docs/ko-rKR/user/translate.md b/docs/ko-rKR/user/translate.md index 67d69e2d96..cdc8b6d87f 100644 --- a/docs/ko-rKR/user/translate.md +++ b/docs/ko-rKR/user/translate.md @@ -2,7 +2,7 @@ title: Translate the App parent: User Guide nav_order: 17 -last_updated: 2026-08-30 +last_updated: 2026-09-11 description: How the app and its documentation are translated via Crowdin, and guidelines for contributing translations. aliases: - translate @@ -45,7 +45,7 @@ If your language is not yet listed on Crowdin: The Android app uses **Compose Multiplatform resources** for all user-visible strings: -``` +```text core/resources/src/commonMain/composeResources/ ├── values/ ← English (default) │ └── strings.xml @@ -58,7 +58,7 @@ core/resources/src/commonMain/composeResources/ In-app documentation follows a similar pattern under `docs/`: -``` +```text docs/ ├── en/user/ ← English source (default) │ ├── onboarding.md diff --git a/docs/lt-rLT/user/desktop.md b/docs/lt-rLT/user/desktop.md index efca3c03c9..2ec0d98037 100644 --- a/docs/lt-rLT/user/desktop.md +++ b/docs/lt-rLT/user/desktop.md @@ -2,7 +2,7 @@ title: Desktop App parent: User Guide nav_order: 14 -last_updated: 2026-08-30 +last_updated: 2026-09-11 description: Install and use the Meshtastic Desktop app on Linux, macOS, and Windows — connections, feature parity, and keyboard shortcuts. aliases: - desktop @@ -58,20 +58,20 @@ Bluetooth Low Energy is supported on desktop via the [Kable](https://github.com/ ## Feature Parity -| Feature | Android | Desktop | Notes | -| ----------------------------------------------------- | ------- | ------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Messaging | ✓ | ✓ | Full parity | -| Node List | ✓ | ✓ | Full parity | -| Map | ✓ | ✓ | Interactive MapLibre map, with base map and overlay pickers and custom tile sources. No offline downloads or local `.mbtiles` archives | -| Map layers (`.kml`/`.kmz`/GeoJSON) | ✓ | ✓ | Same layer store and sheet as Android; imported files draw on the desktop map | -| Site Planner | ✓ | ✓\* | \*Opens in your browser on desktop; the estimate is not drawn on the desktop map | -| Settings | ✓ | ✓ | Full parity | -| Bluetooth (BLE) | ✓ | ✓ | Via Kable on desktop | -| Firmware Update | ✓ | ✓ | In-app USB, BLE, and Wi-Fi (ESP32) update work the same as Android. The USB maintenance flow — nRF52/RP2040 factory erase and bootloader upgrade — is Android-only | -| Notifications | ✓ | ✓ | Native OS notifications | -| Widgets | ✓ | ✗ | Android-only | -| AI Assistant (Chirpy) | ✓\* | ✗ | Google flavor Android only | -| App Functions (system AI) | ✓† | ✗ | Google flavor Android only | +| Feature | Android | Desktop | Notes | +| ----------------------------------------------------- | ------- | ------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Messaging | ✓ | ✓ | Full parity | +| Node List | ✓ | ✓ | Full parity | +| Map | ✓ | ✓ | Interactive MapLibre map, with base map and overlay pickers and custom tile sources. Offline terrain (hillshade and contours) can be downloaded; offline base-map packs and local `.mbtiles` archives cannot | +| Map layers (`.kml`/`.kmz`/GeoJSON) | ✓ | ✓ | Same layer store and sheet as Android; imported files draw on the desktop map | +| Site Planner | ✓ | ✓\* | \*Opens in your browser on desktop; the estimate is not drawn on the desktop map | +| Settings | ✓ | ✓ | Full parity | +| Bluetooth (BLE) | ✓ | ✓ | Via Kable on desktop | +| Firmware Update | ✓ | ✓ | In-app USB, BLE, and Wi-Fi (ESP32) update work the same as Android. The USB maintenance flow — nRF52/RP2040 factory erase and bootloader upgrade — is Android-only | +| Notifications | ✓ | ✓ | Native OS notifications | +| Widgets | ✓ | ✗ | Android-only | +| AI Assistant (Chirpy) | ✓\* | ✗ | Google flavor Android only | +| App Functions (system AI) | ✓† | ✗ | Google flavor Android only | \*Chirpy AI requires Android 14+ on Google flavor builds with supported hardware. @@ -122,7 +122,7 @@ Individual doc pages render with full formatting: ## Building from Source -```bash +```shell git clone https://github.com/meshtastic/Meshtastic-Android.git cd Meshtastic-Android ./gradlew :desktopApp:run @@ -135,7 +135,7 @@ Requirements: ## Known Limitations -- Offline tile downloads and local `.mbtiles` archives are not available on desktop. +- Offline base-map downloads and local `.mbtiles` archives are not available on desktop. Offline terrain is — see [Map & Waypoints](map-and-waypoints). - `.kml`/`.kmz`/GeoJSON layer import works — see [Map & Waypoints](map-and-waypoints#map-layers). Site Planner opens in your browser rather than in the app; to bring its coverage estimate onto the map, click the transmitter pin diff --git a/docs/lt-rLT/user/discovery.md b/docs/lt-rLT/user/discovery.md index 6604396e47..af608e5887 100644 --- a/docs/lt-rLT/user/discovery.md +++ b/docs/lt-rLT/user/discovery.md @@ -2,7 +2,7 @@ title: Local Mesh Discovery parent: User Guide nav_order: 12 -last_updated: 2026-08-30 +last_updated: 2026-09-11 description: Explore your mesh network — the Local Mesh Discovery scanner, traceroute paths, neighbor maps, and node discovery tools. aliases: - discovery @@ -97,7 +97,7 @@ Mesh Beacon lets nodes invite others to join their mesh. A beaconing node period Configure it under **Settings → Module configuration → Mesh Beacon**. The entry appears only on radios running firmware 2.8.0 or newer. A read-only **Region** row at the top of the screen shows the region the beacon advertises: that region, and the preset, are always the ones the radio itself uses, so a beacon cannot invite anyone onto settings your radio is not running. - **Listen for beacons** — receive invitations broadcast by other nodes. -- **Broadcast a beacon** — periodically advertise this mesh to nearby nodes, with an optional **Beacon message** of up to 100 bytes, a **Broadcast interval** picked from fixed intervals between 1 hour and 72 hours, and an **Offered channel** chosen from your radio's own channels. The offered channel is required, and defaults to your primary channel. +- **Broadcast a beacon** — periodically advertise this mesh to nearby nodes, with an optional **Beacon message** of up to 100 bytes, a **Broadcast interval** picked from fixed intervals between 1 hour and 72 hours, and an **Offered channel** chosen from your radio's own channels. The offered channel is required, and defaults to your primary channel. Over remote admin the picker offers the primary channel only. - **Broadcast targets** — optional extra destinations beyond the offered channel. **Add target** appends a row; each row picks a **Channel** and a **Transmit preset**, and **Remove target** deletes it. With no targets, the beacon goes out on the offered channel alone. Two conditions block beacon setup: diff --git a/docs/lt-rLT/user/help-and-docs.md b/docs/lt-rLT/user/help-and-docs.md index f306cde7f7..c83898cea3 100644 --- a/docs/lt-rLT/user/help-and-docs.md +++ b/docs/lt-rLT/user/help-and-docs.md @@ -2,7 +2,7 @@ title: Help & In-App Docs parent: User Guide nav_order: 21 -last_updated: 2026-08-30 +last_updated: 2026-09-11 description: Browse this documentation inside the app, search it, and ask Chirpy — the on-device AI assistant — questions about Meshtastic. aliases: - help @@ -23,7 +23,7 @@ The docs browser lists every user-guide page. Tap a page to read it; images and ### Search -The search box sits at the top of the docs browser. Type in it to filter pages by title and keywords — results update as you type, and the **Clear search** button (✕) empties the box. +Tap the search field at the top of the docs browser to open a full-screen search view. Type to filter pages by title and keywords — results update as you type, under the same field. The back arrow, or the keyboard's search action, closes the view. The field hides while you scroll down the page list and comes back when you scroll up. ![Searching the in-app documentation](../../assets/screenshots/docs-browser_search.png) diff --git a/docs/lt-rLT/user/map-and-waypoints.md b/docs/lt-rLT/user/map-and-waypoints.md index 64c18ff42a..6b7b097759 100644 --- a/docs/lt-rLT/user/map-and-waypoints.md +++ b/docs/lt-rLT/user/map-and-waypoints.md @@ -2,7 +2,7 @@ title: Map & Waypoints parent: User Guide nav_order: 6 -last_updated: 2026-09-01 +last_updated: 2026-09-11 description: View node positions on the map, create and share waypoints, manage map layers and Site Planner, and control position sharing and privacy. aliases: - map @@ -44,7 +44,7 @@ The floating toolbar provides quick access to the compass, the map type and laye ### Filtering the Map -Tap the filter button in the floating toolbar to open **Filter map**. **Display** controls what is drawn: **Only Favorites**, **Show Waypoints**, **Show Precision Circles**, and a slider that hides nodes not heard from recently. **Node roles** is a chip per device role, plus **All** to show every role; a selected chip means that role is shown. **Nodes** narrows the set further with **Hide offline nodes**, **Only show direct nodes**, **Exclude MQTT**, **Show ignored nodes**, and **Include unknown**. +Tap the filter button in the floating toolbar to open **Filter map**. **Display** controls what is drawn: **Only Favorites**, **Show Waypoints**, **Show Precision Circles**, and a slider that hides nodes not heard from recently. **Node roles** is a chip per device role, plus **All** to show every role; a selected chip means that role is shown. **Nodes** narrows the set further with **Hide offline nodes**, **Only show direct nodes**, **Signed only**, **Encrypted only**, **Exclude MQTT**, **Show ignored nodes**, and **Include unknown** — the same rules as the node list's [filter toggles](nodes). A dot on the filter button means at least one filter is hiding something — check it before concluding the mesh is quiet. Turning **Show Waypoints** off hides every waypoint, including your own. **Show ignored nodes** adds them to the map rather than showing only them — unlike the node list's **Only show ignored Nodes**. @@ -147,8 +147,9 @@ picker, all three offer the same raster base maps: Overlays can be toggled on top of any base map, from the layers sheet: - **Weather radar** — NOAA NEXRAD reflectivity (US coverage) -- **Hillshade** — terrain relief, on **F-Droid** and **Desktop** only. Useful for understanding why a - link fails, since LoRa range is limited by terrain +- **Hillshade** — terrain relief from an online tile source, on **F-Droid** and **Desktop** only; Google + Play draws hillshade from downloaded offline terrain instead. Useful for understanding why a link + fails, since LoRa range is limited by terrain ### Adding your own tile source @@ -156,7 +157,7 @@ Any XYZ tile endpoint can be added as a base map, on every flavor and on desktop Tile Sources** at the foot of the base map picker and paste a URL template using `{z}`, `{x}` and `{y}` — plus `{s}` if the provider uses rotating subdomains. A national mapping service, for example: -``` +```text https://wmts.geo.admin.ch/1.0.0/ch.swisstopo.pixelkarte-farbe/default/current/3857/{z}/{x}/{y}.jpeg ``` @@ -173,7 +174,18 @@ On **Google Play**, the layers sheet's **Offline Manager** section downloads its — water, roads and administrative boundaries extracted from the public Protomaps basemap dataset, drawn directly as map shapes rather than raster tiles. Frame the area you want, tap **Start Download**, and once it finishes flip **Show on map** to draw it (it's off by default so it never surprises you by covering up the -live map). You can still import a pre-made `.mbtiles` archive here too, as before. **Desktop** has neither. +live map). You can still import a pre-made `.mbtiles` archive here too, as before. **Desktop** has neither, though +the offline terrain described next does work there. + +### Offline terrain + +Hillshade and elevation contours can be downloaded for offline use as well, so a downloaded area still +shows why a link fails when there is no network. The terrain data comes from Mapterhorn and is credited on +the map. On **Google Play**, each downloaded region in the **Offline Manager** list has a **Download Terrain** +button and **Hillshade** and **Contours** switches; terrain attaches to that region and shares its storage limit, and an area +too large for terrain asks you to zoom in or pick a smaller region. On **F-Droid** and **Desktop**, the +layers sheet has an **Offline Terrain** section of its own: frame the area, tap **Start Download**, and the +hillshade and contours draw from the downloaded tiles from then on. ### Going offline @@ -181,8 +193,8 @@ An **Offline** pill appears over the map whenever the device has no network conn builds, if you've imported a local `.mbtiles` archive, the map switches to it automatically the moment the network drops — no toggle to remember — and switches back once you're reconnected, as long as you haven't picked a different base map yourself in between. On **F-Droid**, a downloaded offline area keeps rendering on -its own; the pill is purely informational there. **Desktop** has no offline downloads at all yet, so the pill -is informational there too. +its own; the pill is purely informational there. **Desktop** downloads offline terrain but no base-map +packs, so the pill is informational there too. ## Related Topics diff --git a/docs/lt-rLT/user/messages-and-channels.md b/docs/lt-rLT/user/messages-and-channels.md index 31f85e6416..eee3d40664 100644 --- a/docs/lt-rLT/user/messages-and-channels.md +++ b/docs/lt-rLT/user/messages-and-channels.md @@ -2,7 +2,7 @@ title: Messages & Channels parent: User Guide nav_order: 3 -last_updated: 2026-08-30 +last_updated: 2026-09-14 description: Send and receive messages, manage channels, configure encryption, search conversations, and use quick chat, reactions, and message actions. aliases: - channels @@ -50,6 +50,21 @@ Key length alone does not change the icon: a 128-bit key and a 256-bit key both Tapping an existing channel opens the same editor, where you can change the name, the PSK, MQTT uplink and downlink, and position precision. Every edit on this screen — adding, editing, deleting, or dragging a channel into a new order — waits on **Send** the same way. +### Archived Channels + +A conversation belongs to the channel it was held on, not to the slot that channel occupied. When a channel leaves your radio — you scan a different channel QR, replace a channel's key, or switch the modem preset — its conversation is **archived** rather than deleted or merged into whatever takes the slot. + +An archived conversation: + +- stays in the **Channels** list under the name the channel had, below the channels currently on your radio, marked with a history icon; +- keeps its full message history, searchable as usual; +- is read-only — you cannot send, reply, or react, because the channel is no longer on the radio to send on, and it is not offered as a share target; +- comes back automatically if you re-add that exact channel, taking its messages back to the live slot. + +Changing the modem preset archives the channel too: the preset is part of a default channel's identity, so **LongFast** and **MediumFast** are different channels even though neither the name nor the key changed. Changing only the region does not archive anything. + +To remove an archived conversation for good, long-press it in the conversation list and delete it. + ## Direct Messages Direct messages (DMs) go to one specific node. When both radios hold each other's public keys, your radio encrypts the message to that node's public key, so no one else on the mesh can read it — not even nodes that share your channel. diff --git a/docs/lt-rLT/user/mqtt.md b/docs/lt-rLT/user/mqtt.md index f6a2cc5d57..295c576617 100644 --- a/docs/lt-rLT/user/mqtt.md +++ b/docs/lt-rLT/user/mqtt.md @@ -2,7 +2,7 @@ title: MQTT parent: User Guide nav_order: 11 -last_updated: 2026-08-30 +last_updated: 2026-09-11 description: Bridge your mesh to the internet — MQTT broker setup, encryption layers, and map reporting. aliases: - mqtt @@ -25,7 +25,7 @@ The MQTT module connects your node to an MQTT broker, allowing: ## How It Works -``` +```text [Your Node] → Radio → [Gateway Node with Wi-Fi] → MQTT Broker → [Remote Gateway] → Radio → [Remote Node] ``` diff --git a/docs/lt-rLT/user/node-metrics.md b/docs/lt-rLT/user/node-metrics.md index f5703edb88..70519b1a76 100644 --- a/docs/lt-rLT/user/node-metrics.md +++ b/docs/lt-rLT/user/node-metrics.md @@ -2,7 +2,7 @@ title: Node Metrics parent: User Guide nav_order: 5 -last_updated: 2026-08-30 +last_updated: 2026-09-09 description: Telemetry dashboards for each mesh node — device health, environment sensors, air quality, signal quality, power, traceroute, and position history. aliases: - metrics @@ -114,7 +114,7 @@ Radio signal quality information: ### Signal Quality Reference -Signal quality is rated from **SNR relative to the active LoRa modem preset's demodulation floor**, not from fixed thresholds — a given SNR means different things on different presets (e.g. −15 dB is fine on LongSlow but unusable on ShortFast). RSSI is shown but is not part of the rating. In the table, _limit_ is the preset's SNR limit. +Signal quality is rated from **SNR relative to the active LoRa modem preset's demodulation floor**, not from fixed thresholds — a given SNR means different things on different presets (e.g. −15 dB is fine on LongSlow but unusable on ShortFast). When RSSI and a noise-floor reading are both available, the app also rates their difference against the preset limit and uses the worse rating. Otherwise, RSSI is display-only. In the table, _limit_ is the preset's SNR limit. | Quality | Criteria | | ---------- | ---------------------------------------------------------------- | diff --git a/docs/lt-rLT/user/nodes.md b/docs/lt-rLT/user/nodes.md index 2ff449f2a6..f44b7ad793 100644 --- a/docs/lt-rLT/user/nodes.md +++ b/docs/lt-rLT/user/nodes.md @@ -2,7 +2,7 @@ title: Nodes parent: User Guide nav_order: 4 -last_updated: 2026-09-04 +last_updated: 2026-09-11 description: Browse, filter, and sort mesh nodes — view details, signal quality, roles, and quick actions. aliases: - node-list @@ -74,19 +74,23 @@ Most users should keep the default **Client** role. Consider a different role wh > 💡 **Tip:** The mesh works best when most nodes are **Client** or **Router**. Too many Client Mute nodes reduce mesh resilience; too many Routers in a dense area can cause congestion. A good rule of thumb: one Router per 5–10 Clients in your area. -### Encryption Indicators +### Security indicators -Nodes display encryption status icons next to their name: +Each node carries one security icon beside its name in the node list. Tap it to read what it means, and choose **Show All Meanings** in that dialog for the full legend. The detail screen shows the same state in words, as a **Security** row that opens the same dialog. -| Icon | Meaning | -| ----------- | ------------------------------------------------------------------------------------------------------------------------------------------------- | -| 🔒 Locked | Communication uses PKI (public key infrastructure) — end-to-end encrypted with verified identity | -| 🔓 Unlocked | No public key has been received for this node, so it cannot be direct messaged — use **Request User Info** on the node detail page to ask for one | -| ⚠️ Mismatch | Public key mismatch — the node's key has changed since last seen (investigate before trusting) | +| Icon | Meaning | Shown for | +| --------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------- | +| Person with a shield check (green) | **Verified contact** — you verified this node's key in person by exchanging contact QR codes, so its identity is confirmed. The strongest trust the list shows. Your own connected node carries it too | Any firmware version | +| Nodes icon with a shield check (green) | **Signed node** — this node signs its broadcasts with its identity key, so its identity is consistent over time, but you have not verified it in person. On firmware 2.8 the icon appears from the version alone, before any signed broadcast has been heard | Firmware 2.8 or newer, and any node whose signed broadcast your node has verified | +| 🔒 Closed lock | A public key is on file and matches, so direct messages to this node are encrypted | Firmware before 2.8, or no reported version | +| 🔓 Open lock | No public key has been received for this node, so it cannot be direct messaged — use **Request User Info** on the node detail page to ask for one | Firmware before 2.8, or no reported version | +| ⚠️ Mismatch | **Public key mismatch** — a different key arrived for this node after one was stored. Investigate before trusting | Any firmware version | -> 💡 **Tip:** Direct messages always use PKI, so the radio needs the other node's public key before it can send one. It refuses the send rather than falling back to channel encryption. Keys arrive inside node info, which is why an open lock usually clears itself once that node is heard from properly. If you see a key mismatch warning, the node may have been reset or compromised. +Every node on firmware 2.8 or newer signs its broadcasts, so on that firmware the signed state is the baseline and the locks are not shown. -To clear a mismatch, first confirm through another trusted channel that the key change was intentional — a factory reset causes one. Then touch & hold the node, choose **Remove**, and let the two radios exchange keys again the next time yours hears it. +Direct messages always use public-key encryption, so your node needs the other node's public key before it can send one. It refuses the send rather than falling back to channel encryption. Keys arrive inside node info, which is why an open lock usually clears itself once that node is heard from properly. + +A mismatch never replaces the key you already hold. The app keeps the first key it recorded and refuses the new one, as the firmware does, so a stray or hostile node info cannot silently break encrypted messaging to a contact. To clear a mismatch, first confirm through another trusted channel that the key change was intentional — a factory reset causes one. Then touch & hold the node, choose **Remove**, and let the two nodes exchange keys again the next time yours hears it. ## Quick Actions @@ -108,7 +112,9 @@ radio is connected and running firmware 2.8 or newer — see ## Sharing a Contact -On a node's detail screen, tap **Share Contact** to produce a link and a QR code for that node. From the same dialog, **Write to NFC tag** saves the link to a writable NFC tag that anyone can tap to open. +On a node's detail screen, tap **Share Contact** to produce a link and a QR code for that node. From the same dialog, **Share link** opens the Android share sheet (on desktop it copies the link instead), **Write to NFC tag** saves it to a writable NFC tag, and **Copy** puts it on the clipboard. While that dialog is open and in front of you, the phone also offers the same link to any NFC reader, so someone can take the contact by tapping their phone against yours with no tag involved. + +Sharing your own contact this way marks it as verified in person, so whoever imports it sees the **Verified contact** icon rather than the signed one. Relaying someone else's contact passes on only what your app had already recorded about them. To add someone else's contact, use the import button on the node list and choose **Scan Shared Contact QR Code**, **Scan Shared Contact NFC**, or **Input Shared Contact URL**. The app asks you to confirm with **Import Shared Contact?**, and warns you when the contact is one you already have. @@ -120,14 +126,16 @@ Type in the search field to filter nodes by name or short name. The filter updat ### Filter Toggles -| Filtras | Aprašymas | -| --------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| **Hide offline nodes** | Show only nodes heard within the last 2 hours | -| **Only show direct nodes** | Show only nodes your radio heard directly, with no relay in between | -| **Include unknown** | Show nodes that haven't sent user info yet. **On by default**, so a node heard before its info arrives stays visible; these carry a badge marking them incomplete, and cannot be direct messaged until their user info brings a public key | -| **Exclude infrastructure** | Hide infrastructure-role nodes (Router, Router Late, Client Base, and legacy Repeater nodes) and any node that cannot be messaged, whatever its role | -| **Exclude MQTT** | Hide nodes heard only via MQTT internet bridge | -| **Only show ignored Nodes** | Replace the list with the nodes you have ignored. Every other node is hidden while this is on, and a banner appears at the top of the list to take you back | +| Filtras | Aprašymas | +| --------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **Hide offline nodes** | Show only nodes heard within the last 2 hours | +| **Only show direct nodes** | Show only nodes your radio heard directly, with no relay in between | +| **Include unknown** | Show nodes that haven't sent user info yet. **On by default**, so a node heard before its info arrives stays visible; these carry a badge marking them incomplete, and cannot be direct messaged until their user info brings a public key | +| **Exclude infrastructure** | Hide infrastructure-role nodes (Router, Router Late, Client Base, and legacy Repeater nodes) and any node that cannot be messaged, whatever its role | +| **Exclude MQTT** | Hide nodes heard only via MQTT internet bridge | +| **Signed only** | Show only nodes whose signed broadcasts your node has actually heard and verified. Stricter than the icon: a node on 2.8 shows as signed by its firmware version before any signed broadcast arrives, and a contact verified in person on older firmware is not signed at all | +| **Encrypted only** | Show nodes with a matching public key on file, the key an encrypted direct message needs. A node with a key mismatch is excluded | +| **Only show ignored Nodes** | Replace the list with the nodes you have ignored. Every other node is hidden while this is on, and a banner appears at the top of the list to take you back | ### Sort Options @@ -149,6 +157,8 @@ Tap the hop-histogram icon in the node list's app bar to open a bar chart of how Tapping a node opens the detail view with comprehensive information. See [Node Metrics](node-metrics) for full details on metrics and telemetry. +Signal quality is rated against your modem preset. The same SNR can be good on a long-range preset and poor on a faster one. Traceroute and neighbor-info SNR colors use that preset too. RSSI text has its own strength colors; it affects the quality rating only when a noise-floor reading is also available. + The Details card carries the node's short name, role, IDs, last heard time, hops away, uptime, and its SNR and RSSI: ![Node detail section](../../assets/screenshots/nodes_detail_section.png) diff --git a/docs/lt-rLT/user/settings-module-admin.md b/docs/lt-rLT/user/settings-module-admin.md index 2ab5fa3925..ca3024b619 100644 --- a/docs/lt-rLT/user/settings-module-admin.md +++ b/docs/lt-rLT/user/settings-module-admin.md @@ -2,7 +2,7 @@ title: Settings — Modules & Admin parent: User Guide nav_order: 8 -last_updated: 2026-08-30 +last_updated: 2026-09-16 description: Configure optional feature modules (MQTT, telemetry, canned messages, TAK, and more) and perform device administration. aliases: - modules @@ -333,7 +333,7 @@ appear only when your own node is selected: ### Apie -**Settings → About** carries the app's own identity rather than the radio's: +**Settings → About** carries the app's own identity rather than the node's: Three sections: @@ -341,7 +341,11 @@ Three sections: - **Apps** — opens with **Need Hardware?**, a rotating carousel of popular devices that links out to where to buy one. It also lists the GitHub repository, the running app version, and **Acknowledgements** (see the next section). -- **Project information** — links to the website and to this documentation. +- **Project information** — links to the website, to this documentation, and to the GNU GPL v3 + license the app is released under. + +The footer states the copyright and that the app is free software with no warranty, which you +may redistribute under the same license. ### Acknowledgements diff --git a/docs/lt-rLT/user/settings-radio-user.md b/docs/lt-rLT/user/settings-radio-user.md index f8613ebc18..bfe637382c 100644 --- a/docs/lt-rLT/user/settings-radio-user.md +++ b/docs/lt-rLT/user/settings-radio-user.md @@ -2,7 +2,7 @@ title: Settings — Radio & User parent: User Guide nav_order: 7 -last_updated: 2026-09-04 +last_updated: 2026-09-11 description: Configure your radio hardware, LoRa presets, user profile, position sharing, power management, and security. aliases: - settings @@ -51,7 +51,7 @@ The footer appears as soon as you change something. **Discard** throws the chang The status message is saved with the same **Save**, but it never reboots the node — and, like the rest of this screen, it can be edited on a remote node you administer. For your own radio there is a shortcut while it is connected: touch & hold your node in the [node list](nodes.md) and choose -**Update status**. Older firmware and a disconnected radio have no shortcut — the field above is +**Update status**. Older firmware and a disconnected node have no shortcut — the field itself is still the way in. ## Configuration @@ -77,13 +77,13 @@ On **Settings → LoRa**. | Setting | Aprašymas | Default | | ------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------- | -| Regionas | Regulatory region for frequency bands. You must set this before transmitting | Unset (must configure) | +| Regionas | Regulatory region for frequency bands. You must set this before transmitting. On firmware 2.8 or newer, the first region set also creates the node's identity key and gives it a new node number | Unset (must configure) | | Presets | Speed/range tradeoff | LongFast | | Number of Hops | Maximum retransmit hops | 3 | | Transmit Power | Transmission power (dBm); 0 = max allowed for region | 0 (region max) | | Frequency Override | Overrides the computed operating frequency outright (MHz). It does not offset the calculated value — leave at 0 unless you know you need a specific frequency | 0 (use calculated) | | Use Preset | On by default. Turn it off to set Spread Factor, Coding Rate and Bandwidth by hand instead of taking them from the modem preset | On | -| Spread Factor | Manual mode only: 7–12. Higher spreads further but slower | From preset | +| Spread Factor | Manual mode only: 5–12. Higher spreads further but slower. On SX127x (RF95) radios the firmware does not accept 5 or 6 and uses 11 instead | From preset | | Coding Rate | Manual mode only: 5–8. More redundancy costs airtime | From preset | | Bandwidth | Manual mode only: the channel bandwidth in kHz, typed in directly. On the 2.4 GHz region the app offers a list of the bandwidths your radio supports instead, and a stored value that is not on that list shows as _Unsupported_ and blocks saving until you pick a supported one | From preset | | Frequency Slot | Which slot within the region's band to use. 0 derives it from the primary channel name | 0 (automatic) | @@ -111,13 +111,13 @@ The Lite, Narrow, Medium Turbo, and Tiny presets need firmware 2.8 or newer — | Short Slow | ~5 km | 6.25 kbps | −10 dB | Suburban short-range; moderate building density | | Medium Fast | ~5 km | 3.52 kbps | −12.5 dB | Suburban areas; moderate building density | | Medium Slow | ~8 km | 1.95 kbps | −15 dB | Suburban/rural; moderate range with slower speed | -| Long Turbo | ~10 km | 1.34 kbps | −12.5 dB | Similar range to Long Fast but with 500 kHz bandwidth; faster throughput | +| Long Turbo | ~10 km | 1.34 kbps | −17.5 dB | Similar range to Long Fast but with 500 kHz bandwidth; faster throughput | | Long Fast | ~10 km | 1.1 kbps | −17.5 dB | **General use (default)** — balanced range and speed | | Long Moderate | ~20 km | 0.34 kbps | −17.5 dB | Rural with some terrain; occasional use | | Lite Fast | ~5 km | 1.76 kbps | −12.5 dB | EU 866 MHz SRD band (125 kHz BW); comparable to Medium Fast | | Lite Slow | ~10 km | 0.98 kbps | −15 dB | EU 866 MHz SRD band (125 kHz BW); comparable to Long Fast | -| Narrow Fast | ~5 km | 2.28 kbps | −10 dB | EU 868 MHz band (62.5 kHz BW); avoids interference with other devices | -| Narrow Slow | ~10 km | 1.30 kbps | −12.5 dB | EU 868 MHz band (62.5 kHz BW); comparable to Long Fast | +| Narrow Fast | ~5 km | 2.28 kbps | −7.5 dB | EU 868 MHz band (62.5 kHz BW); avoids interference with other devices | +| Narrow Slow | ~10 km | 1.30 kbps | −10 dB | EU 868 MHz band (62.5 kHz BW); comparable to Long Fast | | Medium Turbo | ~5 km | 7.0 kbps | −12.5 dB | Like Medium Fast but with 500 kHz bandwidth; not legal in every region. Needs firmware 2.8 or newer | | Tiny Fast | ~10 km | 0.68 kbps | −7.5 dB | Amateur bands that cap occupied bandwidth; these presets use 15.6 kHz. Needs firmware 2.8 or newer, an SX126x or SX127x radio, and a TCXO of ±5 ppm or better | | Tiny Slow | ~20 km | 0.33 kbps | −10 dB | Same band restrictions as Tiny Fast, longer range. Same firmware, radio, and TCXO requirements | diff --git a/docs/lt-rLT/user/signal-meter.md b/docs/lt-rLT/user/signal-meter.md index dcaebdb074..6dde721fe7 100644 --- a/docs/lt-rLT/user/signal-meter.md +++ b/docs/lt-rLT/user/signal-meter.md @@ -2,7 +2,7 @@ title: How the Meshtastic Signal Meter Works parent: User Guide nav_order: 15 -last_updated: 2026-08-30 +last_updated: 2026-09-09 description: How the signal meter rates quality from SNR relative to the LoRa modem preset — spread spectrum, presets, and what the bars really mean. aliases: - signal @@ -32,7 +32,7 @@ Each modem preset has an SNR limit: the lowest SNR at which that preset can stil ## Rating Signal Quality -The app rates signal quality (None, Bad, Fair, or Good) from SNR alone, measured against the active preset's SNR limit. It does not factor in RSSI: without knowing the local noise floor, RSSI alone cannot say whether a signal is decodable. RSSI is still available — on the node detail screen and in the metrics charts. +The app rates signal quality (None, Bad, Fair, or Good) from SNR relative to the active preset's SNR limit. When RSSI and a noise-floor reading are both available, it also rates the difference between them against that limit and uses the worse rating. Otherwise, it uses SNR alone. Because the rating is relative to the preset, the same SNR rates differently on different presets. An SNR of −16 dB rates Good on Long Fast (SNR limit −17.5 dB) but None on Short Fast (SNR limit −7.5 dB). Let `limit` be the active preset's SNR limit: @@ -45,7 +45,7 @@ Because the rating is relative to the preset, the same SNR rates differently on The icon never goes blank, so count the bars carefully: a single bar means None, not a weak but usable link, and Good is a solid wedge rather than a set of bars. A gray three-bar icon labeled Unknown is a different state again — the packet carried no SNR measurement at all, which is not the same as measuring one and finding it too weak. -> ℹ️ **Note:** Traceroute hop colors use fixed thresholds (−7 dB / −15 dB); the per-node signal meter uses the preset-relative rating instead. +> ℹ️ **Note:** Traceroute and neighbor-info SNR colors use the active preset's limits too. RSSI text has separate strength colors: green above −115 dBm, yellow above −120 dBm, orange above −126 dBm, and red at or below −126 dBm. ## Diagnosing Local Interference diff --git a/docs/lt-rLT/user/tak.md b/docs/lt-rLT/user/tak.md index a0cefce5c9..ddffe9e8e1 100644 --- a/docs/lt-rLT/user/tak.md +++ b/docs/lt-rLT/user/tak.md @@ -2,7 +2,7 @@ title: TAK Integration parent: User Guide nav_order: 10 -last_updated: 2026-08-30 +last_updated: 2026-09-11 description: Interoperate with ATAK and WinTAK — CoT position sharing, TAK roles, and plugin setup. aliases: - tak @@ -36,7 +36,7 @@ The TAK module allows Meshtastic nodes to: > ⚠️ **Warning:** The old **Meshtastic ATAK Plugin** is no longer part of this path and cannot > work. It bridged through the cross-process AIDL API, which was removed in app 2.8.0; the mesh > service is now in-process only. Do not install it. Interop today runs over the app's own local -> TAK server plus the Mesh to CoT Converter, both described below, with stock ATAK/iTAK/WinTAK. +> TAK server plus the Mesh to CoT Converter, both described in the following sections, with stock ATAK/iTAK/WinTAK. ### Configuration diff --git a/docs/lt-rLT/user/translate.md b/docs/lt-rLT/user/translate.md index 67d69e2d96..cdc8b6d87f 100644 --- a/docs/lt-rLT/user/translate.md +++ b/docs/lt-rLT/user/translate.md @@ -2,7 +2,7 @@ title: Translate the App parent: User Guide nav_order: 17 -last_updated: 2026-08-30 +last_updated: 2026-09-11 description: How the app and its documentation are translated via Crowdin, and guidelines for contributing translations. aliases: - translate @@ -45,7 +45,7 @@ If your language is not yet listed on Crowdin: The Android app uses **Compose Multiplatform resources** for all user-visible strings: -``` +```text core/resources/src/commonMain/composeResources/ ├── values/ ← English (default) │ └── strings.xml @@ -58,7 +58,7 @@ core/resources/src/commonMain/composeResources/ In-app documentation follows a similar pattern under `docs/`: -``` +```text docs/ ├── en/user/ ← English source (default) │ ├── onboarding.md diff --git a/docs/nl-rNL/user/desktop.md b/docs/nl-rNL/user/desktop.md index 22214d4294..4e5c65b18b 100644 --- a/docs/nl-rNL/user/desktop.md +++ b/docs/nl-rNL/user/desktop.md @@ -2,7 +2,7 @@ title: Desktop App parent: User Guide nav_order: 14 -last_updated: 2026-08-30 +last_updated: 2026-09-11 description: Install and use the Meshtastic Desktop app on Linux, macOS, and Windows — connections, feature parity, and keyboard shortcuts. aliases: - desktop @@ -58,20 +58,20 @@ Bluetooth Low Energy is supported on desktop via the [Kable](https://github.com/ ## Feature Parity -| Feature | Android | Desktop | Notes | -| ----------------------------------------------------- | ------- | ------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Messaging | ✓ | ✓ | Full parity | -| Node List | ✓ | ✓ | Full parity | -| Kaart | ✓ | ✓ | Interactive MapLibre map, with base map and overlay pickers and custom tile sources. No offline downloads or local `.mbtiles` archives | -| Map layers (`.kml`/`.kmz`/GeoJSON) | ✓ | ✓ | Same layer store and sheet as Android; imported files draw on the desktop map | -| Site Planner | ✓ | ✓\* | \*Opens in your browser on desktop; the estimate is not drawn on the desktop map | -| Instellingen | ✓ | ✓ | Full parity | -| Bluetooth (BLE) | ✓ | ✓ | Via Kable on desktop | -| Firmware Update | ✓ | ✓ | In-app USB, BLE, and Wi-Fi (ESP32) update work the same as Android. The USB maintenance flow — nRF52/RP2040 factory erase and bootloader upgrade — is Android-only | -| Notifications | ✓ | ✓ | Native OS notifications | -| Widgets | ✓ | ✗ | Android-only | -| AI Assistant (Chirpy) | ✓\* | ✗ | Google flavor Android only | -| App Functions (system AI) | ✓† | ✗ | Google flavor Android only | +| Feature | Android | Desktop | Notes | +| ----------------------------------------------------- | ------- | ------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Messaging | ✓ | ✓ | Full parity | +| Node List | ✓ | ✓ | Full parity | +| Kaart | ✓ | ✓ | Interactive MapLibre map, with base map and overlay pickers and custom tile sources. Offline terrain (hillshade and contours) can be downloaded; offline base-map packs and local `.mbtiles` archives cannot | +| Map layers (`.kml`/`.kmz`/GeoJSON) | ✓ | ✓ | Same layer store and sheet as Android; imported files draw on the desktop map | +| Site Planner | ✓ | ✓\* | \*Opens in your browser on desktop; the estimate is not drawn on the desktop map | +| Instellingen | ✓ | ✓ | Full parity | +| Bluetooth (BLE) | ✓ | ✓ | Via Kable on desktop | +| Firmware Update | ✓ | ✓ | In-app USB, BLE, and Wi-Fi (ESP32) update work the same as Android. The USB maintenance flow — nRF52/RP2040 factory erase and bootloader upgrade — is Android-only | +| Notifications | ✓ | ✓ | Native OS notifications | +| Widgets | ✓ | ✗ | Android-only | +| AI Assistant (Chirpy) | ✓\* | ✗ | Google flavor Android only | +| App Functions (system AI) | ✓† | ✗ | Google flavor Android only | \*Chirpy AI requires Android 14+ on Google flavor builds with supported hardware. @@ -122,7 +122,7 @@ Individual doc pages render with full formatting: ## Building from Source -```bash +```shell git clone https://github.com/meshtastic/Meshtastic-Android.git cd Meshtastic-Android ./gradlew :desktopApp:run @@ -135,7 +135,7 @@ Requirements: ## Known Limitations -- Offline tile downloads and local `.mbtiles` archives are not available on desktop. +- Offline base-map downloads and local `.mbtiles` archives are not available on desktop. Offline terrain is — see [Map & Waypoints](map-and-waypoints). - `.kml`/`.kmz`/GeoJSON layer import works — see [Map & Waypoints](map-and-waypoints#map-layers). Site Planner opens in your browser rather than in the app; to bring its coverage estimate onto the map, click the transmitter pin diff --git a/docs/nl-rNL/user/discovery.md b/docs/nl-rNL/user/discovery.md index 9501b11bc7..7de0058310 100644 --- a/docs/nl-rNL/user/discovery.md +++ b/docs/nl-rNL/user/discovery.md @@ -2,7 +2,7 @@ title: Local Mesh Discovery parent: User Guide nav_order: 12 -last_updated: 2026-08-30 +last_updated: 2026-09-11 description: Explore your mesh network — the Local Mesh Discovery scanner, traceroute paths, neighbor maps, and node discovery tools. aliases: - discovery @@ -97,7 +97,7 @@ Mesh Beacon lets nodes invite others to join their mesh. A beaconing node period Configure it under **Settings → Module configuration → Mesh Beacon**. The entry appears only on radios running firmware 2.8.0 or newer. A read-only **Region** row at the top of the screen shows the region the beacon advertises: that region, and the preset, are always the ones the radio itself uses, so a beacon cannot invite anyone onto settings your radio is not running. - **Listen for beacons** — receive invitations broadcast by other nodes. -- **Broadcast a beacon** — periodically advertise this mesh to nearby nodes, with an optional **Beacon message** of up to 100 bytes, a **Broadcast interval** picked from fixed intervals between 1 hour and 72 hours, and an **Offered channel** chosen from your radio's own channels. The offered channel is required, and defaults to your primary channel. +- **Broadcast a beacon** — periodically advertise this mesh to nearby nodes, with an optional **Beacon message** of up to 100 bytes, a **Broadcast interval** picked from fixed intervals between 1 hour and 72 hours, and an **Offered channel** chosen from your radio's own channels. The offered channel is required, and defaults to your primary channel. Over remote admin the picker offers the primary channel only. - **Broadcast targets** — optional extra destinations beyond the offered channel. **Add target** appends a row; each row picks a **Channel** and a **Transmit preset**, and **Remove target** deletes it. With no targets, the beacon goes out on the offered channel alone. Two conditions block beacon setup: diff --git a/docs/nl-rNL/user/help-and-docs.md b/docs/nl-rNL/user/help-and-docs.md index f306cde7f7..c83898cea3 100644 --- a/docs/nl-rNL/user/help-and-docs.md +++ b/docs/nl-rNL/user/help-and-docs.md @@ -2,7 +2,7 @@ title: Help & In-App Docs parent: User Guide nav_order: 21 -last_updated: 2026-08-30 +last_updated: 2026-09-11 description: Browse this documentation inside the app, search it, and ask Chirpy — the on-device AI assistant — questions about Meshtastic. aliases: - help @@ -23,7 +23,7 @@ The docs browser lists every user-guide page. Tap a page to read it; images and ### Search -The search box sits at the top of the docs browser. Type in it to filter pages by title and keywords — results update as you type, and the **Clear search** button (✕) empties the box. +Tap the search field at the top of the docs browser to open a full-screen search view. Type to filter pages by title and keywords — results update as you type, under the same field. The back arrow, or the keyboard's search action, closes the view. The field hides while you scroll down the page list and comes back when you scroll up. ![Searching the in-app documentation](../../assets/screenshots/docs-browser_search.png) diff --git a/docs/nl-rNL/user/map-and-waypoints.md b/docs/nl-rNL/user/map-and-waypoints.md index 0cf6dc892d..ee915c1dcf 100644 --- a/docs/nl-rNL/user/map-and-waypoints.md +++ b/docs/nl-rNL/user/map-and-waypoints.md @@ -2,7 +2,7 @@ title: Map & Waypoints parent: User Guide nav_order: 6 -last_updated: 2026-09-01 +last_updated: 2026-09-11 description: View node positions on the map, create and share waypoints, manage map layers and Site Planner, and control position sharing and privacy. aliases: - map @@ -44,7 +44,7 @@ The floating toolbar provides quick access to the compass, the map type and laye ### Filtering the Map -Tap the filter button in the floating toolbar to open **Filter map**. **Display** controls what is drawn: **Only Favorites**, **Show Waypoints**, **Show Precision Circles**, and a slider that hides nodes not heard from recently. **Node roles** is a chip per device role, plus **All** to show every role; a selected chip means that role is shown. **Nodes** narrows the set further with **Hide offline nodes**, **Only show direct nodes**, **Exclude MQTT**, **Show ignored nodes**, and **Include unknown**. +Tap the filter button in the floating toolbar to open **Filter map**. **Display** controls what is drawn: **Only Favorites**, **Show Waypoints**, **Show Precision Circles**, and a slider that hides nodes not heard from recently. **Node roles** is a chip per device role, plus **All** to show every role; a selected chip means that role is shown. **Nodes** narrows the set further with **Hide offline nodes**, **Only show direct nodes**, **Signed only**, **Encrypted only**, **Exclude MQTT**, **Show ignored nodes**, and **Include unknown** — the same rules as the node list's [filter toggles](nodes). A dot on the filter button means at least one filter is hiding something — check it before concluding the mesh is quiet. Turning **Show Waypoints** off hides every waypoint, including your own. **Show ignored nodes** adds them to the map rather than showing only them — unlike the node list's **Only show ignored Nodes**. @@ -147,8 +147,9 @@ picker, all three offer the same raster base maps: Overlays can be toggled on top of any base map, from the layers sheet: - **Weather radar** — NOAA NEXRAD reflectivity (US coverage) -- **Hillshade** — terrain relief, on **F-Droid** and **Desktop** only. Useful for understanding why a - link fails, since LoRa range is limited by terrain +- **Hillshade** — terrain relief from an online tile source, on **F-Droid** and **Desktop** only; Google + Play draws hillshade from downloaded offline terrain instead. Useful for understanding why a link + fails, since LoRa range is limited by terrain ### Adding your own tile source @@ -156,7 +157,7 @@ Any XYZ tile endpoint can be added as a base map, on every flavor and on desktop Tile Sources** at the foot of the base map picker and paste a URL template using `{z}`, `{x}` and `{y}` — plus `{s}` if the provider uses rotating subdomains. A national mapping service, for example: -``` +```text https://wmts.geo.admin.ch/1.0.0/ch.swisstopo.pixelkarte-farbe/default/current/3857/{z}/{x}/{y}.jpeg ``` @@ -173,7 +174,18 @@ On **Google Play**, the layers sheet's **Offline Manager** section downloads its — water, roads and administrative boundaries extracted from the public Protomaps basemap dataset, drawn directly as map shapes rather than raster tiles. Frame the area you want, tap **Start Download**, and once it finishes flip **Show on map** to draw it (it's off by default so it never surprises you by covering up the -live map). You can still import a pre-made `.mbtiles` archive here too, as before. **Desktop** has neither. +live map). You can still import a pre-made `.mbtiles` archive here too, as before. **Desktop** has neither, though +the offline terrain described next does work there. + +### Offline terrain + +Hillshade and elevation contours can be downloaded for offline use as well, so a downloaded area still +shows why a link fails when there is no network. The terrain data comes from Mapterhorn and is credited on +the map. On **Google Play**, each downloaded region in the **Offline Manager** list has a **Download Terrain** +button and **Hillshade** and **Contours** switches; terrain attaches to that region and shares its storage limit, and an area +too large for terrain asks you to zoom in or pick a smaller region. On **F-Droid** and **Desktop**, the +layers sheet has an **Offline Terrain** section of its own: frame the area, tap **Start Download**, and the +hillshade and contours draw from the downloaded tiles from then on. ### Going offline @@ -181,8 +193,8 @@ An **Offline** pill appears over the map whenever the device has no network conn builds, if you've imported a local `.mbtiles` archive, the map switches to it automatically the moment the network drops — no toggle to remember — and switches back once you're reconnected, as long as you haven't picked a different base map yourself in between. On **F-Droid**, a downloaded offline area keeps rendering on -its own; the pill is purely informational there. **Desktop** has no offline downloads at all yet, so the pill -is informational there too. +its own; the pill is purely informational there. **Desktop** downloads offline terrain but no base-map +packs, so the pill is informational there too. ## Related Topics diff --git a/docs/nl-rNL/user/messages-and-channels.md b/docs/nl-rNL/user/messages-and-channels.md index af197f01cd..8ef5dbcdf3 100644 --- a/docs/nl-rNL/user/messages-and-channels.md +++ b/docs/nl-rNL/user/messages-and-channels.md @@ -2,7 +2,7 @@ title: Messages & Channels parent: User Guide nav_order: 3 -last_updated: 2026-08-30 +last_updated: 2026-09-14 description: Send and receive messages, manage channels, configure encryption, search conversations, and use quick chat, reactions, and message actions. aliases: - channels @@ -50,6 +50,21 @@ Key length alone does not change the icon: a 128-bit key and a 256-bit key both Tapping an existing channel opens the same editor, where you can change the name, the PSK, MQTT uplink and downlink, and position precision. Every edit on this screen — adding, editing, deleting, or dragging a channel into a new order — waits on **Send** the same way. +### Archived Channels + +A conversation belongs to the channel it was held on, not to the slot that channel occupied. When a channel leaves your radio — you scan a different channel QR, replace a channel's key, or switch the modem preset — its conversation is **archived** rather than deleted or merged into whatever takes the slot. + +An archived conversation: + +- stays in the **Channels** list under the name the channel had, below the channels currently on your radio, marked with a history icon; +- keeps its full message history, searchable as usual; +- is read-only — you cannot send, reply, or react, because the channel is no longer on the radio to send on, and it is not offered as a share target; +- comes back automatically if you re-add that exact channel, taking its messages back to the live slot. + +Changing the modem preset archives the channel too: the preset is part of a default channel's identity, so **LongFast** and **MediumFast** are different channels even though neither the name nor the key changed. Changing only the region does not archive anything. + +To remove an archived conversation for good, long-press it in the conversation list and delete it. + ## Direct Messages Direct messages (DMs) go to one specific node. When both radios hold each other's public keys, your radio encrypts the message to that node's public key, so no one else on the mesh can read it — not even nodes that share your channel. diff --git a/docs/nl-rNL/user/mqtt.md b/docs/nl-rNL/user/mqtt.md index cca6277f0e..cff6d07649 100644 --- a/docs/nl-rNL/user/mqtt.md +++ b/docs/nl-rNL/user/mqtt.md @@ -2,7 +2,7 @@ title: MQTT parent: User Guide nav_order: 11 -last_updated: 2026-08-30 +last_updated: 2026-09-11 description: Bridge your mesh to the internet — MQTT broker setup, encryption layers, and map reporting. aliases: - mqtt @@ -25,7 +25,7 @@ The MQTT module connects your node to an MQTT broker, allowing: ## How It Works -``` +```text [Your Node] → Radio → [Gateway Node with Wi-Fi] → MQTT Broker → [Remote Gateway] → Radio → [Remote Node] ``` diff --git a/docs/nl-rNL/user/node-metrics.md b/docs/nl-rNL/user/node-metrics.md index 850cba61e7..42f01ff526 100644 --- a/docs/nl-rNL/user/node-metrics.md +++ b/docs/nl-rNL/user/node-metrics.md @@ -2,7 +2,7 @@ title: Node Metrics parent: User Guide nav_order: 5 -last_updated: 2026-08-30 +last_updated: 2026-09-09 description: Telemetry dashboards for each mesh node — device health, environment sensors, air quality, signal quality, power, traceroute, and position history. aliases: - metrics @@ -114,7 +114,7 @@ Radio signal quality information: ### Signal Quality Reference -Signal quality is rated from **SNR relative to the active LoRa modem preset's demodulation floor**, not from fixed thresholds — a given SNR means different things on different presets (e.g. −15 dB is fine on LongSlow but unusable on ShortFast). RSSI is shown but is not part of the rating. In the table, _limit_ is the preset's SNR limit. +Signal quality is rated from **SNR relative to the active LoRa modem preset's demodulation floor**, not from fixed thresholds — a given SNR means different things on different presets (e.g. −15 dB is fine on LongSlow but unusable on ShortFast). When RSSI and a noise-floor reading are both available, the app also rates their difference against the preset limit and uses the worse rating. Otherwise, RSSI is display-only. In the table, _limit_ is the preset's SNR limit. | Quality | Criteria | | ------- | ---------------------------------------------------------------- | diff --git a/docs/nl-rNL/user/nodes.md b/docs/nl-rNL/user/nodes.md index 12ab164026..be088a9ade 100644 --- a/docs/nl-rNL/user/nodes.md +++ b/docs/nl-rNL/user/nodes.md @@ -2,7 +2,7 @@ title: Nodes parent: User Guide nav_order: 4 -last_updated: 2026-09-04 +last_updated: 2026-09-11 description: Browse, filter, and sort mesh nodes — view details, signal quality, roles, and quick actions. aliases: - node-list @@ -74,19 +74,23 @@ Most users should keep the default **Client** role. Consider a different role wh > 💡 **Tip:** The mesh works best when most nodes are **Client** or **Router**. Too many Client Mute nodes reduce mesh resilience; too many Routers in a dense area can cause congestion. A good rule of thumb: one Router per 5–10 Clients in your area. -### Encryption Indicators +### Security indicators -Nodes display encryption status icons next to their name: +Each node carries one security icon beside its name in the node list. Tap it to read what it means, and choose **Show All Meanings** in that dialog for the full legend. The detail screen shows the same state in words, as a **Security** row that opens the same dialog. -| Icon | Meaning | -| ----------- | ------------------------------------------------------------------------------------------------------------------------------------------------- | -| 🔒 Locked | Communication uses PKI (public key infrastructure) — end-to-end encrypted with verified identity | -| 🔓 Unlocked | No public key has been received for this node, so it cannot be direct messaged — use **Request User Info** on the node detail page to ask for one | -| ⚠️ Mismatch | Public key mismatch — the node's key has changed since last seen (investigate before trusting) | +| Icon | Meaning | Shown for | +| --------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------- | +| Person with a shield check (green) | **Verified contact** — you verified this node's key in person by exchanging contact QR codes, so its identity is confirmed. The strongest trust the list shows. Your own connected node carries it too | Any firmware version | +| Nodes icon with a shield check (green) | **Signed node** — this node signs its broadcasts with its identity key, so its identity is consistent over time, but you have not verified it in person. On firmware 2.8 the icon appears from the version alone, before any signed broadcast has been heard | Firmware 2.8 or newer, and any node whose signed broadcast your node has verified | +| 🔒 Closed lock | A public key is on file and matches, so direct messages to this node are encrypted | Firmware before 2.8, or no reported version | +| 🔓 Open lock | No public key has been received for this node, so it cannot be direct messaged — use **Request User Info** on the node detail page to ask for one | Firmware before 2.8, or no reported version | +| ⚠️ Mismatch | **Public key mismatch** — a different key arrived for this node after one was stored. Investigate before trusting | Any firmware version | -> 💡 **Tip:** Direct messages always use PKI, so the radio needs the other node's public key before it can send one. It refuses the send rather than falling back to channel encryption. Keys arrive inside node info, which is why an open lock usually clears itself once that node is heard from properly. If you see a key mismatch warning, the node may have been reset or compromised. +Every node on firmware 2.8 or newer signs its broadcasts, so on that firmware the signed state is the baseline and the locks are not shown. -To clear a mismatch, first confirm through another trusted channel that the key change was intentional — a factory reset causes one. Then touch & hold the node, choose **Remove**, and let the two radios exchange keys again the next time yours hears it. +Direct messages always use public-key encryption, so your node needs the other node's public key before it can send one. It refuses the send rather than falling back to channel encryption. Keys arrive inside node info, which is why an open lock usually clears itself once that node is heard from properly. + +A mismatch never replaces the key you already hold. The app keeps the first key it recorded and refuses the new one, as the firmware does, so a stray or hostile node info cannot silently break encrypted messaging to a contact. To clear a mismatch, first confirm through another trusted channel that the key change was intentional — a factory reset causes one. Then touch & hold the node, choose **Remove**, and let the two nodes exchange keys again the next time yours hears it. ## Quick Actions @@ -108,7 +112,9 @@ radio is connected and running firmware 2.8 or newer — see ## Sharing a Contact -On a node's detail screen, tap **Share Contact** to produce a link and a QR code for that node. From the same dialog, **Write to NFC tag** saves the link to a writable NFC tag that anyone can tap to open. +On a node's detail screen, tap **Share Contact** to produce a link and a QR code for that node. From the same dialog, **Share link** opens the Android share sheet (on desktop it copies the link instead), **Write to NFC tag** saves it to a writable NFC tag, and **Copy** puts it on the clipboard. While that dialog is open and in front of you, the phone also offers the same link to any NFC reader, so someone can take the contact by tapping their phone against yours with no tag involved. + +Sharing your own contact this way marks it as verified in person, so whoever imports it sees the **Verified contact** icon rather than the signed one. Relaying someone else's contact passes on only what your app had already recorded about them. To add someone else's contact, use the import button on the node list and choose **Scan Shared Contact QR Code**, **Scan Shared Contact NFC**, or **Input Shared Contact URL**. The app asks you to confirm with **Import Shared Contact?**, and warns you when the contact is one you already have. @@ -120,14 +126,16 @@ Type in the search field to filter nodes by name or short name. The filter updat ### Filter Toggles -| Filter | Beschrijving | -| --------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| **Hide offline nodes** | Show only nodes heard within the last 2 hours | -| **Only show direct nodes** | Show only nodes your radio heard directly, with no relay in between | -| **Include unknown** | Show nodes that haven't sent user info yet. **On by default**, so a node heard before its info arrives stays visible; these carry a badge marking them incomplete, and cannot be direct messaged until their user info brings a public key | -| **Exclude infrastructure** | Hide infrastructure-role nodes (Router, Router Late, Client Base, and legacy Repeater nodes) and any node that cannot be messaged, whatever its role | -| **Exclude MQTT** | Hide nodes heard only via MQTT internet bridge | -| **Only show ignored Nodes** | Replace the list with the nodes you have ignored. Every other node is hidden while this is on, and a banner appears at the top of the list to take you back | +| Filter | Beschrijving | +| --------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **Hide offline nodes** | Show only nodes heard within the last 2 hours | +| **Only show direct nodes** | Show only nodes your radio heard directly, with no relay in between | +| **Include unknown** | Show nodes that haven't sent user info yet. **On by default**, so a node heard before its info arrives stays visible; these carry a badge marking them incomplete, and cannot be direct messaged until their user info brings a public key | +| **Exclude infrastructure** | Hide infrastructure-role nodes (Router, Router Late, Client Base, and legacy Repeater nodes) and any node that cannot be messaged, whatever its role | +| **Exclude MQTT** | Hide nodes heard only via MQTT internet bridge | +| **Signed only** | Show only nodes whose signed broadcasts your node has actually heard and verified. Stricter than the icon: a node on 2.8 shows as signed by its firmware version before any signed broadcast arrives, and a contact verified in person on older firmware is not signed at all | +| **Encrypted only** | Show nodes with a matching public key on file, the key an encrypted direct message needs. A node with a key mismatch is excluded | +| **Only show ignored Nodes** | Replace the list with the nodes you have ignored. Every other node is hidden while this is on, and a banner appears at the top of the list to take you back | ### Sort Options @@ -149,6 +157,8 @@ Tap the hop-histogram icon in the node list's app bar to open a bar chart of how Tapping a node opens the detail view with comprehensive information. See [Node Metrics](node-metrics) for full details on metrics and telemetry. +Signal quality is rated against your modem preset. The same SNR can be good on a long-range preset and poor on a faster one. Traceroute and neighbor-info SNR colors use that preset too. RSSI text has its own strength colors; it affects the quality rating only when a noise-floor reading is also available. + The Details card carries the node's short name, role, IDs, last heard time, hops away, uptime, and its SNR and RSSI: ![Node detail section](../../assets/screenshots/nodes_detail_section.png) diff --git a/docs/nl-rNL/user/settings-module-admin.md b/docs/nl-rNL/user/settings-module-admin.md index 954611be91..41b6b478c7 100644 --- a/docs/nl-rNL/user/settings-module-admin.md +++ b/docs/nl-rNL/user/settings-module-admin.md @@ -2,7 +2,7 @@ title: Settings — Modules & Admin parent: User Guide nav_order: 8 -last_updated: 2026-08-30 +last_updated: 2026-09-16 description: Configure optional feature modules (MQTT, telemetry, canned messages, TAK, and more) and perform device administration. aliases: - modules @@ -333,7 +333,7 @@ appear only when your own node is selected: ### Over -**Settings → About** carries the app's own identity rather than the radio's: +**Settings → About** carries the app's own identity rather than the node's: Three sections: @@ -341,7 +341,11 @@ Three sections: - **Apps** — opens with **Need Hardware?**, a rotating carousel of popular devices that links out to where to buy one. It also lists the GitHub repository, the running app version, and **Acknowledgements** (see the next section). -- **Project information** — links to the website and to this documentation. +- **Project information** — links to the website, to this documentation, and to the GNU GPL v3 + license the app is released under. + +The footer states the copyright and that the app is free software with no warranty, which you +may redistribute under the same license. ### Acknowledgements diff --git a/docs/nl-rNL/user/settings-radio-user.md b/docs/nl-rNL/user/settings-radio-user.md index 0e7b778d30..ca2a8aa1d1 100644 --- a/docs/nl-rNL/user/settings-radio-user.md +++ b/docs/nl-rNL/user/settings-radio-user.md @@ -2,7 +2,7 @@ title: Settings — Radio & User parent: User Guide nav_order: 7 -last_updated: 2026-09-04 +last_updated: 2026-09-11 description: Configure your radio hardware, LoRa presets, user profile, position sharing, power management, and security. aliases: - instellingen @@ -51,7 +51,7 @@ The footer appears as soon as you change something. **Discard** throws the chang The status message is saved with the same **Save**, but it never reboots the node — and, like the rest of this screen, it can be edited on a remote node you administer. For your own radio there is a shortcut while it is connected: touch & hold your node in the [node list](nodes.md) and choose -**Update status**. Older firmware and a disconnected radio have no shortcut — the field above is +**Update status**. Older firmware and a disconnected node have no shortcut — the field itself is still the way in. ## Configuration @@ -77,13 +77,13 @@ On **Settings → LoRa**. | Setting | Beschrijving | Standaard | | ---------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------- | -| Regio | Regulatory region for frequency bands. You must set this before transmitting | Unset (must configure) | +| Regio | Regulatory region for frequency bands. You must set this before transmitting. On firmware 2.8 or newer, the first region set also creates the node's identity key and gives it a new node number | Unset (must configure) | | Presets | Speed/range tradeoff | LongFast | | Number of Hops | Maximum retransmit hops | 3 | | Transmit Power | Transmission power (dBm); 0 = max allowed for region | 0 (region max) | | Frequency Override | Overrides the computed operating frequency outright (MHz). It does not offset the calculated value — leave at 0 unless you know you need a specific frequency | 0 (use calculated) | | Use Preset | On by default. Turn it off to set Spread Factor, Coding Rate and Bandwidth by hand instead of taking them from the modem preset | On | -| Spread Factor | Manual mode only: 7–12. Higher spreads further but slower | From preset | +| Spread Factor | Manual mode only: 5–12. Higher spreads further but slower. On SX127x (RF95) radios the firmware does not accept 5 or 6 and uses 11 instead | From preset | | Coding Rate | Manual mode only: 5–8. More redundancy costs airtime | From preset | | Bandbreedte | Manual mode only: the channel bandwidth in kHz, typed in directly. On the 2.4 GHz region the app offers a list of the bandwidths your radio supports instead, and a stored value that is not on that list shows as _Unsupported_ and blocks saving until you pick a supported one | From preset | | Frequency Slot | Which slot within the region's band to use. 0 derives it from the primary channel name | 0 (automatic) | @@ -111,13 +111,13 @@ The Lite, Narrow, Medium Turbo, and Tiny presets need firmware 2.8 or newer — | Short Slow | ~5 km | 6.25 kbps | −10 dB | Suburban short-range; moderate building density | | Medium Fast | ~5 km | 3.52 kbps | −12.5 dB | Suburban areas; moderate building density | | Medium Slow | ~8 km | 1.95 kbps | −15 dB | Suburban/rural; moderate range with slower speed | -| Long Turbo | ~10 km | 1.34 kbps | −12.5 dB | Similar range to Long Fast but with 500 kHz bandwidth; faster throughput | +| Long Turbo | ~10 km | 1.34 kbps | −17.5 dB | Similar range to Long Fast but with 500 kHz bandwidth; faster throughput | | Long Fast | ~10 km | 1.1 kbps | −17.5 dB | **General use (default)** — balanced range and speed | | Long Moderate | ~20 km | 0.34 kbps | −17.5 dB | Rural with some terrain; occasional use | | Lite Fast | ~5 km | 1.76 kbps | −12.5 dB | EU 866 MHz SRD band (125 kHz BW); comparable to Medium Fast | | Lite Slow | ~10 km | 0.98 kbps | −15 dB | EU 866 MHz SRD band (125 kHz BW); comparable to Long Fast | -| Narrow Fast | ~5 km | 2.28 kbps | −10 dB | EU 868 MHz band (62.5 kHz BW); avoids interference with other devices | -| Narrow Slow | ~10 km | 1.30 kbps | −12.5 dB | EU 868 MHz band (62.5 kHz BW); comparable to Long Fast | +| Narrow Fast | ~5 km | 2.28 kbps | −7.5 dB | EU 868 MHz band (62.5 kHz BW); avoids interference with other devices | +| Narrow Slow | ~10 km | 1.30 kbps | −10 dB | EU 868 MHz band (62.5 kHz BW); comparable to Long Fast | | Medium Turbo | ~5 km | 7.0 kbps | −12.5 dB | Like Medium Fast but with 500 kHz bandwidth; not legal in every region. Needs firmware 2.8 or newer | | Tiny Fast | ~10 km | 0.68 kbps | −7.5 dB | Amateur bands that cap occupied bandwidth; these presets use 15.6 kHz. Needs firmware 2.8 or newer, an SX126x or SX127x radio, and a TCXO of ±5 ppm or better | | Tiny Slow | ~20 km | 0.33 kbps | −10 dB | Same band restrictions as Tiny Fast, longer range. Same firmware, radio, and TCXO requirements | diff --git a/docs/nl-rNL/user/signal-meter.md b/docs/nl-rNL/user/signal-meter.md index 50595c9d47..a5cddebcea 100644 --- a/docs/nl-rNL/user/signal-meter.md +++ b/docs/nl-rNL/user/signal-meter.md @@ -2,7 +2,7 @@ title: How the Meshtastic Signal Meter Works parent: User Guide nav_order: 15 -last_updated: 2026-08-30 +last_updated: 2026-09-09 description: How the signal meter rates quality from SNR relative to the LoRa modem preset — spread spectrum, presets, and what the bars really mean. aliases: - signal @@ -32,7 +32,7 @@ Each modem preset has an SNR limit: the lowest SNR at which that preset can stil ## Rating Signal Quality -The app rates signal quality (None, Bad, Fair, or Good) from SNR alone, measured against the active preset's SNR limit. It does not factor in RSSI: without knowing the local noise floor, RSSI alone cannot say whether a signal is decodable. RSSI is still available — on the node detail screen and in the metrics charts. +The app rates signal quality (None, Bad, Fair, or Good) from SNR relative to the active preset's SNR limit. When RSSI and a noise-floor reading are both available, it also rates the difference between them against that limit and uses the worse rating. Otherwise, it uses SNR alone. Because the rating is relative to the preset, the same SNR rates differently on different presets. An SNR of −16 dB rates Good on Long Fast (SNR limit −17.5 dB) but None on Short Fast (SNR limit −7.5 dB). Let `limit` be the active preset's SNR limit: @@ -45,7 +45,7 @@ Because the rating is relative to the preset, the same SNR rates differently on The icon never goes blank, so count the bars carefully: a single bar means None, not a weak but usable link, and Good is a solid wedge rather than a set of bars. A gray three-bar icon labeled Unknown is a different state again — the packet carried no SNR measurement at all, which is not the same as measuring one and finding it too weak. -> ℹ️ **Note:** Traceroute hop colors use fixed thresholds (−7 dB / −15 dB); the per-node signal meter uses the preset-relative rating instead. +> ℹ️ **Note:** Traceroute and neighbor-info SNR colors use the active preset's limits too. RSSI text has separate strength colors: green above −115 dBm, yellow above −120 dBm, orange above −126 dBm, and red at or below −126 dBm. ## Diagnosing Local Interference diff --git a/docs/nl-rNL/user/tak.md b/docs/nl-rNL/user/tak.md index 5501ef1a0d..a18d0978d3 100644 --- a/docs/nl-rNL/user/tak.md +++ b/docs/nl-rNL/user/tak.md @@ -2,7 +2,7 @@ title: TAK Integration parent: User Guide nav_order: 10 -last_updated: 2026-08-30 +last_updated: 2026-09-11 description: Interoperate with ATAK and WinTAK — CoT position sharing, TAK roles, and plugin setup. aliases: - tak @@ -36,7 +36,7 @@ The TAK module allows Meshtastic nodes to: > ⚠️ **Warning:** The old **Meshtastic ATAK Plugin** is no longer part of this path and cannot > work. It bridged through the cross-process AIDL API, which was removed in app 2.8.0; the mesh > service is now in-process only. Do not install it. Interop today runs over the app's own local -> TAK server plus the Mesh to CoT Converter, both described below, with stock ATAK/iTAK/WinTAK. +> TAK server plus the Mesh to CoT Converter, both described in the following sections, with stock ATAK/iTAK/WinTAK. ### Configuration diff --git a/docs/nl-rNL/user/translate.md b/docs/nl-rNL/user/translate.md index 67d69e2d96..cdc8b6d87f 100644 --- a/docs/nl-rNL/user/translate.md +++ b/docs/nl-rNL/user/translate.md @@ -2,7 +2,7 @@ title: Translate the App parent: User Guide nav_order: 17 -last_updated: 2026-08-30 +last_updated: 2026-09-11 description: How the app and its documentation are translated via Crowdin, and guidelines for contributing translations. aliases: - translate @@ -45,7 +45,7 @@ If your language is not yet listed on Crowdin: The Android app uses **Compose Multiplatform resources** for all user-visible strings: -``` +```text core/resources/src/commonMain/composeResources/ ├── values/ ← English (default) │ └── strings.xml @@ -58,7 +58,7 @@ core/resources/src/commonMain/composeResources/ In-app documentation follows a similar pattern under `docs/`: -``` +```text docs/ ├── en/user/ ← English source (default) │ ├── onboarding.md diff --git a/docs/no-rNO/user/desktop.md b/docs/no-rNO/user/desktop.md index efca3c03c9..2ec0d98037 100644 --- a/docs/no-rNO/user/desktop.md +++ b/docs/no-rNO/user/desktop.md @@ -2,7 +2,7 @@ title: Desktop App parent: User Guide nav_order: 14 -last_updated: 2026-08-30 +last_updated: 2026-09-11 description: Install and use the Meshtastic Desktop app on Linux, macOS, and Windows — connections, feature parity, and keyboard shortcuts. aliases: - desktop @@ -58,20 +58,20 @@ Bluetooth Low Energy is supported on desktop via the [Kable](https://github.com/ ## Feature Parity -| Feature | Android | Desktop | Notes | -| ----------------------------------------------------- | ------- | ------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Messaging | ✓ | ✓ | Full parity | -| Node List | ✓ | ✓ | Full parity | -| Map | ✓ | ✓ | Interactive MapLibre map, with base map and overlay pickers and custom tile sources. No offline downloads or local `.mbtiles` archives | -| Map layers (`.kml`/`.kmz`/GeoJSON) | ✓ | ✓ | Same layer store and sheet as Android; imported files draw on the desktop map | -| Site Planner | ✓ | ✓\* | \*Opens in your browser on desktop; the estimate is not drawn on the desktop map | -| Settings | ✓ | ✓ | Full parity | -| Bluetooth (BLE) | ✓ | ✓ | Via Kable on desktop | -| Firmware Update | ✓ | ✓ | In-app USB, BLE, and Wi-Fi (ESP32) update work the same as Android. The USB maintenance flow — nRF52/RP2040 factory erase and bootloader upgrade — is Android-only | -| Notifications | ✓ | ✓ | Native OS notifications | -| Widgets | ✓ | ✗ | Android-only | -| AI Assistant (Chirpy) | ✓\* | ✗ | Google flavor Android only | -| App Functions (system AI) | ✓† | ✗ | Google flavor Android only | +| Feature | Android | Desktop | Notes | +| ----------------------------------------------------- | ------- | ------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Messaging | ✓ | ✓ | Full parity | +| Node List | ✓ | ✓ | Full parity | +| Map | ✓ | ✓ | Interactive MapLibre map, with base map and overlay pickers and custom tile sources. Offline terrain (hillshade and contours) can be downloaded; offline base-map packs and local `.mbtiles` archives cannot | +| Map layers (`.kml`/`.kmz`/GeoJSON) | ✓ | ✓ | Same layer store and sheet as Android; imported files draw on the desktop map | +| Site Planner | ✓ | ✓\* | \*Opens in your browser on desktop; the estimate is not drawn on the desktop map | +| Settings | ✓ | ✓ | Full parity | +| Bluetooth (BLE) | ✓ | ✓ | Via Kable on desktop | +| Firmware Update | ✓ | ✓ | In-app USB, BLE, and Wi-Fi (ESP32) update work the same as Android. The USB maintenance flow — nRF52/RP2040 factory erase and bootloader upgrade — is Android-only | +| Notifications | ✓ | ✓ | Native OS notifications | +| Widgets | ✓ | ✗ | Android-only | +| AI Assistant (Chirpy) | ✓\* | ✗ | Google flavor Android only | +| App Functions (system AI) | ✓† | ✗ | Google flavor Android only | \*Chirpy AI requires Android 14+ on Google flavor builds with supported hardware. @@ -122,7 +122,7 @@ Individual doc pages render with full formatting: ## Building from Source -```bash +```shell git clone https://github.com/meshtastic/Meshtastic-Android.git cd Meshtastic-Android ./gradlew :desktopApp:run @@ -135,7 +135,7 @@ Requirements: ## Known Limitations -- Offline tile downloads and local `.mbtiles` archives are not available on desktop. +- Offline base-map downloads and local `.mbtiles` archives are not available on desktop. Offline terrain is — see [Map & Waypoints](map-and-waypoints). - `.kml`/`.kmz`/GeoJSON layer import works — see [Map & Waypoints](map-and-waypoints#map-layers). Site Planner opens in your browser rather than in the app; to bring its coverage estimate onto the map, click the transmitter pin diff --git a/docs/no-rNO/user/discovery.md b/docs/no-rNO/user/discovery.md index 06bfa460db..dbf69b1171 100644 --- a/docs/no-rNO/user/discovery.md +++ b/docs/no-rNO/user/discovery.md @@ -2,7 +2,7 @@ title: Local Mesh Discovery parent: User Guide nav_order: 12 -last_updated: 2026-08-30 +last_updated: 2026-09-11 description: Explore your mesh network — the Local Mesh Discovery scanner, traceroute paths, neighbor maps, and node discovery tools. aliases: - discovery @@ -97,7 +97,7 @@ Mesh Beacon lets nodes invite others to join their mesh. A beaconing node period Configure it under **Settings → Module configuration → Mesh Beacon**. The entry appears only on radios running firmware 2.8.0 or newer. A read-only **Region** row at the top of the screen shows the region the beacon advertises: that region, and the preset, are always the ones the radio itself uses, so a beacon cannot invite anyone onto settings your radio is not running. - **Listen for beacons** — receive invitations broadcast by other nodes. -- **Broadcast a beacon** — periodically advertise this mesh to nearby nodes, with an optional **Beacon message** of up to 100 bytes, a **Broadcast interval** picked from fixed intervals between 1 hour and 72 hours, and an **Offered channel** chosen from your radio's own channels. The offered channel is required, and defaults to your primary channel. +- **Broadcast a beacon** — periodically advertise this mesh to nearby nodes, with an optional **Beacon message** of up to 100 bytes, a **Broadcast interval** picked from fixed intervals between 1 hour and 72 hours, and an **Offered channel** chosen from your radio's own channels. The offered channel is required, and defaults to your primary channel. Over remote admin the picker offers the primary channel only. - **Broadcast targets** — optional extra destinations beyond the offered channel. **Add target** appends a row; each row picks a **Channel** and a **Transmit preset**, and **Remove target** deletes it. With no targets, the beacon goes out on the offered channel alone. Two conditions block beacon setup: diff --git a/docs/no-rNO/user/help-and-docs.md b/docs/no-rNO/user/help-and-docs.md index f306cde7f7..c83898cea3 100644 --- a/docs/no-rNO/user/help-and-docs.md +++ b/docs/no-rNO/user/help-and-docs.md @@ -2,7 +2,7 @@ title: Help & In-App Docs parent: User Guide nav_order: 21 -last_updated: 2026-08-30 +last_updated: 2026-09-11 description: Browse this documentation inside the app, search it, and ask Chirpy — the on-device AI assistant — questions about Meshtastic. aliases: - help @@ -23,7 +23,7 @@ The docs browser lists every user-guide page. Tap a page to read it; images and ### Search -The search box sits at the top of the docs browser. Type in it to filter pages by title and keywords — results update as you type, and the **Clear search** button (✕) empties the box. +Tap the search field at the top of the docs browser to open a full-screen search view. Type to filter pages by title and keywords — results update as you type, under the same field. The back arrow, or the keyboard's search action, closes the view. The field hides while you scroll down the page list and comes back when you scroll up. ![Searching the in-app documentation](../../assets/screenshots/docs-browser_search.png) diff --git a/docs/no-rNO/user/map-and-waypoints.md b/docs/no-rNO/user/map-and-waypoints.md index 2c658f5826..341ad15461 100644 --- a/docs/no-rNO/user/map-and-waypoints.md +++ b/docs/no-rNO/user/map-and-waypoints.md @@ -2,7 +2,7 @@ title: Map & Waypoints parent: User Guide nav_order: 6 -last_updated: 2026-09-01 +last_updated: 2026-09-11 description: View node positions on the map, create and share waypoints, manage map layers and Site Planner, and control position sharing and privacy. aliases: - map @@ -44,7 +44,7 @@ The floating toolbar provides quick access to the compass, the map type and laye ### Filtering the Map -Tap the filter button in the floating toolbar to open **Filter map**. **Display** controls what is drawn: **Only Favorites**, **Show Waypoints**, **Show Precision Circles**, and a slider that hides nodes not heard from recently. **Node roles** is a chip per device role, plus **All** to show every role; a selected chip means that role is shown. **Nodes** narrows the set further with **Hide offline nodes**, **Only show direct nodes**, **Exclude MQTT**, **Show ignored nodes**, and **Include unknown**. +Tap the filter button in the floating toolbar to open **Filter map**. **Display** controls what is drawn: **Only Favorites**, **Show Waypoints**, **Show Precision Circles**, and a slider that hides nodes not heard from recently. **Node roles** is a chip per device role, plus **All** to show every role; a selected chip means that role is shown. **Nodes** narrows the set further with **Hide offline nodes**, **Only show direct nodes**, **Signed only**, **Encrypted only**, **Exclude MQTT**, **Show ignored nodes**, and **Include unknown** — the same rules as the node list's [filter toggles](nodes). A dot on the filter button means at least one filter is hiding something — check it before concluding the mesh is quiet. Turning **Show Waypoints** off hides every waypoint, including your own. **Show ignored nodes** adds them to the map rather than showing only them — unlike the node list's **Only show ignored Nodes**. @@ -147,8 +147,9 @@ picker, all three offer the same raster base maps: Overlays can be toggled on top of any base map, from the layers sheet: - **Weather radar** — NOAA NEXRAD reflectivity (US coverage) -- **Hillshade** — terrain relief, on **F-Droid** and **Desktop** only. Useful for understanding why a - link fails, since LoRa range is limited by terrain +- **Hillshade** — terrain relief from an online tile source, on **F-Droid** and **Desktop** only; Google + Play draws hillshade from downloaded offline terrain instead. Useful for understanding why a link + fails, since LoRa range is limited by terrain ### Adding your own tile source @@ -156,7 +157,7 @@ Any XYZ tile endpoint can be added as a base map, on every flavor and on desktop Tile Sources** at the foot of the base map picker and paste a URL template using `{z}`, `{x}` and `{y}` — plus `{s}` if the provider uses rotating subdomains. A national mapping service, for example: -``` +```text https://wmts.geo.admin.ch/1.0.0/ch.swisstopo.pixelkarte-farbe/default/current/3857/{z}/{x}/{y}.jpeg ``` @@ -173,7 +174,18 @@ On **Google Play**, the layers sheet's **Offline Manager** section downloads its — water, roads and administrative boundaries extracted from the public Protomaps basemap dataset, drawn directly as map shapes rather than raster tiles. Frame the area you want, tap **Start Download**, and once it finishes flip **Show on map** to draw it (it's off by default so it never surprises you by covering up the -live map). You can still import a pre-made `.mbtiles` archive here too, as before. **Desktop** has neither. +live map). You can still import a pre-made `.mbtiles` archive here too, as before. **Desktop** has neither, though +the offline terrain described next does work there. + +### Offline terrain + +Hillshade and elevation contours can be downloaded for offline use as well, so a downloaded area still +shows why a link fails when there is no network. The terrain data comes from Mapterhorn and is credited on +the map. On **Google Play**, each downloaded region in the **Offline Manager** list has a **Download Terrain** +button and **Hillshade** and **Contours** switches; terrain attaches to that region and shares its storage limit, and an area +too large for terrain asks you to zoom in or pick a smaller region. On **F-Droid** and **Desktop**, the +layers sheet has an **Offline Terrain** section of its own: frame the area, tap **Start Download**, and the +hillshade and contours draw from the downloaded tiles from then on. ### Going offline @@ -181,8 +193,8 @@ An **Offline** pill appears over the map whenever the device has no network conn builds, if you've imported a local `.mbtiles` archive, the map switches to it automatically the moment the network drops — no toggle to remember — and switches back once you're reconnected, as long as you haven't picked a different base map yourself in between. On **F-Droid**, a downloaded offline area keeps rendering on -its own; the pill is purely informational there. **Desktop** has no offline downloads at all yet, so the pill -is informational there too. +its own; the pill is purely informational there. **Desktop** downloads offline terrain but no base-map +packs, so the pill is informational there too. ## Related Topics diff --git a/docs/no-rNO/user/messages-and-channels.md b/docs/no-rNO/user/messages-and-channels.md index 0d7f716602..765338e8f1 100644 --- a/docs/no-rNO/user/messages-and-channels.md +++ b/docs/no-rNO/user/messages-and-channels.md @@ -2,7 +2,7 @@ title: Messages & Channels parent: User Guide nav_order: 3 -last_updated: 2026-08-30 +last_updated: 2026-09-14 description: Send and receive messages, manage channels, configure encryption, search conversations, and use quick chat, reactions, and message actions. aliases: - channels @@ -50,6 +50,21 @@ Key length alone does not change the icon: a 128-bit key and a 256-bit key both Tapping an existing channel opens the same editor, where you can change the name, the PSK, MQTT uplink and downlink, and position precision. Every edit on this screen — adding, editing, deleting, or dragging a channel into a new order — waits on **Send** the same way. +### Archived Channels + +A conversation belongs to the channel it was held on, not to the slot that channel occupied. When a channel leaves your radio — you scan a different channel QR, replace a channel's key, or switch the modem preset — its conversation is **archived** rather than deleted or merged into whatever takes the slot. + +An archived conversation: + +- stays in the **Channels** list under the name the channel had, below the channels currently on your radio, marked with a history icon; +- keeps its full message history, searchable as usual; +- is read-only — you cannot send, reply, or react, because the channel is no longer on the radio to send on, and it is not offered as a share target; +- comes back automatically if you re-add that exact channel, taking its messages back to the live slot. + +Changing the modem preset archives the channel too: the preset is part of a default channel's identity, so **LongFast** and **MediumFast** are different channels even though neither the name nor the key changed. Changing only the region does not archive anything. + +To remove an archived conversation for good, long-press it in the conversation list and delete it. + ## Direct Messages Direct messages (DMs) go to one specific node. When both radios hold each other's public keys, your radio encrypts the message to that node's public key, so no one else on the mesh can read it — not even nodes that share your channel. diff --git a/docs/no-rNO/user/mqtt.md b/docs/no-rNO/user/mqtt.md index 67562efdc4..302ebffd71 100644 --- a/docs/no-rNO/user/mqtt.md +++ b/docs/no-rNO/user/mqtt.md @@ -2,7 +2,7 @@ title: MQTT parent: User Guide nav_order: 11 -last_updated: 2026-08-30 +last_updated: 2026-09-11 description: Bridge your mesh to the internet — MQTT broker setup, encryption layers, and map reporting. aliases: - mqtt @@ -25,7 +25,7 @@ The MQTT module connects your node to an MQTT broker, allowing: ## How It Works -``` +```text [Your Node] → Radio → [Gateway Node with Wi-Fi] → MQTT Broker → [Remote Gateway] → Radio → [Remote Node] ``` diff --git a/docs/no-rNO/user/node-metrics.md b/docs/no-rNO/user/node-metrics.md index ccb0ee44ac..df41b9c05e 100644 --- a/docs/no-rNO/user/node-metrics.md +++ b/docs/no-rNO/user/node-metrics.md @@ -2,7 +2,7 @@ title: Node Metrics parent: User Guide nav_order: 5 -last_updated: 2026-08-30 +last_updated: 2026-09-09 description: Telemetry dashboards for each mesh node — device health, environment sensors, air quality, signal quality, power, traceroute, and position history. aliases: - metrics @@ -114,7 +114,7 @@ Radio signal quality information: ### Signal Quality Reference -Signal quality is rated from **SNR relative to the active LoRa modem preset's demodulation floor**, not from fixed thresholds — a given SNR means different things on different presets (e.g. −15 dB is fine on LongSlow but unusable on ShortFast). RSSI is shown but is not part of the rating. In the table, _limit_ is the preset's SNR limit. +Signal quality is rated from **SNR relative to the active LoRa modem preset's demodulation floor**, not from fixed thresholds — a given SNR means different things on different presets (e.g. −15 dB is fine on LongSlow but unusable on ShortFast). When RSSI and a noise-floor reading are both available, the app also rates their difference against the preset limit and uses the worse rating. Otherwise, RSSI is display-only. In the table, _limit_ is the preset's SNR limit. | Quality | Criteria | | ----------- | ---------------------------------------------------------------- | diff --git a/docs/no-rNO/user/nodes.md b/docs/no-rNO/user/nodes.md index 03f34b7d72..578a6ad356 100644 --- a/docs/no-rNO/user/nodes.md +++ b/docs/no-rNO/user/nodes.md @@ -2,7 +2,7 @@ title: Nodes parent: User Guide nav_order: 4 -last_updated: 2026-09-04 +last_updated: 2026-09-11 description: Browse, filter, and sort mesh nodes — view details, signal quality, roles, and quick actions. aliases: - node-list @@ -74,19 +74,23 @@ Most users should keep the default **Client** role. Consider a different role wh > 💡 **Tip:** The mesh works best when most nodes are **Client** or **Router**. Too many Client Mute nodes reduce mesh resilience; too many Routers in a dense area can cause congestion. A good rule of thumb: one Router per 5–10 Clients in your area. -### Encryption Indicators +### Security indicators -Nodes display encryption status icons next to their name: +Each node carries one security icon beside its name in the node list. Tap it to read what it means, and choose **Show All Meanings** in that dialog for the full legend. The detail screen shows the same state in words, as a **Security** row that opens the same dialog. -| Icon | Meaning | -| ----------- | ------------------------------------------------------------------------------------------------------------------------------------------------- | -| 🔒 Locked | Communication uses PKI (public key infrastructure) — end-to-end encrypted with verified identity | -| 🔓 Unlocked | No public key has been received for this node, so it cannot be direct messaged — use **Request User Info** on the node detail page to ask for one | -| ⚠️ Mismatch | Public key mismatch — the node's key has changed since last seen (investigate before trusting) | +| Icon | Meaning | Shown for | +| --------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------- | +| Person with a shield check (green) | **Verified contact** — you verified this node's key in person by exchanging contact QR codes, so its identity is confirmed. The strongest trust the list shows. Your own connected node carries it too | Any firmware version | +| Nodes icon with a shield check (green) | **Signed node** — this node signs its broadcasts with its identity key, so its identity is consistent over time, but you have not verified it in person. On firmware 2.8 the icon appears from the version alone, before any signed broadcast has been heard | Firmware 2.8 or newer, and any node whose signed broadcast your node has verified | +| 🔒 Closed lock | A public key is on file and matches, so direct messages to this node are encrypted | Firmware before 2.8, or no reported version | +| 🔓 Open lock | No public key has been received for this node, so it cannot be direct messaged — use **Request User Info** on the node detail page to ask for one | Firmware before 2.8, or no reported version | +| ⚠️ Mismatch | **Public key mismatch** — a different key arrived for this node after one was stored. Investigate before trusting | Any firmware version | -> 💡 **Tip:** Direct messages always use PKI, so the radio needs the other node's public key before it can send one. It refuses the send rather than falling back to channel encryption. Keys arrive inside node info, which is why an open lock usually clears itself once that node is heard from properly. If you see a key mismatch warning, the node may have been reset or compromised. +Every node on firmware 2.8 or newer signs its broadcasts, so on that firmware the signed state is the baseline and the locks are not shown. -To clear a mismatch, first confirm through another trusted channel that the key change was intentional — a factory reset causes one. Then touch & hold the node, choose **Remove**, and let the two radios exchange keys again the next time yours hears it. +Direct messages always use public-key encryption, so your node needs the other node's public key before it can send one. It refuses the send rather than falling back to channel encryption. Keys arrive inside node info, which is why an open lock usually clears itself once that node is heard from properly. + +A mismatch never replaces the key you already hold. The app keeps the first key it recorded and refuses the new one, as the firmware does, so a stray or hostile node info cannot silently break encrypted messaging to a contact. To clear a mismatch, first confirm through another trusted channel that the key change was intentional — a factory reset causes one. Then touch & hold the node, choose **Remove**, and let the two nodes exchange keys again the next time yours hears it. ## Quick Actions @@ -108,7 +112,9 @@ radio is connected and running firmware 2.8 or newer — see ## Sharing a Contact -On a node's detail screen, tap **Share Contact** to produce a link and a QR code for that node. From the same dialog, **Write to NFC tag** saves the link to a writable NFC tag that anyone can tap to open. +On a node's detail screen, tap **Share Contact** to produce a link and a QR code for that node. From the same dialog, **Share link** opens the Android share sheet (on desktop it copies the link instead), **Write to NFC tag** saves it to a writable NFC tag, and **Copy** puts it on the clipboard. While that dialog is open and in front of you, the phone also offers the same link to any NFC reader, so someone can take the contact by tapping their phone against yours with no tag involved. + +Sharing your own contact this way marks it as verified in person, so whoever imports it sees the **Verified contact** icon rather than the signed one. Relaying someone else's contact passes on only what your app had already recorded about them. To add someone else's contact, use the import button on the node list and choose **Scan Shared Contact QR Code**, **Scan Shared Contact NFC**, or **Input Shared Contact URL**. The app asks you to confirm with **Import Shared Contact?**, and warns you when the contact is one you already have. @@ -120,14 +126,16 @@ Type in the search field to filter nodes by name or short name. The filter updat ### Filter Toggles -| Filter | Beskrivelse | -| --------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| **Hide offline nodes** | Show only nodes heard within the last 2 hours | -| **Only show direct nodes** | Show only nodes your radio heard directly, with no relay in between | -| **Include unknown** | Show nodes that haven't sent user info yet. **On by default**, so a node heard before its info arrives stays visible; these carry a badge marking them incomplete, and cannot be direct messaged until their user info brings a public key | -| **Exclude infrastructure** | Hide infrastructure-role nodes (Router, Router Late, Client Base, and legacy Repeater nodes) and any node that cannot be messaged, whatever its role | -| **Exclude MQTT** | Hide nodes heard only via MQTT internet bridge | -| **Only show ignored Nodes** | Replace the list with the nodes you have ignored. Every other node is hidden while this is on, and a banner appears at the top of the list to take you back | +| Filter | Beskrivelse | +| --------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **Hide offline nodes** | Show only nodes heard within the last 2 hours | +| **Only show direct nodes** | Show only nodes your radio heard directly, with no relay in between | +| **Include unknown** | Show nodes that haven't sent user info yet. **On by default**, so a node heard before its info arrives stays visible; these carry a badge marking them incomplete, and cannot be direct messaged until their user info brings a public key | +| **Exclude infrastructure** | Hide infrastructure-role nodes (Router, Router Late, Client Base, and legacy Repeater nodes) and any node that cannot be messaged, whatever its role | +| **Exclude MQTT** | Hide nodes heard only via MQTT internet bridge | +| **Signed only** | Show only nodes whose signed broadcasts your node has actually heard and verified. Stricter than the icon: a node on 2.8 shows as signed by its firmware version before any signed broadcast arrives, and a contact verified in person on older firmware is not signed at all | +| **Encrypted only** | Show nodes with a matching public key on file, the key an encrypted direct message needs. A node with a key mismatch is excluded | +| **Only show ignored Nodes** | Replace the list with the nodes you have ignored. Every other node is hidden while this is on, and a banner appears at the top of the list to take you back | ### Sort Options @@ -149,6 +157,8 @@ Tap the hop-histogram icon in the node list's app bar to open a bar chart of how Tapping a node opens the detail view with comprehensive information. See [Node Metrics](node-metrics) for full details on metrics and telemetry. +Signal quality is rated against your modem preset. The same SNR can be good on a long-range preset and poor on a faster one. Traceroute and neighbor-info SNR colors use that preset too. RSSI text has its own strength colors; it affects the quality rating only when a noise-floor reading is also available. + The Details card carries the node's short name, role, IDs, last heard time, hops away, uptime, and its SNR and RSSI: ![Node detail section](../../assets/screenshots/nodes_detail_section.png) diff --git a/docs/no-rNO/user/settings-module-admin.md b/docs/no-rNO/user/settings-module-admin.md index aaef7ebd4a..c971adfbc3 100644 --- a/docs/no-rNO/user/settings-module-admin.md +++ b/docs/no-rNO/user/settings-module-admin.md @@ -2,7 +2,7 @@ title: Settings — Modules & Admin parent: User Guide nav_order: 8 -last_updated: 2026-08-30 +last_updated: 2026-09-16 description: Configure optional feature modules (MQTT, telemetry, canned messages, TAK, and more) and perform device administration. aliases: - modules @@ -333,7 +333,7 @@ appear only when your own node is selected: ### Om -**Settings → About** carries the app's own identity rather than the radio's: +**Settings → About** carries the app's own identity rather than the node's: Three sections: @@ -341,7 +341,11 @@ Three sections: - **Apps** — opens with **Need Hardware?**, a rotating carousel of popular devices that links out to where to buy one. It also lists the GitHub repository, the running app version, and **Acknowledgements** (see the next section). -- **Project information** — links to the website and to this documentation. +- **Project information** — links to the website, to this documentation, and to the GNU GPL v3 + license the app is released under. + +The footer states the copyright and that the app is free software with no warranty, which you +may redistribute under the same license. ### Acknowledgements diff --git a/docs/no-rNO/user/settings-radio-user.md b/docs/no-rNO/user/settings-radio-user.md index 79fc2186b6..eb5570f8cb 100644 --- a/docs/no-rNO/user/settings-radio-user.md +++ b/docs/no-rNO/user/settings-radio-user.md @@ -2,7 +2,7 @@ title: Settings — Radio & User parent: User Guide nav_order: 7 -last_updated: 2026-09-04 +last_updated: 2026-09-11 description: Configure your radio hardware, LoRa presets, user profile, position sharing, power management, and security. aliases: - settings @@ -51,7 +51,7 @@ The footer appears as soon as you change something. **Discard** throws the chang The status message is saved with the same **Save**, but it never reboots the node — and, like the rest of this screen, it can be edited on a remote node you administer. For your own radio there is a shortcut while it is connected: touch & hold your node in the [node list](nodes.md) and choose -**Update status**. Older firmware and a disconnected radio have no shortcut — the field above is +**Update status**. Older firmware and a disconnected node have no shortcut — the field itself is still the way in. ## Configuration @@ -77,13 +77,13 @@ On **Settings → LoRa**. | Setting | Beskrivelse | Default | | ------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------- | -| Region | Regulatory region for frequency bands. You must set this before transmitting | Unset (must configure) | +| Region | Regulatory region for frequency bands. You must set this before transmitting. On firmware 2.8 or newer, the first region set also creates the node's identity key and gives it a new node number | Unset (must configure) | | Presets | Speed/range tradeoff | LongFast | | Number of Hops | Maximum retransmit hops | 3 | | Transmit Power | Transmission power (dBm); 0 = max allowed for region | 0 (region max) | | Frequency Override | Overrides the computed operating frequency outright (MHz). It does not offset the calculated value — leave at 0 unless you know you need a specific frequency | 0 (use calculated) | | Use Preset | On by default. Turn it off to set Spread Factor, Coding Rate and Bandwidth by hand instead of taking them from the modem preset | On | -| Spread Factor | Manual mode only: 7–12. Higher spreads further but slower | From preset | +| Spread Factor | Manual mode only: 5–12. Higher spreads further but slower. On SX127x (RF95) radios the firmware does not accept 5 or 6 and uses 11 instead | From preset | | Coding Rate | Manual mode only: 5–8. More redundancy costs airtime | From preset | | Bandwidth | Manual mode only: the channel bandwidth in kHz, typed in directly. On the 2.4 GHz region the app offers a list of the bandwidths your radio supports instead, and a stored value that is not on that list shows as _Unsupported_ and blocks saving until you pick a supported one | From preset | | Frequency Slot | Which slot within the region's band to use. 0 derives it from the primary channel name | 0 (automatic) | @@ -111,13 +111,13 @@ The Lite, Narrow, Medium Turbo, and Tiny presets need firmware 2.8 or newer — | Short Slow | ~5 km | 6.25 kbps | −10 dB | Suburban short-range; moderate building density | | Medium Fast | ~5 km | 3.52 kbps | −12.5 dB | Suburban areas; moderate building density | | Medium Slow | ~8 km | 1.95 kbps | −15 dB | Suburban/rural; moderate range with slower speed | -| Long Turbo | ~10 km | 1.34 kbps | −12.5 dB | Similar range to Long Fast but with 500 kHz bandwidth; faster throughput | +| Long Turbo | ~10 km | 1.34 kbps | −17.5 dB | Similar range to Long Fast but with 500 kHz bandwidth; faster throughput | | Long Fast | ~10 km | 1.1 kbps | −17.5 dB | **General use (default)** — balanced range and speed | | Long Moderate | ~20 km | 0.34 kbps | −17.5 dB | Rural with some terrain; occasional use | | Lite Fast | ~5 km | 1.76 kbps | −12.5 dB | EU 866 MHz SRD band (125 kHz BW); comparable to Medium Fast | | Lite Slow | ~10 km | 0.98 kbps | −15 dB | EU 866 MHz SRD band (125 kHz BW); comparable to Long Fast | -| Narrow Fast | ~5 km | 2.28 kbps | −10 dB | EU 868 MHz band (62.5 kHz BW); avoids interference with other devices | -| Narrow Slow | ~10 km | 1.30 kbps | −12.5 dB | EU 868 MHz band (62.5 kHz BW); comparable to Long Fast | +| Narrow Fast | ~5 km | 2.28 kbps | −7.5 dB | EU 868 MHz band (62.5 kHz BW); avoids interference with other devices | +| Narrow Slow | ~10 km | 1.30 kbps | −10 dB | EU 868 MHz band (62.5 kHz BW); comparable to Long Fast | | Medium Turbo | ~5 km | 7.0 kbps | −12.5 dB | Like Medium Fast but with 500 kHz bandwidth; not legal in every region. Needs firmware 2.8 or newer | | Tiny Fast | ~10 km | 0.68 kbps | −7.5 dB | Amateur bands that cap occupied bandwidth; these presets use 15.6 kHz. Needs firmware 2.8 or newer, an SX126x or SX127x radio, and a TCXO of ±5 ppm or better | | Tiny Slow | ~20 km | 0.33 kbps | −10 dB | Same band restrictions as Tiny Fast, longer range. Same firmware, radio, and TCXO requirements | diff --git a/docs/no-rNO/user/signal-meter.md b/docs/no-rNO/user/signal-meter.md index 059204e843..903f8c26d7 100644 --- a/docs/no-rNO/user/signal-meter.md +++ b/docs/no-rNO/user/signal-meter.md @@ -2,7 +2,7 @@ title: How the Meshtastic Signal Meter Works parent: User Guide nav_order: 15 -last_updated: 2026-08-30 +last_updated: 2026-09-09 description: How the signal meter rates quality from SNR relative to the LoRa modem preset — spread spectrum, presets, and what the bars really mean. aliases: - signal @@ -32,7 +32,7 @@ Each modem preset has an SNR limit: the lowest SNR at which that preset can stil ## Rating Signal Quality -The app rates signal quality (None, Bad, Fair, or Good) from SNR alone, measured against the active preset's SNR limit. It does not factor in RSSI: without knowing the local noise floor, RSSI alone cannot say whether a signal is decodable. RSSI is still available — on the node detail screen and in the metrics charts. +The app rates signal quality (None, Bad, Fair, or Good) from SNR relative to the active preset's SNR limit. When RSSI and a noise-floor reading are both available, it also rates the difference between them against that limit and uses the worse rating. Otherwise, it uses SNR alone. Because the rating is relative to the preset, the same SNR rates differently on different presets. An SNR of −16 dB rates Good on Long Fast (SNR limit −17.5 dB) but None on Short Fast (SNR limit −7.5 dB). Let `limit` be the active preset's SNR limit: @@ -45,7 +45,7 @@ Because the rating is relative to the preset, the same SNR rates differently on The icon never goes blank, so count the bars carefully: a single bar means None, not a weak but usable link, and Good is a solid wedge rather than a set of bars. A gray three-bar icon labeled Unknown is a different state again — the packet carried no SNR measurement at all, which is not the same as measuring one and finding it too weak. -> ℹ️ **Note:** Traceroute hop colors use fixed thresholds (−7 dB / −15 dB); the per-node signal meter uses the preset-relative rating instead. +> ℹ️ **Note:** Traceroute and neighbor-info SNR colors use the active preset's limits too. RSSI text has separate strength colors: green above −115 dBm, yellow above −120 dBm, orange above −126 dBm, and red at or below −126 dBm. ## Diagnosing Local Interference diff --git a/docs/no-rNO/user/tak.md b/docs/no-rNO/user/tak.md index be2e2c3959..de6083ec53 100644 --- a/docs/no-rNO/user/tak.md +++ b/docs/no-rNO/user/tak.md @@ -2,7 +2,7 @@ title: TAK Integration parent: User Guide nav_order: 10 -last_updated: 2026-08-30 +last_updated: 2026-09-11 description: Interoperate with ATAK and WinTAK — CoT position sharing, TAK roles, and plugin setup. aliases: - tak @@ -36,7 +36,7 @@ The TAK module allows Meshtastic nodes to: > ⚠️ **Warning:** The old **Meshtastic ATAK Plugin** is no longer part of this path and cannot > work. It bridged through the cross-process AIDL API, which was removed in app 2.8.0; the mesh > service is now in-process only. Do not install it. Interop today runs over the app's own local -> TAK server plus the Mesh to CoT Converter, both described below, with stock ATAK/iTAK/WinTAK. +> TAK server plus the Mesh to CoT Converter, both described in the following sections, with stock ATAK/iTAK/WinTAK. ### Configuration diff --git a/docs/no-rNO/user/translate.md b/docs/no-rNO/user/translate.md index 67d69e2d96..cdc8b6d87f 100644 --- a/docs/no-rNO/user/translate.md +++ b/docs/no-rNO/user/translate.md @@ -2,7 +2,7 @@ title: Translate the App parent: User Guide nav_order: 17 -last_updated: 2026-08-30 +last_updated: 2026-09-11 description: How the app and its documentation are translated via Crowdin, and guidelines for contributing translations. aliases: - translate @@ -45,7 +45,7 @@ If your language is not yet listed on Crowdin: The Android app uses **Compose Multiplatform resources** for all user-visible strings: -``` +```text core/resources/src/commonMain/composeResources/ ├── values/ ← English (default) │ └── strings.xml @@ -58,7 +58,7 @@ core/resources/src/commonMain/composeResources/ In-app documentation follows a similar pattern under `docs/`: -``` +```text docs/ ├── en/user/ ← English source (default) │ ├── onboarding.md diff --git a/docs/pl-rPL/user/desktop.md b/docs/pl-rPL/user/desktop.md index 7d04fe0f55..3cdf48009b 100644 --- a/docs/pl-rPL/user/desktop.md +++ b/docs/pl-rPL/user/desktop.md @@ -2,7 +2,7 @@ title: Desktop App parent: User Guide nav_order: 14 -last_updated: 2026-08-30 +last_updated: 2026-09-11 description: Install and use the Meshtastic Desktop app on Linux, macOS, and Windows — connections, feature parity, and keyboard shortcuts. aliases: - desktop @@ -58,20 +58,20 @@ Bluetooth Low Energy is supported on desktop via the [Kable](https://github.com/ ## Feature Parity -| Feature | Android | Desktop | Notatki | -| ----------------------------------------------------- | ------- | ------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Messaging | ✓ | ✓ | Full parity | -| Node List | ✓ | ✓ | Full parity | -| Mapa | ✓ | ✓ | Interactive MapLibre map, with base map and overlay pickers and custom tile sources. No offline downloads or local `.mbtiles` archives | -| Map layers (`.kml`/`.kmz`/GeoJSON) | ✓ | ✓ | Same layer store and sheet as Android; imported files draw on the desktop map | -| Site Planner | ✓ | ✓\* | \*Opens in your browser on desktop; the estimate is not drawn on the desktop map | -| Ustawienia | ✓ | ✓ | Full parity | -| Bluetooth (BLE) | ✓ | ✓ | Via Kable on desktop | -| Aktualizacja oprogramowania | ✓ | ✓ | In-app USB, BLE, and Wi-Fi (ESP32) update work the same as Android. The USB maintenance flow — nRF52/RP2040 factory erase and bootloader upgrade — is Android-only | -| Notifications | ✓ | ✓ | Native OS notifications | -| Widgets | ✓ | ✗ | Android-only | -| AI Assistant (Chirpy) | ✓\* | ✗ | Google flavor Android only | -| App Functions (system AI) | ✓† | ✗ | Google flavor Android only | +| Feature | Android | Desktop | Notatki | +| ----------------------------------------------------- | ------- | ------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Messaging | ✓ | ✓ | Full parity | +| Node List | ✓ | ✓ | Full parity | +| Mapa | ✓ | ✓ | Interactive MapLibre map, with base map and overlay pickers and custom tile sources. Offline terrain (hillshade and contours) can be downloaded; offline base-map packs and local `.mbtiles` archives cannot | +| Map layers (`.kml`/`.kmz`/GeoJSON) | ✓ | ✓ | Same layer store and sheet as Android; imported files draw on the desktop map | +| Site Planner | ✓ | ✓\* | \*Opens in your browser on desktop; the estimate is not drawn on the desktop map | +| Ustawienia | ✓ | ✓ | Full parity | +| Bluetooth (BLE) | ✓ | ✓ | Via Kable on desktop | +| Aktualizacja oprogramowania | ✓ | ✓ | In-app USB, BLE, and Wi-Fi (ESP32) update work the same as Android. The USB maintenance flow — nRF52/RP2040 factory erase and bootloader upgrade — is Android-only | +| Notifications | ✓ | ✓ | Native OS notifications | +| Widgets | ✓ | ✗ | Android-only | +| AI Assistant (Chirpy) | ✓\* | ✗ | Google flavor Android only | +| App Functions (system AI) | ✓† | ✗ | Google flavor Android only | \*Chirpy AI requires Android 14+ on Google flavor builds with supported hardware. @@ -122,7 +122,7 @@ Individual doc pages render with full formatting: ## Building from Source -```bash +```shell git clone https://github.com/meshtastic/Meshtastic-Android.git cd Meshtastic-Android ./gradlew :desktopApp:run @@ -135,7 +135,7 @@ Requirements: ## Known Limitations -- Offline tile downloads and local `.mbtiles` archives are not available on desktop. +- Offline base-map downloads and local `.mbtiles` archives are not available on desktop. Offline terrain is — see [Map & Waypoints](map-and-waypoints). - `.kml`/`.kmz`/GeoJSON layer import works — see [Map & Waypoints](map-and-waypoints#map-layers). Site Planner opens in your browser rather than in the app; to bring its coverage estimate onto the map, click the transmitter pin diff --git a/docs/pl-rPL/user/discovery.md b/docs/pl-rPL/user/discovery.md index 6919dc9bf2..5fa1e32e2e 100644 --- a/docs/pl-rPL/user/discovery.md +++ b/docs/pl-rPL/user/discovery.md @@ -2,7 +2,7 @@ title: Local Mesh Discovery parent: User Guide nav_order: 12 -last_updated: 2026-08-30 +last_updated: 2026-09-11 description: Explore your mesh network — the Local Mesh Discovery scanner, traceroute paths, neighbor maps, and node discovery tools. aliases: - discovery @@ -97,7 +97,7 @@ Mesh Beacon lets nodes invite others to join their mesh. A beaconing node period Configure it under **Settings → Module configuration → Mesh Beacon**. The entry appears only on radios running firmware 2.8.0 or newer. A read-only **Region** row at the top of the screen shows the region the beacon advertises: that region, and the preset, are always the ones the radio itself uses, so a beacon cannot invite anyone onto settings your radio is not running. - **Listen for beacons** — receive invitations broadcast by other nodes. -- **Broadcast a beacon** — periodically advertise this mesh to nearby nodes, with an optional **Beacon message** of up to 100 bytes, a **Broadcast interval** picked from fixed intervals between 1 hour and 72 hours, and an **Offered channel** chosen from your radio's own channels. The offered channel is required, and defaults to your primary channel. +- **Broadcast a beacon** — periodically advertise this mesh to nearby nodes, with an optional **Beacon message** of up to 100 bytes, a **Broadcast interval** picked from fixed intervals between 1 hour and 72 hours, and an **Offered channel** chosen from your radio's own channels. The offered channel is required, and defaults to your primary channel. Over remote admin the picker offers the primary channel only. - **Broadcast targets** — optional extra destinations beyond the offered channel. **Add target** appends a row; each row picks a **Channel** and a **Transmit preset**, and **Remove target** deletes it. With no targets, the beacon goes out on the offered channel alone. Two conditions block beacon setup: diff --git a/docs/pl-rPL/user/help-and-docs.md b/docs/pl-rPL/user/help-and-docs.md index f306cde7f7..c83898cea3 100644 --- a/docs/pl-rPL/user/help-and-docs.md +++ b/docs/pl-rPL/user/help-and-docs.md @@ -2,7 +2,7 @@ title: Help & In-App Docs parent: User Guide nav_order: 21 -last_updated: 2026-08-30 +last_updated: 2026-09-11 description: Browse this documentation inside the app, search it, and ask Chirpy — the on-device AI assistant — questions about Meshtastic. aliases: - help @@ -23,7 +23,7 @@ The docs browser lists every user-guide page. Tap a page to read it; images and ### Search -The search box sits at the top of the docs browser. Type in it to filter pages by title and keywords — results update as you type, and the **Clear search** button (✕) empties the box. +Tap the search field at the top of the docs browser to open a full-screen search view. Type to filter pages by title and keywords — results update as you type, under the same field. The back arrow, or the keyboard's search action, closes the view. The field hides while you scroll down the page list and comes back when you scroll up. ![Searching the in-app documentation](../../assets/screenshots/docs-browser_search.png) diff --git a/docs/pl-rPL/user/map-and-waypoints.md b/docs/pl-rPL/user/map-and-waypoints.md index 7a1eb73506..30d257d99e 100644 --- a/docs/pl-rPL/user/map-and-waypoints.md +++ b/docs/pl-rPL/user/map-and-waypoints.md @@ -2,7 +2,7 @@ title: Map & Waypoints parent: User Guide nav_order: 6 -last_updated: 2026-09-01 +last_updated: 2026-09-11 description: View node positions on the map, create and share waypoints, manage map layers and Site Planner, and control position sharing and privacy. aliases: - map @@ -44,7 +44,7 @@ The floating toolbar provides quick access to the compass, the map type and laye ### Filtering the Map -Tap the filter button in the floating toolbar to open **Filter map**. **Display** controls what is drawn: **Only Favorites**, **Show Waypoints**, **Show Precision Circles**, and a slider that hides nodes not heard from recently. **Node roles** is a chip per device role, plus **All** to show every role; a selected chip means that role is shown. **Nodes** narrows the set further with **Hide offline nodes**, **Only show direct nodes**, **Exclude MQTT**, **Show ignored nodes**, and **Include unknown**. +Tap the filter button in the floating toolbar to open **Filter map**. **Display** controls what is drawn: **Only Favorites**, **Show Waypoints**, **Show Precision Circles**, and a slider that hides nodes not heard from recently. **Node roles** is a chip per device role, plus **All** to show every role; a selected chip means that role is shown. **Nodes** narrows the set further with **Hide offline nodes**, **Only show direct nodes**, **Signed only**, **Encrypted only**, **Exclude MQTT**, **Show ignored nodes**, and **Include unknown** — the same rules as the node list's [filter toggles](nodes). A dot on the filter button means at least one filter is hiding something — check it before concluding the mesh is quiet. Turning **Show Waypoints** off hides every waypoint, including your own. **Show ignored nodes** adds them to the map rather than showing only them — unlike the node list's **Only show ignored Nodes**. @@ -147,8 +147,9 @@ picker, all three offer the same raster base maps: Overlays can be toggled on top of any base map, from the layers sheet: - **Weather radar** — NOAA NEXRAD reflectivity (US coverage) -- **Hillshade** — terrain relief, on **F-Droid** and **Desktop** only. Useful for understanding why a - link fails, since LoRa range is limited by terrain +- **Hillshade** — terrain relief from an online tile source, on **F-Droid** and **Desktop** only; Google + Play draws hillshade from downloaded offline terrain instead. Useful for understanding why a link + fails, since LoRa range is limited by terrain ### Adding your own tile source @@ -156,7 +157,7 @@ Any XYZ tile endpoint can be added as a base map, on every flavor and on desktop Tile Sources** at the foot of the base map picker and paste a URL template using `{z}`, `{x}` and `{y}` — plus `{s}` if the provider uses rotating subdomains. A national mapping service, for example: -``` +```text https://wmts.geo.admin.ch/1.0.0/ch.swisstopo.pixelkarte-farbe/default/current/3857/{z}/{x}/{y}.jpeg ``` @@ -173,7 +174,18 @@ On **Google Play**, the layers sheet's **Offline Manager** section downloads its — water, roads and administrative boundaries extracted from the public Protomaps basemap dataset, drawn directly as map shapes rather than raster tiles. Frame the area you want, tap **Start Download**, and once it finishes flip **Show on map** to draw it (it's off by default so it never surprises you by covering up the -live map). You can still import a pre-made `.mbtiles` archive here too, as before. **Desktop** has neither. +live map). You can still import a pre-made `.mbtiles` archive here too, as before. **Desktop** has neither, though +the offline terrain described next does work there. + +### Offline terrain + +Hillshade and elevation contours can be downloaded for offline use as well, so a downloaded area still +shows why a link fails when there is no network. The terrain data comes from Mapterhorn and is credited on +the map. On **Google Play**, each downloaded region in the **Offline Manager** list has a **Download Terrain** +button and **Hillshade** and **Contours** switches; terrain attaches to that region and shares its storage limit, and an area +too large for terrain asks you to zoom in or pick a smaller region. On **F-Droid** and **Desktop**, the +layers sheet has an **Offline Terrain** section of its own: frame the area, tap **Start Download**, and the +hillshade and contours draw from the downloaded tiles from then on. ### Going offline @@ -181,8 +193,8 @@ An **Offline** pill appears over the map whenever the device has no network conn builds, if you've imported a local `.mbtiles` archive, the map switches to it automatically the moment the network drops — no toggle to remember — and switches back once you're reconnected, as long as you haven't picked a different base map yourself in between. On **F-Droid**, a downloaded offline area keeps rendering on -its own; the pill is purely informational there. **Desktop** has no offline downloads at all yet, so the pill -is informational there too. +its own; the pill is purely informational there. **Desktop** downloads offline terrain but no base-map +packs, so the pill is informational there too. ## Related Topics diff --git a/docs/pl-rPL/user/messages-and-channels.md b/docs/pl-rPL/user/messages-and-channels.md index bf40e2fee0..9e9c20d177 100644 --- a/docs/pl-rPL/user/messages-and-channels.md +++ b/docs/pl-rPL/user/messages-and-channels.md @@ -2,7 +2,7 @@ title: Messages & Channels parent: User Guide nav_order: 3 -last_updated: 2026-08-30 +last_updated: 2026-09-14 description: Send and receive messages, manage channels, configure encryption, search conversations, and use quick chat, reactions, and message actions. aliases: - channels @@ -50,6 +50,21 @@ Key length alone does not change the icon: a 128-bit key and a 256-bit key both Tapping an existing channel opens the same editor, where you can change the name, the PSK, MQTT uplink and downlink, and position precision. Every edit on this screen — adding, editing, deleting, or dragging a channel into a new order — waits on **Send** the same way. +### Archived Channels + +A conversation belongs to the channel it was held on, not to the slot that channel occupied. When a channel leaves your radio — you scan a different channel QR, replace a channel's key, or switch the modem preset — its conversation is **archived** rather than deleted or merged into whatever takes the slot. + +An archived conversation: + +- stays in the **Channels** list under the name the channel had, below the channels currently on your radio, marked with a history icon; +- keeps its full message history, searchable as usual; +- is read-only — you cannot send, reply, or react, because the channel is no longer on the radio to send on, and it is not offered as a share target; +- comes back automatically if you re-add that exact channel, taking its messages back to the live slot. + +Changing the modem preset archives the channel too: the preset is part of a default channel's identity, so **LongFast** and **MediumFast** are different channels even though neither the name nor the key changed. Changing only the region does not archive anything. + +To remove an archived conversation for good, long-press it in the conversation list and delete it. + ## Bezpośrednie Wiadomości Direct messages (DMs) go to one specific node. When both radios hold each other's public keys, your radio encrypts the message to that node's public key, so no one else on the mesh can read it — not even nodes that share your channel. diff --git a/docs/pl-rPL/user/mqtt.md b/docs/pl-rPL/user/mqtt.md index 159fb9ba8c..0b2dffe8c1 100644 --- a/docs/pl-rPL/user/mqtt.md +++ b/docs/pl-rPL/user/mqtt.md @@ -2,7 +2,7 @@ title: MQTT parent: User Guide nav_order: 11 -last_updated: 2026-08-30 +last_updated: 2026-09-11 description: Bridge your mesh to the internet — MQTT broker setup, encryption layers, and map reporting. aliases: - mqtt @@ -25,7 +25,7 @@ The MQTT module connects your node to an MQTT broker, allowing: ## How It Works -``` +```text [Your Node] → Radio → [Gateway Node with Wi-Fi] → MQTT Broker → [Remote Gateway] → Radio → [Remote Node] ``` diff --git a/docs/pl-rPL/user/node-metrics.md b/docs/pl-rPL/user/node-metrics.md index 5d5fe0dbdf..889f129c56 100644 --- a/docs/pl-rPL/user/node-metrics.md +++ b/docs/pl-rPL/user/node-metrics.md @@ -2,7 +2,7 @@ title: Node Metrics parent: User Guide nav_order: 5 -last_updated: 2026-08-30 +last_updated: 2026-09-09 description: Telemetry dashboards for each mesh node — device health, environment sensors, air quality, signal quality, power, traceroute, and position history. aliases: - metrics @@ -114,7 +114,7 @@ Radio signal quality information: ### Signal Quality Reference -Signal quality is rated from **SNR relative to the active LoRa modem preset's demodulation floor**, not from fixed thresholds — a given SNR means different things on different presets (e.g. −15 dB is fine on LongSlow but unusable on ShortFast). RSSI is shown but is not part of the rating. In the table, _limit_ is the preset's SNR limit. +Signal quality is rated from **SNR relative to the active LoRa modem preset's demodulation floor**, not from fixed thresholds — a given SNR means different things on different presets (e.g. −15 dB is fine on LongSlow but unusable on ShortFast). When RSSI and a noise-floor reading are both available, the app also rates their difference against the preset limit and uses the worse rating. Otherwise, RSSI is display-only. In the table, _limit_ is the preset's SNR limit. | Quality | Criteria | | ------------- | ---------------------------------------------------------------- | diff --git a/docs/pl-rPL/user/nodes.md b/docs/pl-rPL/user/nodes.md index 9cdc72e532..f5492b0f62 100644 --- a/docs/pl-rPL/user/nodes.md +++ b/docs/pl-rPL/user/nodes.md @@ -2,7 +2,7 @@ title: Węzły parent: User Guide nav_order: 4 -last_updated: 2026-09-04 +last_updated: 2026-09-11 description: Browse, filter, and sort mesh nodes — view details, signal quality, roles, and quick actions. aliases: - node-list @@ -74,19 +74,23 @@ Most users should keep the default **Client** role. Consider a different role wh > 💡 **Tip:** The mesh works best when most nodes are **Client** or **Router**. Too many Client Mute nodes reduce mesh resilience; too many Routers in a dense area can cause congestion. A good rule of thumb: one Router per 5–10 Clients in your area. -### Encryption Indicators +### Security indicators -Nodes display encryption status icons next to their name: +Each node carries one security icon beside its name in the node list. Tap it to read what it means, and choose **Show All Meanings** in that dialog for the full legend. The detail screen shows the same state in words, as a **Security** row that opens the same dialog. -| Icon | Meaning | -| ----------- | ------------------------------------------------------------------------------------------------------------------------------------------------- | -| 🔒 Locked | Communication uses PKI (public key infrastructure) — end-to-end encrypted with verified identity | -| 🔓 Unlocked | No public key has been received for this node, so it cannot be direct messaged — use **Request User Info** on the node detail page to ask for one | -| ⚠️ Mismatch | Public key mismatch — the node's key has changed since last seen (investigate before trusting) | +| Icon | Meaning | Shown for | +| --------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------- | +| Person with a shield check (green) | **Verified contact** — you verified this node's key in person by exchanging contact QR codes, so its identity is confirmed. The strongest trust the list shows. Your own connected node carries it too | Any firmware version | +| Nodes icon with a shield check (green) | **Signed node** — this node signs its broadcasts with its identity key, so its identity is consistent over time, but you have not verified it in person. On firmware 2.8 the icon appears from the version alone, before any signed broadcast has been heard | Firmware 2.8 or newer, and any node whose signed broadcast your node has verified | +| 🔒 Closed lock | A public key is on file and matches, so direct messages to this node are encrypted | Firmware before 2.8, or no reported version | +| 🔓 Open lock | No public key has been received for this node, so it cannot be direct messaged — use **Request User Info** on the node detail page to ask for one | Firmware before 2.8, or no reported version | +| ⚠️ Mismatch | **Public key mismatch** — a different key arrived for this node after one was stored. Investigate before trusting | Any firmware version | -> 💡 **Tip:** Direct messages always use PKI, so the radio needs the other node's public key before it can send one. It refuses the send rather than falling back to channel encryption. Keys arrive inside node info, which is why an open lock usually clears itself once that node is heard from properly. If you see a key mismatch warning, the node may have been reset or compromised. +Every node on firmware 2.8 or newer signs its broadcasts, so on that firmware the signed state is the baseline and the locks are not shown. -To clear a mismatch, first confirm through another trusted channel that the key change was intentional — a factory reset causes one. Then touch & hold the node, choose **Remove**, and let the two radios exchange keys again the next time yours hears it. +Direct messages always use public-key encryption, so your node needs the other node's public key before it can send one. It refuses the send rather than falling back to channel encryption. Keys arrive inside node info, which is why an open lock usually clears itself once that node is heard from properly. + +A mismatch never replaces the key you already hold. The app keeps the first key it recorded and refuses the new one, as the firmware does, so a stray or hostile node info cannot silently break encrypted messaging to a contact. To clear a mismatch, first confirm through another trusted channel that the key change was intentional — a factory reset causes one. Then touch & hold the node, choose **Remove**, and let the two nodes exchange keys again the next time yours hears it. ## Quick Actions @@ -108,7 +112,9 @@ radio is connected and running firmware 2.8 or newer — see ## Sharing a Contact -On a node's detail screen, tap **Share Contact** to produce a link and a QR code for that node. From the same dialog, **Write to NFC tag** saves the link to a writable NFC tag that anyone can tap to open. +On a node's detail screen, tap **Share Contact** to produce a link and a QR code for that node. From the same dialog, **Share link** opens the Android share sheet (on desktop it copies the link instead), **Write to NFC tag** saves it to a writable NFC tag, and **Copy** puts it on the clipboard. While that dialog is open and in front of you, the phone also offers the same link to any NFC reader, so someone can take the contact by tapping their phone against yours with no tag involved. + +Sharing your own contact this way marks it as verified in person, so whoever imports it sees the **Verified contact** icon rather than the signed one. Relaying someone else's contact passes on only what your app had already recorded about them. To add someone else's contact, use the import button on the node list and choose **Scan Shared Contact QR Code**, **Scan Shared Contact NFC**, or **Input Shared Contact URL**. The app asks you to confirm with **Import Shared Contact?**, and warns you when the contact is one you already have. @@ -120,14 +126,16 @@ Type in the search field to filter nodes by name or short name. The filter updat ### Filter Toggles -| Filtr | Opis | -| --------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| **Hide offline nodes** | Show only nodes heard within the last 2 hours | -| **Only show direct nodes** | Show only nodes your radio heard directly, with no relay in between | -| **Include unknown** | Show nodes that haven't sent user info yet. **On by default**, so a node heard before its info arrives stays visible; these carry a badge marking them incomplete, and cannot be direct messaged until their user info brings a public key | -| **Exclude infrastructure** | Hide infrastructure-role nodes (Router, Router Late, Client Base, and legacy Repeater nodes) and any node that cannot be messaged, whatever its role | -| **Exclude MQTT** | Hide nodes heard only via MQTT internet bridge | -| **Only show ignored Nodes** | Replace the list with the nodes you have ignored. Every other node is hidden while this is on, and a banner appears at the top of the list to take you back | +| Filtr | Opis | +| --------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **Hide offline nodes** | Show only nodes heard within the last 2 hours | +| **Only show direct nodes** | Show only nodes your radio heard directly, with no relay in between | +| **Include unknown** | Show nodes that haven't sent user info yet. **On by default**, so a node heard before its info arrives stays visible; these carry a badge marking them incomplete, and cannot be direct messaged until their user info brings a public key | +| **Exclude infrastructure** | Hide infrastructure-role nodes (Router, Router Late, Client Base, and legacy Repeater nodes) and any node that cannot be messaged, whatever its role | +| **Exclude MQTT** | Hide nodes heard only via MQTT internet bridge | +| **Signed only** | Show only nodes whose signed broadcasts your node has actually heard and verified. Stricter than the icon: a node on 2.8 shows as signed by its firmware version before any signed broadcast arrives, and a contact verified in person on older firmware is not signed at all | +| **Encrypted only** | Show nodes with a matching public key on file, the key an encrypted direct message needs. A node with a key mismatch is excluded | +| **Only show ignored Nodes** | Replace the list with the nodes you have ignored. Every other node is hidden while this is on, and a banner appears at the top of the list to take you back | ### Sort Options @@ -149,6 +157,8 @@ Tap the hop-histogram icon in the node list's app bar to open a bar chart of how Tapping a node opens the detail view with comprehensive information. See [Node Metrics](node-metrics) for full details on metrics and telemetry. +Signal quality is rated against your modem preset. The same SNR can be good on a long-range preset and poor on a faster one. Traceroute and neighbor-info SNR colors use that preset too. RSSI text has its own strength colors; it affects the quality rating only when a noise-floor reading is also available. + The Details card carries the node's short name, role, IDs, last heard time, hops away, uptime, and its SNR and RSSI: ![Node detail section](../../assets/screenshots/nodes_detail_section.png) diff --git a/docs/pl-rPL/user/settings-module-admin.md b/docs/pl-rPL/user/settings-module-admin.md index d876d6d4c9..3f393d0d26 100644 --- a/docs/pl-rPL/user/settings-module-admin.md +++ b/docs/pl-rPL/user/settings-module-admin.md @@ -2,7 +2,7 @@ title: Settings — Modules & Admin parent: User Guide nav_order: 8 -last_updated: 2026-08-30 +last_updated: 2026-09-16 description: Configure optional feature modules (MQTT, telemetry, canned messages, TAK, and more) and perform device administration. aliases: - modules @@ -333,7 +333,7 @@ appear only when your own node is selected: ### O aplikacji -**Settings → About** carries the app's own identity rather than the radio's: +**Settings → About** carries the app's own identity rather than the node's: Three sections: @@ -341,7 +341,11 @@ Three sections: - **Apps** — opens with **Need Hardware?**, a rotating carousel of popular devices that links out to where to buy one. It also lists the GitHub repository, the running app version, and **Acknowledgements** (see the next section). -- **Project information** — links to the website and to this documentation. +- **Project information** — links to the website, to this documentation, and to the GNU GPL v3 + license the app is released under. + +The footer states the copyright and that the app is free software with no warranty, which you +may redistribute under the same license. ### Potwierdzenia diff --git a/docs/pl-rPL/user/settings-radio-user.md b/docs/pl-rPL/user/settings-radio-user.md index 0bac3e475f..0dd11884c1 100644 --- a/docs/pl-rPL/user/settings-radio-user.md +++ b/docs/pl-rPL/user/settings-radio-user.md @@ -2,7 +2,7 @@ title: Settings — Radio & User parent: User Guide nav_order: 7 -last_updated: 2026-09-04 +last_updated: 2026-09-11 description: Configure your radio hardware, LoRa presets, user profile, position sharing, power management, and security. aliases: - ustawienia @@ -51,7 +51,7 @@ The footer appears as soon as you change something. **Discard** throws the chang The status message is saved with the same **Save**, but it never reboots the node — and, like the rest of this screen, it can be edited on a remote node you administer. For your own radio there is a shortcut while it is connected: touch & hold your node in the [node list](nodes.md) and choose -**Update status**. Older firmware and a disconnected radio have no shortcut — the field above is +**Update status**. Older firmware and a disconnected node have no shortcut — the field itself is still the way in. ## Konfiguracja @@ -77,13 +77,13 @@ On **Settings → LoRa**. | Setting | Opis | Domyślny | | ------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------- | -| Region | Regulatory region for frequency bands. You must set this before transmitting | Unset (must configure) | +| Region | Regulatory region for frequency bands. You must set this before transmitting. On firmware 2.8 or newer, the first region set also creates the node's identity key and gives it a new node number | Unset (must configure) | | Presety | Speed/range tradeoff | LongFast | | Number of Hops | Maximum retransmit hops | 3 | | Moc nadawania | Transmission power (dBm); 0 = max allowed for region | 0 (region max) | | Frequency Override | Overrides the computed operating frequency outright (MHz). It does not offset the calculated value — leave at 0 unless you know you need a specific frequency | 0 (use calculated) | | Użyj predefiniowanych ustawień | On by default. Turn it off to set Spread Factor, Coding Rate and Bandwidth by hand instead of taking them from the modem preset | On | -| Spread Factor | Manual mode only: 7–12. Higher spreads further but slower | From preset | +| Spread Factor | Manual mode only: 5–12. Higher spreads further but slower. On SX127x (RF95) radios the firmware does not accept 5 or 6 and uses 11 instead | From preset | | Szybkość kodowania | Manual mode only: 5–8. More redundancy costs airtime | From preset | | Pasmo | Manual mode only: the channel bandwidth in kHz, typed in directly. On the 2.4 GHz region the app offers a list of the bandwidths your radio supports instead, and a stored value that is not on that list shows as _Unsupported_ and blocks saving until you pick a supported one | From preset | | Slot częstotliwości | Which slot within the region's band to use. 0 derives it from the primary channel name | 0 (automatic) | @@ -111,13 +111,13 @@ The Lite, Narrow, Medium Turbo, and Tiny presets need firmware 2.8 or newer — | Short Slow | ~5 km | 6.25 kbps | −10 dB | Suburban short-range; moderate building density | | Medium Fast | ~5 km | 3.52 kbps | −12.5 dB | Suburban areas; moderate building density | | Medium Slow | ~8 km | 1.95 kbps | −15 dB | Suburban/rural; moderate range with slower speed | -| Long Turbo | ~10 km | 1.34 kbps | −12.5 dB | Similar range to Long Fast but with 500 kHz bandwidth; faster throughput | +| Long Turbo | ~10 km | 1.34 kbps | −17.5 dB | Similar range to Long Fast but with 500 kHz bandwidth; faster throughput | | Long Fast | ~10 km | 1.1 kbps | −17.5 dB | **General use (default)** — balanced range and speed | | Long Moderate | ~20 km | 0.34 kbps | −17.5 dB | Rural with some terrain; occasional use | | Lite Fast | ~5 km | 1.76 kbps | −12.5 dB | EU 866 MHz SRD band (125 kHz BW); comparable to Medium Fast | | Lite Slow | ~10 km | 0.98 kbps | −15 dB | EU 866 MHz SRD band (125 kHz BW); comparable to Long Fast | -| Narrow Fast | ~5 km | 2.28 kbps | −10 dB | EU 868 MHz band (62.5 kHz BW); avoids interference with other devices | -| Narrow Slow | ~10 km | 1.30 kbps | −12.5 dB | EU 868 MHz band (62.5 kHz BW); comparable to Long Fast | +| Narrow Fast | ~5 km | 2.28 kbps | −7.5 dB | EU 868 MHz band (62.5 kHz BW); avoids interference with other devices | +| Narrow Slow | ~10 km | 1.30 kbps | −10 dB | EU 868 MHz band (62.5 kHz BW); comparable to Long Fast | | Medium Turbo | ~5 km | 7.0 kbps | −12.5 dB | Like Medium Fast but with 500 kHz bandwidth; not legal in every region. Needs firmware 2.8 or newer | | Tiny Fast | ~10 km | 0.68 kbps | −7.5 dB | Amateur bands that cap occupied bandwidth; these presets use 15.6 kHz. Needs firmware 2.8 or newer, an SX126x or SX127x radio, and a TCXO of ±5 ppm or better | | Tiny Slow | ~20 km | 0.33 kbps | −10 dB | Same band restrictions as Tiny Fast, longer range. Same firmware, radio, and TCXO requirements | diff --git a/docs/pl-rPL/user/signal-meter.md b/docs/pl-rPL/user/signal-meter.md index fefb7c055f..13dce09251 100644 --- a/docs/pl-rPL/user/signal-meter.md +++ b/docs/pl-rPL/user/signal-meter.md @@ -2,7 +2,7 @@ title: How the Meshtastic Signal Meter Works parent: User Guide nav_order: 15 -last_updated: 2026-08-30 +last_updated: 2026-09-09 description: How the signal meter rates quality from SNR relative to the LoRa modem preset — spread spectrum, presets, and what the bars really mean. aliases: - signal @@ -32,7 +32,7 @@ Each modem preset has an SNR limit: the lowest SNR at which that preset can stil ## Rating Signal Quality -The app rates signal quality (None, Bad, Fair, or Good) from SNR alone, measured against the active preset's SNR limit. It does not factor in RSSI: without knowing the local noise floor, RSSI alone cannot say whether a signal is decodable. RSSI is still available — on the node detail screen and in the metrics charts. +The app rates signal quality (None, Bad, Fair, or Good) from SNR relative to the active preset's SNR limit. When RSSI and a noise-floor reading are both available, it also rates the difference between them against that limit and uses the worse rating. Otherwise, it uses SNR alone. Because the rating is relative to the preset, the same SNR rates differently on different presets. An SNR of −16 dB rates Good on Long Fast (SNR limit −17.5 dB) but None on Short Fast (SNR limit −7.5 dB). Let `limit` be the active preset's SNR limit: @@ -45,7 +45,7 @@ Because the rating is relative to the preset, the same SNR rates differently on The icon never goes blank, so count the bars carefully: a single bar means None, not a weak but usable link, and Good is a solid wedge rather than a set of bars. A gray three-bar icon labeled Unknown is a different state again — the packet carried no SNR measurement at all, which is not the same as measuring one and finding it too weak. -> ℹ️ **Note:** Traceroute hop colors use fixed thresholds (−7 dB / −15 dB); the per-node signal meter uses the preset-relative rating instead. +> ℹ️ **Note:** Traceroute and neighbor-info SNR colors use the active preset's limits too. RSSI text has separate strength colors: green above −115 dBm, yellow above −120 dBm, orange above −126 dBm, and red at or below −126 dBm. ## Diagnosing Local Interference diff --git a/docs/pl-rPL/user/tak.md b/docs/pl-rPL/user/tak.md index 9cbce7229b..f587000f36 100644 --- a/docs/pl-rPL/user/tak.md +++ b/docs/pl-rPL/user/tak.md @@ -2,7 +2,7 @@ title: TAK Integration parent: User Guide nav_order: 10 -last_updated: 2026-08-30 +last_updated: 2026-09-11 description: Interoperate with ATAK and WinTAK — CoT position sharing, TAK roles, and plugin setup. aliases: - tak @@ -36,7 +36,7 @@ The TAK module allows Meshtastic nodes to: > ⚠️ **Warning:** The old **Meshtastic ATAK Plugin** is no longer part of this path and cannot > work. It bridged through the cross-process AIDL API, which was removed in app 2.8.0; the mesh > service is now in-process only. Do not install it. Interop today runs over the app's own local -> TAK server plus the Mesh to CoT Converter, both described below, with stock ATAK/iTAK/WinTAK. +> TAK server plus the Mesh to CoT Converter, both described in the following sections, with stock ATAK/iTAK/WinTAK. ### Konfiguracja diff --git a/docs/pl-rPL/user/translate.md b/docs/pl-rPL/user/translate.md index 17a179963b..6d2288e419 100644 --- a/docs/pl-rPL/user/translate.md +++ b/docs/pl-rPL/user/translate.md @@ -2,7 +2,7 @@ title: Translate the App parent: User Guide nav_order: 17 -last_updated: 2026-08-30 +last_updated: 2026-09-11 description: How the app and its documentation are translated via Crowdin, and guidelines for contributing translations. aliases: - translate @@ -45,7 +45,7 @@ If your language is not yet listed on Crowdin: The Android app uses **Compose Multiplatform resources** for all user-visible strings: -``` +```text core/resources/src/commonMain/composeResources/ ├── values/ ← English (default) │ └── strings.xml @@ -58,7 +58,7 @@ core/resources/src/commonMain/composeResources/ In-app documentation follows a similar pattern under `docs/`: -``` +```text docs/ ├── en/user/ ← English source (default) │ ├── onboarding.md diff --git a/docs/pt-rBR/user/desktop.md b/docs/pt-rBR/user/desktop.md index c24ebdb9e1..c8a13e8d02 100644 --- a/docs/pt-rBR/user/desktop.md +++ b/docs/pt-rBR/user/desktop.md @@ -2,7 +2,7 @@ title: Desktop App parent: User Guide nav_order: 14 -last_updated: 2026-08-30 +last_updated: 2026-09-11 description: Install and use the Meshtastic Desktop app on Linux, macOS, and Windows — connections, feature parity, and keyboard shortcuts. aliases: - desktop @@ -58,20 +58,20 @@ Bluetooth Low Energy is supported on desktop via the [Kable](https://github.com/ ## Feature Parity -| Feature | Android | Desktop | Notes | -| ----------------------------------------------------- | ------- | ------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Messaging | ✓ | ✓ | Full parity | -| Node List | ✓ | ✓ | Full parity | -| Mapa | ✓ | ✓ | Interactive MapLibre map, with base map and overlay pickers and custom tile sources. No offline downloads or local `.mbtiles` archives | -| Map layers (`.kml`/`.kmz`/GeoJSON) | ✓ | ✓ | Same layer store and sheet as Android; imported files draw on the desktop map | -| Site Planner | ✓ | ✓\* | \*Opens in your browser on desktop; the estimate is not drawn on the desktop map | -| Configurações | ✓ | ✓ | Full parity | -| Bluetooth (BLE) | ✓ | ✓ | Via Kable on desktop | -| Firmware Update | ✓ | ✓ | In-app USB, BLE, and Wi-Fi (ESP32) update work the same as Android. The USB maintenance flow — nRF52/RP2040 factory erase and bootloader upgrade — is Android-only | -| Notifications | ✓ | ✓ | Native OS notifications | -| Widgets | ✓ | ✗ | Android-only | -| AI Assistant (Chirpy) | ✓\* | ✗ | Google flavor Android only | -| App Functions (system AI) | ✓† | ✗ | Google flavor Android only | +| Feature | Android | Desktop | Notes | +| ----------------------------------------------------- | ------- | ------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Messaging | ✓ | ✓ | Full parity | +| Node List | ✓ | ✓ | Full parity | +| Mapa | ✓ | ✓ | Interactive MapLibre map, with base map and overlay pickers and custom tile sources. Offline terrain (hillshade and contours) can be downloaded; offline base-map packs and local `.mbtiles` archives cannot | +| Map layers (`.kml`/`.kmz`/GeoJSON) | ✓ | ✓ | Same layer store and sheet as Android; imported files draw on the desktop map | +| Site Planner | ✓ | ✓\* | \*Opens in your browser on desktop; the estimate is not drawn on the desktop map | +| Configurações | ✓ | ✓ | Full parity | +| Bluetooth (BLE) | ✓ | ✓ | Via Kable on desktop | +| Firmware Update | ✓ | ✓ | In-app USB, BLE, and Wi-Fi (ESP32) update work the same as Android. The USB maintenance flow — nRF52/RP2040 factory erase and bootloader upgrade — is Android-only | +| Notifications | ✓ | ✓ | Native OS notifications | +| Widgets | ✓ | ✗ | Android-only | +| AI Assistant (Chirpy) | ✓\* | ✗ | Google flavor Android only | +| App Functions (system AI) | ✓† | ✗ | Google flavor Android only | \*Chirpy AI requires Android 14+ on Google flavor builds with supported hardware. @@ -122,7 +122,7 @@ Individual doc pages render with full formatting: ## Building from Source -```bash +```shell git clone https://github.com/meshtastic/Meshtastic-Android.git cd Meshtastic-Android ./gradlew :desktopApp:run @@ -135,7 +135,7 @@ Requirements: ## Known Limitations -- Offline tile downloads and local `.mbtiles` archives are not available on desktop. +- Offline base-map downloads and local `.mbtiles` archives are not available on desktop. Offline terrain is — see [Map & Waypoints](map-and-waypoints). - `.kml`/`.kmz`/GeoJSON layer import works — see [Map & Waypoints](map-and-waypoints#map-layers). Site Planner opens in your browser rather than in the app; to bring its coverage estimate onto the map, click the transmitter pin diff --git a/docs/pt-rBR/user/discovery.md b/docs/pt-rBR/user/discovery.md index baf0f92a38..5a792ebe8e 100644 --- a/docs/pt-rBR/user/discovery.md +++ b/docs/pt-rBR/user/discovery.md @@ -2,7 +2,7 @@ title: Local Mesh Discovery parent: User Guide nav_order: 12 -last_updated: 2026-08-30 +last_updated: 2026-09-11 description: Explore your mesh network — the Local Mesh Discovery scanner, traceroute paths, neighbor maps, and node discovery tools. aliases: - discovery @@ -97,7 +97,7 @@ Mesh Beacon lets nodes invite others to join their mesh. A beaconing node period Configure it under **Settings → Module configuration → Mesh Beacon**. The entry appears only on radios running firmware 2.8.0 or newer. A read-only **Region** row at the top of the screen shows the region the beacon advertises: that region, and the preset, are always the ones the radio itself uses, so a beacon cannot invite anyone onto settings your radio is not running. - **Listen for beacons** — receive invitations broadcast by other nodes. -- **Broadcast a beacon** — periodically advertise this mesh to nearby nodes, with an optional **Beacon message** of up to 100 bytes, a **Broadcast interval** picked from fixed intervals between 1 hour and 72 hours, and an **Offered channel** chosen from your radio's own channels. The offered channel is required, and defaults to your primary channel. +- **Broadcast a beacon** — periodically advertise this mesh to nearby nodes, with an optional **Beacon message** of up to 100 bytes, a **Broadcast interval** picked from fixed intervals between 1 hour and 72 hours, and an **Offered channel** chosen from your radio's own channels. The offered channel is required, and defaults to your primary channel. Over remote admin the picker offers the primary channel only. - **Broadcast targets** — optional extra destinations beyond the offered channel. **Add target** appends a row; each row picks a **Channel** and a **Transmit preset**, and **Remove target** deletes it. With no targets, the beacon goes out on the offered channel alone. Two conditions block beacon setup: diff --git a/docs/pt-rBR/user/help-and-docs.md b/docs/pt-rBR/user/help-and-docs.md index f306cde7f7..c83898cea3 100644 --- a/docs/pt-rBR/user/help-and-docs.md +++ b/docs/pt-rBR/user/help-and-docs.md @@ -2,7 +2,7 @@ title: Help & In-App Docs parent: User Guide nav_order: 21 -last_updated: 2026-08-30 +last_updated: 2026-09-11 description: Browse this documentation inside the app, search it, and ask Chirpy — the on-device AI assistant — questions about Meshtastic. aliases: - help @@ -23,7 +23,7 @@ The docs browser lists every user-guide page. Tap a page to read it; images and ### Search -The search box sits at the top of the docs browser. Type in it to filter pages by title and keywords — results update as you type, and the **Clear search** button (✕) empties the box. +Tap the search field at the top of the docs browser to open a full-screen search view. Type to filter pages by title and keywords — results update as you type, under the same field. The back arrow, or the keyboard's search action, closes the view. The field hides while you scroll down the page list and comes back when you scroll up. ![Searching the in-app documentation](../../assets/screenshots/docs-browser_search.png) diff --git a/docs/pt-rBR/user/map-and-waypoints.md b/docs/pt-rBR/user/map-and-waypoints.md index 37b9660b59..eb83cd0514 100644 --- a/docs/pt-rBR/user/map-and-waypoints.md +++ b/docs/pt-rBR/user/map-and-waypoints.md @@ -2,7 +2,7 @@ title: Map & Waypoints parent: User Guide nav_order: 6 -last_updated: 2026-09-01 +last_updated: 2026-09-11 description: View node positions on the map, create and share waypoints, manage map layers and Site Planner, and control position sharing and privacy. aliases: - map @@ -44,7 +44,7 @@ The floating toolbar provides quick access to the compass, the map type and laye ### Filtering the Map -Tap the filter button in the floating toolbar to open **Filter map**. **Display** controls what is drawn: **Only Favorites**, **Show Waypoints**, **Show Precision Circles**, and a slider that hides nodes not heard from recently. **Node roles** is a chip per device role, plus **All** to show every role; a selected chip means that role is shown. **Nodes** narrows the set further with **Hide offline nodes**, **Only show direct nodes**, **Exclude MQTT**, **Show ignored nodes**, and **Include unknown**. +Tap the filter button in the floating toolbar to open **Filter map**. **Display** controls what is drawn: **Only Favorites**, **Show Waypoints**, **Show Precision Circles**, and a slider that hides nodes not heard from recently. **Node roles** is a chip per device role, plus **All** to show every role; a selected chip means that role is shown. **Nodes** narrows the set further with **Hide offline nodes**, **Only show direct nodes**, **Signed only**, **Encrypted only**, **Exclude MQTT**, **Show ignored nodes**, and **Include unknown** — the same rules as the node list's [filter toggles](nodes). A dot on the filter button means at least one filter is hiding something — check it before concluding the mesh is quiet. Turning **Show Waypoints** off hides every waypoint, including your own. **Show ignored nodes** adds them to the map rather than showing only them — unlike the node list's **Only show ignored Nodes**. @@ -147,8 +147,9 @@ picker, all three offer the same raster base maps: Overlays can be toggled on top of any base map, from the layers sheet: - **Weather radar** — NOAA NEXRAD reflectivity (US coverage) -- **Hillshade** — terrain relief, on **F-Droid** and **Desktop** only. Useful for understanding why a - link fails, since LoRa range is limited by terrain +- **Hillshade** — terrain relief from an online tile source, on **F-Droid** and **Desktop** only; Google + Play draws hillshade from downloaded offline terrain instead. Useful for understanding why a link + fails, since LoRa range is limited by terrain ### Adding your own tile source @@ -156,7 +157,7 @@ Any XYZ tile endpoint can be added as a base map, on every flavor and on desktop Tile Sources** at the foot of the base map picker and paste a URL template using `{z}`, `{x}` and `{y}` — plus `{s}` if the provider uses rotating subdomains. A national mapping service, for example: -``` +```text https://wmts.geo.admin.ch/1.0.0/ch.swisstopo.pixelkarte-farbe/default/current/3857/{z}/{x}/{y}.jpeg ``` @@ -173,7 +174,18 @@ On **Google Play**, the layers sheet's **Offline Manager** section downloads its — water, roads and administrative boundaries extracted from the public Protomaps basemap dataset, drawn directly as map shapes rather than raster tiles. Frame the area you want, tap **Start Download**, and once it finishes flip **Show on map** to draw it (it's off by default so it never surprises you by covering up the -live map). You can still import a pre-made `.mbtiles` archive here too, as before. **Desktop** has neither. +live map). You can still import a pre-made `.mbtiles` archive here too, as before. **Desktop** has neither, though +the offline terrain described next does work there. + +### Offline terrain + +Hillshade and elevation contours can be downloaded for offline use as well, so a downloaded area still +shows why a link fails when there is no network. The terrain data comes from Mapterhorn and is credited on +the map. On **Google Play**, each downloaded region in the **Offline Manager** list has a **Download Terrain** +button and **Hillshade** and **Contours** switches; terrain attaches to that region and shares its storage limit, and an area +too large for terrain asks you to zoom in or pick a smaller region. On **F-Droid** and **Desktop**, the +layers sheet has an **Offline Terrain** section of its own: frame the area, tap **Start Download**, and the +hillshade and contours draw from the downloaded tiles from then on. ### Going offline @@ -181,8 +193,8 @@ An **Offline** pill appears over the map whenever the device has no network conn builds, if you've imported a local `.mbtiles` archive, the map switches to it automatically the moment the network drops — no toggle to remember — and switches back once you're reconnected, as long as you haven't picked a different base map yourself in between. On **F-Droid**, a downloaded offline area keeps rendering on -its own; the pill is purely informational there. **Desktop** has no offline downloads at all yet, so the pill -is informational there too. +its own; the pill is purely informational there. **Desktop** downloads offline terrain but no base-map +packs, so the pill is informational there too. ## Related Topics diff --git a/docs/pt-rBR/user/messages-and-channels.md b/docs/pt-rBR/user/messages-and-channels.md index 954496a5db..3d4e137868 100644 --- a/docs/pt-rBR/user/messages-and-channels.md +++ b/docs/pt-rBR/user/messages-and-channels.md @@ -2,7 +2,7 @@ title: Messages & Channels parent: User Guide nav_order: 3 -last_updated: 2026-08-30 +last_updated: 2026-09-14 description: Send and receive messages, manage channels, configure encryption, search conversations, and use quick chat, reactions, and message actions. aliases: - channels @@ -50,6 +50,21 @@ Key length alone does not change the icon: a 128-bit key and a 256-bit key both Tapping an existing channel opens the same editor, where you can change the name, the PSK, MQTT uplink and downlink, and position precision. Every edit on this screen — adding, editing, deleting, or dragging a channel into a new order — waits on **Send** the same way. +### Archived Channels + +A conversation belongs to the channel it was held on, not to the slot that channel occupied. When a channel leaves your radio — you scan a different channel QR, replace a channel's key, or switch the modem preset — its conversation is **archived** rather than deleted or merged into whatever takes the slot. + +An archived conversation: + +- stays in the **Channels** list under the name the channel had, below the channels currently on your radio, marked with a history icon; +- keeps its full message history, searchable as usual; +- is read-only — you cannot send, reply, or react, because the channel is no longer on the radio to send on, and it is not offered as a share target; +- comes back automatically if you re-add that exact channel, taking its messages back to the live slot. + +Changing the modem preset archives the channel too: the preset is part of a default channel's identity, so **LongFast** and **MediumFast** are different channels even though neither the name nor the key changed. Changing only the region does not archive anything. + +To remove an archived conversation for good, long-press it in the conversation list and delete it. + ## Direct Messages Direct messages (DMs) go to one specific node. When both radios hold each other's public keys, your radio encrypts the message to that node's public key, so no one else on the mesh can read it — not even nodes that share your channel. diff --git a/docs/pt-rBR/user/mqtt.md b/docs/pt-rBR/user/mqtt.md index 713c2c300c..549856c18d 100644 --- a/docs/pt-rBR/user/mqtt.md +++ b/docs/pt-rBR/user/mqtt.md @@ -2,7 +2,7 @@ title: MQTT parent: User Guide nav_order: 11 -last_updated: 2026-08-30 +last_updated: 2026-09-11 description: Bridge your mesh to the internet — MQTT broker setup, encryption layers, and map reporting. aliases: - mqtt @@ -25,7 +25,7 @@ The MQTT module connects your node to an MQTT broker, allowing: ## How It Works -``` +```text [Your Node] → Radio → [Gateway Node with Wi-Fi] → MQTT Broker → [Remote Gateway] → Radio → [Remote Node] ``` diff --git a/docs/pt-rBR/user/node-metrics.md b/docs/pt-rBR/user/node-metrics.md index e5159adbad..62c7790ff3 100644 --- a/docs/pt-rBR/user/node-metrics.md +++ b/docs/pt-rBR/user/node-metrics.md @@ -2,7 +2,7 @@ title: Node Metrics parent: User Guide nav_order: 5 -last_updated: 2026-08-30 +last_updated: 2026-09-09 description: Telemetry dashboards for each mesh node — device health, environment sensors, air quality, signal quality, power, traceroute, and position history. aliases: - metrics @@ -114,7 +114,7 @@ Radio signal quality information: ### Signal Quality Reference -Signal quality is rated from **SNR relative to the active LoRa modem preset's demodulation floor**, not from fixed thresholds — a given SNR means different things on different presets (e.g. −15 dB is fine on LongSlow but unusable on ShortFast). RSSI is shown but is not part of the rating. In the table, _limit_ is the preset's SNR limit. +Signal quality is rated from **SNR relative to the active LoRa modem preset's demodulation floor**, not from fixed thresholds — a given SNR means different things on different presets (e.g. −15 dB is fine on LongSlow but unusable on ShortFast). When RSSI and a noise-floor reading are both available, the app also rates their difference against the preset limit and uses the worse rating. Otherwise, RSSI is display-only. In the table, _limit_ is the preset's SNR limit. | Quality | Criteria | | ------- | ---------------------------------------------------------------- | diff --git a/docs/pt-rBR/user/nodes.md b/docs/pt-rBR/user/nodes.md index fd9da5a982..500600fbc3 100644 --- a/docs/pt-rBR/user/nodes.md +++ b/docs/pt-rBR/user/nodes.md @@ -2,7 +2,7 @@ title: Nós parent: User Guide nav_order: 4 -last_updated: 2026-09-04 +last_updated: 2026-09-11 description: Browse, filter, and sort mesh nodes — view details, signal quality, roles, and quick actions. aliases: - node-list @@ -74,19 +74,23 @@ Most users should keep the default **Client** role. Consider a different role wh > 💡 **Tip:** The mesh works best when most nodes are **Client** or **Router**. Too many Client Mute nodes reduce mesh resilience; too many Routers in a dense area can cause congestion. A good rule of thumb: one Router per 5–10 Clients in your area. -### Encryption Indicators +### Security indicators -Nodes display encryption status icons next to their name: +Each node carries one security icon beside its name in the node list. Tap it to read what it means, and choose **Show All Meanings** in that dialog for the full legend. The detail screen shows the same state in words, as a **Security** row that opens the same dialog. -| Icon | Meaning | -| ----------- | ------------------------------------------------------------------------------------------------------------------------------------------------- | -| 🔒 Locked | Communication uses PKI (public key infrastructure) — end-to-end encrypted with verified identity | -| 🔓 Unlocked | No public key has been received for this node, so it cannot be direct messaged — use **Request User Info** on the node detail page to ask for one | -| ⚠️ Mismatch | Public key mismatch — the node's key has changed since last seen (investigate before trusting) | +| Icon | Meaning | Shown for | +| --------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------- | +| Person with a shield check (green) | **Verified contact** — you verified this node's key in person by exchanging contact QR codes, so its identity is confirmed. The strongest trust the list shows. Your own connected node carries it too | Any firmware version | +| Nodes icon with a shield check (green) | **Signed node** — this node signs its broadcasts with its identity key, so its identity is consistent over time, but you have not verified it in person. On firmware 2.8 the icon appears from the version alone, before any signed broadcast has been heard | Firmware 2.8 or newer, and any node whose signed broadcast your node has verified | +| 🔒 Closed lock | A public key is on file and matches, so direct messages to this node are encrypted | Firmware before 2.8, or no reported version | +| 🔓 Open lock | No public key has been received for this node, so it cannot be direct messaged — use **Request User Info** on the node detail page to ask for one | Firmware before 2.8, or no reported version | +| ⚠️ Mismatch | **Public key mismatch** — a different key arrived for this node after one was stored. Investigate before trusting | Any firmware version | -> 💡 **Tip:** Direct messages always use PKI, so the radio needs the other node's public key before it can send one. It refuses the send rather than falling back to channel encryption. Keys arrive inside node info, which is why an open lock usually clears itself once that node is heard from properly. If you see a key mismatch warning, the node may have been reset or compromised. +Every node on firmware 2.8 or newer signs its broadcasts, so on that firmware the signed state is the baseline and the locks are not shown. -To clear a mismatch, first confirm through another trusted channel that the key change was intentional — a factory reset causes one. Then touch & hold the node, choose **Remove**, and let the two radios exchange keys again the next time yours hears it. +Direct messages always use public-key encryption, so your node needs the other node's public key before it can send one. It refuses the send rather than falling back to channel encryption. Keys arrive inside node info, which is why an open lock usually clears itself once that node is heard from properly. + +A mismatch never replaces the key you already hold. The app keeps the first key it recorded and refuses the new one, as the firmware does, so a stray or hostile node info cannot silently break encrypted messaging to a contact. To clear a mismatch, first confirm through another trusted channel that the key change was intentional — a factory reset causes one. Then touch & hold the node, choose **Remove**, and let the two nodes exchange keys again the next time yours hears it. ## Quick Actions @@ -108,7 +112,9 @@ radio is connected and running firmware 2.8 or newer — see ## Sharing a Contact -On a node's detail screen, tap **Share Contact** to produce a link and a QR code for that node. From the same dialog, **Write to NFC tag** saves the link to a writable NFC tag that anyone can tap to open. +On a node's detail screen, tap **Share Contact** to produce a link and a QR code for that node. From the same dialog, **Share link** opens the Android share sheet (on desktop it copies the link instead), **Write to NFC tag** saves it to a writable NFC tag, and **Copy** puts it on the clipboard. While that dialog is open and in front of you, the phone also offers the same link to any NFC reader, so someone can take the contact by tapping their phone against yours with no tag involved. + +Sharing your own contact this way marks it as verified in person, so whoever imports it sees the **Verified contact** icon rather than the signed one. Relaying someone else's contact passes on only what your app had already recorded about them. To add someone else's contact, use the import button on the node list and choose **Scan Shared Contact QR Code**, **Scan Shared Contact NFC**, or **Input Shared Contact URL**. The app asks you to confirm with **Import Shared Contact?**, and warns you when the contact is one you already have. @@ -120,14 +126,16 @@ Type in the search field to filter nodes by name or short name. The filter updat ### Filter Toggles -| Filtro | Descrição | -| --------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| **Hide offline nodes** | Show only nodes heard within the last 2 hours | -| **Only show direct nodes** | Show only nodes your radio heard directly, with no relay in between | -| **Include unknown** | Show nodes that haven't sent user info yet. **On by default**, so a node heard before its info arrives stays visible; these carry a badge marking them incomplete, and cannot be direct messaged until their user info brings a public key | -| **Exclude infrastructure** | Hide infrastructure-role nodes (Router, Router Late, Client Base, and legacy Repeater nodes) and any node that cannot be messaged, whatever its role | -| **Exclude MQTT** | Hide nodes heard only via MQTT internet bridge | -| **Only show ignored Nodes** | Replace the list with the nodes you have ignored. Every other node is hidden while this is on, and a banner appears at the top of the list to take you back | +| Filtro | Descrição | +| --------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **Hide offline nodes** | Show only nodes heard within the last 2 hours | +| **Only show direct nodes** | Show only nodes your radio heard directly, with no relay in between | +| **Include unknown** | Show nodes that haven't sent user info yet. **On by default**, so a node heard before its info arrives stays visible; these carry a badge marking them incomplete, and cannot be direct messaged until their user info brings a public key | +| **Exclude infrastructure** | Hide infrastructure-role nodes (Router, Router Late, Client Base, and legacy Repeater nodes) and any node that cannot be messaged, whatever its role | +| **Exclude MQTT** | Hide nodes heard only via MQTT internet bridge | +| **Signed only** | Show only nodes whose signed broadcasts your node has actually heard and verified. Stricter than the icon: a node on 2.8 shows as signed by its firmware version before any signed broadcast arrives, and a contact verified in person on older firmware is not signed at all | +| **Encrypted only** | Show nodes with a matching public key on file, the key an encrypted direct message needs. A node with a key mismatch is excluded | +| **Only show ignored Nodes** | Replace the list with the nodes you have ignored. Every other node is hidden while this is on, and a banner appears at the top of the list to take you back | ### Sort Options @@ -149,6 +157,8 @@ Tap the hop-histogram icon in the node list's app bar to open a bar chart of how Tapping a node opens the detail view with comprehensive information. See [Node Metrics](node-metrics) for full details on metrics and telemetry. +Signal quality is rated against your modem preset. The same SNR can be good on a long-range preset and poor on a faster one. Traceroute and neighbor-info SNR colors use that preset too. RSSI text has its own strength colors; it affects the quality rating only when a noise-floor reading is also available. + The Details card carries the node's short name, role, IDs, last heard time, hops away, uptime, and its SNR and RSSI: ![Node detail section](../../assets/screenshots/nodes_detail_section.png) diff --git a/docs/pt-rBR/user/settings-module-admin.md b/docs/pt-rBR/user/settings-module-admin.md index 04abf71695..d258aedf12 100644 --- a/docs/pt-rBR/user/settings-module-admin.md +++ b/docs/pt-rBR/user/settings-module-admin.md @@ -2,7 +2,7 @@ title: Settings — Modules & Admin parent: User Guide nav_order: 8 -last_updated: 2026-08-30 +last_updated: 2026-09-16 description: Configure optional feature modules (MQTT, telemetry, canned messages, TAK, and more) and perform device administration. aliases: - modules @@ -333,7 +333,7 @@ appear only when your own node is selected: ### Sobre -**Settings → About** carries the app's own identity rather than the radio's: +**Settings → About** carries the app's own identity rather than the node's: Three sections: @@ -341,7 +341,11 @@ Three sections: - **Apps** — opens with **Need Hardware?**, a rotating carousel of popular devices that links out to where to buy one. It also lists the GitHub repository, the running app version, and **Acknowledgements** (see the next section). -- **Project information** — links to the website and to this documentation. +- **Project information** — links to the website, to this documentation, and to the GNU GPL v3 + license the app is released under. + +The footer states the copyright and that the app is free software with no warranty, which you +may redistribute under the same license. ### Agradecimentos diff --git a/docs/pt-rBR/user/settings-radio-user.md b/docs/pt-rBR/user/settings-radio-user.md index 9220cb424a..95ada23a3f 100644 --- a/docs/pt-rBR/user/settings-radio-user.md +++ b/docs/pt-rBR/user/settings-radio-user.md @@ -2,7 +2,7 @@ title: Settings — Radio & User parent: User Guide nav_order: 7 -last_updated: 2026-09-04 +last_updated: 2026-09-11 description: Configure your radio hardware, LoRa presets, user profile, position sharing, power management, and security. aliases: - configurações @@ -51,7 +51,7 @@ The footer appears as soon as you change something. **Discard** throws the chang The status message is saved with the same **Save**, but it never reboots the node — and, like the rest of this screen, it can be edited on a remote node you administer. For your own radio there is a shortcut while it is connected: touch & hold your node in the [node list](nodes.md) and choose -**Update status**. Older firmware and a disconnected radio have no shortcut — the field above is +**Update status**. Older firmware and a disconnected node have no shortcut — the field itself is still the way in. ## Configuration @@ -77,13 +77,13 @@ On **Settings → LoRa**. | Setting | Descrição | Padrão | | --------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------- | -| Região | Regulatory region for frequency bands. You must set this before transmitting | Unset (must configure) | +| Região | Regulatory region for frequency bands. You must set this before transmitting. On firmware 2.8 or newer, the first region set also creates the node's identity key and gives it a new node number | Unset (must configure) | | Presets | Speed/range tradeoff | LongFast | | Number of Hops | Maximum retransmit hops | 3 | | Transmit Power | Transmission power (dBm); 0 = max allowed for region | 0 (region max) | | Frequency Override | Overrides the computed operating frequency outright (MHz). It does not offset the calculated value — leave at 0 unless you know you need a specific frequency | 0 (use calculated) | | Use Preset | On by default. Turn it off to set Spread Factor, Coding Rate and Bandwidth by hand instead of taking them from the modem preset | On | -| Spread Factor | Manual mode only: 7–12. Higher spreads further but slower | From preset | +| Spread Factor | Manual mode only: 5–12. Higher spreads further but slower. On SX127x (RF95) radios the firmware does not accept 5 or 6 and uses 11 instead | From preset | | Coding Rate | Manual mode only: 5–8. More redundancy costs airtime | From preset | | Largura da banda | Manual mode only: the channel bandwidth in kHz, typed in directly. On the 2.4 GHz region the app offers a list of the bandwidths your radio supports instead, and a stored value that is not on that list shows as _Unsupported_ and blocks saving until you pick a supported one | From preset | | Frequency Slot | Which slot within the region's band to use. 0 derives it from the primary channel name | 0 (automatic) | @@ -111,13 +111,13 @@ The Lite, Narrow, Medium Turbo, and Tiny presets need firmware 2.8 or newer — | Short Slow | ~5 km | 6.25 kbps | −10 dB | Suburban short-range; moderate building density | | Medium Fast | ~5 km | 3.52 kbps | −12.5 dB | Suburban areas; moderate building density | | Medium Slow | ~8 km | 1.95 kbps | −15 dB | Suburban/rural; moderate range with slower speed | -| Long Turbo | ~10 km | 1.34 kbps | −12.5 dB | Similar range to Long Fast but with 500 kHz bandwidth; faster throughput | +| Long Turbo | ~10 km | 1.34 kbps | −17.5 dB | Similar range to Long Fast but with 500 kHz bandwidth; faster throughput | | Long Fast | ~10 km | 1.1 kbps | −17.5 dB | **General use (default)** — balanced range and speed | | Long Moderate | ~20 km | 0.34 kbps | −17.5 dB | Rural with some terrain; occasional use | | Lite Fast | ~5 km | 1.76 kbps | −12.5 dB | EU 866 MHz SRD band (125 kHz BW); comparable to Medium Fast | | Lite Slow | ~10 km | 0.98 kbps | −15 dB | EU 866 MHz SRD band (125 kHz BW); comparable to Long Fast | -| Narrow Fast | ~5 km | 2.28 kbps | −10 dB | EU 868 MHz band (62.5 kHz BW); avoids interference with other devices | -| Narrow Slow | ~10 km | 1.30 kbps | −12.5 dB | EU 868 MHz band (62.5 kHz BW); comparable to Long Fast | +| Narrow Fast | ~5 km | 2.28 kbps | −7.5 dB | EU 868 MHz band (62.5 kHz BW); avoids interference with other devices | +| Narrow Slow | ~10 km | 1.30 kbps | −10 dB | EU 868 MHz band (62.5 kHz BW); comparable to Long Fast | | Medium Turbo | ~5 km | 7.0 kbps | −12.5 dB | Like Medium Fast but with 500 kHz bandwidth; not legal in every region. Needs firmware 2.8 or newer | | Tiny Fast | ~10 km | 0.68 kbps | −7.5 dB | Amateur bands that cap occupied bandwidth; these presets use 15.6 kHz. Needs firmware 2.8 or newer, an SX126x or SX127x radio, and a TCXO of ±5 ppm or better | | Tiny Slow | ~20 km | 0.33 kbps | −10 dB | Same band restrictions as Tiny Fast, longer range. Same firmware, radio, and TCXO requirements | diff --git a/docs/pt-rBR/user/signal-meter.md b/docs/pt-rBR/user/signal-meter.md index 86a3f20270..f9c17bdd15 100644 --- a/docs/pt-rBR/user/signal-meter.md +++ b/docs/pt-rBR/user/signal-meter.md @@ -2,7 +2,7 @@ title: How the Meshtastic Signal Meter Works parent: User Guide nav_order: 15 -last_updated: 2026-08-30 +last_updated: 2026-09-09 description: How the signal meter rates quality from SNR relative to the LoRa modem preset — spread spectrum, presets, and what the bars really mean. aliases: - signal @@ -32,7 +32,7 @@ Each modem preset has an SNR limit: the lowest SNR at which that preset can stil ## Rating Signal Quality -The app rates signal quality (None, Bad, Fair, or Good) from SNR alone, measured against the active preset's SNR limit. It does not factor in RSSI: without knowing the local noise floor, RSSI alone cannot say whether a signal is decodable. RSSI is still available — on the node detail screen and in the metrics charts. +The app rates signal quality (None, Bad, Fair, or Good) from SNR relative to the active preset's SNR limit. When RSSI and a noise-floor reading are both available, it also rates the difference between them against that limit and uses the worse rating. Otherwise, it uses SNR alone. Because the rating is relative to the preset, the same SNR rates differently on different presets. An SNR of −16 dB rates Good on Long Fast (SNR limit −17.5 dB) but None on Short Fast (SNR limit −7.5 dB). Let `limit` be the active preset's SNR limit: @@ -45,7 +45,7 @@ Because the rating is relative to the preset, the same SNR rates differently on The icon never goes blank, so count the bars carefully: a single bar means None, not a weak but usable link, and Good is a solid wedge rather than a set of bars. A gray three-bar icon labeled Unknown is a different state again — the packet carried no SNR measurement at all, which is not the same as measuring one and finding it too weak. -> ℹ️ **Note:** Traceroute hop colors use fixed thresholds (−7 dB / −15 dB); the per-node signal meter uses the preset-relative rating instead. +> ℹ️ **Note:** Traceroute and neighbor-info SNR colors use the active preset's limits too. RSSI text has separate strength colors: green above −115 dBm, yellow above −120 dBm, orange above −126 dBm, and red at or below −126 dBm. ## Diagnosing Local Interference diff --git a/docs/pt-rBR/user/tak.md b/docs/pt-rBR/user/tak.md index d86ceba6be..df6048197e 100644 --- a/docs/pt-rBR/user/tak.md +++ b/docs/pt-rBR/user/tak.md @@ -2,7 +2,7 @@ title: TAK Integration parent: User Guide nav_order: 10 -last_updated: 2026-08-30 +last_updated: 2026-09-11 description: Interoperate with ATAK and WinTAK — CoT position sharing, TAK roles, and plugin setup. aliases: - tak @@ -36,7 +36,7 @@ The TAK module allows Meshtastic nodes to: > ⚠️ **Warning:** The old **Meshtastic ATAK Plugin** is no longer part of this path and cannot > work. It bridged through the cross-process AIDL API, which was removed in app 2.8.0; the mesh > service is now in-process only. Do not install it. Interop today runs over the app's own local -> TAK server plus the Mesh to CoT Converter, both described below, with stock ATAK/iTAK/WinTAK. +> TAK server plus the Mesh to CoT Converter, both described in the following sections, with stock ATAK/iTAK/WinTAK. ### Configuration diff --git a/docs/pt-rBR/user/translate.md b/docs/pt-rBR/user/translate.md index 67d69e2d96..cdc8b6d87f 100644 --- a/docs/pt-rBR/user/translate.md +++ b/docs/pt-rBR/user/translate.md @@ -2,7 +2,7 @@ title: Translate the App parent: User Guide nav_order: 17 -last_updated: 2026-08-30 +last_updated: 2026-09-11 description: How the app and its documentation are translated via Crowdin, and guidelines for contributing translations. aliases: - translate @@ -45,7 +45,7 @@ If your language is not yet listed on Crowdin: The Android app uses **Compose Multiplatform resources** for all user-visible strings: -``` +```text core/resources/src/commonMain/composeResources/ ├── values/ ← English (default) │ └── strings.xml @@ -58,7 +58,7 @@ core/resources/src/commonMain/composeResources/ In-app documentation follows a similar pattern under `docs/`: -``` +```text docs/ ├── en/user/ ← English source (default) │ ├── onboarding.md diff --git a/docs/pt-rPT/user/desktop.md b/docs/pt-rPT/user/desktop.md index 14520436a0..3de0c40438 100644 --- a/docs/pt-rPT/user/desktop.md +++ b/docs/pt-rPT/user/desktop.md @@ -2,7 +2,7 @@ title: Desktop App parent: User Guide nav_order: 14 -last_updated: 2026-08-30 +last_updated: 2026-09-11 description: Install and use the Meshtastic Desktop app on Linux, macOS, and Windows — connections, feature parity, and keyboard shortcuts. aliases: - desktop @@ -58,20 +58,20 @@ Bluetooth Low Energy is supported on desktop via the [Kable](https://github.com/ ## Feature Parity -| Feature | Android | Desktop | Notes | -| ----------------------------------------------------- | ------- | ------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Messaging | ✓ | ✓ | Full parity | -| Node List | ✓ | ✓ | Full parity | -| Mapa | ✓ | ✓ | Interactive MapLibre map, with base map and overlay pickers and custom tile sources. No offline downloads or local `.mbtiles` archives | -| Map layers (`.kml`/`.kmz`/GeoJSON) | ✓ | ✓ | Same layer store and sheet as Android; imported files draw on the desktop map | -| Site Planner | ✓ | ✓\* | \*Opens in your browser on desktop; the estimate is not drawn on the desktop map | -| Definições | ✓ | ✓ | Full parity | -| Bluetooth (BLE) | ✓ | ✓ | Via Kable on desktop | -| Firmware Update | ✓ | ✓ | In-app USB, BLE, and Wi-Fi (ESP32) update work the same as Android. The USB maintenance flow — nRF52/RP2040 factory erase and bootloader upgrade — is Android-only | -| Notifications | ✓ | ✓ | Native OS notifications | -| Widgets | ✓ | ✗ | Android-only | -| AI Assistant (Chirpy) | ✓\* | ✗ | Google flavor Android only | -| App Functions (system AI) | ✓† | ✗ | Google flavor Android only | +| Feature | Android | Desktop | Notes | +| ----------------------------------------------------- | ------- | ------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Messaging | ✓ | ✓ | Full parity | +| Node List | ✓ | ✓ | Full parity | +| Mapa | ✓ | ✓ | Interactive MapLibre map, with base map and overlay pickers and custom tile sources. Offline terrain (hillshade and contours) can be downloaded; offline base-map packs and local `.mbtiles` archives cannot | +| Map layers (`.kml`/`.kmz`/GeoJSON) | ✓ | ✓ | Same layer store and sheet as Android; imported files draw on the desktop map | +| Site Planner | ✓ | ✓\* | \*Opens in your browser on desktop; the estimate is not drawn on the desktop map | +| Definições | ✓ | ✓ | Full parity | +| Bluetooth (BLE) | ✓ | ✓ | Via Kable on desktop | +| Firmware Update | ✓ | ✓ | In-app USB, BLE, and Wi-Fi (ESP32) update work the same as Android. The USB maintenance flow — nRF52/RP2040 factory erase and bootloader upgrade — is Android-only | +| Notifications | ✓ | ✓ | Native OS notifications | +| Widgets | ✓ | ✗ | Android-only | +| AI Assistant (Chirpy) | ✓\* | ✗ | Google flavor Android only | +| App Functions (system AI) | ✓† | ✗ | Google flavor Android only | \*Chirpy AI requires Android 14+ on Google flavor builds with supported hardware. @@ -122,7 +122,7 @@ Individual doc pages render with full formatting: ## Building from Source -```bash +```shell git clone https://github.com/meshtastic/Meshtastic-Android.git cd Meshtastic-Android ./gradlew :desktopApp:run @@ -135,7 +135,7 @@ Requirements: ## Known Limitations -- Offline tile downloads and local `.mbtiles` archives are not available on desktop. +- Offline base-map downloads and local `.mbtiles` archives are not available on desktop. Offline terrain is — see [Map & Waypoints](map-and-waypoints). - `.kml`/`.kmz`/GeoJSON layer import works — see [Map & Waypoints](map-and-waypoints#map-layers). Site Planner opens in your browser rather than in the app; to bring its coverage estimate onto the map, click the transmitter pin diff --git a/docs/pt-rPT/user/discovery.md b/docs/pt-rPT/user/discovery.md index 27f8629d64..59cd9ea9b0 100644 --- a/docs/pt-rPT/user/discovery.md +++ b/docs/pt-rPT/user/discovery.md @@ -2,7 +2,7 @@ title: Local Mesh Discovery parent: User Guide nav_order: 12 -last_updated: 2026-08-30 +last_updated: 2026-09-11 description: Explore your mesh network — the Local Mesh Discovery scanner, traceroute paths, neighbor maps, and node discovery tools. aliases: - discovery @@ -97,7 +97,7 @@ Mesh Beacon lets nodes invite others to join their mesh. A beaconing node period Configure it under **Settings → Module configuration → Mesh Beacon**. The entry appears only on radios running firmware 2.8.0 or newer. A read-only **Region** row at the top of the screen shows the region the beacon advertises: that region, and the preset, are always the ones the radio itself uses, so a beacon cannot invite anyone onto settings your radio is not running. - **Listen for beacons** — receive invitations broadcast by other nodes. -- **Broadcast a beacon** — periodically advertise this mesh to nearby nodes, with an optional **Beacon message** of up to 100 bytes, a **Broadcast interval** picked from fixed intervals between 1 hour and 72 hours, and an **Offered channel** chosen from your radio's own channels. The offered channel is required, and defaults to your primary channel. +- **Broadcast a beacon** — periodically advertise this mesh to nearby nodes, with an optional **Beacon message** of up to 100 bytes, a **Broadcast interval** picked from fixed intervals between 1 hour and 72 hours, and an **Offered channel** chosen from your radio's own channels. The offered channel is required, and defaults to your primary channel. Over remote admin the picker offers the primary channel only. - **Broadcast targets** — optional extra destinations beyond the offered channel. **Add target** appends a row; each row picks a **Channel** and a **Transmit preset**, and **Remove target** deletes it. With no targets, the beacon goes out on the offered channel alone. Two conditions block beacon setup: diff --git a/docs/pt-rPT/user/help-and-docs.md b/docs/pt-rPT/user/help-and-docs.md index f306cde7f7..c83898cea3 100644 --- a/docs/pt-rPT/user/help-and-docs.md +++ b/docs/pt-rPT/user/help-and-docs.md @@ -2,7 +2,7 @@ title: Help & In-App Docs parent: User Guide nav_order: 21 -last_updated: 2026-08-30 +last_updated: 2026-09-11 description: Browse this documentation inside the app, search it, and ask Chirpy — the on-device AI assistant — questions about Meshtastic. aliases: - help @@ -23,7 +23,7 @@ The docs browser lists every user-guide page. Tap a page to read it; images and ### Search -The search box sits at the top of the docs browser. Type in it to filter pages by title and keywords — results update as you type, and the **Clear search** button (✕) empties the box. +Tap the search field at the top of the docs browser to open a full-screen search view. Type to filter pages by title and keywords — results update as you type, under the same field. The back arrow, or the keyboard's search action, closes the view. The field hides while you scroll down the page list and comes back when you scroll up. ![Searching the in-app documentation](../../assets/screenshots/docs-browser_search.png) diff --git a/docs/pt-rPT/user/map-and-waypoints.md b/docs/pt-rPT/user/map-and-waypoints.md index 1404648a25..a9412a02f6 100644 --- a/docs/pt-rPT/user/map-and-waypoints.md +++ b/docs/pt-rPT/user/map-and-waypoints.md @@ -2,7 +2,7 @@ title: Map & Waypoints parent: User Guide nav_order: 6 -last_updated: 2026-09-01 +last_updated: 2026-09-11 description: View node positions on the map, create and share waypoints, manage map layers and Site Planner, and control position sharing and privacy. aliases: - map @@ -44,7 +44,7 @@ The floating toolbar provides quick access to the compass, the map type and laye ### Filtering the Map -Tap the filter button in the floating toolbar to open **Filter map**. **Display** controls what is drawn: **Only Favorites**, **Show Waypoints**, **Show Precision Circles**, and a slider that hides nodes not heard from recently. **Node roles** is a chip per device role, plus **All** to show every role; a selected chip means that role is shown. **Nodes** narrows the set further with **Hide offline nodes**, **Only show direct nodes**, **Exclude MQTT**, **Show ignored nodes**, and **Include unknown**. +Tap the filter button in the floating toolbar to open **Filter map**. **Display** controls what is drawn: **Only Favorites**, **Show Waypoints**, **Show Precision Circles**, and a slider that hides nodes not heard from recently. **Node roles** is a chip per device role, plus **All** to show every role; a selected chip means that role is shown. **Nodes** narrows the set further with **Hide offline nodes**, **Only show direct nodes**, **Signed only**, **Encrypted only**, **Exclude MQTT**, **Show ignored nodes**, and **Include unknown** — the same rules as the node list's [filter toggles](nodes). A dot on the filter button means at least one filter is hiding something — check it before concluding the mesh is quiet. Turning **Show Waypoints** off hides every waypoint, including your own. **Show ignored nodes** adds them to the map rather than showing only them — unlike the node list's **Only show ignored Nodes**. @@ -147,8 +147,9 @@ picker, all three offer the same raster base maps: Overlays can be toggled on top of any base map, from the layers sheet: - **Weather radar** — NOAA NEXRAD reflectivity (US coverage) -- **Hillshade** — terrain relief, on **F-Droid** and **Desktop** only. Useful for understanding why a - link fails, since LoRa range is limited by terrain +- **Hillshade** — terrain relief from an online tile source, on **F-Droid** and **Desktop** only; Google + Play draws hillshade from downloaded offline terrain instead. Useful for understanding why a link + fails, since LoRa range is limited by terrain ### Adding your own tile source @@ -156,7 +157,7 @@ Any XYZ tile endpoint can be added as a base map, on every flavor and on desktop Tile Sources** at the foot of the base map picker and paste a URL template using `{z}`, `{x}` and `{y}` — plus `{s}` if the provider uses rotating subdomains. A national mapping service, for example: -``` +```text https://wmts.geo.admin.ch/1.0.0/ch.swisstopo.pixelkarte-farbe/default/current/3857/{z}/{x}/{y}.jpeg ``` @@ -173,7 +174,18 @@ On **Google Play**, the layers sheet's **Offline Manager** section downloads its — water, roads and administrative boundaries extracted from the public Protomaps basemap dataset, drawn directly as map shapes rather than raster tiles. Frame the area you want, tap **Start Download**, and once it finishes flip **Show on map** to draw it (it's off by default so it never surprises you by covering up the -live map). You can still import a pre-made `.mbtiles` archive here too, as before. **Desktop** has neither. +live map). You can still import a pre-made `.mbtiles` archive here too, as before. **Desktop** has neither, though +the offline terrain described next does work there. + +### Offline terrain + +Hillshade and elevation contours can be downloaded for offline use as well, so a downloaded area still +shows why a link fails when there is no network. The terrain data comes from Mapterhorn and is credited on +the map. On **Google Play**, each downloaded region in the **Offline Manager** list has a **Download Terrain** +button and **Hillshade** and **Contours** switches; terrain attaches to that region and shares its storage limit, and an area +too large for terrain asks you to zoom in or pick a smaller region. On **F-Droid** and **Desktop**, the +layers sheet has an **Offline Terrain** section of its own: frame the area, tap **Start Download**, and the +hillshade and contours draw from the downloaded tiles from then on. ### Going offline @@ -181,8 +193,8 @@ An **Offline** pill appears over the map whenever the device has no network conn builds, if you've imported a local `.mbtiles` archive, the map switches to it automatically the moment the network drops — no toggle to remember — and switches back once you're reconnected, as long as you haven't picked a different base map yourself in between. On **F-Droid**, a downloaded offline area keeps rendering on -its own; the pill is purely informational there. **Desktop** has no offline downloads at all yet, so the pill -is informational there too. +its own; the pill is purely informational there. **Desktop** downloads offline terrain but no base-map +packs, so the pill is informational there too. ## Related Topics diff --git a/docs/pt-rPT/user/messages-and-channels.md b/docs/pt-rPT/user/messages-and-channels.md index 2da700d5fe..aafe25657b 100644 --- a/docs/pt-rPT/user/messages-and-channels.md +++ b/docs/pt-rPT/user/messages-and-channels.md @@ -2,7 +2,7 @@ title: Messages & Channels parent: User Guide nav_order: 3 -last_updated: 2026-08-30 +last_updated: 2026-09-14 description: Send and receive messages, manage channels, configure encryption, search conversations, and use quick chat, reactions, and message actions. aliases: - channels @@ -50,6 +50,21 @@ Key length alone does not change the icon: a 128-bit key and a 256-bit key both Tapping an existing channel opens the same editor, where you can change the name, the PSK, MQTT uplink and downlink, and position precision. Every edit on this screen — adding, editing, deleting, or dragging a channel into a new order — waits on **Send** the same way. +### Archived Channels + +A conversation belongs to the channel it was held on, not to the slot that channel occupied. When a channel leaves your radio — you scan a different channel QR, replace a channel's key, or switch the modem preset — its conversation is **archived** rather than deleted or merged into whatever takes the slot. + +An archived conversation: + +- stays in the **Channels** list under the name the channel had, below the channels currently on your radio, marked with a history icon; +- keeps its full message history, searchable as usual; +- is read-only — you cannot send, reply, or react, because the channel is no longer on the radio to send on, and it is not offered as a share target; +- comes back automatically if you re-add that exact channel, taking its messages back to the live slot. + +Changing the modem preset archives the channel too: the preset is part of a default channel's identity, so **LongFast** and **MediumFast** are different channels even though neither the name nor the key changed. Changing only the region does not archive anything. + +To remove an archived conversation for good, long-press it in the conversation list and delete it. + ## Direct Messages Direct messages (DMs) go to one specific node. When both radios hold each other's public keys, your radio encrypts the message to that node's public key, so no one else on the mesh can read it — not even nodes that share your channel. diff --git a/docs/pt-rPT/user/mqtt.md b/docs/pt-rPT/user/mqtt.md index af5f3ce562..6e9c1a683d 100644 --- a/docs/pt-rPT/user/mqtt.md +++ b/docs/pt-rPT/user/mqtt.md @@ -2,7 +2,7 @@ title: MQTT parent: User Guide nav_order: 11 -last_updated: 2026-08-30 +last_updated: 2026-09-11 description: Bridge your mesh to the internet — MQTT broker setup, encryption layers, and map reporting. aliases: - mqtt @@ -25,7 +25,7 @@ The MQTT module connects your node to an MQTT broker, allowing: ## How It Works -``` +```text [Your Node] → Radio → [Gateway Node with Wi-Fi] → MQTT Broker → [Remote Gateway] → Radio → [Remote Node] ``` diff --git a/docs/pt-rPT/user/node-metrics.md b/docs/pt-rPT/user/node-metrics.md index 6bb9f5eb6b..d75899bd0a 100644 --- a/docs/pt-rPT/user/node-metrics.md +++ b/docs/pt-rPT/user/node-metrics.md @@ -2,7 +2,7 @@ title: Node Metrics parent: User Guide nav_order: 5 -last_updated: 2026-08-30 +last_updated: 2026-09-09 description: Telemetry dashboards for each mesh node — device health, environment sensors, air quality, signal quality, power, traceroute, and position history. aliases: - metrics @@ -114,7 +114,7 @@ Radio signal quality information: ### Signal Quality Reference -Signal quality is rated from **SNR relative to the active LoRa modem preset's demodulation floor**, not from fixed thresholds — a given SNR means different things on different presets (e.g. −15 dB is fine on LongSlow but unusable on ShortFast). RSSI is shown but is not part of the rating. In the table, _limit_ is the preset's SNR limit. +Signal quality is rated from **SNR relative to the active LoRa modem preset's demodulation floor**, not from fixed thresholds — a given SNR means different things on different presets (e.g. −15 dB is fine on LongSlow but unusable on ShortFast). When RSSI and a noise-floor reading are both available, the app also rates their difference against the preset limit and uses the worse rating. Otherwise, RSSI is display-only. In the table, _limit_ is the preset's SNR limit. | Quality | Criteria | | -------- | ---------------------------------------------------------------- | diff --git a/docs/pt-rPT/user/nodes.md b/docs/pt-rPT/user/nodes.md index 10456f19b7..5f1a7abe32 100644 --- a/docs/pt-rPT/user/nodes.md +++ b/docs/pt-rPT/user/nodes.md @@ -2,7 +2,7 @@ title: Nodes parent: User Guide nav_order: 4 -last_updated: 2026-09-04 +last_updated: 2026-09-11 description: Browse, filter, and sort mesh nodes — view details, signal quality, roles, and quick actions. aliases: - node-list @@ -74,19 +74,23 @@ Most users should keep the default **Client** role. Consider a different role wh > 💡 **Tip:** The mesh works best when most nodes are **Client** or **Router**. Too many Client Mute nodes reduce mesh resilience; too many Routers in a dense area can cause congestion. A good rule of thumb: one Router per 5–10 Clients in your area. -### Encryption Indicators +### Security indicators -Nodes display encryption status icons next to their name: +Each node carries one security icon beside its name in the node list. Tap it to read what it means, and choose **Show All Meanings** in that dialog for the full legend. The detail screen shows the same state in words, as a **Security** row that opens the same dialog. -| Icon | Meaning | -| ----------- | ------------------------------------------------------------------------------------------------------------------------------------------------- | -| 🔒 Locked | Communication uses PKI (public key infrastructure) — end-to-end encrypted with verified identity | -| 🔓 Unlocked | No public key has been received for this node, so it cannot be direct messaged — use **Request User Info** on the node detail page to ask for one | -| ⚠️ Mismatch | Public key mismatch — the node's key has changed since last seen (investigate before trusting) | +| Icon | Meaning | Shown for | +| --------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------- | +| Person with a shield check (green) | **Verified contact** — you verified this node's key in person by exchanging contact QR codes, so its identity is confirmed. The strongest trust the list shows. Your own connected node carries it too | Any firmware version | +| Nodes icon with a shield check (green) | **Signed node** — this node signs its broadcasts with its identity key, so its identity is consistent over time, but you have not verified it in person. On firmware 2.8 the icon appears from the version alone, before any signed broadcast has been heard | Firmware 2.8 or newer, and any node whose signed broadcast your node has verified | +| 🔒 Closed lock | A public key is on file and matches, so direct messages to this node are encrypted | Firmware before 2.8, or no reported version | +| 🔓 Open lock | No public key has been received for this node, so it cannot be direct messaged — use **Request User Info** on the node detail page to ask for one | Firmware before 2.8, or no reported version | +| ⚠️ Mismatch | **Public key mismatch** — a different key arrived for this node after one was stored. Investigate before trusting | Any firmware version | -> 💡 **Tip:** Direct messages always use PKI, so the radio needs the other node's public key before it can send one. It refuses the send rather than falling back to channel encryption. Keys arrive inside node info, which is why an open lock usually clears itself once that node is heard from properly. If you see a key mismatch warning, the node may have been reset or compromised. +Every node on firmware 2.8 or newer signs its broadcasts, so on that firmware the signed state is the baseline and the locks are not shown. -To clear a mismatch, first confirm through another trusted channel that the key change was intentional — a factory reset causes one. Then touch & hold the node, choose **Remove**, and let the two radios exchange keys again the next time yours hears it. +Direct messages always use public-key encryption, so your node needs the other node's public key before it can send one. It refuses the send rather than falling back to channel encryption. Keys arrive inside node info, which is why an open lock usually clears itself once that node is heard from properly. + +A mismatch never replaces the key you already hold. The app keeps the first key it recorded and refuses the new one, as the firmware does, so a stray or hostile node info cannot silently break encrypted messaging to a contact. To clear a mismatch, first confirm through another trusted channel that the key change was intentional — a factory reset causes one. Then touch & hold the node, choose **Remove**, and let the two nodes exchange keys again the next time yours hears it. ## Quick Actions @@ -108,7 +112,9 @@ radio is connected and running firmware 2.8 or newer — see ## Sharing a Contact -On a node's detail screen, tap **Share Contact** to produce a link and a QR code for that node. From the same dialog, **Write to NFC tag** saves the link to a writable NFC tag that anyone can tap to open. +On a node's detail screen, tap **Share Contact** to produce a link and a QR code for that node. From the same dialog, **Share link** opens the Android share sheet (on desktop it copies the link instead), **Write to NFC tag** saves it to a writable NFC tag, and **Copy** puts it on the clipboard. While that dialog is open and in front of you, the phone also offers the same link to any NFC reader, so someone can take the contact by tapping their phone against yours with no tag involved. + +Sharing your own contact this way marks it as verified in person, so whoever imports it sees the **Verified contact** icon rather than the signed one. Relaying someone else's contact passes on only what your app had already recorded about them. To add someone else's contact, use the import button on the node list and choose **Scan Shared Contact QR Code**, **Scan Shared Contact NFC**, or **Input Shared Contact URL**. The app asks you to confirm with **Import Shared Contact?**, and warns you when the contact is one you already have. @@ -120,14 +126,16 @@ Type in the search field to filter nodes by name or short name. The filter updat ### Filter Toggles -| Filtrar | Descrição | -| --------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| **Hide offline nodes** | Show only nodes heard within the last 2 hours | -| **Only show direct nodes** | Show only nodes your radio heard directly, with no relay in between | -| **Include unknown** | Show nodes that haven't sent user info yet. **On by default**, so a node heard before its info arrives stays visible; these carry a badge marking them incomplete, and cannot be direct messaged until their user info brings a public key | -| **Exclude infrastructure** | Hide infrastructure-role nodes (Router, Router Late, Client Base, and legacy Repeater nodes) and any node that cannot be messaged, whatever its role | -| **Exclude MQTT** | Hide nodes heard only via MQTT internet bridge | -| **Only show ignored Nodes** | Replace the list with the nodes you have ignored. Every other node is hidden while this is on, and a banner appears at the top of the list to take you back | +| Filtrar | Descrição | +| --------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **Hide offline nodes** | Show only nodes heard within the last 2 hours | +| **Only show direct nodes** | Show only nodes your radio heard directly, with no relay in between | +| **Include unknown** | Show nodes that haven't sent user info yet. **On by default**, so a node heard before its info arrives stays visible; these carry a badge marking them incomplete, and cannot be direct messaged until their user info brings a public key | +| **Exclude infrastructure** | Hide infrastructure-role nodes (Router, Router Late, Client Base, and legacy Repeater nodes) and any node that cannot be messaged, whatever its role | +| **Exclude MQTT** | Hide nodes heard only via MQTT internet bridge | +| **Signed only** | Show only nodes whose signed broadcasts your node has actually heard and verified. Stricter than the icon: a node on 2.8 shows as signed by its firmware version before any signed broadcast arrives, and a contact verified in person on older firmware is not signed at all | +| **Encrypted only** | Show nodes with a matching public key on file, the key an encrypted direct message needs. A node with a key mismatch is excluded | +| **Only show ignored Nodes** | Replace the list with the nodes you have ignored. Every other node is hidden while this is on, and a banner appears at the top of the list to take you back | ### Sort Options @@ -149,6 +157,8 @@ Tap the hop-histogram icon in the node list's app bar to open a bar chart of how Tapping a node opens the detail view with comprehensive information. See [Node Metrics](node-metrics) for full details on metrics and telemetry. +Signal quality is rated against your modem preset. The same SNR can be good on a long-range preset and poor on a faster one. Traceroute and neighbor-info SNR colors use that preset too. RSSI text has its own strength colors; it affects the quality rating only when a noise-floor reading is also available. + The Details card carries the node's short name, role, IDs, last heard time, hops away, uptime, and its SNR and RSSI: ![Node detail section](../../assets/screenshots/nodes_detail_section.png) diff --git a/docs/pt-rPT/user/settings-module-admin.md b/docs/pt-rPT/user/settings-module-admin.md index 856e89a5b8..3b9ba7d28e 100644 --- a/docs/pt-rPT/user/settings-module-admin.md +++ b/docs/pt-rPT/user/settings-module-admin.md @@ -2,7 +2,7 @@ title: Settings — Modules & Admin parent: User Guide nav_order: 8 -last_updated: 2026-08-30 +last_updated: 2026-09-16 description: Configure optional feature modules (MQTT, telemetry, canned messages, TAK, and more) and perform device administration. aliases: - modules @@ -333,7 +333,7 @@ appear only when your own node is selected: ### Sobre -**Settings → About** carries the app's own identity rather than the radio's: +**Settings → About** carries the app's own identity rather than the node's: Three sections: @@ -341,7 +341,11 @@ Three sections: - **Apps** — opens with **Need Hardware?**, a rotating carousel of popular devices that links out to where to buy one. It also lists the GitHub repository, the running app version, and **Acknowledgements** (see the next section). -- **Project information** — links to the website and to this documentation. +- **Project information** — links to the website, to this documentation, and to the GNU GPL v3 + license the app is released under. + +The footer states the copyright and that the app is free software with no warranty, which you +may redistribute under the same license. ### Acknowledgements diff --git a/docs/pt-rPT/user/settings-radio-user.md b/docs/pt-rPT/user/settings-radio-user.md index 0479ba97cf..334d8e94a1 100644 --- a/docs/pt-rPT/user/settings-radio-user.md +++ b/docs/pt-rPT/user/settings-radio-user.md @@ -2,7 +2,7 @@ title: Settings — Radio & User parent: User Guide nav_order: 7 -last_updated: 2026-09-04 +last_updated: 2026-09-11 description: Configure your radio hardware, LoRa presets, user profile, position sharing, power management, and security. aliases: - definições @@ -51,7 +51,7 @@ The footer appears as soon as you change something. **Discard** throws the chang The status message is saved with the same **Save**, but it never reboots the node — and, like the rest of this screen, it can be edited on a remote node you administer. For your own radio there is a shortcut while it is connected: touch & hold your node in the [node list](nodes.md) and choose -**Update status**. Older firmware and a disconnected radio have no shortcut — the field above is +**Update status**. Older firmware and a disconnected node have no shortcut — the field itself is still the way in. ## Configuração @@ -77,13 +77,13 @@ On **Settings → LoRa**. | Setting | Descrição | Predefinição | | ------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------- | -| Região | Regulatory region for frequency bands. You must set this before transmitting | Unset (must configure) | +| Região | Regulatory region for frequency bands. You must set this before transmitting. On firmware 2.8 or newer, the first region set also creates the node's identity key and gives it a new node number | Unset (must configure) | | Presets | Speed/range tradeoff | LongFast | | Number of Hops | Maximum retransmit hops | 3 | | Transmit Power | Transmission power (dBm); 0 = max allowed for region | 0 (region max) | | Frequency Override | Overrides the computed operating frequency outright (MHz). It does not offset the calculated value — leave at 0 unless you know you need a specific frequency | 0 (use calculated) | | Use Preset | On by default. Turn it off to set Spread Factor, Coding Rate and Bandwidth by hand instead of taking them from the modem preset | On | -| Spread Factor | Manual mode only: 7–12. Higher spreads further but slower | From preset | +| Spread Factor | Manual mode only: 5–12. Higher spreads further but slower. On SX127x (RF95) radios the firmware does not accept 5 or 6 and uses 11 instead | From preset | | Coding Rate | Manual mode only: 5–8. More redundancy costs airtime | From preset | | Largura de banda | Manual mode only: the channel bandwidth in kHz, typed in directly. On the 2.4 GHz region the app offers a list of the bandwidths your radio supports instead, and a stored value that is not on that list shows as _Unsupported_ and blocks saving until you pick a supported one | From preset | | Frequency Slot | Which slot within the region's band to use. 0 derives it from the primary channel name | 0 (automatic) | @@ -111,13 +111,13 @@ The Lite, Narrow, Medium Turbo, and Tiny presets need firmware 2.8 or newer — | Short Slow | ~5 km | 6.25 kbps | −10 dB | Suburban short-range; moderate building density | | Medium Fast | ~5 km | 3.52 kbps | −12.5 dB | Suburban areas; moderate building density | | Medium Slow | ~8 km | 1.95 kbps | −15 dB | Suburban/rural; moderate range with slower speed | -| Long Turbo | ~10 km | 1.34 kbps | −12.5 dB | Similar range to Long Fast but with 500 kHz bandwidth; faster throughput | +| Long Turbo | ~10 km | 1.34 kbps | −17.5 dB | Similar range to Long Fast but with 500 kHz bandwidth; faster throughput | | Long Fast | ~10 km | 1.1 kbps | −17.5 dB | **General use (default)** — balanced range and speed | | Long Moderate | ~20 km | 0.34 kbps | −17.5 dB | Rural with some terrain; occasional use | | Lite Fast | ~5 km | 1.76 kbps | −12.5 dB | EU 866 MHz SRD band (125 kHz BW); comparable to Medium Fast | | Lite Slow | ~10 km | 0.98 kbps | −15 dB | EU 866 MHz SRD band (125 kHz BW); comparable to Long Fast | -| Narrow Fast | ~5 km | 2.28 kbps | −10 dB | EU 868 MHz band (62.5 kHz BW); avoids interference with other devices | -| Narrow Slow | ~10 km | 1.30 kbps | −12.5 dB | EU 868 MHz band (62.5 kHz BW); comparable to Long Fast | +| Narrow Fast | ~5 km | 2.28 kbps | −7.5 dB | EU 868 MHz band (62.5 kHz BW); avoids interference with other devices | +| Narrow Slow | ~10 km | 1.30 kbps | −10 dB | EU 868 MHz band (62.5 kHz BW); comparable to Long Fast | | Medium Turbo | ~5 km | 7.0 kbps | −12.5 dB | Like Medium Fast but with 500 kHz bandwidth; not legal in every region. Needs firmware 2.8 or newer | | Tiny Fast | ~10 km | 0.68 kbps | −7.5 dB | Amateur bands that cap occupied bandwidth; these presets use 15.6 kHz. Needs firmware 2.8 or newer, an SX126x or SX127x radio, and a TCXO of ±5 ppm or better | | Tiny Slow | ~20 km | 0.33 kbps | −10 dB | Same band restrictions as Tiny Fast, longer range. Same firmware, radio, and TCXO requirements | diff --git a/docs/pt-rPT/user/signal-meter.md b/docs/pt-rPT/user/signal-meter.md index 29e2e377df..68f44f9119 100644 --- a/docs/pt-rPT/user/signal-meter.md +++ b/docs/pt-rPT/user/signal-meter.md @@ -2,7 +2,7 @@ title: How the Meshtastic Signal Meter Works parent: User Guide nav_order: 15 -last_updated: 2026-08-30 +last_updated: 2026-09-09 description: How the signal meter rates quality from SNR relative to the LoRa modem preset — spread spectrum, presets, and what the bars really mean. aliases: - signal @@ -32,7 +32,7 @@ Each modem preset has an SNR limit: the lowest SNR at which that preset can stil ## Rating Signal Quality -The app rates signal quality (None, Bad, Fair, or Good) from SNR alone, measured against the active preset's SNR limit. It does not factor in RSSI: without knowing the local noise floor, RSSI alone cannot say whether a signal is decodable. RSSI is still available — on the node detail screen and in the metrics charts. +The app rates signal quality (None, Bad, Fair, or Good) from SNR relative to the active preset's SNR limit. When RSSI and a noise-floor reading are both available, it also rates the difference between them against that limit and uses the worse rating. Otherwise, it uses SNR alone. Because the rating is relative to the preset, the same SNR rates differently on different presets. An SNR of −16 dB rates Good on Long Fast (SNR limit −17.5 dB) but None on Short Fast (SNR limit −7.5 dB). Let `limit` be the active preset's SNR limit: @@ -45,7 +45,7 @@ Because the rating is relative to the preset, the same SNR rates differently on The icon never goes blank, so count the bars carefully: a single bar means None, not a weak but usable link, and Good is a solid wedge rather than a set of bars. A gray three-bar icon labeled Unknown is a different state again — the packet carried no SNR measurement at all, which is not the same as measuring one and finding it too weak. -> ℹ️ **Note:** Traceroute hop colors use fixed thresholds (−7 dB / −15 dB); the per-node signal meter uses the preset-relative rating instead. +> ℹ️ **Note:** Traceroute and neighbor-info SNR colors use the active preset's limits too. RSSI text has separate strength colors: green above −115 dBm, yellow above −120 dBm, orange above −126 dBm, and red at or below −126 dBm. ## Diagnosing Local Interference diff --git a/docs/pt-rPT/user/tak.md b/docs/pt-rPT/user/tak.md index 76f7086af9..b9e64b3927 100644 --- a/docs/pt-rPT/user/tak.md +++ b/docs/pt-rPT/user/tak.md @@ -2,7 +2,7 @@ title: TAK Integration parent: User Guide nav_order: 10 -last_updated: 2026-08-30 +last_updated: 2026-09-11 description: Interoperate with ATAK and WinTAK — CoT position sharing, TAK roles, and plugin setup. aliases: - tak @@ -36,7 +36,7 @@ The TAK module allows Meshtastic nodes to: > ⚠️ **Warning:** The old **Meshtastic ATAK Plugin** is no longer part of this path and cannot > work. It bridged through the cross-process AIDL API, which was removed in app 2.8.0; the mesh > service is now in-process only. Do not install it. Interop today runs over the app's own local -> TAK server plus the Mesh to CoT Converter, both described below, with stock ATAK/iTAK/WinTAK. +> TAK server plus the Mesh to CoT Converter, both described in the following sections, with stock ATAK/iTAK/WinTAK. ### Configuração diff --git a/docs/pt-rPT/user/translate.md b/docs/pt-rPT/user/translate.md index 67d69e2d96..cdc8b6d87f 100644 --- a/docs/pt-rPT/user/translate.md +++ b/docs/pt-rPT/user/translate.md @@ -2,7 +2,7 @@ title: Translate the App parent: User Guide nav_order: 17 -last_updated: 2026-08-30 +last_updated: 2026-09-11 description: How the app and its documentation are translated via Crowdin, and guidelines for contributing translations. aliases: - translate @@ -45,7 +45,7 @@ If your language is not yet listed on Crowdin: The Android app uses **Compose Multiplatform resources** for all user-visible strings: -``` +```text core/resources/src/commonMain/composeResources/ ├── values/ ← English (default) │ └── strings.xml @@ -58,7 +58,7 @@ core/resources/src/commonMain/composeResources/ In-app documentation follows a similar pattern under `docs/`: -``` +```text docs/ ├── en/user/ ← English source (default) │ ├── onboarding.md diff --git a/docs/ro-rRO/user/desktop.md b/docs/ro-rRO/user/desktop.md index ae45164e2a..85884d9ef0 100644 --- a/docs/ro-rRO/user/desktop.md +++ b/docs/ro-rRO/user/desktop.md @@ -2,7 +2,7 @@ title: Desktop App parent: User Guide nav_order: 14 -last_updated: 2026-08-30 +last_updated: 2026-09-11 description: Install and use the Meshtastic Desktop app on Linux, macOS, and Windows — connections, feature parity, and keyboard shortcuts. aliases: - desktop @@ -58,20 +58,20 @@ Bluetooth Low Energy is supported on desktop via the [Kable](https://github.com/ ## Feature Parity -| Feature | Android | Desktop | Notițe | -| ----------------------------------------------------- | ------- | ------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Messaging | ✓ | ✓ | Full parity | -| Node List | ✓ | ✓ | Full parity | -| Map | ✓ | ✓ | Interactive MapLibre map, with base map and overlay pickers and custom tile sources. No offline downloads or local `.mbtiles` archives | -| Map layers (`.kml`/`.kmz`/GeoJSON) | ✓ | ✓ | Same layer store and sheet as Android; imported files draw on the desktop map | -| Site Planner | ✓ | ✓\* | \*Opens in your browser on desktop; the estimate is not drawn on the desktop map | -| Setări | ✓ | ✓ | Full parity | -| Bluetooth (BLE) | ✓ | ✓ | Via Kable on desktop | -| Actualizare firmware | ✓ | ✓ | In-app USB, BLE, and Wi-Fi (ESP32) update work the same as Android. The USB maintenance flow — nRF52/RP2040 factory erase and bootloader upgrade — is Android-only | -| Notifications | ✓ | ✓ | Native OS notifications | -| Widgets | ✓ | ✗ | Android-only | -| AI Assistant (Chirpy) | ✓\* | ✗ | Google flavor Android only | -| App Functions (system AI) | ✓† | ✗ | Google flavor Android only | +| Feature | Android | Desktop | Notițe | +| ----------------------------------------------------- | ------- | ------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Messaging | ✓ | ✓ | Full parity | +| Node List | ✓ | ✓ | Full parity | +| Map | ✓ | ✓ | Interactive MapLibre map, with base map and overlay pickers and custom tile sources. Offline terrain (hillshade and contours) can be downloaded; offline base-map packs and local `.mbtiles` archives cannot | +| Map layers (`.kml`/`.kmz`/GeoJSON) | ✓ | ✓ | Same layer store and sheet as Android; imported files draw on the desktop map | +| Site Planner | ✓ | ✓\* | \*Opens in your browser on desktop; the estimate is not drawn on the desktop map | +| Setări | ✓ | ✓ | Full parity | +| Bluetooth (BLE) | ✓ | ✓ | Via Kable on desktop | +| Actualizare firmware | ✓ | ✓ | In-app USB, BLE, and Wi-Fi (ESP32) update work the same as Android. The USB maintenance flow — nRF52/RP2040 factory erase and bootloader upgrade — is Android-only | +| Notifications | ✓ | ✓ | Native OS notifications | +| Widgets | ✓ | ✗ | Android-only | +| AI Assistant (Chirpy) | ✓\* | ✗ | Google flavor Android only | +| App Functions (system AI) | ✓† | ✗ | Google flavor Android only | \*Chirpy AI requires Android 14+ on Google flavor builds with supported hardware. @@ -122,7 +122,7 @@ Individual doc pages render with full formatting: ## Building from Source -```bash +```shell git clone https://github.com/meshtastic/Meshtastic-Android.git cd Meshtastic-Android ./gradlew :desktopApp:run @@ -135,7 +135,7 @@ Requirements: ## Known Limitations -- Offline tile downloads and local `.mbtiles` archives are not available on desktop. +- Offline base-map downloads and local `.mbtiles` archives are not available on desktop. Offline terrain is — see [Map & Waypoints](map-and-waypoints). - `.kml`/`.kmz`/GeoJSON layer import works — see [Map & Waypoints](map-and-waypoints#map-layers). Site Planner opens in your browser rather than in the app; to bring its coverage estimate onto the map, click the transmitter pin diff --git a/docs/ro-rRO/user/discovery.md b/docs/ro-rRO/user/discovery.md index 1ecb3734e7..0c0fce955e 100644 --- a/docs/ro-rRO/user/discovery.md +++ b/docs/ro-rRO/user/discovery.md @@ -2,7 +2,7 @@ title: Local Mesh Discovery parent: User Guide nav_order: 12 -last_updated: 2026-08-30 +last_updated: 2026-09-11 description: Explore your mesh network — the Local Mesh Discovery scanner, traceroute paths, neighbor maps, and node discovery tools. aliases: - discovery @@ -97,7 +97,7 @@ Mesh Beacon lets nodes invite others to join their mesh. A beaconing node period Configure it under **Settings → Module configuration → Mesh Beacon**. The entry appears only on radios running firmware 2.8.0 or newer. A read-only **Region** row at the top of the screen shows the region the beacon advertises: that region, and the preset, are always the ones the radio itself uses, so a beacon cannot invite anyone onto settings your radio is not running. - **Listen for beacons** — receive invitations broadcast by other nodes. -- **Broadcast a beacon** — periodically advertise this mesh to nearby nodes, with an optional **Beacon message** of up to 100 bytes, a **Broadcast interval** picked from fixed intervals between 1 hour and 72 hours, and an **Offered channel** chosen from your radio's own channels. The offered channel is required, and defaults to your primary channel. +- **Broadcast a beacon** — periodically advertise this mesh to nearby nodes, with an optional **Beacon message** of up to 100 bytes, a **Broadcast interval** picked from fixed intervals between 1 hour and 72 hours, and an **Offered channel** chosen from your radio's own channels. The offered channel is required, and defaults to your primary channel. Over remote admin the picker offers the primary channel only. - **Broadcast targets** — optional extra destinations beyond the offered channel. **Add target** appends a row; each row picks a **Channel** and a **Transmit preset**, and **Remove target** deletes it. With no targets, the beacon goes out on the offered channel alone. Two conditions block beacon setup: diff --git a/docs/ro-rRO/user/help-and-docs.md b/docs/ro-rRO/user/help-and-docs.md index f306cde7f7..c83898cea3 100644 --- a/docs/ro-rRO/user/help-and-docs.md +++ b/docs/ro-rRO/user/help-and-docs.md @@ -2,7 +2,7 @@ title: Help & In-App Docs parent: User Guide nav_order: 21 -last_updated: 2026-08-30 +last_updated: 2026-09-11 description: Browse this documentation inside the app, search it, and ask Chirpy — the on-device AI assistant — questions about Meshtastic. aliases: - help @@ -23,7 +23,7 @@ The docs browser lists every user-guide page. Tap a page to read it; images and ### Search -The search box sits at the top of the docs browser. Type in it to filter pages by title and keywords — results update as you type, and the **Clear search** button (✕) empties the box. +Tap the search field at the top of the docs browser to open a full-screen search view. Type to filter pages by title and keywords — results update as you type, under the same field. The back arrow, or the keyboard's search action, closes the view. The field hides while you scroll down the page list and comes back when you scroll up. ![Searching the in-app documentation](../../assets/screenshots/docs-browser_search.png) diff --git a/docs/ro-rRO/user/map-and-waypoints.md b/docs/ro-rRO/user/map-and-waypoints.md index ec97a6b4bc..59541f3eda 100644 --- a/docs/ro-rRO/user/map-and-waypoints.md +++ b/docs/ro-rRO/user/map-and-waypoints.md @@ -2,7 +2,7 @@ title: Map & Waypoints parent: User Guide nav_order: 6 -last_updated: 2026-09-01 +last_updated: 2026-09-11 description: View node positions on the map, create and share waypoints, manage map layers and Site Planner, and control position sharing and privacy. aliases: - map @@ -44,7 +44,7 @@ The floating toolbar provides quick access to the compass, the map type and laye ### Filtering the Map -Tap the filter button in the floating toolbar to open **Filter map**. **Display** controls what is drawn: **Only Favorites**, **Show Waypoints**, **Show Precision Circles**, and a slider that hides nodes not heard from recently. **Node roles** is a chip per device role, plus **All** to show every role; a selected chip means that role is shown. **Nodes** narrows the set further with **Hide offline nodes**, **Only show direct nodes**, **Exclude MQTT**, **Show ignored nodes**, and **Include unknown**. +Tap the filter button in the floating toolbar to open **Filter map**. **Display** controls what is drawn: **Only Favorites**, **Show Waypoints**, **Show Precision Circles**, and a slider that hides nodes not heard from recently. **Node roles** is a chip per device role, plus **All** to show every role; a selected chip means that role is shown. **Nodes** narrows the set further with **Hide offline nodes**, **Only show direct nodes**, **Signed only**, **Encrypted only**, **Exclude MQTT**, **Show ignored nodes**, and **Include unknown** — the same rules as the node list's [filter toggles](nodes). A dot on the filter button means at least one filter is hiding something — check it before concluding the mesh is quiet. Turning **Show Waypoints** off hides every waypoint, including your own. **Show ignored nodes** adds them to the map rather than showing only them — unlike the node list's **Only show ignored Nodes**. @@ -147,8 +147,9 @@ picker, all three offer the same raster base maps: Overlays can be toggled on top of any base map, from the layers sheet: - **Weather radar** — NOAA NEXRAD reflectivity (US coverage) -- **Hillshade** — terrain relief, on **F-Droid** and **Desktop** only. Useful for understanding why a - link fails, since LoRa range is limited by terrain +- **Hillshade** — terrain relief from an online tile source, on **F-Droid** and **Desktop** only; Google + Play draws hillshade from downloaded offline terrain instead. Useful for understanding why a link + fails, since LoRa range is limited by terrain ### Adding your own tile source @@ -156,7 +157,7 @@ Any XYZ tile endpoint can be added as a base map, on every flavor and on desktop Tile Sources** at the foot of the base map picker and paste a URL template using `{z}`, `{x}` and `{y}` — plus `{s}` if the provider uses rotating subdomains. A national mapping service, for example: -``` +```text https://wmts.geo.admin.ch/1.0.0/ch.swisstopo.pixelkarte-farbe/default/current/3857/{z}/{x}/{y}.jpeg ``` @@ -173,7 +174,18 @@ On **Google Play**, the layers sheet's **Offline Manager** section downloads its — water, roads and administrative boundaries extracted from the public Protomaps basemap dataset, drawn directly as map shapes rather than raster tiles. Frame the area you want, tap **Start Download**, and once it finishes flip **Show on map** to draw it (it's off by default so it never surprises you by covering up the -live map). You can still import a pre-made `.mbtiles` archive here too, as before. **Desktop** has neither. +live map). You can still import a pre-made `.mbtiles` archive here too, as before. **Desktop** has neither, though +the offline terrain described next does work there. + +### Offline terrain + +Hillshade and elevation contours can be downloaded for offline use as well, so a downloaded area still +shows why a link fails when there is no network. The terrain data comes from Mapterhorn and is credited on +the map. On **Google Play**, each downloaded region in the **Offline Manager** list has a **Download Terrain** +button and **Hillshade** and **Contours** switches; terrain attaches to that region and shares its storage limit, and an area +too large for terrain asks you to zoom in or pick a smaller region. On **F-Droid** and **Desktop**, the +layers sheet has an **Offline Terrain** section of its own: frame the area, tap **Start Download**, and the +hillshade and contours draw from the downloaded tiles from then on. ### Going offline @@ -181,8 +193,8 @@ An **Offline** pill appears over the map whenever the device has no network conn builds, if you've imported a local `.mbtiles` archive, the map switches to it automatically the moment the network drops — no toggle to remember — and switches back once you're reconnected, as long as you haven't picked a different base map yourself in between. On **F-Droid**, a downloaded offline area keeps rendering on -its own; the pill is purely informational there. **Desktop** has no offline downloads at all yet, so the pill -is informational there too. +its own; the pill is purely informational there. **Desktop** downloads offline terrain but no base-map +packs, so the pill is informational there too. ## Related Topics diff --git a/docs/ro-rRO/user/messages-and-channels.md b/docs/ro-rRO/user/messages-and-channels.md index fd690aee06..7b78cec45a 100644 --- a/docs/ro-rRO/user/messages-and-channels.md +++ b/docs/ro-rRO/user/messages-and-channels.md @@ -2,7 +2,7 @@ title: Messages & Channels parent: User Guide nav_order: 3 -last_updated: 2026-08-30 +last_updated: 2026-09-14 description: Send and receive messages, manage channels, configure encryption, search conversations, and use quick chat, reactions, and message actions. aliases: - channels @@ -50,6 +50,21 @@ Key length alone does not change the icon: a 128-bit key and a 256-bit key both Tapping an existing channel opens the same editor, where you can change the name, the PSK, MQTT uplink and downlink, and position precision. Every edit on this screen — adding, editing, deleting, or dragging a channel into a new order — waits on **Send** the same way. +### Archived Channels + +A conversation belongs to the channel it was held on, not to the slot that channel occupied. When a channel leaves your radio — you scan a different channel QR, replace a channel's key, or switch the modem preset — its conversation is **archived** rather than deleted or merged into whatever takes the slot. + +An archived conversation: + +- stays in the **Channels** list under the name the channel had, below the channels currently on your radio, marked with a history icon; +- keeps its full message history, searchable as usual; +- is read-only — you cannot send, reply, or react, because the channel is no longer on the radio to send on, and it is not offered as a share target; +- comes back automatically if you re-add that exact channel, taking its messages back to the live slot. + +Changing the modem preset archives the channel too: the preset is part of a default channel's identity, so **LongFast** and **MediumFast** are different channels even though neither the name nor the key changed. Changing only the region does not archive anything. + +To remove an archived conversation for good, long-press it in the conversation list and delete it. + ## Direct Messages Direct messages (DMs) go to one specific node. When both radios hold each other's public keys, your radio encrypts the message to that node's public key, so no one else on the mesh can read it — not even nodes that share your channel. diff --git a/docs/ro-rRO/user/mqtt.md b/docs/ro-rRO/user/mqtt.md index 6b127d7d49..d7f10b33e3 100644 --- a/docs/ro-rRO/user/mqtt.md +++ b/docs/ro-rRO/user/mqtt.md @@ -2,7 +2,7 @@ title: MQTT parent: User Guide nav_order: 11 -last_updated: 2026-08-30 +last_updated: 2026-09-11 description: Bridge your mesh to the internet — MQTT broker setup, encryption layers, and map reporting. aliases: - mqtt @@ -25,7 +25,7 @@ The MQTT module connects your node to an MQTT broker, allowing: ## How It Works -``` +```text [Your Node] → Radio → [Gateway Node with Wi-Fi] → MQTT Broker → [Remote Gateway] → Radio → [Remote Node] ``` diff --git a/docs/ro-rRO/user/node-metrics.md b/docs/ro-rRO/user/node-metrics.md index 41f5b8410d..377b6f05e6 100644 --- a/docs/ro-rRO/user/node-metrics.md +++ b/docs/ro-rRO/user/node-metrics.md @@ -2,7 +2,7 @@ title: Node Metrics parent: User Guide nav_order: 5 -last_updated: 2026-08-30 +last_updated: 2026-09-09 description: Telemetry dashboards for each mesh node — device health, environment sensors, air quality, signal quality, power, traceroute, and position history. aliases: - metrics @@ -114,7 +114,7 @@ Radio signal quality information: ### Signal Quality Reference -Signal quality is rated from **SNR relative to the active LoRa modem preset's demodulation floor**, not from fixed thresholds — a given SNR means different things on different presets (e.g. −15 dB is fine on LongSlow but unusable on ShortFast). RSSI is shown but is not part of the rating. In the table, _limit_ is the preset's SNR limit. +Signal quality is rated from **SNR relative to the active LoRa modem preset's demodulation floor**, not from fixed thresholds — a given SNR means different things on different presets (e.g. −15 dB is fine on LongSlow but unusable on ShortFast). When RSSI and a noise-floor reading are both available, the app also rates their difference against the preset limit and uses the worse rating. Otherwise, RSSI is display-only. In the table, _limit_ is the preset's SNR limit. | Quality | Criteria | | ---------- | ---------------------------------------------------------------- | diff --git a/docs/ro-rRO/user/nodes.md b/docs/ro-rRO/user/nodes.md index ac12f9d7d0..b78715b040 100644 --- a/docs/ro-rRO/user/nodes.md +++ b/docs/ro-rRO/user/nodes.md @@ -2,7 +2,7 @@ title: Noduri parent: User Guide nav_order: 4 -last_updated: 2026-09-04 +last_updated: 2026-09-11 description: Browse, filter, and sort mesh nodes — view details, signal quality, roles, and quick actions. aliases: - node-list @@ -74,19 +74,23 @@ Most users should keep the default **Client** role. Consider a different role wh > 💡 **Tip:** The mesh works best when most nodes are **Client** or **Router**. Too many Client Mute nodes reduce mesh resilience; too many Routers in a dense area can cause congestion. A good rule of thumb: one Router per 5–10 Clients in your area. -### Encryption Indicators +### Security indicators -Nodes display encryption status icons next to their name: +Each node carries one security icon beside its name in the node list. Tap it to read what it means, and choose **Show All Meanings** in that dialog for the full legend. The detail screen shows the same state in words, as a **Security** row that opens the same dialog. -| Icon | Meaning | -| ----------- | ------------------------------------------------------------------------------------------------------------------------------------------------- | -| 🔒 Locked | Communication uses PKI (public key infrastructure) — end-to-end encrypted with verified identity | -| 🔓 Unlocked | No public key has been received for this node, so it cannot be direct messaged — use **Request User Info** on the node detail page to ask for one | -| ⚠️ Mismatch | Public key mismatch — the node's key has changed since last seen (investigate before trusting) | +| Icon | Meaning | Shown for | +| --------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------- | +| Person with a shield check (green) | **Verified contact** — you verified this node's key in person by exchanging contact QR codes, so its identity is confirmed. The strongest trust the list shows. Your own connected node carries it too | Any firmware version | +| Nodes icon with a shield check (green) | **Signed node** — this node signs its broadcasts with its identity key, so its identity is consistent over time, but you have not verified it in person. On firmware 2.8 the icon appears from the version alone, before any signed broadcast has been heard | Firmware 2.8 or newer, and any node whose signed broadcast your node has verified | +| 🔒 Closed lock | A public key is on file and matches, so direct messages to this node are encrypted | Firmware before 2.8, or no reported version | +| 🔓 Open lock | No public key has been received for this node, so it cannot be direct messaged — use **Request User Info** on the node detail page to ask for one | Firmware before 2.8, or no reported version | +| ⚠️ Mismatch | **Public key mismatch** — a different key arrived for this node after one was stored. Investigate before trusting | Any firmware version | -> 💡 **Tip:** Direct messages always use PKI, so the radio needs the other node's public key before it can send one. It refuses the send rather than falling back to channel encryption. Keys arrive inside node info, which is why an open lock usually clears itself once that node is heard from properly. If you see a key mismatch warning, the node may have been reset or compromised. +Every node on firmware 2.8 or newer signs its broadcasts, so on that firmware the signed state is the baseline and the locks are not shown. -To clear a mismatch, first confirm through another trusted channel that the key change was intentional — a factory reset causes one. Then touch & hold the node, choose **Remove**, and let the two radios exchange keys again the next time yours hears it. +Direct messages always use public-key encryption, so your node needs the other node's public key before it can send one. It refuses the send rather than falling back to channel encryption. Keys arrive inside node info, which is why an open lock usually clears itself once that node is heard from properly. + +A mismatch never replaces the key you already hold. The app keeps the first key it recorded and refuses the new one, as the firmware does, so a stray or hostile node info cannot silently break encrypted messaging to a contact. To clear a mismatch, first confirm through another trusted channel that the key change was intentional — a factory reset causes one. Then touch & hold the node, choose **Remove**, and let the two nodes exchange keys again the next time yours hears it. ## Quick Actions @@ -108,7 +112,9 @@ radio is connected and running firmware 2.8 or newer — see ## Sharing a Contact -On a node's detail screen, tap **Share Contact** to produce a link and a QR code for that node. From the same dialog, **Write to NFC tag** saves the link to a writable NFC tag that anyone can tap to open. +On a node's detail screen, tap **Share Contact** to produce a link and a QR code for that node. From the same dialog, **Share link** opens the Android share sheet (on desktop it copies the link instead), **Write to NFC tag** saves it to a writable NFC tag, and **Copy** puts it on the clipboard. While that dialog is open and in front of you, the phone also offers the same link to any NFC reader, so someone can take the contact by tapping their phone against yours with no tag involved. + +Sharing your own contact this way marks it as verified in person, so whoever imports it sees the **Verified contact** icon rather than the signed one. Relaying someone else's contact passes on only what your app had already recorded about them. To add someone else's contact, use the import button on the node list and choose **Scan Shared Contact QR Code**, **Scan Shared Contact NFC**, or **Input Shared Contact URL**. The app asks you to confirm with **Import Shared Contact?**, and warns you when the contact is one you already have. @@ -120,14 +126,16 @@ Type in the search field to filter nodes by name or short name. The filter updat ### Filter Toggles -| Filtru | Descriere | -| --------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| **Hide offline nodes** | Show only nodes heard within the last 2 hours | -| **Only show direct nodes** | Show only nodes your radio heard directly, with no relay in between | -| **Include unknown** | Show nodes that haven't sent user info yet. **On by default**, so a node heard before its info arrives stays visible; these carry a badge marking them incomplete, and cannot be direct messaged until their user info brings a public key | -| **Exclude infrastructure** | Hide infrastructure-role nodes (Router, Router Late, Client Base, and legacy Repeater nodes) and any node that cannot be messaged, whatever its role | -| **Exclude MQTT** | Hide nodes heard only via MQTT internet bridge | -| **Only show ignored Nodes** | Replace the list with the nodes you have ignored. Every other node is hidden while this is on, and a banner appears at the top of the list to take you back | +| Filtru | Descriere | +| --------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **Hide offline nodes** | Show only nodes heard within the last 2 hours | +| **Only show direct nodes** | Show only nodes your radio heard directly, with no relay in between | +| **Include unknown** | Show nodes that haven't sent user info yet. **On by default**, so a node heard before its info arrives stays visible; these carry a badge marking them incomplete, and cannot be direct messaged until their user info brings a public key | +| **Exclude infrastructure** | Hide infrastructure-role nodes (Router, Router Late, Client Base, and legacy Repeater nodes) and any node that cannot be messaged, whatever its role | +| **Exclude MQTT** | Hide nodes heard only via MQTT internet bridge | +| **Signed only** | Show only nodes whose signed broadcasts your node has actually heard and verified. Stricter than the icon: a node on 2.8 shows as signed by its firmware version before any signed broadcast arrives, and a contact verified in person on older firmware is not signed at all | +| **Encrypted only** | Show nodes with a matching public key on file, the key an encrypted direct message needs. A node with a key mismatch is excluded | +| **Only show ignored Nodes** | Replace the list with the nodes you have ignored. Every other node is hidden while this is on, and a banner appears at the top of the list to take you back | ### Sort Options @@ -149,6 +157,8 @@ Tap the hop-histogram icon in the node list's app bar to open a bar chart of how Tapping a node opens the detail view with comprehensive information. See [Node Metrics](node-metrics) for full details on metrics and telemetry. +Signal quality is rated against your modem preset. The same SNR can be good on a long-range preset and poor on a faster one. Traceroute and neighbor-info SNR colors use that preset too. RSSI text has its own strength colors; it affects the quality rating only when a noise-floor reading is also available. + The Details card carries the node's short name, role, IDs, last heard time, hops away, uptime, and its SNR and RSSI: ![Node detail section](../../assets/screenshots/nodes_detail_section.png) diff --git a/docs/ro-rRO/user/settings-module-admin.md b/docs/ro-rRO/user/settings-module-admin.md index b65c2ce784..74c77ebc4f 100644 --- a/docs/ro-rRO/user/settings-module-admin.md +++ b/docs/ro-rRO/user/settings-module-admin.md @@ -2,7 +2,7 @@ title: Settings — Modules & Admin parent: User Guide nav_order: 8 -last_updated: 2026-08-30 +last_updated: 2026-09-16 description: Configure optional feature modules (MQTT, telemetry, canned messages, TAK, and more) and perform device administration. aliases: - modules @@ -333,7 +333,7 @@ appear only when your own node is selected: ### Despre -**Settings → About** carries the app's own identity rather than the radio's: +**Settings → About** carries the app's own identity rather than the node's: Three sections: @@ -341,7 +341,11 @@ Three sections: - **Apps** — opens with **Need Hardware?**, a rotating carousel of popular devices that links out to where to buy one. It also lists the GitHub repository, the running app version, and **Acknowledgements** (see the next section). -- **Project information** — links to the website and to this documentation. +- **Project information** — links to the website, to this documentation, and to the GNU GPL v3 + license the app is released under. + +The footer states the copyright and that the app is free software with no warranty, which you +may redistribute under the same license. ### Mulțumiri diff --git a/docs/ro-rRO/user/settings-radio-user.md b/docs/ro-rRO/user/settings-radio-user.md index bfcd5a88d8..9df694d513 100644 --- a/docs/ro-rRO/user/settings-radio-user.md +++ b/docs/ro-rRO/user/settings-radio-user.md @@ -2,7 +2,7 @@ title: Settings — Radio & User parent: User Guide nav_order: 7 -last_updated: 2026-09-04 +last_updated: 2026-09-11 description: Configure your radio hardware, LoRa presets, user profile, position sharing, power management, and security. aliases: - setari @@ -51,7 +51,7 @@ The footer appears as soon as you change something. **Discard** throws the chang The status message is saved with the same **Save**, but it never reboots the node — and, like the rest of this screen, it can be edited on a remote node you administer. For your own radio there is a shortcut while it is connected: touch & hold your node in the [node list](nodes.md) and choose -**Update status**. Older firmware and a disconnected radio have no shortcut — the field above is +**Update status**. Older firmware and a disconnected node have no shortcut — the field itself is still the way in. ## Configuration @@ -77,13 +77,13 @@ On **Settings → LoRa**. | Setting | Descriere | Prestabilit | | ------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------- | -| Regiune | Regulatory region for frequency bands. You must set this before transmitting | Unset (must configure) | +| Regiune | Regulatory region for frequency bands. You must set this before transmitting. On firmware 2.8 or newer, the first region set also creates the node's identity key and gives it a new node number | Unset (must configure) | | Presetări | Speed/range tradeoff | LongFast | | Numărul de Hops | Maximum retransmit hops | 3 | | Putere transmisie | Transmission power (dBm); 0 = max allowed for region | 0 (region max) | | Suprascriere frecvență | Overrides the computed operating frequency outright (MHz). It does not offset the calculated value — leave at 0 unless you know you need a specific frequency | 0 (use calculated) | | Utilizare presetare | On by default. Turn it off to set Spread Factor, Coding Rate and Bandwidth by hand instead of taking them from the modem preset | On | -| Factor de răspândire | Manual mode only: 7–12. Higher spreads further but slower | From preset | +| Factor de răspândire | Manual mode only: 5–12. Higher spreads further but slower. On SX127x (RF95) radios the firmware does not accept 5 or 6 and uses 11 instead | From preset | | Rata de codificare | Manual mode only: 5–8. More redundancy costs airtime | From preset | | Lățime bandă | Manual mode only: the channel bandwidth in kHz, typed in directly. On the 2.4 GHz region the app offers a list of the bandwidths your radio supports instead, and a stored value that is not on that list shows as _Unsupported_ and blocks saving until you pick a supported one | From preset | | Slot pentru frecvenţă | Which slot within the region's band to use. 0 derives it from the primary channel name | 0 (automatic) | @@ -111,13 +111,13 @@ The Lite, Narrow, Medium Turbo, and Tiny presets need firmware 2.8 or newer — | Short Slow | ~5 km | 6.25 kbps | −10 dB | Suburban short-range; moderate building density | | Medium Fast | ~5 km | 3.52 kbps | −12.5 dB | Suburban areas; moderate building density | | Medium Slow | ~8 km | 1.95 kbps | −15 dB | Suburban/rural; moderate range with slower speed | -| Long Turbo | ~10 km | 1.34 kbps | −12.5 dB | Similar range to Long Fast but with 500 kHz bandwidth; faster throughput | +| Long Turbo | ~10 km | 1.34 kbps | −17.5 dB | Similar range to Long Fast but with 500 kHz bandwidth; faster throughput | | Long Fast | ~10 km | 1.1 kbps | −17.5 dB | **General use (default)** — balanced range and speed | | Long Moderate | ~20 km | 0.34 kbps | −17.5 dB | Rural with some terrain; occasional use | | Lite Fast | ~5 km | 1.76 kbps | −12.5 dB | EU 866 MHz SRD band (125 kHz BW); comparable to Medium Fast | | Lite Slow | ~10 km | 0.98 kbps | −15 dB | EU 866 MHz SRD band (125 kHz BW); comparable to Long Fast | -| Narrow Fast | ~5 km | 2.28 kbps | −10 dB | EU 868 MHz band (62.5 kHz BW); avoids interference with other devices | -| Narrow Slow | ~10 km | 1.30 kbps | −12.5 dB | EU 868 MHz band (62.5 kHz BW); comparable to Long Fast | +| Narrow Fast | ~5 km | 2.28 kbps | −7.5 dB | EU 868 MHz band (62.5 kHz BW); avoids interference with other devices | +| Narrow Slow | ~10 km | 1.30 kbps | −10 dB | EU 868 MHz band (62.5 kHz BW); comparable to Long Fast | | Medium Turbo | ~5 km | 7.0 kbps | −12.5 dB | Like Medium Fast but with 500 kHz bandwidth; not legal in every region. Needs firmware 2.8 or newer | | Tiny Fast | ~10 km | 0.68 kbps | −7.5 dB | Amateur bands that cap occupied bandwidth; these presets use 15.6 kHz. Needs firmware 2.8 or newer, an SX126x or SX127x radio, and a TCXO of ±5 ppm or better | | Tiny Slow | ~20 km | 0.33 kbps | −10 dB | Same band restrictions as Tiny Fast, longer range. Same firmware, radio, and TCXO requirements | diff --git a/docs/ro-rRO/user/signal-meter.md b/docs/ro-rRO/user/signal-meter.md index 8f2c181340..13ffcb8be4 100644 --- a/docs/ro-rRO/user/signal-meter.md +++ b/docs/ro-rRO/user/signal-meter.md @@ -2,7 +2,7 @@ title: How the Meshtastic Signal Meter Works parent: User Guide nav_order: 15 -last_updated: 2026-08-30 +last_updated: 2026-09-09 description: How the signal meter rates quality from SNR relative to the LoRa modem preset — spread spectrum, presets, and what the bars really mean. aliases: - signal @@ -32,7 +32,7 @@ Each modem preset has an SNR limit: the lowest SNR at which that preset can stil ## Rating Signal Quality -The app rates signal quality (None, Bad, Fair, or Good) from SNR alone, measured against the active preset's SNR limit. It does not factor in RSSI: without knowing the local noise floor, RSSI alone cannot say whether a signal is decodable. RSSI is still available — on the node detail screen and in the metrics charts. +The app rates signal quality (None, Bad, Fair, or Good) from SNR relative to the active preset's SNR limit. When RSSI and a noise-floor reading are both available, it also rates the difference between them against that limit and uses the worse rating. Otherwise, it uses SNR alone. Because the rating is relative to the preset, the same SNR rates differently on different presets. An SNR of −16 dB rates Good on Long Fast (SNR limit −17.5 dB) but None on Short Fast (SNR limit −7.5 dB). Let `limit` be the active preset's SNR limit: @@ -45,7 +45,7 @@ Because the rating is relative to the preset, the same SNR rates differently on The icon never goes blank, so count the bars carefully: a single bar means None, not a weak but usable link, and Good is a solid wedge rather than a set of bars. A gray three-bar icon labeled Unknown is a different state again — the packet carried no SNR measurement at all, which is not the same as measuring one and finding it too weak. -> ℹ️ **Note:** Traceroute hop colors use fixed thresholds (−7 dB / −15 dB); the per-node signal meter uses the preset-relative rating instead. +> ℹ️ **Note:** Traceroute and neighbor-info SNR colors use the active preset's limits too. RSSI text has separate strength colors: green above −115 dBm, yellow above −120 dBm, orange above −126 dBm, and red at or below −126 dBm. ## Diagnosing Local Interference diff --git a/docs/ro-rRO/user/tak.md b/docs/ro-rRO/user/tak.md index 6d04e55a74..b04d2788d9 100644 --- a/docs/ro-rRO/user/tak.md +++ b/docs/ro-rRO/user/tak.md @@ -2,7 +2,7 @@ title: TAK Integration parent: User Guide nav_order: 10 -last_updated: 2026-08-30 +last_updated: 2026-09-11 description: Interoperate with ATAK and WinTAK — CoT position sharing, TAK roles, and plugin setup. aliases: - tak @@ -36,7 +36,7 @@ The TAK module allows Meshtastic nodes to: > ⚠️ **Warning:** The old **Meshtastic ATAK Plugin** is no longer part of this path and cannot > work. It bridged through the cross-process AIDL API, which was removed in app 2.8.0; the mesh > service is now in-process only. Do not install it. Interop today runs over the app's own local -> TAK server plus the Mesh to CoT Converter, both described below, with stock ATAK/iTAK/WinTAK. +> TAK server plus the Mesh to CoT Converter, both described in the following sections, with stock ATAK/iTAK/WinTAK. ### Configuration diff --git a/docs/ro-rRO/user/translate.md b/docs/ro-rRO/user/translate.md index 2a1a249c13..0e6196e5c3 100644 --- a/docs/ro-rRO/user/translate.md +++ b/docs/ro-rRO/user/translate.md @@ -2,7 +2,7 @@ title: Translate the App parent: User Guide nav_order: 17 -last_updated: 2026-08-30 +last_updated: 2026-09-11 description: How the app and its documentation are translated via Crowdin, and guidelines for contributing translations. aliases: - translate @@ -45,7 +45,7 @@ If your language is not yet listed on Crowdin: The Android app uses **Compose Multiplatform resources** for all user-visible strings: -``` +```text core/resources/src/commonMain/composeResources/ ├── values/ ← English (default) │ └── strings.xml @@ -58,7 +58,7 @@ core/resources/src/commonMain/composeResources/ In-app documentation follows a similar pattern under `docs/`: -``` +```text docs/ ├── en/user/ ← English source (default) │ ├── onboarding.md diff --git a/docs/ru-rRU/user/desktop.md b/docs/ru-rRU/user/desktop.md index 89aead292f..631283aa2b 100644 --- a/docs/ru-rRU/user/desktop.md +++ b/docs/ru-rRU/user/desktop.md @@ -2,7 +2,7 @@ title: Настольное приложение parent: Руководство пользователя nav_order: 14 -last_updated: 2026-08-30 +last_updated: 2026-09-11 description: Установка и использование приложения Meshtastic Desktop на Linux, macOS и Windows — подключения, функционал и сочетания клавиш. aliases: - desktop @@ -58,20 +58,20 @@ Bluetooth Low Energy is supported on desktop via the [Kable](https://github.com/ ## Паритет функций -| Функция | Android | Desktop | Заметки | -| ----------------------------------------------------- | ------- | ------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Обмен сообщениями | ✓ | ✓ | Полное равенство | -| Список узлов | ✓ | ✓ | Полное равенство | -| Карта | ✓ | ✓ | Interactive MapLibre map, with base map and overlay pickers and custom tile sources. No offline downloads or local `.mbtiles` archives | -| Map layers (`.kml`/`.kmz`/GeoJSON) | ✓ | ✓ | Same layer store and sheet as Android; imported files draw on the desktop map | -| Планировщик участков | ✓ | ✓\* | \*Opens in your browser on desktop; the estimate is not drawn on the desktop map | -| Настройки | ✓ | ✓ | Полное равенство | -| Bluetooth (BLE) | ✓ | ✓ | Через Kable в настольном приложении | -| Обновление прошивки | ✓ | ✓ | In-app USB, BLE, and Wi-Fi (ESP32) update work the same as Android. The USB maintenance flow — nRF52/RP2040 factory erase and bootloader upgrade — is Android-only | -| Уведомления | ✓ | ✓ | Системные уведомления | -| Виджеты | ✓ | ✗ | Только Android | -| AI-ассистент (Chirpy) | ✓\* | ✗ | Только в Google-версии для Android | -| Функции приложения (системный ИИ) | ✓† | ✗ | Только в Google-версии для Android | +| Функция | Android | Desktop | Заметки | +| ----------------------------------------------------- | ------- | ------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Обмен сообщениями | ✓ | ✓ | Полное равенство | +| Список узлов | ✓ | ✓ | Полное равенство | +| Карта | ✓ | ✓ | Interactive MapLibre map, with base map and overlay pickers and custom tile sources. Offline terrain (hillshade and contours) can be downloaded; offline base-map packs and local `.mbtiles` archives cannot | +| Map layers (`.kml`/`.kmz`/GeoJSON) | ✓ | ✓ | Same layer store and sheet as Android; imported files draw on the desktop map | +| Планировщик участков | ✓ | ✓\* | \*Opens in your browser on desktop; the estimate is not drawn on the desktop map | +| Настройки | ✓ | ✓ | Полное равенство | +| Bluetooth (BLE) | ✓ | ✓ | Через Kable в настольном приложении | +| Обновление прошивки | ✓ | ✓ | In-app USB, BLE, and Wi-Fi (ESP32) update work the same as Android. The USB maintenance flow — nRF52/RP2040 factory erase and bootloader upgrade — is Android-only | +| Уведомления | ✓ | ✓ | Системные уведомления | +| Виджеты | ✓ | ✗ | Только Android | +| AI-ассистент (Chirpy) | ✓\* | ✗ | Только в Google-версии для Android | +| Функции приложения (системный ИИ) | ✓† | ✗ | Только в Google-версии для Android | \*Chirpy AI требует Android 14+ в Google-версии на поддерживаемом оборудовании. @@ -122,7 +122,7 @@ The desktop app includes a built-in documentation browser for quick access to he ## Сборка из исходного кода -```bash +```shell git clone https://github.com/meshtastic/Meshtastic-Android.git cd Meshtastic-Android ./gradlew :desktopApp:run @@ -135,7 +135,7 @@ cd Meshtastic-Android ## Известные ограничения -- Offline tile downloads and local `.mbtiles` archives are not available on desktop. +- Offline base-map downloads and local `.mbtiles` archives are not available on desktop. Offline terrain is — see [Map & Waypoints](map-and-waypoints). - `.kml`/`.kmz`/GeoJSON layer import works — see [Map & Waypoints](map-and-waypoints#map-layers). Site Planner opens in your browser rather than in the app; to bring its coverage estimate onto the map, click the transmitter pin diff --git a/docs/ru-rRU/user/discovery.md b/docs/ru-rRU/user/discovery.md index 5c10275fcf..46b52afac5 100644 --- a/docs/ru-rRU/user/discovery.md +++ b/docs/ru-rRU/user/discovery.md @@ -2,7 +2,7 @@ title: Локальное обнаружение сети parent: Руководство пользователя nav_order: 12 -last_updated: 2026-08-30 +last_updated: 2026-09-11 description: Исследуйте свою mesh-сеть — сканер локального обнаружения mesh-сети, трассировка путей, карты соседей и инструменты обнаружения нодов. aliases: - discovery @@ -97,7 +97,7 @@ If a scan is interrupted — the app is closed, or the radio goes away — the a Configure it under **Settings → Module configuration → Mesh Beacon**. The entry appears only on radios running firmware 2.8.0 or newer. A read-only **Region** row at the top of the screen shows the region the beacon advertises: that region, and the preset, are always the ones the radio itself uses, so a beacon cannot invite anyone onto settings your radio is not running. - **Слушать маяки** — принимать приглашения, передаваемые другими нодами. -- **Broadcast a beacon** — periodically advertise this mesh to nearby nodes, with an optional **Beacon message** of up to 100 bytes, a **Broadcast interval** picked from fixed intervals between 1 hour and 72 hours, and an **Offered channel** chosen from your radio's own channels. The offered channel is required, and defaults to your primary channel. +- **Broadcast a beacon** — periodically advertise this mesh to nearby nodes, with an optional **Beacon message** of up to 100 bytes, a **Broadcast interval** picked from fixed intervals between 1 hour and 72 hours, and an **Offered channel** chosen from your radio's own channels. The offered channel is required, and defaults to your primary channel. Over remote admin the picker offers the primary channel only. - **Broadcast targets** — optional extra destinations beyond the offered channel. **Add target** appends a row; each row picks a **Channel** and a **Transmit preset**, and **Remove target** deletes it. With no targets, the beacon goes out on the offered channel alone. Two conditions block beacon setup: diff --git a/docs/ru-rRU/user/help-and-docs.md b/docs/ru-rRU/user/help-and-docs.md index fb805db660..866b8d8b35 100644 --- a/docs/ru-rRU/user/help-and-docs.md +++ b/docs/ru-rRU/user/help-and-docs.md @@ -2,7 +2,7 @@ title: Справка и встроенная документация parent: Руководство пользователя nav_order: 21 -last_updated: 2026-08-30 +last_updated: 2026-09-11 description: Просматривайте эту документацию внутри приложения, выполняйте по ней поиск и задавайте вопросы о Meshtastic ассистенту Chirpy — встроенному ИИ-помощнику на устройстве. aliases: - help @@ -23,7 +23,7 @@ aliases: ### Поиск -The search box sits at the top of the docs browser. Type in it to filter pages by title and keywords — results update as you type, and the **Clear search** button (✕) empties the box. +Tap the search field at the top of the docs browser to open a full-screen search view. Type to filter pages by title and keywords — results update as you type, under the same field. The back arrow, or the keyboard's search action, closes the view. The field hides while you scroll down the page list and comes back when you scroll up. ![Поиск во встроенной документации](../../assets/screenshots/docs-browser_search.png) diff --git a/docs/ru-rRU/user/map-and-waypoints.md b/docs/ru-rRU/user/map-and-waypoints.md index 1469cde8b5..9a7efda43b 100644 --- a/docs/ru-rRU/user/map-and-waypoints.md +++ b/docs/ru-rRU/user/map-and-waypoints.md @@ -2,7 +2,7 @@ title: Карта и путевые точки parent: Руководство пользователя nav_order: 6 -last_updated: 2026-09-01 +last_updated: 2026-09-11 description: Просматривайте расположение нод на карте, создавайте и делитесь путевыми точками, управляйте слоями карты и планировщиком участков, а также контролируйте передачу геоданных и приватность. aliases: - map @@ -44,7 +44,7 @@ The floating toolbar provides quick access to the compass, the map type and laye ### Filtering the Map -Tap the filter button in the floating toolbar to open **Filter map**. **Display** controls what is drawn: **Only Favorites**, **Show Waypoints**, **Show Precision Circles**, and a slider that hides nodes not heard from recently. **Node roles** is a chip per device role, plus **All** to show every role; a selected chip means that role is shown. **Nodes** narrows the set further with **Hide offline nodes**, **Only show direct nodes**, **Exclude MQTT**, **Show ignored nodes**, and **Include unknown**. +Tap the filter button in the floating toolbar to open **Filter map**. **Display** controls what is drawn: **Only Favorites**, **Show Waypoints**, **Show Precision Circles**, and a slider that hides nodes not heard from recently. **Node roles** is a chip per device role, plus **All** to show every role; a selected chip means that role is shown. **Nodes** narrows the set further with **Hide offline nodes**, **Only show direct nodes**, **Signed only**, **Encrypted only**, **Exclude MQTT**, **Show ignored nodes**, and **Include unknown** — the same rules as the node list's [filter toggles](nodes). A dot on the filter button means at least one filter is hiding something — check it before concluding the mesh is quiet. Turning **Show Waypoints** off hides every waypoint, including your own. **Show ignored nodes** adds them to the map rather than showing only them — unlike the node list's **Only show ignored Nodes**. @@ -147,8 +147,9 @@ picker, all three offer the same raster base maps: Overlays can be toggled on top of any base map, from the layers sheet: - **Weather radar** — NOAA NEXRAD reflectivity (US coverage) -- **Hillshade** — terrain relief, on **F-Droid** and **Desktop** only. Useful for understanding why a - link fails, since LoRa range is limited by terrain +- **Hillshade** — terrain relief from an online tile source, on **F-Droid** and **Desktop** only; Google + Play draws hillshade from downloaded offline terrain instead. Useful for understanding why a link + fails, since LoRa range is limited by terrain ### Adding your own tile source @@ -156,7 +157,7 @@ Any XYZ tile endpoint can be added as a base map, on every flavor and on desktop Tile Sources** at the foot of the base map picker and paste a URL template using `{z}`, `{x}` and `{y}` — plus `{s}` if the provider uses rotating subdomains. A national mapping service, for example: -``` +```text https://wmts.geo.admin.ch/1.0.0/ch.swisstopo.pixelkarte-farbe/default/current/3857/{z}/{x}/{y}.jpeg ``` @@ -173,7 +174,18 @@ On **Google Play**, the layers sheet's **Offline Manager** section downloads its — water, roads and administrative boundaries extracted from the public Protomaps basemap dataset, drawn directly as map shapes rather than raster tiles. Frame the area you want, tap **Start Download**, and once it finishes flip **Show on map** to draw it (it's off by default so it never surprises you by covering up the -live map). You can still import a pre-made `.mbtiles` archive here too, as before. **Desktop** has neither. +live map). You can still import a pre-made `.mbtiles` archive here too, as before. **Desktop** has neither, though +the offline terrain described next does work there. + +### Offline terrain + +Hillshade and elevation contours can be downloaded for offline use as well, so a downloaded area still +shows why a link fails when there is no network. The terrain data comes from Mapterhorn and is credited on +the map. On **Google Play**, each downloaded region in the **Offline Manager** list has a **Download Terrain** +button and **Hillshade** and **Contours** switches; terrain attaches to that region and shares its storage limit, and an area +too large for terrain asks you to zoom in or pick a smaller region. On **F-Droid** and **Desktop**, the +layers sheet has an **Offline Terrain** section of its own: frame the area, tap **Start Download**, and the +hillshade and contours draw from the downloaded tiles from then on. ### Going offline @@ -181,8 +193,8 @@ An **Offline** pill appears over the map whenever the device has no network conn builds, if you've imported a local `.mbtiles` archive, the map switches to it automatically the moment the network drops — no toggle to remember — and switches back once you're reconnected, as long as you haven't picked a different base map yourself in between. On **F-Droid**, a downloaded offline area keeps rendering on -its own; the pill is purely informational there. **Desktop** has no offline downloads at all yet, so the pill -is informational there too. +its own; the pill is purely informational there. **Desktop** downloads offline terrain but no base-map +packs, so the pill is informational there too. ## Связанные темы diff --git a/docs/ru-rRU/user/messages-and-channels.md b/docs/ru-rRU/user/messages-and-channels.md index 2813bc793a..c99c234384 100644 --- a/docs/ru-rRU/user/messages-and-channels.md +++ b/docs/ru-rRU/user/messages-and-channels.md @@ -2,7 +2,7 @@ title: Сообщения и каналы parent: Руководство пользователя nav_order: 3 -last_updated: 2026-08-30 +last_updated: 2026-09-14 description: Отправляйте и получайте сообщения, управляйте каналами, настраивайте шифрование, ищите по перепискам, а также используйте быстрый чат, реакции и действия с сообщениями. aliases: - channels @@ -50,6 +50,21 @@ Key length alone does not change the icon: a 128-bit key and a 256-bit key both Tapping an existing channel opens the same editor, where you can change the name, the PSK, MQTT uplink and downlink, and position precision. Every edit on this screen — adding, editing, deleting, or dragging a channel into a new order — waits on **Send** the same way. +### Archived Channels + +A conversation belongs to the channel it was held on, not to the slot that channel occupied. When a channel leaves your radio — you scan a different channel QR, replace a channel's key, or switch the modem preset — its conversation is **archived** rather than deleted or merged into whatever takes the slot. + +An archived conversation: + +- stays in the **Channels** list under the name the channel had, below the channels currently on your radio, marked with a history icon; +- keeps its full message history, searchable as usual; +- is read-only — you cannot send, reply, or react, because the channel is no longer on the radio to send on, and it is not offered as a share target; +- comes back automatically if you re-add that exact channel, taking its messages back to the live slot. + +Changing the modem preset archives the channel too: the preset is part of a default channel's identity, so **LongFast** and **MediumFast** are different channels even though neither the name nor the key changed. Changing only the region does not archive anything. + +To remove an archived conversation for good, long-press it in the conversation list and delete it. + ## Личные сообщения Direct messages (DMs) go to one specific node. When both radios hold each other's public keys, your radio encrypts the message to that node's public key, so no one else on the mesh can read it — not even nodes that share your channel. diff --git a/docs/ru-rRU/user/mqtt.md b/docs/ru-rRU/user/mqtt.md index 832ee92f73..aad9337632 100644 --- a/docs/ru-rRU/user/mqtt.md +++ b/docs/ru-rRU/user/mqtt.md @@ -2,7 +2,7 @@ title: MQTT parent: Руководство пользователя nav_order: 11 -last_updated: 2026-08-30 +last_updated: 2026-09-11 description: Подключите свою mesh-сеть к интернету — настройка MQTT-брокера, уровни шифрования и отчётность на карте. aliases: - mqtt @@ -25,7 +25,7 @@ MQTT соединяет твою mesh-сеть Meshtastic с интернето ## Как это работает -``` +```text [Your Node] → Radio → [Gateway Node with Wi-Fi] → MQTT Broker → [Remote Gateway] → Radio → [Remote Node] ``` diff --git a/docs/ru-rRU/user/node-metrics.md b/docs/ru-rRU/user/node-metrics.md index 0968c90abd..6abcf809cb 100644 --- a/docs/ru-rRU/user/node-metrics.md +++ b/docs/ru-rRU/user/node-metrics.md @@ -2,7 +2,7 @@ title: Метрики ноды parent: Руководство пользователя nav_order: 5 -last_updated: 2026-08-30 +last_updated: 2026-09-09 description: Панели телеметрии для каждой ноды mesh-сети — состояние устройства, датчики окружающей среды, качество воздуха, качество сигнала, питание, трассировка и история местоположения. aliases: - metrics @@ -114,7 +114,7 @@ CO₂ readings are color-coded by severity so you can read air quality at a glan ### Оценка качества сигнала -Качество сигнала оценивается по **SNR относительно минимального уровня демодуляции активного пресета LoRa-модема**, а не по фиксированным порогам — конкретное значение SNR означает разное на разных пресетах (например, −15 дБ нормально для LongSlow, но неприемлемо для ShortFast). RSSI отображается, но не учитывается в оценке. In the table, _limit_ is the preset's SNR limit. +Качество сигнала оценивается по **SNR относительно минимального уровня демодуляции активного пресета LoRa-модема**, а не по фиксированным порогам — конкретное значение SNR означает разное на разных пресетах (например, −15 дБ нормально для LongSlow, но неприемлемо для ShortFast). When RSSI and a noise-floor reading are both available, the app also rates their difference against the preset limit and uses the worse rating. Otherwise, RSSI is display-only. In the table, _limit_ is the preset's SNR limit. | Качество | Критерии | | ----------- | -------------------------------- | diff --git a/docs/ru-rRU/user/nodes.md b/docs/ru-rRU/user/nodes.md index 68c5336ab0..9e342c77fc 100644 --- a/docs/ru-rRU/user/nodes.md +++ b/docs/ru-rRU/user/nodes.md @@ -2,7 +2,7 @@ title: Ноды parent: Руководство пользователя nav_order: 4 -last_updated: 2026-09-04 +last_updated: 2026-09-11 description: Просматривайте, фильтруйте и сортируйте ноды сети — просматривайте подробности, качество сигнала, роли и быстрые действия. aliases: - node-list @@ -74,19 +74,23 @@ There is no separate "away" tier. > 💡 **Совет:** Сеть работает лучше, когда большинство нод **Client** или **Router**. Too many Client Mute nodes reduce mesh resilience; too many Routers in a dense area can cause congestion. Хорошее практическое правило: один роутер на 5–10 клиентов в вашей зоне. -### Индикаторы шифрования +### Security indicators -У нод рядом с именем отображаются значки статуса шифрования: +Each node carries one security icon beside its name in the node list. Tap it to read what it means, and choose **Show All Meanings** in that dialog for the full legend. The detail screen shows the same state in words, as a **Security** row that opens the same dialog. -| Значок | Значение | -| ----------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- | -| 🔒 Заблокировано | Связь использует PKI (инфраструктуру публичных ключей) — сквозное шифрование с проверкой идентичности | -| 🔓 Разблокировано | No public key has been received for this node, so it cannot be direct messaged — use **Request User Info** on the node detail page to ask for one | -| ⚠️ Несовпадение | Несовпадение открытого ключа — ключ ноды изменился с последнего раза (разберитесь, прежде чем доверять) | +| Значок | Значение | Shown for | +| --------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------- | +| Person with a shield check (green) | **Verified contact** — you verified this node's key in person by exchanging contact QR codes, so its identity is confirmed. The strongest trust the list shows. Your own connected node carries it too | Any firmware version | +| Nodes icon with a shield check (green) | **Signed node** — this node signs its broadcasts with its identity key, so its identity is consistent over time, but you have not verified it in person. On firmware 2.8 the icon appears from the version alone, before any signed broadcast has been heard | Firmware 2.8 or newer, and any node whose signed broadcast your node has verified | +| 🔒 Closed lock | A public key is on file and matches, so direct messages to this node are encrypted | Firmware before 2.8, or no reported version | +| 🔓 Open lock | No public key has been received for this node, so it cannot be direct messaged — use **Request User Info** on the node detail page to ask for one | Firmware before 2.8, or no reported version | +| ⚠️ Несовпадение | **Public key mismatch** — a different key arrived for this node after one was stored. Investigate before trusting | Any firmware version | -> 💡 **Tip:** Direct messages always use PKI, so the radio needs the other node's public key before it can send one. It refuses the send rather than falling back to channel encryption. Keys arrive inside node info, which is why an open lock usually clears itself once that node is heard from properly. Если видишь предупреждение о несоответствии ключа, нода могла быть сброшена или скомпрометирована. +Every node on firmware 2.8 or newer signs its broadcasts, so on that firmware the signed state is the baseline and the locks are not shown. -To clear a mismatch, first confirm through another trusted channel that the key change was intentional — a factory reset causes one. Then touch & hold the node, choose **Remove**, and let the two radios exchange keys again the next time yours hears it. +Direct messages always use public-key encryption, so your node needs the other node's public key before it can send one. It refuses the send rather than falling back to channel encryption. Keys arrive inside node info, which is why an open lock usually clears itself once that node is heard from properly. + +A mismatch never replaces the key you already hold. The app keeps the first key it recorded and refuses the new one, as the firmware does, so a stray or hostile node info cannot silently break encrypted messaging to a contact. To clear a mismatch, first confirm through another trusted channel that the key change was intentional — a factory reset causes one. Then touch & hold the node, choose **Remove**, and let the two nodes exchange keys again the next time yours hears it. ## Быстрые действия @@ -108,7 +112,9 @@ radio is connected and running firmware 2.8 or newer — see ## Sharing a Contact -On a node's detail screen, tap **Share Contact** to produce a link and a QR code for that node. From the same dialog, **Write to NFC tag** saves the link to a writable NFC tag that anyone can tap to open. +On a node's detail screen, tap **Share Contact** to produce a link and a QR code for that node. From the same dialog, **Share link** opens the Android share sheet (on desktop it copies the link instead), **Write to NFC tag** saves it to a writable NFC tag, and **Copy** puts it on the clipboard. While that dialog is open and in front of you, the phone also offers the same link to any NFC reader, so someone can take the contact by tapping their phone against yours with no tag involved. + +Sharing your own contact this way marks it as verified in person, so whoever imports it sees the **Verified contact** icon rather than the signed one. Relaying someone else's contact passes on only what your app had already recorded about them. To add someone else's contact, use the import button on the node list and choose **Scan Shared Contact QR Code**, **Scan Shared Contact NFC**, or **Input Shared Contact URL**. The app asks you to confirm with **Import Shared Contact?**, and warns you when the contact is one you already have. @@ -120,14 +126,16 @@ To add someone else's contact, use the import button on the node list and choose ### Переключатели фильтра -| Фильтр | Описание | -| ---------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| **Hide offline nodes** | Показывать только ноды, услышанные за последние 2 часа | -| **Only show direct nodes** | Show only nodes your radio heard directly, with no relay in between | -| **Включить неизвестные** | Show nodes that haven't sent user info yet. **On by default**, so a node heard before its info arrives stays visible; these carry a badge marking them incomplete, and cannot be direct messaged until their user info brings a public key | -| **Исключить инфраструктуру** | Hide infrastructure-role nodes (Router, Router Late, Client Base, and legacy Repeater nodes) and any node that cannot be messaged, whatever its role | -| **Исключить MQTT** | Скрыть ноды, слышимые только через интернет-мост MQTT | -| **Only show ignored Nodes** | Replace the list with the nodes you have ignored. Every other node is hidden while this is on, and a banner appears at the top of the list to take you back | +| Фильтр | Описание | +| ---------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **Hide offline nodes** | Показывать только ноды, услышанные за последние 2 часа | +| **Only show direct nodes** | Show only nodes your radio heard directly, with no relay in between | +| **Включить неизвестные** | Show nodes that haven't sent user info yet. **On by default**, so a node heard before its info arrives stays visible; these carry a badge marking them incomplete, and cannot be direct messaged until their user info brings a public key | +| **Исключить инфраструктуру** | Hide infrastructure-role nodes (Router, Router Late, Client Base, and legacy Repeater nodes) and any node that cannot be messaged, whatever its role | +| **Исключить MQTT** | Скрыть ноды, слышимые только через интернет-мост MQTT | +| **Signed only** | Show only nodes whose signed broadcasts your node has actually heard and verified. Stricter than the icon: a node on 2.8 shows as signed by its firmware version before any signed broadcast arrives, and a contact verified in person on older firmware is not signed at all | +| **Encrypted only** | Show nodes with a matching public key on file, the key an encrypted direct message needs. A node with a key mismatch is excluded | +| **Only show ignored Nodes** | Replace the list with the nodes you have ignored. Every other node is hidden while this is on, and a banner appears at the top of the list to take you back | ### Параметры сортировки @@ -149,6 +157,8 @@ To add someone else's contact, use the import button on the node list and choose Нажатие на ноду открывает подробный вид с полной информацией. Смотри [Метрики ноды](node-metrics) для полной информации о метриках и телеметрии. +Signal quality is rated against your modem preset. The same SNR can be good on a long-range preset and poor on a faster one. Traceroute and neighbor-info SNR colors use that preset too. RSSI text has its own strength colors; it affects the quality rating only when a noise-floor reading is also available. + The Details card carries the node's short name, role, IDs, last heard time, hops away, uptime, and its SNR and RSSI: ![Раздел деталей ноды](../../assets/screenshots/nodes_detail_section.png) diff --git a/docs/ru-rRU/user/settings-module-admin.md b/docs/ru-rRU/user/settings-module-admin.md index e4f223f144..4517ff4a84 100644 --- a/docs/ru-rRU/user/settings-module-admin.md +++ b/docs/ru-rRU/user/settings-module-admin.md @@ -2,7 +2,7 @@ title: Настройки — Модули и администрирование parent: Руководство пользователя nav_order: 8 -last_updated: 2026-08-30 +last_updated: 2026-09-16 description: Настрой дополнительные функциональные модули (MQTT, телеметрия, готовые сообщения, TAK и другие) и выполняй администрирование устройств. aliases: - modules @@ -333,7 +333,7 @@ appear only when your own node is selected: ### О приложении -**Settings → About** carries the app's own identity rather than the radio's: +**Settings → About** carries the app's own identity rather than the node's: Three sections: @@ -341,7 +341,11 @@ Three sections: - **Apps** — opens with **Need Hardware?**, a rotating carousel of popular devices that links out to where to buy one. It also lists the GitHub repository, the running app version, and **Acknowledgements** (see the next section). -- **Project information** — links to the website and to this documentation. +- **Project information** — links to the website, to this documentation, and to the GNU GPL v3 + license the app is released under. + +The footer states the copyright and that the app is free software with no warranty, which you +may redistribute under the same license. ### Благодарности diff --git a/docs/ru-rRU/user/settings-radio-user.md b/docs/ru-rRU/user/settings-radio-user.md index 05ed3bf4d9..bc90da8ac4 100644 --- a/docs/ru-rRU/user/settings-radio-user.md +++ b/docs/ru-rRU/user/settings-radio-user.md @@ -2,7 +2,7 @@ title: Настройки - Радио и пользователь parent: Руководство пользователя nav_order: 7 -last_updated: 2026-09-04 +last_updated: 2026-09-11 description: Настройте ваше радиоустройство, пресеты LoRa, пользовательский профиль, обмен местоположением, управление питанием и безопасность. aliases: - настройки @@ -51,7 +51,7 @@ The footer appears as soon as you change something. **Discard** throws the chang The status message is saved with the same **Save**, but it never reboots the node — and, like the rest of this screen, it can be edited on a remote node you administer. For your own radio there is a shortcut while it is connected: touch & hold your node in the [node list](nodes.md) and choose -**Update status**. Older firmware and a disconnected radio have no shortcut — the field above is +**Update status**. Older firmware and a disconnected node have no shortcut — the field itself is still the way in. ## Настройки @@ -77,13 +77,13 @@ On **Settings → LoRa**. | Настройка | Описание | По умолчанию | | --------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------- | -| Регион / Страна | Regulatory region for frequency bands. You must set this before transmitting | Не установлено (необходимо настроить) | +| Регион / Страна | Regulatory region for frequency bands. You must set this before transmitting. On firmware 2.8 or newer, the first region set also creates the node's identity key and gives it a new node number | Не установлено (необходимо настроить) | | Шаблоны | Компромисс между скоростью и дальностью | LongFast | | Количество прыжков | Максимальное количество ретрансляций | 3 | | Мощность передатчика | Мощность передачи (дБм); 0 = максимально разрешённая для региона | 0 (максимум региона) | | Переопределить частоту | Overrides the computed operating frequency outright (MHz). It does not offset the calculated value — leave at 0 unless you know you need a specific frequency | 0 (use calculated) | | Использовать шаблон | On by default. Turn it off to set Spread Factor, Coding Rate and Bandwidth by hand instead of taking them from the modem preset | On | -| Коэффициент распространения | Manual mode only: 7–12. Higher spreads further but slower | From preset | +| Коэффициент распространения | Manual mode only: 5–12. Higher spreads further but slower. On SX127x (RF95) radios the firmware does not accept 5 or 6 and uses 11 instead | From preset | | Частота кодирования | Manual mode only: 5–8. More redundancy costs airtime | From preset | | Ширина канала | Manual mode only: the channel bandwidth in kHz, typed in directly. On the 2.4 GHz region the app offers a list of the bandwidths your radio supports instead, and a stored value that is not on that list shows as _Unsupported_ and blocks saving until you pick a supported one | From preset | | Частота слота | Which slot within the region's band to use. 0 derives it from the primary channel name | 0 (automatic) | @@ -111,13 +111,13 @@ The Lite, Narrow, Medium Turbo, and Tiny presets need firmware 2.8 or newer — | Short Slow | ~5 км | 6.25 kbps | -10 дБ | Пригородная зона с коротким диапазоном; умеренная плотность застройки | | Medium Fast | ~5 км | 3.52 kbps | −12.5 дБ | Пригородные районы; умеренная плотность застройки | | Medium Slow | ~8 км | 1.95 kbps | -15 дБ | Пригородный/сельский; умеренный диапазон со сниженной скоростью | -| Long Turbo | ~10 км | 1.34 kbps | −12.5 дБ | Диапазон, похожий на Long Fast, но с полосой пропускания 500 кГц; более высокая пропускная способность | +| Long Turbo | ~10 км | 1.34 kbps | −17.5 дБ | Диапазон, похожий на Long Fast, но с полосой пропускания 500 кГц; более высокая пропускная способность | | Long Fast | ~10 км | 1.1 кб/с | −17.5 дБ | **Общее использование (по умолчанию)** — сбалансированный диапазон и скорость | | Long Moderate | ~20 км | 0.34 кб/с | −17.5 дБ | Сельская местность с некоторым рельефом; случайное использование | | Lite Fast | ~5 км | 1.76 kbps | −12.5 дБ | Диапазон RU 866 МГц SRD (ширина полосы 125 кГц); аналогично Medium Fast | | Lite Slow | ~10 км | 0.98 kbps | -15 дБ | Диапазон RU 866 МГц SRD (ширина полосы 125 кГц); аналогично Long Fast | -| Narrow Fast | ~5 км | 2.28 kbps | -10 дБ | Диапазон RU 868 МГц (полоса пропускания 62,5 кГц); предотвращает помехи с другими устройствами | -| Narrow Slow | ~10 км | 1.30 kbps | −12.5 дБ | Диапазон RU 868 МГц (ширина полосы 62,5 кГц); аналогично Long Fast | +| Narrow Fast | ~5 км | 2.28 kbps | −7.5 дБ | Диапазон RU 868 МГц (полоса пропускания 62,5 кГц); предотвращает помехи с другими устройствами | +| Narrow Slow | ~10 км | 1.30 kbps | -10 дБ | Диапазон RU 868 МГц (ширина полосы 62,5 кГц); аналогично Long Fast | | Medium Turbo | ~5 км | 7.0 kbps | −12.5 дБ | Like Medium Fast but with 500 kHz bandwidth; not legal in every region. Needs firmware 2.8 or newer | | Tiny Fast | ~10 км | 0.68 kbps | −7.5 дБ | Amateur bands that cap occupied bandwidth; these presets use 15.6 kHz. Needs firmware 2.8 or newer, an SX126x or SX127x radio, and a TCXO of ±5 ppm or better | | Tiny Slow | ~20 км | 0.33 kbps | -10 дБ | Same band restrictions as Tiny Fast, longer range. Same firmware, radio, and TCXO requirements | diff --git a/docs/ru-rRU/user/signal-meter.md b/docs/ru-rRU/user/signal-meter.md index fd7e00d6b8..b4c9592661 100644 --- a/docs/ru-rRU/user/signal-meter.md +++ b/docs/ru-rRU/user/signal-meter.md @@ -2,7 +2,7 @@ title: Как работает измеритель сигнала Meshtastic parent: Руководство пользователя nav_order: 15 -last_updated: 2026-08-30 +last_updated: 2026-09-09 description: Как индикатор сигнала оценивает качество по SNR относительно пресетов модема LoRa — расширенная спектральная модуляция, предустановки и что на самом деле означают полоски. aliases: - signal @@ -32,7 +32,7 @@ Each modem preset has an SNR limit: the lowest SNR at which that preset can stil ## Rating Signal Quality -The app rates signal quality (None, Bad, Fair, or Good) from SNR alone, measured against the active preset's SNR limit. It does not factor in RSSI: without knowing the local noise floor, RSSI alone cannot say whether a signal is decodable. RSSI is still available — on the node detail screen and in the metrics charts. +The app rates signal quality (None, Bad, Fair, or Good) from SNR relative to the active preset's SNR limit. When RSSI and a noise-floor reading are both available, it also rates the difference between them against that limit and uses the worse rating. Otherwise, it uses SNR alone. Because the rating is relative to the preset, the same SNR rates differently on different presets. An SNR of −16 dB rates Good on Long Fast (SNR limit −17.5 dB) but None on Short Fast (SNR limit −7.5 dB). Let `limit` be the active preset's SNR limit: @@ -45,7 +45,7 @@ Because the rating is relative to the preset, the same SNR rates differently on The icon never goes blank, so count the bars carefully: a single bar means None, not a weak but usable link, and Good is a solid wedge rather than a set of bars. A gray three-bar icon labeled Unknown is a different state again — the packet carried no SNR measurement at all, which is not the same as measuring one and finding it too weak. -> ℹ️ **Note:** Traceroute hop colors use fixed thresholds (−7 dB / −15 dB); the per-node signal meter uses the preset-relative rating instead. +> ℹ️ **Note:** Traceroute and neighbor-info SNR colors use the active preset's limits too. RSSI text has separate strength colors: green above −115 dBm, yellow above −120 dBm, orange above −126 dBm, and red at or below −126 dBm. ## Diagnosing Local Interference diff --git a/docs/ru-rRU/user/tak.md b/docs/ru-rRU/user/tak.md index d5fd2c99d3..d440f23a4b 100644 --- a/docs/ru-rRU/user/tak.md +++ b/docs/ru-rRU/user/tak.md @@ -2,7 +2,7 @@ title: Интеграция TAK parent: Руководство пользователя nav_order: 10 -last_updated: 2026-08-30 +last_updated: 2026-09-11 description: Взаимодействие с ATAK и WinTAK — передача данных CoT о местоположении, роли TAK и настройка плагина. aliases: - tak @@ -36,7 +36,7 @@ Meshtastic integrates with the Team Awareness Kit (TAK) ecosystem, enabling inte > ⚠️ **Warning:** The old **Meshtastic ATAK Plugin** is no longer part of this path and cannot > work. It bridged through the cross-process AIDL API, which was removed in app 2.8.0; the mesh > service is now in-process only. Do not install it. Interop today runs over the app's own local -> TAK server plus the Mesh to CoT Converter, both described below, with stock ATAK/iTAK/WinTAK. +> TAK server plus the Mesh to CoT Converter, both described in the following sections, with stock ATAK/iTAK/WinTAK. ### Настройки diff --git a/docs/ru-rRU/user/translate.md b/docs/ru-rRU/user/translate.md index 45c77644e0..bcaeeb1380 100644 --- a/docs/ru-rRU/user/translate.md +++ b/docs/ru-rRU/user/translate.md @@ -2,7 +2,7 @@ title: Перевод приложения parent: Руководство пользователя nav_order: 17 -last_updated: 2026-08-30 +last_updated: 2026-09-11 description: Как приложение и его документация переводятся через Crowdin и рекомендации по внесению переводов. aliases: - translate @@ -45,7 +45,7 @@ aliases: Приложение для Android использует **Compose Multiplatform resources** для всех строк, видимых пользователю: -``` +```text core/resources/src/commonMain/composeResources/ ├── values/ ← English (default) │ └── strings.xml @@ -58,7 +58,7 @@ core/resources/src/commonMain/composeResources/ Встроенная документация следует аналогичной схеме в папке `docs/`: -``` +```text docs/ ├── en/user/ ← English source (default) │ ├── onboarding.md diff --git a/docs/sk-rSK/user/desktop.md b/docs/sk-rSK/user/desktop.md index 7cfd6b53e5..90a4c7ac8c 100644 --- a/docs/sk-rSK/user/desktop.md +++ b/docs/sk-rSK/user/desktop.md @@ -2,7 +2,7 @@ title: Desktop App parent: User Guide nav_order: 14 -last_updated: 2026-08-30 +last_updated: 2026-09-11 description: Install and use the Meshtastic Desktop app on Linux, macOS, and Windows — connections, feature parity, and keyboard shortcuts. aliases: - desktop @@ -58,20 +58,20 @@ Bluetooth Low Energy is supported on desktop via the [Kable](https://github.com/ ## Feature Parity -| Feature | Android | Desktop | Notes | -| ----------------------------------------------------- | ------- | ------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Messaging | ✓ | ✓ | Full parity | -| Node List | ✓ | ✓ | Full parity | -| Mapa | ✓ | ✓ | Interactive MapLibre map, with base map and overlay pickers and custom tile sources. No offline downloads or local `.mbtiles` archives | -| Map layers (`.kml`/`.kmz`/GeoJSON) | ✓ | ✓ | Same layer store and sheet as Android; imported files draw on the desktop map | -| Site Planner | ✓ | ✓\* | \*Opens in your browser on desktop; the estimate is not drawn on the desktop map | -| Nastavenia | ✓ | ✓ | Full parity | -| Bluetooth (BLE) | ✓ | ✓ | Via Kable on desktop | -| Firmware Update | ✓ | ✓ | In-app USB, BLE, and Wi-Fi (ESP32) update work the same as Android. The USB maintenance flow — nRF52/RP2040 factory erase and bootloader upgrade — is Android-only | -| Notifications | ✓ | ✓ | Native OS notifications | -| Widgets | ✓ | ✗ | Android-only | -| AI Assistant (Chirpy) | ✓\* | ✗ | Google flavor Android only | -| App Functions (system AI) | ✓† | ✗ | Google flavor Android only | +| Feature | Android | Desktop | Notes | +| ----------------------------------------------------- | ------- | ------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Messaging | ✓ | ✓ | Full parity | +| Node List | ✓ | ✓ | Full parity | +| Mapa | ✓ | ✓ | Interactive MapLibre map, with base map and overlay pickers and custom tile sources. Offline terrain (hillshade and contours) can be downloaded; offline base-map packs and local `.mbtiles` archives cannot | +| Map layers (`.kml`/`.kmz`/GeoJSON) | ✓ | ✓ | Same layer store and sheet as Android; imported files draw on the desktop map | +| Site Planner | ✓ | ✓\* | \*Opens in your browser on desktop; the estimate is not drawn on the desktop map | +| Nastavenia | ✓ | ✓ | Full parity | +| Bluetooth (BLE) | ✓ | ✓ | Via Kable on desktop | +| Firmware Update | ✓ | ✓ | In-app USB, BLE, and Wi-Fi (ESP32) update work the same as Android. The USB maintenance flow — nRF52/RP2040 factory erase and bootloader upgrade — is Android-only | +| Notifications | ✓ | ✓ | Native OS notifications | +| Widgets | ✓ | ✗ | Android-only | +| AI Assistant (Chirpy) | ✓\* | ✗ | Google flavor Android only | +| App Functions (system AI) | ✓† | ✗ | Google flavor Android only | \*Chirpy AI requires Android 14+ on Google flavor builds with supported hardware. @@ -122,7 +122,7 @@ Individual doc pages render with full formatting: ## Building from Source -```bash +```shell git clone https://github.com/meshtastic/Meshtastic-Android.git cd Meshtastic-Android ./gradlew :desktopApp:run @@ -135,7 +135,7 @@ Requirements: ## Known Limitations -- Offline tile downloads and local `.mbtiles` archives are not available on desktop. +- Offline base-map downloads and local `.mbtiles` archives are not available on desktop. Offline terrain is — see [Map & Waypoints](map-and-waypoints). - `.kml`/`.kmz`/GeoJSON layer import works — see [Map & Waypoints](map-and-waypoints#map-layers). Site Planner opens in your browser rather than in the app; to bring its coverage estimate onto the map, click the transmitter pin diff --git a/docs/sk-rSK/user/discovery.md b/docs/sk-rSK/user/discovery.md index ff22ca3a0f..f5b81c4a21 100644 --- a/docs/sk-rSK/user/discovery.md +++ b/docs/sk-rSK/user/discovery.md @@ -2,7 +2,7 @@ title: Local Mesh Discovery parent: User Guide nav_order: 12 -last_updated: 2026-08-30 +last_updated: 2026-09-11 description: Explore your mesh network — the Local Mesh Discovery scanner, traceroute paths, neighbor maps, and node discovery tools. aliases: - discovery @@ -97,7 +97,7 @@ Mesh Beacon lets nodes invite others to join their mesh. A beaconing node period Configure it under **Settings → Module configuration → Mesh Beacon**. The entry appears only on radios running firmware 2.8.0 or newer. A read-only **Region** row at the top of the screen shows the region the beacon advertises: that region, and the preset, are always the ones the radio itself uses, so a beacon cannot invite anyone onto settings your radio is not running. - **Listen for beacons** — receive invitations broadcast by other nodes. -- **Broadcast a beacon** — periodically advertise this mesh to nearby nodes, with an optional **Beacon message** of up to 100 bytes, a **Broadcast interval** picked from fixed intervals between 1 hour and 72 hours, and an **Offered channel** chosen from your radio's own channels. The offered channel is required, and defaults to your primary channel. +- **Broadcast a beacon** — periodically advertise this mesh to nearby nodes, with an optional **Beacon message** of up to 100 bytes, a **Broadcast interval** picked from fixed intervals between 1 hour and 72 hours, and an **Offered channel** chosen from your radio's own channels. The offered channel is required, and defaults to your primary channel. Over remote admin the picker offers the primary channel only. - **Broadcast targets** — optional extra destinations beyond the offered channel. **Add target** appends a row; each row picks a **Channel** and a **Transmit preset**, and **Remove target** deletes it. With no targets, the beacon goes out on the offered channel alone. Two conditions block beacon setup: diff --git a/docs/sk-rSK/user/help-and-docs.md b/docs/sk-rSK/user/help-and-docs.md index f306cde7f7..c83898cea3 100644 --- a/docs/sk-rSK/user/help-and-docs.md +++ b/docs/sk-rSK/user/help-and-docs.md @@ -2,7 +2,7 @@ title: Help & In-App Docs parent: User Guide nav_order: 21 -last_updated: 2026-08-30 +last_updated: 2026-09-11 description: Browse this documentation inside the app, search it, and ask Chirpy — the on-device AI assistant — questions about Meshtastic. aliases: - help @@ -23,7 +23,7 @@ The docs browser lists every user-guide page. Tap a page to read it; images and ### Search -The search box sits at the top of the docs browser. Type in it to filter pages by title and keywords — results update as you type, and the **Clear search** button (✕) empties the box. +Tap the search field at the top of the docs browser to open a full-screen search view. Type to filter pages by title and keywords — results update as you type, under the same field. The back arrow, or the keyboard's search action, closes the view. The field hides while you scroll down the page list and comes back when you scroll up. ![Searching the in-app documentation](../../assets/screenshots/docs-browser_search.png) diff --git a/docs/sk-rSK/user/map-and-waypoints.md b/docs/sk-rSK/user/map-and-waypoints.md index ba74ad94c7..a8d50f6b76 100644 --- a/docs/sk-rSK/user/map-and-waypoints.md +++ b/docs/sk-rSK/user/map-and-waypoints.md @@ -2,7 +2,7 @@ title: Map & Waypoints parent: User Guide nav_order: 6 -last_updated: 2026-09-01 +last_updated: 2026-09-11 description: View node positions on the map, create and share waypoints, manage map layers and Site Planner, and control position sharing and privacy. aliases: - map @@ -44,7 +44,7 @@ The floating toolbar provides quick access to the compass, the map type and laye ### Filtering the Map -Tap the filter button in the floating toolbar to open **Filter map**. **Display** controls what is drawn: **Only Favorites**, **Show Waypoints**, **Show Precision Circles**, and a slider that hides nodes not heard from recently. **Node roles** is a chip per device role, plus **All** to show every role; a selected chip means that role is shown. **Nodes** narrows the set further with **Hide offline nodes**, **Only show direct nodes**, **Exclude MQTT**, **Show ignored nodes**, and **Include unknown**. +Tap the filter button in the floating toolbar to open **Filter map**. **Display** controls what is drawn: **Only Favorites**, **Show Waypoints**, **Show Precision Circles**, and a slider that hides nodes not heard from recently. **Node roles** is a chip per device role, plus **All** to show every role; a selected chip means that role is shown. **Nodes** narrows the set further with **Hide offline nodes**, **Only show direct nodes**, **Signed only**, **Encrypted only**, **Exclude MQTT**, **Show ignored nodes**, and **Include unknown** — the same rules as the node list's [filter toggles](nodes). A dot on the filter button means at least one filter is hiding something — check it before concluding the mesh is quiet. Turning **Show Waypoints** off hides every waypoint, including your own. **Show ignored nodes** adds them to the map rather than showing only them — unlike the node list's **Only show ignored Nodes**. @@ -147,8 +147,9 @@ picker, all three offer the same raster base maps: Overlays can be toggled on top of any base map, from the layers sheet: - **Weather radar** — NOAA NEXRAD reflectivity (US coverage) -- **Hillshade** — terrain relief, on **F-Droid** and **Desktop** only. Useful for understanding why a - link fails, since LoRa range is limited by terrain +- **Hillshade** — terrain relief from an online tile source, on **F-Droid** and **Desktop** only; Google + Play draws hillshade from downloaded offline terrain instead. Useful for understanding why a link + fails, since LoRa range is limited by terrain ### Adding your own tile source @@ -156,7 +157,7 @@ Any XYZ tile endpoint can be added as a base map, on every flavor and on desktop Tile Sources** at the foot of the base map picker and paste a URL template using `{z}`, `{x}` and `{y}` — plus `{s}` if the provider uses rotating subdomains. A national mapping service, for example: -``` +```text https://wmts.geo.admin.ch/1.0.0/ch.swisstopo.pixelkarte-farbe/default/current/3857/{z}/{x}/{y}.jpeg ``` @@ -173,7 +174,18 @@ On **Google Play**, the layers sheet's **Offline Manager** section downloads its — water, roads and administrative boundaries extracted from the public Protomaps basemap dataset, drawn directly as map shapes rather than raster tiles. Frame the area you want, tap **Start Download**, and once it finishes flip **Show on map** to draw it (it's off by default so it never surprises you by covering up the -live map). You can still import a pre-made `.mbtiles` archive here too, as before. **Desktop** has neither. +live map). You can still import a pre-made `.mbtiles` archive here too, as before. **Desktop** has neither, though +the offline terrain described next does work there. + +### Offline terrain + +Hillshade and elevation contours can be downloaded for offline use as well, so a downloaded area still +shows why a link fails when there is no network. The terrain data comes from Mapterhorn and is credited on +the map. On **Google Play**, each downloaded region in the **Offline Manager** list has a **Download Terrain** +button and **Hillshade** and **Contours** switches; terrain attaches to that region and shares its storage limit, and an area +too large for terrain asks you to zoom in or pick a smaller region. On **F-Droid** and **Desktop**, the +layers sheet has an **Offline Terrain** section of its own: frame the area, tap **Start Download**, and the +hillshade and contours draw from the downloaded tiles from then on. ### Going offline @@ -181,8 +193,8 @@ An **Offline** pill appears over the map whenever the device has no network conn builds, if you've imported a local `.mbtiles` archive, the map switches to it automatically the moment the network drops — no toggle to remember — and switches back once you're reconnected, as long as you haven't picked a different base map yourself in between. On **F-Droid**, a downloaded offline area keeps rendering on -its own; the pill is purely informational there. **Desktop** has no offline downloads at all yet, so the pill -is informational there too. +its own; the pill is purely informational there. **Desktop** downloads offline terrain but no base-map +packs, so the pill is informational there too. ## Related Topics diff --git a/docs/sk-rSK/user/messages-and-channels.md b/docs/sk-rSK/user/messages-and-channels.md index d9b9ec322c..9639578354 100644 --- a/docs/sk-rSK/user/messages-and-channels.md +++ b/docs/sk-rSK/user/messages-and-channels.md @@ -2,7 +2,7 @@ title: Messages & Channels parent: User Guide nav_order: 3 -last_updated: 2026-08-30 +last_updated: 2026-09-14 description: Send and receive messages, manage channels, configure encryption, search conversations, and use quick chat, reactions, and message actions. aliases: - channels @@ -50,6 +50,21 @@ Key length alone does not change the icon: a 128-bit key and a 256-bit key both Tapping an existing channel opens the same editor, where you can change the name, the PSK, MQTT uplink and downlink, and position precision. Every edit on this screen — adding, editing, deleting, or dragging a channel into a new order — waits on **Send** the same way. +### Archived Channels + +A conversation belongs to the channel it was held on, not to the slot that channel occupied. When a channel leaves your radio — you scan a different channel QR, replace a channel's key, or switch the modem preset — its conversation is **archived** rather than deleted or merged into whatever takes the slot. + +An archived conversation: + +- stays in the **Channels** list under the name the channel had, below the channels currently on your radio, marked with a history icon; +- keeps its full message history, searchable as usual; +- is read-only — you cannot send, reply, or react, because the channel is no longer on the radio to send on, and it is not offered as a share target; +- comes back automatically if you re-add that exact channel, taking its messages back to the live slot. + +Changing the modem preset archives the channel too: the preset is part of a default channel's identity, so **LongFast** and **MediumFast** are different channels even though neither the name nor the key changed. Changing only the region does not archive anything. + +To remove an archived conversation for good, long-press it in the conversation list and delete it. + ## Direct Messages Direct messages (DMs) go to one specific node. When both radios hold each other's public keys, your radio encrypts the message to that node's public key, so no one else on the mesh can read it — not even nodes that share your channel. diff --git a/docs/sk-rSK/user/mqtt.md b/docs/sk-rSK/user/mqtt.md index c797b7acfc..9f850eab9f 100644 --- a/docs/sk-rSK/user/mqtt.md +++ b/docs/sk-rSK/user/mqtt.md @@ -2,7 +2,7 @@ title: MQTT parent: User Guide nav_order: 11 -last_updated: 2026-08-30 +last_updated: 2026-09-11 description: Bridge your mesh to the internet — MQTT broker setup, encryption layers, and map reporting. aliases: - mqtt @@ -25,7 +25,7 @@ The MQTT module connects your node to an MQTT broker, allowing: ## How It Works -``` +```text [Your Node] → Radio → [Gateway Node with Wi-Fi] → MQTT Broker → [Remote Gateway] → Radio → [Remote Node] ``` diff --git a/docs/sk-rSK/user/node-metrics.md b/docs/sk-rSK/user/node-metrics.md index 20715decc4..e9bf3ba9e7 100644 --- a/docs/sk-rSK/user/node-metrics.md +++ b/docs/sk-rSK/user/node-metrics.md @@ -2,7 +2,7 @@ title: Node Metrics parent: User Guide nav_order: 5 -last_updated: 2026-08-30 +last_updated: 2026-09-09 description: Telemetry dashboards for each mesh node — device health, environment sensors, air quality, signal quality, power, traceroute, and position history. aliases: - metrics @@ -114,7 +114,7 @@ Radio signal quality information: ### Signal Quality Reference -Signal quality is rated from **SNR relative to the active LoRa modem preset's demodulation floor**, not from fixed thresholds — a given SNR means different things on different presets (e.g. −15 dB is fine on LongSlow but unusable on ShortFast). RSSI is shown but is not part of the rating. In the table, _limit_ is the preset's SNR limit. +Signal quality is rated from **SNR relative to the active LoRa modem preset's demodulation floor**, not from fixed thresholds — a given SNR means different things on different presets (e.g. −15 dB is fine on LongSlow but unusable on ShortFast). When RSSI and a noise-floor reading are both available, the app also rates their difference against the preset limit and uses the worse rating. Otherwise, RSSI is display-only. In the table, _limit_ is the preset's SNR limit. | Quality | Criteria | | --------- | ---------------------------------------------------------------- | diff --git a/docs/sk-rSK/user/nodes.md b/docs/sk-rSK/user/nodes.md index a9020555f4..9508b244a7 100644 --- a/docs/sk-rSK/user/nodes.md +++ b/docs/sk-rSK/user/nodes.md @@ -2,7 +2,7 @@ title: Nodes parent: User Guide nav_order: 4 -last_updated: 2026-09-04 +last_updated: 2026-09-11 description: Browse, filter, and sort mesh nodes — view details, signal quality, roles, and quick actions. aliases: - node-list @@ -74,19 +74,23 @@ Most users should keep the default **Client** role. Consider a different role wh > 💡 **Tip:** The mesh works best when most nodes are **Client** or **Router**. Too many Client Mute nodes reduce mesh resilience; too many Routers in a dense area can cause congestion. A good rule of thumb: one Router per 5–10 Clients in your area. -### Encryption Indicators +### Security indicators -Nodes display encryption status icons next to their name: +Each node carries one security icon beside its name in the node list. Tap it to read what it means, and choose **Show All Meanings** in that dialog for the full legend. The detail screen shows the same state in words, as a **Security** row that opens the same dialog. -| Icon | Meaning | -| ----------- | ------------------------------------------------------------------------------------------------------------------------------------------------- | -| 🔒 Locked | Communication uses PKI (public key infrastructure) — end-to-end encrypted with verified identity | -| 🔓 Unlocked | No public key has been received for this node, so it cannot be direct messaged — use **Request User Info** on the node detail page to ask for one | -| ⚠️ Mismatch | Public key mismatch — the node's key has changed since last seen (investigate before trusting) | +| Icon | Meaning | Shown for | +| --------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------- | +| Person with a shield check (green) | **Verified contact** — you verified this node's key in person by exchanging contact QR codes, so its identity is confirmed. The strongest trust the list shows. Your own connected node carries it too | Any firmware version | +| Nodes icon with a shield check (green) | **Signed node** — this node signs its broadcasts with its identity key, so its identity is consistent over time, but you have not verified it in person. On firmware 2.8 the icon appears from the version alone, before any signed broadcast has been heard | Firmware 2.8 or newer, and any node whose signed broadcast your node has verified | +| 🔒 Closed lock | A public key is on file and matches, so direct messages to this node are encrypted | Firmware before 2.8, or no reported version | +| 🔓 Open lock | No public key has been received for this node, so it cannot be direct messaged — use **Request User Info** on the node detail page to ask for one | Firmware before 2.8, or no reported version | +| ⚠️ Mismatch | **Public key mismatch** — a different key arrived for this node after one was stored. Investigate before trusting | Any firmware version | -> 💡 **Tip:** Direct messages always use PKI, so the radio needs the other node's public key before it can send one. It refuses the send rather than falling back to channel encryption. Keys arrive inside node info, which is why an open lock usually clears itself once that node is heard from properly. If you see a key mismatch warning, the node may have been reset or compromised. +Every node on firmware 2.8 or newer signs its broadcasts, so on that firmware the signed state is the baseline and the locks are not shown. -To clear a mismatch, first confirm through another trusted channel that the key change was intentional — a factory reset causes one. Then touch & hold the node, choose **Remove**, and let the two radios exchange keys again the next time yours hears it. +Direct messages always use public-key encryption, so your node needs the other node's public key before it can send one. It refuses the send rather than falling back to channel encryption. Keys arrive inside node info, which is why an open lock usually clears itself once that node is heard from properly. + +A mismatch never replaces the key you already hold. The app keeps the first key it recorded and refuses the new one, as the firmware does, so a stray or hostile node info cannot silently break encrypted messaging to a contact. To clear a mismatch, first confirm through another trusted channel that the key change was intentional — a factory reset causes one. Then touch & hold the node, choose **Remove**, and let the two nodes exchange keys again the next time yours hears it. ## Quick Actions @@ -108,7 +112,9 @@ radio is connected and running firmware 2.8 or newer — see ## Sharing a Contact -On a node's detail screen, tap **Share Contact** to produce a link and a QR code for that node. From the same dialog, **Write to NFC tag** saves the link to a writable NFC tag that anyone can tap to open. +On a node's detail screen, tap **Share Contact** to produce a link and a QR code for that node. From the same dialog, **Share link** opens the Android share sheet (on desktop it copies the link instead), **Write to NFC tag** saves it to a writable NFC tag, and **Copy** puts it on the clipboard. While that dialog is open and in front of you, the phone also offers the same link to any NFC reader, so someone can take the contact by tapping their phone against yours with no tag involved. + +Sharing your own contact this way marks it as verified in person, so whoever imports it sees the **Verified contact** icon rather than the signed one. Relaying someone else's contact passes on only what your app had already recorded about them. To add someone else's contact, use the import button on the node list and choose **Scan Shared Contact QR Code**, **Scan Shared Contact NFC**, or **Input Shared Contact URL**. The app asks you to confirm with **Import Shared Contact?**, and warns you when the contact is one you already have. @@ -120,14 +126,16 @@ Type in the search field to filter nodes by name or short name. The filter updat ### Filter Toggles -| Filter | Popis | -| --------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| **Hide offline nodes** | Show only nodes heard within the last 2 hours | -| **Only show direct nodes** | Show only nodes your radio heard directly, with no relay in between | -| **Include unknown** | Show nodes that haven't sent user info yet. **On by default**, so a node heard before its info arrives stays visible; these carry a badge marking them incomplete, and cannot be direct messaged until their user info brings a public key | -| **Exclude infrastructure** | Hide infrastructure-role nodes (Router, Router Late, Client Base, and legacy Repeater nodes) and any node that cannot be messaged, whatever its role | -| **Exclude MQTT** | Hide nodes heard only via MQTT internet bridge | -| **Only show ignored Nodes** | Replace the list with the nodes you have ignored. Every other node is hidden while this is on, and a banner appears at the top of the list to take you back | +| Filter | Popis | +| --------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **Hide offline nodes** | Show only nodes heard within the last 2 hours | +| **Only show direct nodes** | Show only nodes your radio heard directly, with no relay in between | +| **Include unknown** | Show nodes that haven't sent user info yet. **On by default**, so a node heard before its info arrives stays visible; these carry a badge marking them incomplete, and cannot be direct messaged until their user info brings a public key | +| **Exclude infrastructure** | Hide infrastructure-role nodes (Router, Router Late, Client Base, and legacy Repeater nodes) and any node that cannot be messaged, whatever its role | +| **Exclude MQTT** | Hide nodes heard only via MQTT internet bridge | +| **Signed only** | Show only nodes whose signed broadcasts your node has actually heard and verified. Stricter than the icon: a node on 2.8 shows as signed by its firmware version before any signed broadcast arrives, and a contact verified in person on older firmware is not signed at all | +| **Encrypted only** | Show nodes with a matching public key on file, the key an encrypted direct message needs. A node with a key mismatch is excluded | +| **Only show ignored Nodes** | Replace the list with the nodes you have ignored. Every other node is hidden while this is on, and a banner appears at the top of the list to take you back | ### Sort Options @@ -149,6 +157,8 @@ Tap the hop-histogram icon in the node list's app bar to open a bar chart of how Tapping a node opens the detail view with comprehensive information. See [Node Metrics](node-metrics) for full details on metrics and telemetry. +Signal quality is rated against your modem preset. The same SNR can be good on a long-range preset and poor on a faster one. Traceroute and neighbor-info SNR colors use that preset too. RSSI text has its own strength colors; it affects the quality rating only when a noise-floor reading is also available. + The Details card carries the node's short name, role, IDs, last heard time, hops away, uptime, and its SNR and RSSI: ![Node detail section](../../assets/screenshots/nodes_detail_section.png) diff --git a/docs/sk-rSK/user/settings-module-admin.md b/docs/sk-rSK/user/settings-module-admin.md index 1b6e40543c..d809b6c7aa 100644 --- a/docs/sk-rSK/user/settings-module-admin.md +++ b/docs/sk-rSK/user/settings-module-admin.md @@ -2,7 +2,7 @@ title: Settings — Modules & Admin parent: User Guide nav_order: 8 -last_updated: 2026-08-30 +last_updated: 2026-09-16 description: Configure optional feature modules (MQTT, telemetry, canned messages, TAK, and more) and perform device administration. aliases: - modules @@ -333,7 +333,7 @@ appear only when your own node is selected: ### O aplikácii -**Settings → About** carries the app's own identity rather than the radio's: +**Settings → About** carries the app's own identity rather than the node's: Three sections: @@ -341,7 +341,11 @@ Three sections: - **Apps** — opens with **Need Hardware?**, a rotating carousel of popular devices that links out to where to buy one. It also lists the GitHub repository, the running app version, and **Acknowledgements** (see the next section). -- **Project information** — links to the website and to this documentation. +- **Project information** — links to the website, to this documentation, and to the GNU GPL v3 + license the app is released under. + +The footer states the copyright and that the app is free software with no warranty, which you +may redistribute under the same license. ### Acknowledgements diff --git a/docs/sk-rSK/user/settings-radio-user.md b/docs/sk-rSK/user/settings-radio-user.md index 9c35232939..142fcbeece 100644 --- a/docs/sk-rSK/user/settings-radio-user.md +++ b/docs/sk-rSK/user/settings-radio-user.md @@ -2,7 +2,7 @@ title: Settings — Radio & User parent: User Guide nav_order: 7 -last_updated: 2026-09-04 +last_updated: 2026-09-11 description: Configure your radio hardware, LoRa presets, user profile, position sharing, power management, and security. aliases: - nastavenia @@ -51,7 +51,7 @@ The footer appears as soon as you change something. **Discard** throws the chang The status message is saved with the same **Save**, but it never reboots the node — and, like the rest of this screen, it can be edited on a remote node you administer. For your own radio there is a shortcut while it is connected: touch & hold your node in the [node list](nodes.md) and choose -**Update status**. Older firmware and a disconnected radio have no shortcut — the field above is +**Update status**. Older firmware and a disconnected node have no shortcut — the field itself is still the way in. ## Configuration @@ -77,13 +77,13 @@ On **Settings → LoRa**. | Setting | Popis | Predvolené | | ------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------- | -| Región | Regulatory region for frequency bands. You must set this before transmitting | Unset (must configure) | +| Región | Regulatory region for frequency bands. You must set this before transmitting. On firmware 2.8 or newer, the first region set also creates the node's identity key and gives it a new node number | Unset (must configure) | | Presets | Speed/range tradeoff | LongFast | | Number of Hops | Maximum retransmit hops | 3 | | Transmit Power | Transmission power (dBm); 0 = max allowed for region | 0 (region max) | | Frequency Override | Overrides the computed operating frequency outright (MHz). It does not offset the calculated value — leave at 0 unless you know you need a specific frequency | 0 (use calculated) | | Use Preset | On by default. Turn it off to set Spread Factor, Coding Rate and Bandwidth by hand instead of taking them from the modem preset | On | -| Spread Factor | Manual mode only: 7–12. Higher spreads further but slower | From preset | +| Spread Factor | Manual mode only: 5–12. Higher spreads further but slower. On SX127x (RF95) radios the firmware does not accept 5 or 6 and uses 11 instead | From preset | | Coding Rate | Manual mode only: 5–8. More redundancy costs airtime | From preset | | Šírka pásma | Manual mode only: the channel bandwidth in kHz, typed in directly. On the 2.4 GHz region the app offers a list of the bandwidths your radio supports instead, and a stored value that is not on that list shows as _Unsupported_ and blocks saving until you pick a supported one | From preset | | Frequency Slot | Which slot within the region's band to use. 0 derives it from the primary channel name | 0 (automatic) | @@ -111,13 +111,13 @@ The Lite, Narrow, Medium Turbo, and Tiny presets need firmware 2.8 or newer — | Short Slow | ~5 km | 6.25 kbps | −10 dB | Suburban short-range; moderate building density | | Medium Fast | ~5 km | 3.52 kbps | −12.5 dB | Suburban areas; moderate building density | | Medium Slow | ~8 km | 1.95 kbps | −15 dB | Suburban/rural; moderate range with slower speed | -| Long Turbo | ~10 km | 1.34 kbps | −12.5 dB | Similar range to Long Fast but with 500 kHz bandwidth; faster throughput | +| Long Turbo | ~10 km | 1.34 kbps | −17.5 dB | Similar range to Long Fast but with 500 kHz bandwidth; faster throughput | | Long Fast | ~10 km | 1.1 kbps | −17.5 dB | **General use (default)** — balanced range and speed | | Long Moderate | ~20 km | 0.34 kbps | −17.5 dB | Rural with some terrain; occasional use | | Lite Fast | ~5 km | 1.76 kbps | −12.5 dB | EU 866 MHz SRD band (125 kHz BW); comparable to Medium Fast | | Lite Slow | ~10 km | 0.98 kbps | −15 dB | EU 866 MHz SRD band (125 kHz BW); comparable to Long Fast | -| Narrow Fast | ~5 km | 2.28 kbps | −10 dB | EU 868 MHz band (62.5 kHz BW); avoids interference with other devices | -| Narrow Slow | ~10 km | 1.30 kbps | −12.5 dB | EU 868 MHz band (62.5 kHz BW); comparable to Long Fast | +| Narrow Fast | ~5 km | 2.28 kbps | −7.5 dB | EU 868 MHz band (62.5 kHz BW); avoids interference with other devices | +| Narrow Slow | ~10 km | 1.30 kbps | −10 dB | EU 868 MHz band (62.5 kHz BW); comparable to Long Fast | | Medium Turbo | ~5 km | 7.0 kbps | −12.5 dB | Like Medium Fast but with 500 kHz bandwidth; not legal in every region. Needs firmware 2.8 or newer | | Tiny Fast | ~10 km | 0.68 kbps | −7.5 dB | Amateur bands that cap occupied bandwidth; these presets use 15.6 kHz. Needs firmware 2.8 or newer, an SX126x or SX127x radio, and a TCXO of ±5 ppm or better | | Tiny Slow | ~20 km | 0.33 kbps | −10 dB | Same band restrictions as Tiny Fast, longer range. Same firmware, radio, and TCXO requirements | diff --git a/docs/sk-rSK/user/signal-meter.md b/docs/sk-rSK/user/signal-meter.md index 880048f052..1f2e39e987 100644 --- a/docs/sk-rSK/user/signal-meter.md +++ b/docs/sk-rSK/user/signal-meter.md @@ -2,7 +2,7 @@ title: How the Meshtastic Signal Meter Works parent: User Guide nav_order: 15 -last_updated: 2026-08-30 +last_updated: 2026-09-09 description: How the signal meter rates quality from SNR relative to the LoRa modem preset — spread spectrum, presets, and what the bars really mean. aliases: - signal @@ -32,7 +32,7 @@ Each modem preset has an SNR limit: the lowest SNR at which that preset can stil ## Rating Signal Quality -The app rates signal quality (None, Bad, Fair, or Good) from SNR alone, measured against the active preset's SNR limit. It does not factor in RSSI: without knowing the local noise floor, RSSI alone cannot say whether a signal is decodable. RSSI is still available — on the node detail screen and in the metrics charts. +The app rates signal quality (None, Bad, Fair, or Good) from SNR relative to the active preset's SNR limit. When RSSI and a noise-floor reading are both available, it also rates the difference between them against that limit and uses the worse rating. Otherwise, it uses SNR alone. Because the rating is relative to the preset, the same SNR rates differently on different presets. An SNR of −16 dB rates Good on Long Fast (SNR limit −17.5 dB) but None on Short Fast (SNR limit −7.5 dB). Let `limit` be the active preset's SNR limit: @@ -45,7 +45,7 @@ Because the rating is relative to the preset, the same SNR rates differently on The icon never goes blank, so count the bars carefully: a single bar means None, not a weak but usable link, and Good is a solid wedge rather than a set of bars. A gray three-bar icon labeled Unknown is a different state again — the packet carried no SNR measurement at all, which is not the same as measuring one and finding it too weak. -> ℹ️ **Note:** Traceroute hop colors use fixed thresholds (−7 dB / −15 dB); the per-node signal meter uses the preset-relative rating instead. +> ℹ️ **Note:** Traceroute and neighbor-info SNR colors use the active preset's limits too. RSSI text has separate strength colors: green above −115 dBm, yellow above −120 dBm, orange above −126 dBm, and red at or below −126 dBm. ## Diagnosing Local Interference diff --git a/docs/sk-rSK/user/tak.md b/docs/sk-rSK/user/tak.md index d29f214d1c..c7196bbd5f 100644 --- a/docs/sk-rSK/user/tak.md +++ b/docs/sk-rSK/user/tak.md @@ -2,7 +2,7 @@ title: TAK Integration parent: User Guide nav_order: 10 -last_updated: 2026-08-30 +last_updated: 2026-09-11 description: Interoperate with ATAK and WinTAK — CoT position sharing, TAK roles, and plugin setup. aliases: - tak @@ -36,7 +36,7 @@ The TAK module allows Meshtastic nodes to: > ⚠️ **Warning:** The old **Meshtastic ATAK Plugin** is no longer part of this path and cannot > work. It bridged through the cross-process AIDL API, which was removed in app 2.8.0; the mesh > service is now in-process only. Do not install it. Interop today runs over the app's own local -> TAK server plus the Mesh to CoT Converter, both described below, with stock ATAK/iTAK/WinTAK. +> TAK server plus the Mesh to CoT Converter, both described in the following sections, with stock ATAK/iTAK/WinTAK. ### Configuration diff --git a/docs/sk-rSK/user/translate.md b/docs/sk-rSK/user/translate.md index 67d69e2d96..cdc8b6d87f 100644 --- a/docs/sk-rSK/user/translate.md +++ b/docs/sk-rSK/user/translate.md @@ -2,7 +2,7 @@ title: Translate the App parent: User Guide nav_order: 17 -last_updated: 2026-08-30 +last_updated: 2026-09-11 description: How the app and its documentation are translated via Crowdin, and guidelines for contributing translations. aliases: - translate @@ -45,7 +45,7 @@ If your language is not yet listed on Crowdin: The Android app uses **Compose Multiplatform resources** for all user-visible strings: -``` +```text core/resources/src/commonMain/composeResources/ ├── values/ ← English (default) │ └── strings.xml @@ -58,7 +58,7 @@ core/resources/src/commonMain/composeResources/ In-app documentation follows a similar pattern under `docs/`: -``` +```text docs/ ├── en/user/ ← English source (default) │ ├── onboarding.md diff --git a/docs/sl-rSI/user/desktop.md b/docs/sl-rSI/user/desktop.md index efca3c03c9..2ec0d98037 100644 --- a/docs/sl-rSI/user/desktop.md +++ b/docs/sl-rSI/user/desktop.md @@ -2,7 +2,7 @@ title: Desktop App parent: User Guide nav_order: 14 -last_updated: 2026-08-30 +last_updated: 2026-09-11 description: Install and use the Meshtastic Desktop app on Linux, macOS, and Windows — connections, feature parity, and keyboard shortcuts. aliases: - desktop @@ -58,20 +58,20 @@ Bluetooth Low Energy is supported on desktop via the [Kable](https://github.com/ ## Feature Parity -| Feature | Android | Desktop | Notes | -| ----------------------------------------------------- | ------- | ------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Messaging | ✓ | ✓ | Full parity | -| Node List | ✓ | ✓ | Full parity | -| Map | ✓ | ✓ | Interactive MapLibre map, with base map and overlay pickers and custom tile sources. No offline downloads or local `.mbtiles` archives | -| Map layers (`.kml`/`.kmz`/GeoJSON) | ✓ | ✓ | Same layer store and sheet as Android; imported files draw on the desktop map | -| Site Planner | ✓ | ✓\* | \*Opens in your browser on desktop; the estimate is not drawn on the desktop map | -| Settings | ✓ | ✓ | Full parity | -| Bluetooth (BLE) | ✓ | ✓ | Via Kable on desktop | -| Firmware Update | ✓ | ✓ | In-app USB, BLE, and Wi-Fi (ESP32) update work the same as Android. The USB maintenance flow — nRF52/RP2040 factory erase and bootloader upgrade — is Android-only | -| Notifications | ✓ | ✓ | Native OS notifications | -| Widgets | ✓ | ✗ | Android-only | -| AI Assistant (Chirpy) | ✓\* | ✗ | Google flavor Android only | -| App Functions (system AI) | ✓† | ✗ | Google flavor Android only | +| Feature | Android | Desktop | Notes | +| ----------------------------------------------------- | ------- | ------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Messaging | ✓ | ✓ | Full parity | +| Node List | ✓ | ✓ | Full parity | +| Map | ✓ | ✓ | Interactive MapLibre map, with base map and overlay pickers and custom tile sources. Offline terrain (hillshade and contours) can be downloaded; offline base-map packs and local `.mbtiles` archives cannot | +| Map layers (`.kml`/`.kmz`/GeoJSON) | ✓ | ✓ | Same layer store and sheet as Android; imported files draw on the desktop map | +| Site Planner | ✓ | ✓\* | \*Opens in your browser on desktop; the estimate is not drawn on the desktop map | +| Settings | ✓ | ✓ | Full parity | +| Bluetooth (BLE) | ✓ | ✓ | Via Kable on desktop | +| Firmware Update | ✓ | ✓ | In-app USB, BLE, and Wi-Fi (ESP32) update work the same as Android. The USB maintenance flow — nRF52/RP2040 factory erase and bootloader upgrade — is Android-only | +| Notifications | ✓ | ✓ | Native OS notifications | +| Widgets | ✓ | ✗ | Android-only | +| AI Assistant (Chirpy) | ✓\* | ✗ | Google flavor Android only | +| App Functions (system AI) | ✓† | ✗ | Google flavor Android only | \*Chirpy AI requires Android 14+ on Google flavor builds with supported hardware. @@ -122,7 +122,7 @@ Individual doc pages render with full formatting: ## Building from Source -```bash +```shell git clone https://github.com/meshtastic/Meshtastic-Android.git cd Meshtastic-Android ./gradlew :desktopApp:run @@ -135,7 +135,7 @@ Requirements: ## Known Limitations -- Offline tile downloads and local `.mbtiles` archives are not available on desktop. +- Offline base-map downloads and local `.mbtiles` archives are not available on desktop. Offline terrain is — see [Map & Waypoints](map-and-waypoints). - `.kml`/`.kmz`/GeoJSON layer import works — see [Map & Waypoints](map-and-waypoints#map-layers). Site Planner opens in your browser rather than in the app; to bring its coverage estimate onto the map, click the transmitter pin diff --git a/docs/sl-rSI/user/discovery.md b/docs/sl-rSI/user/discovery.md index 17e78a17c0..b14485db93 100644 --- a/docs/sl-rSI/user/discovery.md +++ b/docs/sl-rSI/user/discovery.md @@ -2,7 +2,7 @@ title: Local Mesh Discovery parent: User Guide nav_order: 12 -last_updated: 2026-08-30 +last_updated: 2026-09-11 description: Explore your mesh network — the Local Mesh Discovery scanner, traceroute paths, neighbor maps, and node discovery tools. aliases: - discovery @@ -97,7 +97,7 @@ Mesh Beacon lets nodes invite others to join their mesh. A beaconing node period Configure it under **Settings → Module configuration → Mesh Beacon**. The entry appears only on radios running firmware 2.8.0 or newer. A read-only **Region** row at the top of the screen shows the region the beacon advertises: that region, and the preset, are always the ones the radio itself uses, so a beacon cannot invite anyone onto settings your radio is not running. - **Listen for beacons** — receive invitations broadcast by other nodes. -- **Broadcast a beacon** — periodically advertise this mesh to nearby nodes, with an optional **Beacon message** of up to 100 bytes, a **Broadcast interval** picked from fixed intervals between 1 hour and 72 hours, and an **Offered channel** chosen from your radio's own channels. The offered channel is required, and defaults to your primary channel. +- **Broadcast a beacon** — periodically advertise this mesh to nearby nodes, with an optional **Beacon message** of up to 100 bytes, a **Broadcast interval** picked from fixed intervals between 1 hour and 72 hours, and an **Offered channel** chosen from your radio's own channels. The offered channel is required, and defaults to your primary channel. Over remote admin the picker offers the primary channel only. - **Broadcast targets** — optional extra destinations beyond the offered channel. **Add target** appends a row; each row picks a **Channel** and a **Transmit preset**, and **Remove target** deletes it. With no targets, the beacon goes out on the offered channel alone. Two conditions block beacon setup: diff --git a/docs/sl-rSI/user/help-and-docs.md b/docs/sl-rSI/user/help-and-docs.md index f306cde7f7..c83898cea3 100644 --- a/docs/sl-rSI/user/help-and-docs.md +++ b/docs/sl-rSI/user/help-and-docs.md @@ -2,7 +2,7 @@ title: Help & In-App Docs parent: User Guide nav_order: 21 -last_updated: 2026-08-30 +last_updated: 2026-09-11 description: Browse this documentation inside the app, search it, and ask Chirpy — the on-device AI assistant — questions about Meshtastic. aliases: - help @@ -23,7 +23,7 @@ The docs browser lists every user-guide page. Tap a page to read it; images and ### Search -The search box sits at the top of the docs browser. Type in it to filter pages by title and keywords — results update as you type, and the **Clear search** button (✕) empties the box. +Tap the search field at the top of the docs browser to open a full-screen search view. Type to filter pages by title and keywords — results update as you type, under the same field. The back arrow, or the keyboard's search action, closes the view. The field hides while you scroll down the page list and comes back when you scroll up. ![Searching the in-app documentation](../../assets/screenshots/docs-browser_search.png) diff --git a/docs/sl-rSI/user/map-and-waypoints.md b/docs/sl-rSI/user/map-and-waypoints.md index 1a4a701580..53c70b4da9 100644 --- a/docs/sl-rSI/user/map-and-waypoints.md +++ b/docs/sl-rSI/user/map-and-waypoints.md @@ -2,7 +2,7 @@ title: Map & Waypoints parent: User Guide nav_order: 6 -last_updated: 2026-09-01 +last_updated: 2026-09-11 description: View node positions on the map, create and share waypoints, manage map layers and Site Planner, and control position sharing and privacy. aliases: - map @@ -44,7 +44,7 @@ The floating toolbar provides quick access to the compass, the map type and laye ### Filtering the Map -Tap the filter button in the floating toolbar to open **Filter map**. **Display** controls what is drawn: **Only Favorites**, **Show Waypoints**, **Show Precision Circles**, and a slider that hides nodes not heard from recently. **Node roles** is a chip per device role, plus **All** to show every role; a selected chip means that role is shown. **Nodes** narrows the set further with **Hide offline nodes**, **Only show direct nodes**, **Exclude MQTT**, **Show ignored nodes**, and **Include unknown**. +Tap the filter button in the floating toolbar to open **Filter map**. **Display** controls what is drawn: **Only Favorites**, **Show Waypoints**, **Show Precision Circles**, and a slider that hides nodes not heard from recently. **Node roles** is a chip per device role, plus **All** to show every role; a selected chip means that role is shown. **Nodes** narrows the set further with **Hide offline nodes**, **Only show direct nodes**, **Signed only**, **Encrypted only**, **Exclude MQTT**, **Show ignored nodes**, and **Include unknown** — the same rules as the node list's [filter toggles](nodes). A dot on the filter button means at least one filter is hiding something — check it before concluding the mesh is quiet. Turning **Show Waypoints** off hides every waypoint, including your own. **Show ignored nodes** adds them to the map rather than showing only them — unlike the node list's **Only show ignored Nodes**. @@ -147,8 +147,9 @@ picker, all three offer the same raster base maps: Overlays can be toggled on top of any base map, from the layers sheet: - **Weather radar** — NOAA NEXRAD reflectivity (US coverage) -- **Hillshade** — terrain relief, on **F-Droid** and **Desktop** only. Useful for understanding why a - link fails, since LoRa range is limited by terrain +- **Hillshade** — terrain relief from an online tile source, on **F-Droid** and **Desktop** only; Google + Play draws hillshade from downloaded offline terrain instead. Useful for understanding why a link + fails, since LoRa range is limited by terrain ### Adding your own tile source @@ -156,7 +157,7 @@ Any XYZ tile endpoint can be added as a base map, on every flavor and on desktop Tile Sources** at the foot of the base map picker and paste a URL template using `{z}`, `{x}` and `{y}` — plus `{s}` if the provider uses rotating subdomains. A national mapping service, for example: -``` +```text https://wmts.geo.admin.ch/1.0.0/ch.swisstopo.pixelkarte-farbe/default/current/3857/{z}/{x}/{y}.jpeg ``` @@ -173,7 +174,18 @@ On **Google Play**, the layers sheet's **Offline Manager** section downloads its — water, roads and administrative boundaries extracted from the public Protomaps basemap dataset, drawn directly as map shapes rather than raster tiles. Frame the area you want, tap **Start Download**, and once it finishes flip **Show on map** to draw it (it's off by default so it never surprises you by covering up the -live map). You can still import a pre-made `.mbtiles` archive here too, as before. **Desktop** has neither. +live map). You can still import a pre-made `.mbtiles` archive here too, as before. **Desktop** has neither, though +the offline terrain described next does work there. + +### Offline terrain + +Hillshade and elevation contours can be downloaded for offline use as well, so a downloaded area still +shows why a link fails when there is no network. The terrain data comes from Mapterhorn and is credited on +the map. On **Google Play**, each downloaded region in the **Offline Manager** list has a **Download Terrain** +button and **Hillshade** and **Contours** switches; terrain attaches to that region and shares its storage limit, and an area +too large for terrain asks you to zoom in or pick a smaller region. On **F-Droid** and **Desktop**, the +layers sheet has an **Offline Terrain** section of its own: frame the area, tap **Start Download**, and the +hillshade and contours draw from the downloaded tiles from then on. ### Going offline @@ -181,8 +193,8 @@ An **Offline** pill appears over the map whenever the device has no network conn builds, if you've imported a local `.mbtiles` archive, the map switches to it automatically the moment the network drops — no toggle to remember — and switches back once you're reconnected, as long as you haven't picked a different base map yourself in between. On **F-Droid**, a downloaded offline area keeps rendering on -its own; the pill is purely informational there. **Desktop** has no offline downloads at all yet, so the pill -is informational there too. +its own; the pill is purely informational there. **Desktop** downloads offline terrain but no base-map +packs, so the pill is informational there too. ## Related Topics diff --git a/docs/sl-rSI/user/messages-and-channels.md b/docs/sl-rSI/user/messages-and-channels.md index 1c5e1894e4..aa64fb1ed3 100644 --- a/docs/sl-rSI/user/messages-and-channels.md +++ b/docs/sl-rSI/user/messages-and-channels.md @@ -2,7 +2,7 @@ title: Messages & Channels parent: User Guide nav_order: 3 -last_updated: 2026-08-30 +last_updated: 2026-09-14 description: Send and receive messages, manage channels, configure encryption, search conversations, and use quick chat, reactions, and message actions. aliases: - channels @@ -50,6 +50,21 @@ Key length alone does not change the icon: a 128-bit key and a 256-bit key both Tapping an existing channel opens the same editor, where you can change the name, the PSK, MQTT uplink and downlink, and position precision. Every edit on this screen — adding, editing, deleting, or dragging a channel into a new order — waits on **Send** the same way. +### Archived Channels + +A conversation belongs to the channel it was held on, not to the slot that channel occupied. When a channel leaves your radio — you scan a different channel QR, replace a channel's key, or switch the modem preset — its conversation is **archived** rather than deleted or merged into whatever takes the slot. + +An archived conversation: + +- stays in the **Channels** list under the name the channel had, below the channels currently on your radio, marked with a history icon; +- keeps its full message history, searchable as usual; +- is read-only — you cannot send, reply, or react, because the channel is no longer on the radio to send on, and it is not offered as a share target; +- comes back automatically if you re-add that exact channel, taking its messages back to the live slot. + +Changing the modem preset archives the channel too: the preset is part of a default channel's identity, so **LongFast** and **MediumFast** are different channels even though neither the name nor the key changed. Changing only the region does not archive anything. + +To remove an archived conversation for good, long-press it in the conversation list and delete it. + ## Direct Messages Direct messages (DMs) go to one specific node. When both radios hold each other's public keys, your radio encrypts the message to that node's public key, so no one else on the mesh can read it — not even nodes that share your channel. diff --git a/docs/sl-rSI/user/mqtt.md b/docs/sl-rSI/user/mqtt.md index f58679f87e..848209c4c0 100644 --- a/docs/sl-rSI/user/mqtt.md +++ b/docs/sl-rSI/user/mqtt.md @@ -2,7 +2,7 @@ title: MQTT parent: User Guide nav_order: 11 -last_updated: 2026-08-30 +last_updated: 2026-09-11 description: Bridge your mesh to the internet — MQTT broker setup, encryption layers, and map reporting. aliases: - mqtt @@ -25,7 +25,7 @@ The MQTT module connects your node to an MQTT broker, allowing: ## How It Works -``` +```text [Your Node] → Radio → [Gateway Node with Wi-Fi] → MQTT Broker → [Remote Gateway] → Radio → [Remote Node] ``` diff --git a/docs/sl-rSI/user/node-metrics.md b/docs/sl-rSI/user/node-metrics.md index 1c80826ee5..3bcc552e7d 100644 --- a/docs/sl-rSI/user/node-metrics.md +++ b/docs/sl-rSI/user/node-metrics.md @@ -2,7 +2,7 @@ title: Node Metrics parent: User Guide nav_order: 5 -last_updated: 2026-08-30 +last_updated: 2026-09-09 description: Telemetry dashboards for each mesh node — device health, environment sensors, air quality, signal quality, power, traceroute, and position history. aliases: - metrics @@ -114,7 +114,7 @@ Radio signal quality information: ### Signal Quality Reference -Signal quality is rated from **SNR relative to the active LoRa modem preset's demodulation floor**, not from fixed thresholds — a given SNR means different things on different presets (e.g. −15 dB is fine on LongSlow but unusable on ShortFast). RSSI is shown but is not part of the rating. In the table, _limit_ is the preset's SNR limit. +Signal quality is rated from **SNR relative to the active LoRa modem preset's demodulation floor**, not from fixed thresholds — a given SNR means different things on different presets (e.g. −15 dB is fine on LongSlow but unusable on ShortFast). When RSSI and a noise-floor reading are both available, the app also rates their difference against the preset limit and uses the worse rating. Otherwise, RSSI is display-only. In the table, _limit_ is the preset's SNR limit. | Quality | Criteria | | --------- | ---------------------------------------------------------------- | diff --git a/docs/sl-rSI/user/nodes.md b/docs/sl-rSI/user/nodes.md index a8270bace7..1ce945f477 100644 --- a/docs/sl-rSI/user/nodes.md +++ b/docs/sl-rSI/user/nodes.md @@ -2,7 +2,7 @@ title: Nodes parent: User Guide nav_order: 4 -last_updated: 2026-09-04 +last_updated: 2026-09-11 description: Browse, filter, and sort mesh nodes — view details, signal quality, roles, and quick actions. aliases: - node-list @@ -74,19 +74,23 @@ Most users should keep the default **Client** role. Consider a different role wh > 💡 **Tip:** The mesh works best when most nodes are **Client** or **Router**. Too many Client Mute nodes reduce mesh resilience; too many Routers in a dense area can cause congestion. A good rule of thumb: one Router per 5–10 Clients in your area. -### Encryption Indicators +### Security indicators -Nodes display encryption status icons next to their name: +Each node carries one security icon beside its name in the node list. Tap it to read what it means, and choose **Show All Meanings** in that dialog for the full legend. The detail screen shows the same state in words, as a **Security** row that opens the same dialog. -| Icon | Meaning | -| ----------- | ------------------------------------------------------------------------------------------------------------------------------------------------- | -| 🔒 Locked | Communication uses PKI (public key infrastructure) — end-to-end encrypted with verified identity | -| 🔓 Unlocked | No public key has been received for this node, so it cannot be direct messaged — use **Request User Info** on the node detail page to ask for one | -| ⚠️ Mismatch | Public key mismatch — the node's key has changed since last seen (investigate before trusting) | +| Icon | Meaning | Shown for | +| --------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------- | +| Person with a shield check (green) | **Verified contact** — you verified this node's key in person by exchanging contact QR codes, so its identity is confirmed. The strongest trust the list shows. Your own connected node carries it too | Any firmware version | +| Nodes icon with a shield check (green) | **Signed node** — this node signs its broadcasts with its identity key, so its identity is consistent over time, but you have not verified it in person. On firmware 2.8 the icon appears from the version alone, before any signed broadcast has been heard | Firmware 2.8 or newer, and any node whose signed broadcast your node has verified | +| 🔒 Closed lock | A public key is on file and matches, so direct messages to this node are encrypted | Firmware before 2.8, or no reported version | +| 🔓 Open lock | No public key has been received for this node, so it cannot be direct messaged — use **Request User Info** on the node detail page to ask for one | Firmware before 2.8, or no reported version | +| ⚠️ Mismatch | **Public key mismatch** — a different key arrived for this node after one was stored. Investigate before trusting | Any firmware version | -> 💡 **Tip:** Direct messages always use PKI, so the radio needs the other node's public key before it can send one. It refuses the send rather than falling back to channel encryption. Keys arrive inside node info, which is why an open lock usually clears itself once that node is heard from properly. If you see a key mismatch warning, the node may have been reset or compromised. +Every node on firmware 2.8 or newer signs its broadcasts, so on that firmware the signed state is the baseline and the locks are not shown. -To clear a mismatch, first confirm through another trusted channel that the key change was intentional — a factory reset causes one. Then touch & hold the node, choose **Remove**, and let the two radios exchange keys again the next time yours hears it. +Direct messages always use public-key encryption, so your node needs the other node's public key before it can send one. It refuses the send rather than falling back to channel encryption. Keys arrive inside node info, which is why an open lock usually clears itself once that node is heard from properly. + +A mismatch never replaces the key you already hold. The app keeps the first key it recorded and refuses the new one, as the firmware does, so a stray or hostile node info cannot silently break encrypted messaging to a contact. To clear a mismatch, first confirm through another trusted channel that the key change was intentional — a factory reset causes one. Then touch & hold the node, choose **Remove**, and let the two nodes exchange keys again the next time yours hears it. ## Quick Actions @@ -108,7 +112,9 @@ radio is connected and running firmware 2.8 or newer — see ## Sharing a Contact -On a node's detail screen, tap **Share Contact** to produce a link and a QR code for that node. From the same dialog, **Write to NFC tag** saves the link to a writable NFC tag that anyone can tap to open. +On a node's detail screen, tap **Share Contact** to produce a link and a QR code for that node. From the same dialog, **Share link** opens the Android share sheet (on desktop it copies the link instead), **Write to NFC tag** saves it to a writable NFC tag, and **Copy** puts it on the clipboard. While that dialog is open and in front of you, the phone also offers the same link to any NFC reader, so someone can take the contact by tapping their phone against yours with no tag involved. + +Sharing your own contact this way marks it as verified in person, so whoever imports it sees the **Verified contact** icon rather than the signed one. Relaying someone else's contact passes on only what your app had already recorded about them. To add someone else's contact, use the import button on the node list and choose **Scan Shared Contact QR Code**, **Scan Shared Contact NFC**, or **Input Shared Contact URL**. The app asks you to confirm with **Import Shared Contact?**, and warns you when the contact is one you already have. @@ -120,14 +126,16 @@ Type in the search field to filter nodes by name or short name. The filter updat ### Filter Toggles -| Filter | Opis | -| --------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| **Hide offline nodes** | Show only nodes heard within the last 2 hours | -| **Only show direct nodes** | Show only nodes your radio heard directly, with no relay in between | -| **Include unknown** | Show nodes that haven't sent user info yet. **On by default**, so a node heard before its info arrives stays visible; these carry a badge marking them incomplete, and cannot be direct messaged until their user info brings a public key | -| **Exclude infrastructure** | Hide infrastructure-role nodes (Router, Router Late, Client Base, and legacy Repeater nodes) and any node that cannot be messaged, whatever its role | -| **Exclude MQTT** | Hide nodes heard only via MQTT internet bridge | -| **Only show ignored Nodes** | Replace the list with the nodes you have ignored. Every other node is hidden while this is on, and a banner appears at the top of the list to take you back | +| Filter | Opis | +| --------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **Hide offline nodes** | Show only nodes heard within the last 2 hours | +| **Only show direct nodes** | Show only nodes your radio heard directly, with no relay in between | +| **Include unknown** | Show nodes that haven't sent user info yet. **On by default**, so a node heard before its info arrives stays visible; these carry a badge marking them incomplete, and cannot be direct messaged until their user info brings a public key | +| **Exclude infrastructure** | Hide infrastructure-role nodes (Router, Router Late, Client Base, and legacy Repeater nodes) and any node that cannot be messaged, whatever its role | +| **Exclude MQTT** | Hide nodes heard only via MQTT internet bridge | +| **Signed only** | Show only nodes whose signed broadcasts your node has actually heard and verified. Stricter than the icon: a node on 2.8 shows as signed by its firmware version before any signed broadcast arrives, and a contact verified in person on older firmware is not signed at all | +| **Encrypted only** | Show nodes with a matching public key on file, the key an encrypted direct message needs. A node with a key mismatch is excluded | +| **Only show ignored Nodes** | Replace the list with the nodes you have ignored. Every other node is hidden while this is on, and a banner appears at the top of the list to take you back | ### Sort Options @@ -149,6 +157,8 @@ Tap the hop-histogram icon in the node list's app bar to open a bar chart of how Tapping a node opens the detail view with comprehensive information. See [Node Metrics](node-metrics) for full details on metrics and telemetry. +Signal quality is rated against your modem preset. The same SNR can be good on a long-range preset and poor on a faster one. Traceroute and neighbor-info SNR colors use that preset too. RSSI text has its own strength colors; it affects the quality rating only when a noise-floor reading is also available. + The Details card carries the node's short name, role, IDs, last heard time, hops away, uptime, and its SNR and RSSI: ![Node detail section](../../assets/screenshots/nodes_detail_section.png) diff --git a/docs/sl-rSI/user/settings-module-admin.md b/docs/sl-rSI/user/settings-module-admin.md index 0a84e4e94e..c9793abacb 100644 --- a/docs/sl-rSI/user/settings-module-admin.md +++ b/docs/sl-rSI/user/settings-module-admin.md @@ -2,7 +2,7 @@ title: Settings — Modules & Admin parent: User Guide nav_order: 8 -last_updated: 2026-08-30 +last_updated: 2026-09-16 description: Configure optional feature modules (MQTT, telemetry, canned messages, TAK, and more) and perform device administration. aliases: - modules @@ -333,7 +333,7 @@ appear only when your own node is selected: ### O programu -**Settings → About** carries the app's own identity rather than the radio's: +**Settings → About** carries the app's own identity rather than the node's: Three sections: @@ -341,7 +341,11 @@ Three sections: - **Apps** — opens with **Need Hardware?**, a rotating carousel of popular devices that links out to where to buy one. It also lists the GitHub repository, the running app version, and **Acknowledgements** (see the next section). -- **Project information** — links to the website and to this documentation. +- **Project information** — links to the website, to this documentation, and to the GNU GPL v3 + license the app is released under. + +The footer states the copyright and that the app is free software with no warranty, which you +may redistribute under the same license. ### Acknowledgements diff --git a/docs/sl-rSI/user/settings-radio-user.md b/docs/sl-rSI/user/settings-radio-user.md index 5e3073c95a..024444edeb 100644 --- a/docs/sl-rSI/user/settings-radio-user.md +++ b/docs/sl-rSI/user/settings-radio-user.md @@ -2,7 +2,7 @@ title: Settings — Radio & User parent: User Guide nav_order: 7 -last_updated: 2026-09-04 +last_updated: 2026-09-11 description: Configure your radio hardware, LoRa presets, user profile, position sharing, power management, and security. aliases: - settings @@ -51,7 +51,7 @@ The footer appears as soon as you change something. **Discard** throws the chang The status message is saved with the same **Save**, but it never reboots the node — and, like the rest of this screen, it can be edited on a remote node you administer. For your own radio there is a shortcut while it is connected: touch & hold your node in the [node list](nodes.md) and choose -**Update status**. Older firmware and a disconnected radio have no shortcut — the field above is +**Update status**. Older firmware and a disconnected node have no shortcut — the field itself is still the way in. ## Configuration @@ -77,13 +77,13 @@ On **Settings → LoRa**. | Setting | Opis | Default | | ------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------- | -| Regija | Regulatory region for frequency bands. You must set this before transmitting | Unset (must configure) | +| Regija | Regulatory region for frequency bands. You must set this before transmitting. On firmware 2.8 or newer, the first region set also creates the node's identity key and gives it a new node number | Unset (must configure) | | Presets | Speed/range tradeoff | LongFast | | Number of Hops | Maximum retransmit hops | 3 | | Transmit Power | Transmission power (dBm); 0 = max allowed for region | 0 (region max) | | Frequency Override | Overrides the computed operating frequency outright (MHz). It does not offset the calculated value — leave at 0 unless you know you need a specific frequency | 0 (use calculated) | | Use Preset | On by default. Turn it off to set Spread Factor, Coding Rate and Bandwidth by hand instead of taking them from the modem preset | On | -| Spread Factor | Manual mode only: 7–12. Higher spreads further but slower | From preset | +| Spread Factor | Manual mode only: 5–12. Higher spreads further but slower. On SX127x (RF95) radios the firmware does not accept 5 or 6 and uses 11 instead | From preset | | Coding Rate | Manual mode only: 5–8. More redundancy costs airtime | From preset | | Bandwidth | Manual mode only: the channel bandwidth in kHz, typed in directly. On the 2.4 GHz region the app offers a list of the bandwidths your radio supports instead, and a stored value that is not on that list shows as _Unsupported_ and blocks saving until you pick a supported one | From preset | | Frequency Slot | Which slot within the region's band to use. 0 derives it from the primary channel name | 0 (automatic) | @@ -111,13 +111,13 @@ The Lite, Narrow, Medium Turbo, and Tiny presets need firmware 2.8 or newer — | Short Slow | ~5 km | 6.25 kbps | −10 dB | Suburban short-range; moderate building density | | Medium Fast | ~5 km | 3.52 kbps | −12.5 dB | Suburban areas; moderate building density | | Medium Slow | ~8 km | 1.95 kbps | −15 dB | Suburban/rural; moderate range with slower speed | -| Long Turbo | ~10 km | 1.34 kbps | −12.5 dB | Similar range to Long Fast but with 500 kHz bandwidth; faster throughput | +| Long Turbo | ~10 km | 1.34 kbps | −17.5 dB | Similar range to Long Fast but with 500 kHz bandwidth; faster throughput | | Long Fast | ~10 km | 1.1 kbps | −17.5 dB | **General use (default)** — balanced range and speed | | Long Moderate | ~20 km | 0.34 kbps | −17.5 dB | Rural with some terrain; occasional use | | Lite Fast | ~5 km | 1.76 kbps | −12.5 dB | EU 866 MHz SRD band (125 kHz BW); comparable to Medium Fast | | Lite Slow | ~10 km | 0.98 kbps | −15 dB | EU 866 MHz SRD band (125 kHz BW); comparable to Long Fast | -| Narrow Fast | ~5 km | 2.28 kbps | −10 dB | EU 868 MHz band (62.5 kHz BW); avoids interference with other devices | -| Narrow Slow | ~10 km | 1.30 kbps | −12.5 dB | EU 868 MHz band (62.5 kHz BW); comparable to Long Fast | +| Narrow Fast | ~5 km | 2.28 kbps | −7.5 dB | EU 868 MHz band (62.5 kHz BW); avoids interference with other devices | +| Narrow Slow | ~10 km | 1.30 kbps | −10 dB | EU 868 MHz band (62.5 kHz BW); comparable to Long Fast | | Medium Turbo | ~5 km | 7.0 kbps | −12.5 dB | Like Medium Fast but with 500 kHz bandwidth; not legal in every region. Needs firmware 2.8 or newer | | Tiny Fast | ~10 km | 0.68 kbps | −7.5 dB | Amateur bands that cap occupied bandwidth; these presets use 15.6 kHz. Needs firmware 2.8 or newer, an SX126x or SX127x radio, and a TCXO of ±5 ppm or better | | Tiny Slow | ~20 km | 0.33 kbps | −10 dB | Same band restrictions as Tiny Fast, longer range. Same firmware, radio, and TCXO requirements | diff --git a/docs/sl-rSI/user/signal-meter.md b/docs/sl-rSI/user/signal-meter.md index c2bad48c21..8929a9e1f8 100644 --- a/docs/sl-rSI/user/signal-meter.md +++ b/docs/sl-rSI/user/signal-meter.md @@ -2,7 +2,7 @@ title: How the Meshtastic Signal Meter Works parent: User Guide nav_order: 15 -last_updated: 2026-08-30 +last_updated: 2026-09-09 description: How the signal meter rates quality from SNR relative to the LoRa modem preset — spread spectrum, presets, and what the bars really mean. aliases: - signal @@ -32,7 +32,7 @@ Each modem preset has an SNR limit: the lowest SNR at which that preset can stil ## Rating Signal Quality -The app rates signal quality (None, Bad, Fair, or Good) from SNR alone, measured against the active preset's SNR limit. It does not factor in RSSI: without knowing the local noise floor, RSSI alone cannot say whether a signal is decodable. RSSI is still available — on the node detail screen and in the metrics charts. +The app rates signal quality (None, Bad, Fair, or Good) from SNR relative to the active preset's SNR limit. When RSSI and a noise-floor reading are both available, it also rates the difference between them against that limit and uses the worse rating. Otherwise, it uses SNR alone. Because the rating is relative to the preset, the same SNR rates differently on different presets. An SNR of −16 dB rates Good on Long Fast (SNR limit −17.5 dB) but None on Short Fast (SNR limit −7.5 dB). Let `limit` be the active preset's SNR limit: @@ -45,7 +45,7 @@ Because the rating is relative to the preset, the same SNR rates differently on The icon never goes blank, so count the bars carefully: a single bar means None, not a weak but usable link, and Good is a solid wedge rather than a set of bars. A gray three-bar icon labeled Unknown is a different state again — the packet carried no SNR measurement at all, which is not the same as measuring one and finding it too weak. -> ℹ️ **Note:** Traceroute hop colors use fixed thresholds (−7 dB / −15 dB); the per-node signal meter uses the preset-relative rating instead. +> ℹ️ **Note:** Traceroute and neighbor-info SNR colors use the active preset's limits too. RSSI text has separate strength colors: green above −115 dBm, yellow above −120 dBm, orange above −126 dBm, and red at or below −126 dBm. ## Diagnosing Local Interference diff --git a/docs/sl-rSI/user/tak.md b/docs/sl-rSI/user/tak.md index d58d8d5836..1bc2ac7f03 100644 --- a/docs/sl-rSI/user/tak.md +++ b/docs/sl-rSI/user/tak.md @@ -2,7 +2,7 @@ title: TAK Integration parent: User Guide nav_order: 10 -last_updated: 2026-08-30 +last_updated: 2026-09-11 description: Interoperate with ATAK and WinTAK — CoT position sharing, TAK roles, and plugin setup. aliases: - tak @@ -36,7 +36,7 @@ The TAK module allows Meshtastic nodes to: > ⚠️ **Warning:** The old **Meshtastic ATAK Plugin** is no longer part of this path and cannot > work. It bridged through the cross-process AIDL API, which was removed in app 2.8.0; the mesh > service is now in-process only. Do not install it. Interop today runs over the app's own local -> TAK server plus the Mesh to CoT Converter, both described below, with stock ATAK/iTAK/WinTAK. +> TAK server plus the Mesh to CoT Converter, both described in the following sections, with stock ATAK/iTAK/WinTAK. ### Configuration diff --git a/docs/sl-rSI/user/translate.md b/docs/sl-rSI/user/translate.md index 67d69e2d96..cdc8b6d87f 100644 --- a/docs/sl-rSI/user/translate.md +++ b/docs/sl-rSI/user/translate.md @@ -2,7 +2,7 @@ title: Translate the App parent: User Guide nav_order: 17 -last_updated: 2026-08-30 +last_updated: 2026-09-11 description: How the app and its documentation are translated via Crowdin, and guidelines for contributing translations. aliases: - translate @@ -45,7 +45,7 @@ If your language is not yet listed on Crowdin: The Android app uses **Compose Multiplatform resources** for all user-visible strings: -``` +```text core/resources/src/commonMain/composeResources/ ├── values/ ← English (default) │ └── strings.xml @@ -58,7 +58,7 @@ core/resources/src/commonMain/composeResources/ In-app documentation follows a similar pattern under `docs/`: -``` +```text docs/ ├── en/user/ ← English source (default) │ ├── onboarding.md diff --git a/docs/sq-rAL/user/desktop.md b/docs/sq-rAL/user/desktop.md index efca3c03c9..2ec0d98037 100644 --- a/docs/sq-rAL/user/desktop.md +++ b/docs/sq-rAL/user/desktop.md @@ -2,7 +2,7 @@ title: Desktop App parent: User Guide nav_order: 14 -last_updated: 2026-08-30 +last_updated: 2026-09-11 description: Install and use the Meshtastic Desktop app on Linux, macOS, and Windows — connections, feature parity, and keyboard shortcuts. aliases: - desktop @@ -58,20 +58,20 @@ Bluetooth Low Energy is supported on desktop via the [Kable](https://github.com/ ## Feature Parity -| Feature | Android | Desktop | Notes | -| ----------------------------------------------------- | ------- | ------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Messaging | ✓ | ✓ | Full parity | -| Node List | ✓ | ✓ | Full parity | -| Map | ✓ | ✓ | Interactive MapLibre map, with base map and overlay pickers and custom tile sources. No offline downloads or local `.mbtiles` archives | -| Map layers (`.kml`/`.kmz`/GeoJSON) | ✓ | ✓ | Same layer store and sheet as Android; imported files draw on the desktop map | -| Site Planner | ✓ | ✓\* | \*Opens in your browser on desktop; the estimate is not drawn on the desktop map | -| Settings | ✓ | ✓ | Full parity | -| Bluetooth (BLE) | ✓ | ✓ | Via Kable on desktop | -| Firmware Update | ✓ | ✓ | In-app USB, BLE, and Wi-Fi (ESP32) update work the same as Android. The USB maintenance flow — nRF52/RP2040 factory erase and bootloader upgrade — is Android-only | -| Notifications | ✓ | ✓ | Native OS notifications | -| Widgets | ✓ | ✗ | Android-only | -| AI Assistant (Chirpy) | ✓\* | ✗ | Google flavor Android only | -| App Functions (system AI) | ✓† | ✗ | Google flavor Android only | +| Feature | Android | Desktop | Notes | +| ----------------------------------------------------- | ------- | ------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Messaging | ✓ | ✓ | Full parity | +| Node List | ✓ | ✓ | Full parity | +| Map | ✓ | ✓ | Interactive MapLibre map, with base map and overlay pickers and custom tile sources. Offline terrain (hillshade and contours) can be downloaded; offline base-map packs and local `.mbtiles` archives cannot | +| Map layers (`.kml`/`.kmz`/GeoJSON) | ✓ | ✓ | Same layer store and sheet as Android; imported files draw on the desktop map | +| Site Planner | ✓ | ✓\* | \*Opens in your browser on desktop; the estimate is not drawn on the desktop map | +| Settings | ✓ | ✓ | Full parity | +| Bluetooth (BLE) | ✓ | ✓ | Via Kable on desktop | +| Firmware Update | ✓ | ✓ | In-app USB, BLE, and Wi-Fi (ESP32) update work the same as Android. The USB maintenance flow — nRF52/RP2040 factory erase and bootloader upgrade — is Android-only | +| Notifications | ✓ | ✓ | Native OS notifications | +| Widgets | ✓ | ✗ | Android-only | +| AI Assistant (Chirpy) | ✓\* | ✗ | Google flavor Android only | +| App Functions (system AI) | ✓† | ✗ | Google flavor Android only | \*Chirpy AI requires Android 14+ on Google flavor builds with supported hardware. @@ -122,7 +122,7 @@ Individual doc pages render with full formatting: ## Building from Source -```bash +```shell git clone https://github.com/meshtastic/Meshtastic-Android.git cd Meshtastic-Android ./gradlew :desktopApp:run @@ -135,7 +135,7 @@ Requirements: ## Known Limitations -- Offline tile downloads and local `.mbtiles` archives are not available on desktop. +- Offline base-map downloads and local `.mbtiles` archives are not available on desktop. Offline terrain is — see [Map & Waypoints](map-and-waypoints). - `.kml`/`.kmz`/GeoJSON layer import works — see [Map & Waypoints](map-and-waypoints#map-layers). Site Planner opens in your browser rather than in the app; to bring its coverage estimate onto the map, click the transmitter pin diff --git a/docs/sq-rAL/user/discovery.md b/docs/sq-rAL/user/discovery.md index 49604cc196..ea04b614ad 100644 --- a/docs/sq-rAL/user/discovery.md +++ b/docs/sq-rAL/user/discovery.md @@ -2,7 +2,7 @@ title: Local Mesh Discovery parent: User Guide nav_order: 12 -last_updated: 2026-08-30 +last_updated: 2026-09-11 description: Explore your mesh network — the Local Mesh Discovery scanner, traceroute paths, neighbor maps, and node discovery tools. aliases: - discovery @@ -97,7 +97,7 @@ Mesh Beacon lets nodes invite others to join their mesh. A beaconing node period Configure it under **Settings → Module configuration → Mesh Beacon**. The entry appears only on radios running firmware 2.8.0 or newer. A read-only **Region** row at the top of the screen shows the region the beacon advertises: that region, and the preset, are always the ones the radio itself uses, so a beacon cannot invite anyone onto settings your radio is not running. - **Listen for beacons** — receive invitations broadcast by other nodes. -- **Broadcast a beacon** — periodically advertise this mesh to nearby nodes, with an optional **Beacon message** of up to 100 bytes, a **Broadcast interval** picked from fixed intervals between 1 hour and 72 hours, and an **Offered channel** chosen from your radio's own channels. The offered channel is required, and defaults to your primary channel. +- **Broadcast a beacon** — periodically advertise this mesh to nearby nodes, with an optional **Beacon message** of up to 100 bytes, a **Broadcast interval** picked from fixed intervals between 1 hour and 72 hours, and an **Offered channel** chosen from your radio's own channels. The offered channel is required, and defaults to your primary channel. Over remote admin the picker offers the primary channel only. - **Broadcast targets** — optional extra destinations beyond the offered channel. **Add target** appends a row; each row picks a **Channel** and a **Transmit preset**, and **Remove target** deletes it. With no targets, the beacon goes out on the offered channel alone. Two conditions block beacon setup: diff --git a/docs/sq-rAL/user/help-and-docs.md b/docs/sq-rAL/user/help-and-docs.md index f306cde7f7..c83898cea3 100644 --- a/docs/sq-rAL/user/help-and-docs.md +++ b/docs/sq-rAL/user/help-and-docs.md @@ -2,7 +2,7 @@ title: Help & In-App Docs parent: User Guide nav_order: 21 -last_updated: 2026-08-30 +last_updated: 2026-09-11 description: Browse this documentation inside the app, search it, and ask Chirpy — the on-device AI assistant — questions about Meshtastic. aliases: - help @@ -23,7 +23,7 @@ The docs browser lists every user-guide page. Tap a page to read it; images and ### Search -The search box sits at the top of the docs browser. Type in it to filter pages by title and keywords — results update as you type, and the **Clear search** button (✕) empties the box. +Tap the search field at the top of the docs browser to open a full-screen search view. Type to filter pages by title and keywords — results update as you type, under the same field. The back arrow, or the keyboard's search action, closes the view. The field hides while you scroll down the page list and comes back when you scroll up. ![Searching the in-app documentation](../../assets/screenshots/docs-browser_search.png) diff --git a/docs/sq-rAL/user/map-and-waypoints.md b/docs/sq-rAL/user/map-and-waypoints.md index f6d49fc52e..fd843de196 100644 --- a/docs/sq-rAL/user/map-and-waypoints.md +++ b/docs/sq-rAL/user/map-and-waypoints.md @@ -2,7 +2,7 @@ title: Map & Waypoints parent: User Guide nav_order: 6 -last_updated: 2026-09-01 +last_updated: 2026-09-11 description: View node positions on the map, create and share waypoints, manage map layers and Site Planner, and control position sharing and privacy. aliases: - map @@ -44,7 +44,7 @@ The floating toolbar provides quick access to the compass, the map type and laye ### Filtering the Map -Tap the filter button in the floating toolbar to open **Filter map**. **Display** controls what is drawn: **Only Favorites**, **Show Waypoints**, **Show Precision Circles**, and a slider that hides nodes not heard from recently. **Node roles** is a chip per device role, plus **All** to show every role; a selected chip means that role is shown. **Nodes** narrows the set further with **Hide offline nodes**, **Only show direct nodes**, **Exclude MQTT**, **Show ignored nodes**, and **Include unknown**. +Tap the filter button in the floating toolbar to open **Filter map**. **Display** controls what is drawn: **Only Favorites**, **Show Waypoints**, **Show Precision Circles**, and a slider that hides nodes not heard from recently. **Node roles** is a chip per device role, plus **All** to show every role; a selected chip means that role is shown. **Nodes** narrows the set further with **Hide offline nodes**, **Only show direct nodes**, **Signed only**, **Encrypted only**, **Exclude MQTT**, **Show ignored nodes**, and **Include unknown** — the same rules as the node list's [filter toggles](nodes). A dot on the filter button means at least one filter is hiding something — check it before concluding the mesh is quiet. Turning **Show Waypoints** off hides every waypoint, including your own. **Show ignored nodes** adds them to the map rather than showing only them — unlike the node list's **Only show ignored Nodes**. @@ -147,8 +147,9 @@ picker, all three offer the same raster base maps: Overlays can be toggled on top of any base map, from the layers sheet: - **Weather radar** — NOAA NEXRAD reflectivity (US coverage) -- **Hillshade** — terrain relief, on **F-Droid** and **Desktop** only. Useful for understanding why a - link fails, since LoRa range is limited by terrain +- **Hillshade** — terrain relief from an online tile source, on **F-Droid** and **Desktop** only; Google + Play draws hillshade from downloaded offline terrain instead. Useful for understanding why a link + fails, since LoRa range is limited by terrain ### Adding your own tile source @@ -156,7 +157,7 @@ Any XYZ tile endpoint can be added as a base map, on every flavor and on desktop Tile Sources** at the foot of the base map picker and paste a URL template using `{z}`, `{x}` and `{y}` — plus `{s}` if the provider uses rotating subdomains. A national mapping service, for example: -``` +```text https://wmts.geo.admin.ch/1.0.0/ch.swisstopo.pixelkarte-farbe/default/current/3857/{z}/{x}/{y}.jpeg ``` @@ -173,7 +174,18 @@ On **Google Play**, the layers sheet's **Offline Manager** section downloads its — water, roads and administrative boundaries extracted from the public Protomaps basemap dataset, drawn directly as map shapes rather than raster tiles. Frame the area you want, tap **Start Download**, and once it finishes flip **Show on map** to draw it (it's off by default so it never surprises you by covering up the -live map). You can still import a pre-made `.mbtiles` archive here too, as before. **Desktop** has neither. +live map). You can still import a pre-made `.mbtiles` archive here too, as before. **Desktop** has neither, though +the offline terrain described next does work there. + +### Offline terrain + +Hillshade and elevation contours can be downloaded for offline use as well, so a downloaded area still +shows why a link fails when there is no network. The terrain data comes from Mapterhorn and is credited on +the map. On **Google Play**, each downloaded region in the **Offline Manager** list has a **Download Terrain** +button and **Hillshade** and **Contours** switches; terrain attaches to that region and shares its storage limit, and an area +too large for terrain asks you to zoom in or pick a smaller region. On **F-Droid** and **Desktop**, the +layers sheet has an **Offline Terrain** section of its own: frame the area, tap **Start Download**, and the +hillshade and contours draw from the downloaded tiles from then on. ### Going offline @@ -181,8 +193,8 @@ An **Offline** pill appears over the map whenever the device has no network conn builds, if you've imported a local `.mbtiles` archive, the map switches to it automatically the moment the network drops — no toggle to remember — and switches back once you're reconnected, as long as you haven't picked a different base map yourself in between. On **F-Droid**, a downloaded offline area keeps rendering on -its own; the pill is purely informational there. **Desktop** has no offline downloads at all yet, so the pill -is informational there too. +its own; the pill is purely informational there. **Desktop** downloads offline terrain but no base-map +packs, so the pill is informational there too. ## Related Topics diff --git a/docs/sq-rAL/user/messages-and-channels.md b/docs/sq-rAL/user/messages-and-channels.md index a9cc5fe697..3f6e44897d 100644 --- a/docs/sq-rAL/user/messages-and-channels.md +++ b/docs/sq-rAL/user/messages-and-channels.md @@ -2,7 +2,7 @@ title: Messages & Channels parent: User Guide nav_order: 3 -last_updated: 2026-08-30 +last_updated: 2026-09-14 description: Send and receive messages, manage channels, configure encryption, search conversations, and use quick chat, reactions, and message actions. aliases: - channels @@ -50,6 +50,21 @@ Key length alone does not change the icon: a 128-bit key and a 256-bit key both Tapping an existing channel opens the same editor, where you can change the name, the PSK, MQTT uplink and downlink, and position precision. Every edit on this screen — adding, editing, deleting, or dragging a channel into a new order — waits on **Send** the same way. +### Archived Channels + +A conversation belongs to the channel it was held on, not to the slot that channel occupied. When a channel leaves your radio — you scan a different channel QR, replace a channel's key, or switch the modem preset — its conversation is **archived** rather than deleted or merged into whatever takes the slot. + +An archived conversation: + +- stays in the **Channels** list under the name the channel had, below the channels currently on your radio, marked with a history icon; +- keeps its full message history, searchable as usual; +- is read-only — you cannot send, reply, or react, because the channel is no longer on the radio to send on, and it is not offered as a share target; +- comes back automatically if you re-add that exact channel, taking its messages back to the live slot. + +Changing the modem preset archives the channel too: the preset is part of a default channel's identity, so **LongFast** and **MediumFast** are different channels even though neither the name nor the key changed. Changing only the region does not archive anything. + +To remove an archived conversation for good, long-press it in the conversation list and delete it. + ## Direct Messages Direct messages (DMs) go to one specific node. When both radios hold each other's public keys, your radio encrypts the message to that node's public key, so no one else on the mesh can read it — not even nodes that share your channel. diff --git a/docs/sq-rAL/user/mqtt.md b/docs/sq-rAL/user/mqtt.md index c0bd20391b..0ebee20ed7 100644 --- a/docs/sq-rAL/user/mqtt.md +++ b/docs/sq-rAL/user/mqtt.md @@ -2,7 +2,7 @@ title: MQTT parent: User Guide nav_order: 11 -last_updated: 2026-08-30 +last_updated: 2026-09-11 description: Bridge your mesh to the internet — MQTT broker setup, encryption layers, and map reporting. aliases: - mqtt @@ -25,7 +25,7 @@ The MQTT module connects your node to an MQTT broker, allowing: ## How It Works -``` +```text [Your Node] → Radio → [Gateway Node with Wi-Fi] → MQTT Broker → [Remote Gateway] → Radio → [Remote Node] ``` diff --git a/docs/sq-rAL/user/node-metrics.md b/docs/sq-rAL/user/node-metrics.md index aac9d2da7f..7fe5369dd3 100644 --- a/docs/sq-rAL/user/node-metrics.md +++ b/docs/sq-rAL/user/node-metrics.md @@ -2,7 +2,7 @@ title: Node Metrics parent: User Guide nav_order: 5 -last_updated: 2026-08-30 +last_updated: 2026-09-09 description: Telemetry dashboards for each mesh node — device health, environment sensors, air quality, signal quality, power, traceroute, and position history. aliases: - metrics @@ -114,7 +114,7 @@ Radio signal quality information: ### Signal Quality Reference -Signal quality is rated from **SNR relative to the active LoRa modem preset's demodulation floor**, not from fixed thresholds — a given SNR means different things on different presets (e.g. −15 dB is fine on LongSlow but unusable on ShortFast). RSSI is shown but is not part of the rating. In the table, _limit_ is the preset's SNR limit. +Signal quality is rated from **SNR relative to the active LoRa modem preset's demodulation floor**, not from fixed thresholds — a given SNR means different things on different presets (e.g. −15 dB is fine on LongSlow but unusable on ShortFast). When RSSI and a noise-floor reading are both available, the app also rates their difference against the preset limit and uses the worse rating. Otherwise, RSSI is display-only. In the table, _limit_ is the preset's SNR limit. | Quality | Criteria | | ------- | ---------------------------------------------------------------- | diff --git a/docs/sq-rAL/user/nodes.md b/docs/sq-rAL/user/nodes.md index 7edf68689a..98c704d9e0 100644 --- a/docs/sq-rAL/user/nodes.md +++ b/docs/sq-rAL/user/nodes.md @@ -2,7 +2,7 @@ title: Nodes parent: User Guide nav_order: 4 -last_updated: 2026-09-04 +last_updated: 2026-09-11 description: Browse, filter, and sort mesh nodes — view details, signal quality, roles, and quick actions. aliases: - node-list @@ -74,19 +74,23 @@ Most users should keep the default **Client** role. Consider a different role wh > 💡 **Tip:** The mesh works best when most nodes are **Client** or **Router**. Too many Client Mute nodes reduce mesh resilience; too many Routers in a dense area can cause congestion. A good rule of thumb: one Router per 5–10 Clients in your area. -### Encryption Indicators +### Security indicators -Nodes display encryption status icons next to their name: +Each node carries one security icon beside its name in the node list. Tap it to read what it means, and choose **Show All Meanings** in that dialog for the full legend. The detail screen shows the same state in words, as a **Security** row that opens the same dialog. -| Icon | Meaning | -| ----------- | ------------------------------------------------------------------------------------------------------------------------------------------------- | -| 🔒 Locked | Communication uses PKI (public key infrastructure) — end-to-end encrypted with verified identity | -| 🔓 Unlocked | No public key has been received for this node, so it cannot be direct messaged — use **Request User Info** on the node detail page to ask for one | -| ⚠️ Mismatch | Public key mismatch — the node's key has changed since last seen (investigate before trusting) | +| Icon | Meaning | Shown for | +| --------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------- | +| Person with a shield check (green) | **Verified contact** — you verified this node's key in person by exchanging contact QR codes, so its identity is confirmed. The strongest trust the list shows. Your own connected node carries it too | Any firmware version | +| Nodes icon with a shield check (green) | **Signed node** — this node signs its broadcasts with its identity key, so its identity is consistent over time, but you have not verified it in person. On firmware 2.8 the icon appears from the version alone, before any signed broadcast has been heard | Firmware 2.8 or newer, and any node whose signed broadcast your node has verified | +| 🔒 Closed lock | A public key is on file and matches, so direct messages to this node are encrypted | Firmware before 2.8, or no reported version | +| 🔓 Open lock | No public key has been received for this node, so it cannot be direct messaged — use **Request User Info** on the node detail page to ask for one | Firmware before 2.8, or no reported version | +| ⚠️ Mismatch | **Public key mismatch** — a different key arrived for this node after one was stored. Investigate before trusting | Any firmware version | -> 💡 **Tip:** Direct messages always use PKI, so the radio needs the other node's public key before it can send one. It refuses the send rather than falling back to channel encryption. Keys arrive inside node info, which is why an open lock usually clears itself once that node is heard from properly. If you see a key mismatch warning, the node may have been reset or compromised. +Every node on firmware 2.8 or newer signs its broadcasts, so on that firmware the signed state is the baseline and the locks are not shown. -To clear a mismatch, first confirm through another trusted channel that the key change was intentional — a factory reset causes one. Then touch & hold the node, choose **Remove**, and let the two radios exchange keys again the next time yours hears it. +Direct messages always use public-key encryption, so your node needs the other node's public key before it can send one. It refuses the send rather than falling back to channel encryption. Keys arrive inside node info, which is why an open lock usually clears itself once that node is heard from properly. + +A mismatch never replaces the key you already hold. The app keeps the first key it recorded and refuses the new one, as the firmware does, so a stray or hostile node info cannot silently break encrypted messaging to a contact. To clear a mismatch, first confirm through another trusted channel that the key change was intentional — a factory reset causes one. Then touch & hold the node, choose **Remove**, and let the two nodes exchange keys again the next time yours hears it. ## Quick Actions @@ -108,7 +112,9 @@ radio is connected and running firmware 2.8 or newer — see ## Sharing a Contact -On a node's detail screen, tap **Share Contact** to produce a link and a QR code for that node. From the same dialog, **Write to NFC tag** saves the link to a writable NFC tag that anyone can tap to open. +On a node's detail screen, tap **Share Contact** to produce a link and a QR code for that node. From the same dialog, **Share link** opens the Android share sheet (on desktop it copies the link instead), **Write to NFC tag** saves it to a writable NFC tag, and **Copy** puts it on the clipboard. While that dialog is open and in front of you, the phone also offers the same link to any NFC reader, so someone can take the contact by tapping their phone against yours with no tag involved. + +Sharing your own contact this way marks it as verified in person, so whoever imports it sees the **Verified contact** icon rather than the signed one. Relaying someone else's contact passes on only what your app had already recorded about them. To add someone else's contact, use the import button on the node list and choose **Scan Shared Contact QR Code**, **Scan Shared Contact NFC**, or **Input Shared Contact URL**. The app asks you to confirm with **Import Shared Contact?**, and warns you when the contact is one you already have. @@ -120,14 +126,16 @@ Type in the search field to filter nodes by name or short name. The filter updat ### Filter Toggles -| Filtrimi | Përshkrimi | -| --------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| **Hide offline nodes** | Show only nodes heard within the last 2 hours | -| **Only show direct nodes** | Show only nodes your radio heard directly, with no relay in between | -| **Include unknown** | Show nodes that haven't sent user info yet. **On by default**, so a node heard before its info arrives stays visible; these carry a badge marking them incomplete, and cannot be direct messaged until their user info brings a public key | -| **Exclude infrastructure** | Hide infrastructure-role nodes (Router, Router Late, Client Base, and legacy Repeater nodes) and any node that cannot be messaged, whatever its role | -| **Exclude MQTT** | Hide nodes heard only via MQTT internet bridge | -| **Only show ignored Nodes** | Replace the list with the nodes you have ignored. Every other node is hidden while this is on, and a banner appears at the top of the list to take you back | +| Filtrimi | Përshkrimi | +| --------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **Hide offline nodes** | Show only nodes heard within the last 2 hours | +| **Only show direct nodes** | Show only nodes your radio heard directly, with no relay in between | +| **Include unknown** | Show nodes that haven't sent user info yet. **On by default**, so a node heard before its info arrives stays visible; these carry a badge marking them incomplete, and cannot be direct messaged until their user info brings a public key | +| **Exclude infrastructure** | Hide infrastructure-role nodes (Router, Router Late, Client Base, and legacy Repeater nodes) and any node that cannot be messaged, whatever its role | +| **Exclude MQTT** | Hide nodes heard only via MQTT internet bridge | +| **Signed only** | Show only nodes whose signed broadcasts your node has actually heard and verified. Stricter than the icon: a node on 2.8 shows as signed by its firmware version before any signed broadcast arrives, and a contact verified in person on older firmware is not signed at all | +| **Encrypted only** | Show nodes with a matching public key on file, the key an encrypted direct message needs. A node with a key mismatch is excluded | +| **Only show ignored Nodes** | Replace the list with the nodes you have ignored. Every other node is hidden while this is on, and a banner appears at the top of the list to take you back | ### Sort Options @@ -149,6 +157,8 @@ Tap the hop-histogram icon in the node list's app bar to open a bar chart of how Tapping a node opens the detail view with comprehensive information. See [Node Metrics](node-metrics) for full details on metrics and telemetry. +Signal quality is rated against your modem preset. The same SNR can be good on a long-range preset and poor on a faster one. Traceroute and neighbor-info SNR colors use that preset too. RSSI text has its own strength colors; it affects the quality rating only when a noise-floor reading is also available. + The Details card carries the node's short name, role, IDs, last heard time, hops away, uptime, and its SNR and RSSI: ![Node detail section](../../assets/screenshots/nodes_detail_section.png) diff --git a/docs/sq-rAL/user/settings-module-admin.md b/docs/sq-rAL/user/settings-module-admin.md index 98eae259d8..8efe87a896 100644 --- a/docs/sq-rAL/user/settings-module-admin.md +++ b/docs/sq-rAL/user/settings-module-admin.md @@ -2,7 +2,7 @@ title: Settings — Modules & Admin parent: User Guide nav_order: 8 -last_updated: 2026-08-30 +last_updated: 2026-09-16 description: Configure optional feature modules (MQTT, telemetry, canned messages, TAK, and more) and perform device administration. aliases: - modules @@ -333,7 +333,7 @@ appear only when your own node is selected: ### Rreth -**Settings → About** carries the app's own identity rather than the radio's: +**Settings → About** carries the app's own identity rather than the node's: Three sections: @@ -341,7 +341,11 @@ Three sections: - **Apps** — opens with **Need Hardware?**, a rotating carousel of popular devices that links out to where to buy one. It also lists the GitHub repository, the running app version, and **Acknowledgements** (see the next section). -- **Project information** — links to the website and to this documentation. +- **Project information** — links to the website, to this documentation, and to the GNU GPL v3 + license the app is released under. + +The footer states the copyright and that the app is free software with no warranty, which you +may redistribute under the same license. ### Acknowledgements diff --git a/docs/sq-rAL/user/settings-radio-user.md b/docs/sq-rAL/user/settings-radio-user.md index 1d8b84e2dd..b5ceb266e3 100644 --- a/docs/sq-rAL/user/settings-radio-user.md +++ b/docs/sq-rAL/user/settings-radio-user.md @@ -2,7 +2,7 @@ title: Settings — Radio & User parent: User Guide nav_order: 7 -last_updated: 2026-09-04 +last_updated: 2026-09-11 description: Configure your radio hardware, LoRa presets, user profile, position sharing, power management, and security. aliases: - settings @@ -51,7 +51,7 @@ The footer appears as soon as you change something. **Discard** throws the chang The status message is saved with the same **Save**, but it never reboots the node — and, like the rest of this screen, it can be edited on a remote node you administer. For your own radio there is a shortcut while it is connected: touch & hold your node in the [node list](nodes.md) and choose -**Update status**. Older firmware and a disconnected radio have no shortcut — the field above is +**Update status**. Older firmware and a disconnected node have no shortcut — the field itself is still the way in. ## Configuration @@ -77,13 +77,13 @@ On **Settings → LoRa**. | Setting | Përshkrimi | Default | | ------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------- | -| Rajon | Regulatory region for frequency bands. You must set this before transmitting | Unset (must configure) | +| Rajon | Regulatory region for frequency bands. You must set this before transmitting. On firmware 2.8 or newer, the first region set also creates the node's identity key and gives it a new node number | Unset (must configure) | | Presets | Speed/range tradeoff | LongFast | | Number of Hops | Maximum retransmit hops | 3 | | Transmit Power | Transmission power (dBm); 0 = max allowed for region | 0 (region max) | | Frequency Override | Overrides the computed operating frequency outright (MHz). It does not offset the calculated value — leave at 0 unless you know you need a specific frequency | 0 (use calculated) | | Use Preset | On by default. Turn it off to set Spread Factor, Coding Rate and Bandwidth by hand instead of taking them from the modem preset | On | -| Spread Factor | Manual mode only: 7–12. Higher spreads further but slower | From preset | +| Spread Factor | Manual mode only: 5–12. Higher spreads further but slower. On SX127x (RF95) radios the firmware does not accept 5 or 6 and uses 11 instead | From preset | | Coding Rate | Manual mode only: 5–8. More redundancy costs airtime | From preset | | Bandwidth | Manual mode only: the channel bandwidth in kHz, typed in directly. On the 2.4 GHz region the app offers a list of the bandwidths your radio supports instead, and a stored value that is not on that list shows as _Unsupported_ and blocks saving until you pick a supported one | From preset | | Frequency Slot | Which slot within the region's band to use. 0 derives it from the primary channel name | 0 (automatic) | @@ -111,13 +111,13 @@ The Lite, Narrow, Medium Turbo, and Tiny presets need firmware 2.8 or newer — | Short Slow | ~5 km | 6.25 kbps | −10 dB | Suburban short-range; moderate building density | | Medium Fast | ~5 km | 3.52 kbps | −12.5 dB | Suburban areas; moderate building density | | Medium Slow | ~8 km | 1.95 kbps | −15 dB | Suburban/rural; moderate range with slower speed | -| Long Turbo | ~10 km | 1.34 kbps | −12.5 dB | Similar range to Long Fast but with 500 kHz bandwidth; faster throughput | +| Long Turbo | ~10 km | 1.34 kbps | −17.5 dB | Similar range to Long Fast but with 500 kHz bandwidth; faster throughput | | Long Fast | ~10 km | 1.1 kbps | −17.5 dB | **General use (default)** — balanced range and speed | | Long Moderate | ~20 km | 0.34 kbps | −17.5 dB | Rural with some terrain; occasional use | | Lite Fast | ~5 km | 1.76 kbps | −12.5 dB | EU 866 MHz SRD band (125 kHz BW); comparable to Medium Fast | | Lite Slow | ~10 km | 0.98 kbps | −15 dB | EU 866 MHz SRD band (125 kHz BW); comparable to Long Fast | -| Narrow Fast | ~5 km | 2.28 kbps | −10 dB | EU 868 MHz band (62.5 kHz BW); avoids interference with other devices | -| Narrow Slow | ~10 km | 1.30 kbps | −12.5 dB | EU 868 MHz band (62.5 kHz BW); comparable to Long Fast | +| Narrow Fast | ~5 km | 2.28 kbps | −7.5 dB | EU 868 MHz band (62.5 kHz BW); avoids interference with other devices | +| Narrow Slow | ~10 km | 1.30 kbps | −10 dB | EU 868 MHz band (62.5 kHz BW); comparable to Long Fast | | Medium Turbo | ~5 km | 7.0 kbps | −12.5 dB | Like Medium Fast but with 500 kHz bandwidth; not legal in every region. Needs firmware 2.8 or newer | | Tiny Fast | ~10 km | 0.68 kbps | −7.5 dB | Amateur bands that cap occupied bandwidth; these presets use 15.6 kHz. Needs firmware 2.8 or newer, an SX126x or SX127x radio, and a TCXO of ±5 ppm or better | | Tiny Slow | ~20 km | 0.33 kbps | −10 dB | Same band restrictions as Tiny Fast, longer range. Same firmware, radio, and TCXO requirements | diff --git a/docs/sq-rAL/user/signal-meter.md b/docs/sq-rAL/user/signal-meter.md index 44633fd9a1..4e3f3442e3 100644 --- a/docs/sq-rAL/user/signal-meter.md +++ b/docs/sq-rAL/user/signal-meter.md @@ -2,7 +2,7 @@ title: How the Meshtastic Signal Meter Works parent: User Guide nav_order: 15 -last_updated: 2026-08-30 +last_updated: 2026-09-09 description: How the signal meter rates quality from SNR relative to the LoRa modem preset — spread spectrum, presets, and what the bars really mean. aliases: - signal @@ -32,7 +32,7 @@ Each modem preset has an SNR limit: the lowest SNR at which that preset can stil ## Rating Signal Quality -The app rates signal quality (None, Bad, Fair, or Good) from SNR alone, measured against the active preset's SNR limit. It does not factor in RSSI: without knowing the local noise floor, RSSI alone cannot say whether a signal is decodable. RSSI is still available — on the node detail screen and in the metrics charts. +The app rates signal quality (None, Bad, Fair, or Good) from SNR relative to the active preset's SNR limit. When RSSI and a noise-floor reading are both available, it also rates the difference between them against that limit and uses the worse rating. Otherwise, it uses SNR alone. Because the rating is relative to the preset, the same SNR rates differently on different presets. An SNR of −16 dB rates Good on Long Fast (SNR limit −17.5 dB) but None on Short Fast (SNR limit −7.5 dB). Let `limit` be the active preset's SNR limit: @@ -45,7 +45,7 @@ Because the rating is relative to the preset, the same SNR rates differently on The icon never goes blank, so count the bars carefully: a single bar means None, not a weak but usable link, and Good is a solid wedge rather than a set of bars. A gray three-bar icon labeled Unknown is a different state again — the packet carried no SNR measurement at all, which is not the same as measuring one and finding it too weak. -> ℹ️ **Note:** Traceroute hop colors use fixed thresholds (−7 dB / −15 dB); the per-node signal meter uses the preset-relative rating instead. +> ℹ️ **Note:** Traceroute and neighbor-info SNR colors use the active preset's limits too. RSSI text has separate strength colors: green above −115 dBm, yellow above −120 dBm, orange above −126 dBm, and red at or below −126 dBm. ## Diagnosing Local Interference diff --git a/docs/sq-rAL/user/tak.md b/docs/sq-rAL/user/tak.md index 6f6363c1b1..f04441be4d 100644 --- a/docs/sq-rAL/user/tak.md +++ b/docs/sq-rAL/user/tak.md @@ -2,7 +2,7 @@ title: TAK Integration parent: User Guide nav_order: 10 -last_updated: 2026-08-30 +last_updated: 2026-09-11 description: Interoperate with ATAK and WinTAK — CoT position sharing, TAK roles, and plugin setup. aliases: - tak @@ -36,7 +36,7 @@ The TAK module allows Meshtastic nodes to: > ⚠️ **Warning:** The old **Meshtastic ATAK Plugin** is no longer part of this path and cannot > work. It bridged through the cross-process AIDL API, which was removed in app 2.8.0; the mesh > service is now in-process only. Do not install it. Interop today runs over the app's own local -> TAK server plus the Mesh to CoT Converter, both described below, with stock ATAK/iTAK/WinTAK. +> TAK server plus the Mesh to CoT Converter, both described in the following sections, with stock ATAK/iTAK/WinTAK. ### Configuration diff --git a/docs/sq-rAL/user/translate.md b/docs/sq-rAL/user/translate.md index 67d69e2d96..cdc8b6d87f 100644 --- a/docs/sq-rAL/user/translate.md +++ b/docs/sq-rAL/user/translate.md @@ -2,7 +2,7 @@ title: Translate the App parent: User Guide nav_order: 17 -last_updated: 2026-08-30 +last_updated: 2026-09-11 description: How the app and its documentation are translated via Crowdin, and guidelines for contributing translations. aliases: - translate @@ -45,7 +45,7 @@ If your language is not yet listed on Crowdin: The Android app uses **Compose Multiplatform resources** for all user-visible strings: -``` +```text core/resources/src/commonMain/composeResources/ ├── values/ ← English (default) │ └── strings.xml @@ -58,7 +58,7 @@ core/resources/src/commonMain/composeResources/ In-app documentation follows a similar pattern under `docs/`: -``` +```text docs/ ├── en/user/ ← English source (default) │ ├── onboarding.md diff --git a/docs/sr-rLatn/user/desktop.md b/docs/sr-rLatn/user/desktop.md index 3324b319e0..62e33b50e1 100644 --- a/docs/sr-rLatn/user/desktop.md +++ b/docs/sr-rLatn/user/desktop.md @@ -2,7 +2,7 @@ title: Desktop App parent: User Guide nav_order: 14 -last_updated: 2026-08-30 +last_updated: 2026-09-11 description: Install and use the Meshtastic Desktop app on Linux, macOS, and Windows — connections, feature parity, and keyboard shortcuts. aliases: - desktop @@ -58,20 +58,20 @@ Bluetooth Low Energy is supported on desktop via the [Kable](https://github.com/ ## Feature Parity -| Feature | Android | Desktop | Белешке | -| ----------------------------------------------------- | ------- | ------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Messaging | ✓ | ✓ | Full parity | -| Node List | ✓ | ✓ | Full parity | -| Мапа | ✓ | ✓ | Interactive MapLibre map, with base map and overlay pickers and custom tile sources. No offline downloads or local `.mbtiles` archives | -| Map layers (`.kml`/`.kmz`/GeoJSON) | ✓ | ✓ | Same layer store and sheet as Android; imported files draw on the desktop map | -| Site Planner | ✓ | ✓\* | \*Opens in your browser on desktop; the estimate is not drawn on the desktop map | -| Подешавања | ✓ | ✓ | Full parity | -| Bluetooth (BLE) | ✓ | ✓ | Via Kable on desktop | -| Firmware Update | ✓ | ✓ | In-app USB, BLE, and Wi-Fi (ESP32) update work the same as Android. The USB maintenance flow — nRF52/RP2040 factory erase and bootloader upgrade — is Android-only | -| Notifications | ✓ | ✓ | Native OS notifications | -| Widgets | ✓ | ✗ | Android-only | -| AI Assistant (Chirpy) | ✓\* | ✗ | Google flavor Android only | -| App Functions (system AI) | ✓† | ✗ | Google flavor Android only | +| Feature | Android | Desktop | Белешке | +| ----------------------------------------------------- | ------- | ------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Messaging | ✓ | ✓ | Full parity | +| Node List | ✓ | ✓ | Full parity | +| Мапа | ✓ | ✓ | Interactive MapLibre map, with base map and overlay pickers and custom tile sources. Offline terrain (hillshade and contours) can be downloaded; offline base-map packs and local `.mbtiles` archives cannot | +| Map layers (`.kml`/`.kmz`/GeoJSON) | ✓ | ✓ | Same layer store and sheet as Android; imported files draw on the desktop map | +| Site Planner | ✓ | ✓\* | \*Opens in your browser on desktop; the estimate is not drawn on the desktop map | +| Подешавања | ✓ | ✓ | Full parity | +| Bluetooth (BLE) | ✓ | ✓ | Via Kable on desktop | +| Firmware Update | ✓ | ✓ | In-app USB, BLE, and Wi-Fi (ESP32) update work the same as Android. The USB maintenance flow — nRF52/RP2040 factory erase and bootloader upgrade — is Android-only | +| Notifications | ✓ | ✓ | Native OS notifications | +| Widgets | ✓ | ✗ | Android-only | +| AI Assistant (Chirpy) | ✓\* | ✗ | Google flavor Android only | +| App Functions (system AI) | ✓† | ✗ | Google flavor Android only | \*Chirpy AI requires Android 14+ on Google flavor builds with supported hardware. @@ -122,7 +122,7 @@ Individual doc pages render with full formatting: ## Building from Source -```bash +```shell git clone https://github.com/meshtastic/Meshtastic-Android.git cd Meshtastic-Android ./gradlew :desktopApp:run @@ -135,7 +135,7 @@ Requirements: ## Known Limitations -- Offline tile downloads and local `.mbtiles` archives are not available on desktop. +- Offline base-map downloads and local `.mbtiles` archives are not available on desktop. Offline terrain is — see [Map & Waypoints](map-and-waypoints). - `.kml`/`.kmz`/GeoJSON layer import works — see [Map & Waypoints](map-and-waypoints#map-layers). Site Planner opens in your browser rather than in the app; to bring its coverage estimate onto the map, click the transmitter pin diff --git a/docs/sr-rLatn/user/discovery.md b/docs/sr-rLatn/user/discovery.md index 1dc7b393a2..e555756417 100644 --- a/docs/sr-rLatn/user/discovery.md +++ b/docs/sr-rLatn/user/discovery.md @@ -2,7 +2,7 @@ title: Local Mesh Discovery parent: User Guide nav_order: 12 -last_updated: 2026-08-30 +last_updated: 2026-09-11 description: Explore your mesh network — the Local Mesh Discovery scanner, traceroute paths, neighbor maps, and node discovery tools. aliases: - discovery @@ -97,7 +97,7 @@ Mesh Beacon lets nodes invite others to join their mesh. A beaconing node period Configure it under **Settings → Module configuration → Mesh Beacon**. The entry appears only on radios running firmware 2.8.0 or newer. A read-only **Region** row at the top of the screen shows the region the beacon advertises: that region, and the preset, are always the ones the radio itself uses, so a beacon cannot invite anyone onto settings your radio is not running. - **Listen for beacons** — receive invitations broadcast by other nodes. -- **Broadcast a beacon** — periodically advertise this mesh to nearby nodes, with an optional **Beacon message** of up to 100 bytes, a **Broadcast interval** picked from fixed intervals between 1 hour and 72 hours, and an **Offered channel** chosen from your radio's own channels. The offered channel is required, and defaults to your primary channel. +- **Broadcast a beacon** — periodically advertise this mesh to nearby nodes, with an optional **Beacon message** of up to 100 bytes, a **Broadcast interval** picked from fixed intervals between 1 hour and 72 hours, and an **Offered channel** chosen from your radio's own channels. The offered channel is required, and defaults to your primary channel. Over remote admin the picker offers the primary channel only. - **Broadcast targets** — optional extra destinations beyond the offered channel. **Add target** appends a row; each row picks a **Channel** and a **Transmit preset**, and **Remove target** deletes it. With no targets, the beacon goes out on the offered channel alone. Two conditions block beacon setup: diff --git a/docs/sr-rLatn/user/help-and-docs.md b/docs/sr-rLatn/user/help-and-docs.md index 8ec969318c..7848eeae23 100644 --- a/docs/sr-rLatn/user/help-and-docs.md +++ b/docs/sr-rLatn/user/help-and-docs.md @@ -2,7 +2,7 @@ title: Help & In-App Docs parent: User Guide nav_order: 21 -last_updated: 2026-08-30 +last_updated: 2026-09-11 description: Browse this documentation inside the app, search it, and ask Chirpy — the on-device AI assistant — questions about Meshtastic. aliases: - help @@ -23,7 +23,7 @@ The docs browser lists every user-guide page. Tap a page to read it; images and ### Претражи -The search box sits at the top of the docs browser. Type in it to filter pages by title and keywords — results update as you type, and the **Clear search** button (✕) empties the box. +Tap the search field at the top of the docs browser to open a full-screen search view. Type to filter pages by title and keywords — results update as you type, under the same field. The back arrow, or the keyboard's search action, closes the view. The field hides while you scroll down the page list and comes back when you scroll up. ![Searching the in-app documentation](../../assets/screenshots/docs-browser_search.png) diff --git a/docs/sr-rLatn/user/map-and-waypoints.md b/docs/sr-rLatn/user/map-and-waypoints.md index 0d859567c8..164468f971 100644 --- a/docs/sr-rLatn/user/map-and-waypoints.md +++ b/docs/sr-rLatn/user/map-and-waypoints.md @@ -2,7 +2,7 @@ title: Map & Waypoints parent: User Guide nav_order: 6 -last_updated: 2026-09-01 +last_updated: 2026-09-11 description: View node positions on the map, create and share waypoints, manage map layers and Site Planner, and control position sharing and privacy. aliases: - map @@ -44,7 +44,7 @@ The floating toolbar provides quick access to the compass, the map type and laye ### Filtering the Map -Tap the filter button in the floating toolbar to open **Filter map**. **Display** controls what is drawn: **Only Favorites**, **Show Waypoints**, **Show Precision Circles**, and a slider that hides nodes not heard from recently. **Node roles** is a chip per device role, plus **All** to show every role; a selected chip means that role is shown. **Nodes** narrows the set further with **Hide offline nodes**, **Only show direct nodes**, **Exclude MQTT**, **Show ignored nodes**, and **Include unknown**. +Tap the filter button in the floating toolbar to open **Filter map**. **Display** controls what is drawn: **Only Favorites**, **Show Waypoints**, **Show Precision Circles**, and a slider that hides nodes not heard from recently. **Node roles** is a chip per device role, plus **All** to show every role; a selected chip means that role is shown. **Nodes** narrows the set further with **Hide offline nodes**, **Only show direct nodes**, **Signed only**, **Encrypted only**, **Exclude MQTT**, **Show ignored nodes**, and **Include unknown** — the same rules as the node list's [filter toggles](nodes). A dot on the filter button means at least one filter is hiding something — check it before concluding the mesh is quiet. Turning **Show Waypoints** off hides every waypoint, including your own. **Show ignored nodes** adds them to the map rather than showing only them — unlike the node list's **Only show ignored Nodes**. @@ -147,8 +147,9 @@ picker, all three offer the same raster base maps: Overlays can be toggled on top of any base map, from the layers sheet: - **Weather radar** — NOAA NEXRAD reflectivity (US coverage) -- **Hillshade** — terrain relief, on **F-Droid** and **Desktop** only. Useful for understanding why a - link fails, since LoRa range is limited by terrain +- **Hillshade** — terrain relief from an online tile source, on **F-Droid** and **Desktop** only; Google + Play draws hillshade from downloaded offline terrain instead. Useful for understanding why a link + fails, since LoRa range is limited by terrain ### Adding your own tile source @@ -156,7 +157,7 @@ Any XYZ tile endpoint can be added as a base map, on every flavor and on desktop Tile Sources** at the foot of the base map picker and paste a URL template using `{z}`, `{x}` and `{y}` — plus `{s}` if the provider uses rotating subdomains. A national mapping service, for example: -``` +```text https://wmts.geo.admin.ch/1.0.0/ch.swisstopo.pixelkarte-farbe/default/current/3857/{z}/{x}/{y}.jpeg ``` @@ -173,7 +174,18 @@ On **Google Play**, the layers sheet's **Offline Manager** section downloads its — water, roads and administrative boundaries extracted from the public Protomaps basemap dataset, drawn directly as map shapes rather than raster tiles. Frame the area you want, tap **Start Download**, and once it finishes flip **Show on map** to draw it (it's off by default so it never surprises you by covering up the -live map). You can still import a pre-made `.mbtiles` archive here too, as before. **Desktop** has neither. +live map). You can still import a pre-made `.mbtiles` archive here too, as before. **Desktop** has neither, though +the offline terrain described next does work there. + +### Offline terrain + +Hillshade and elevation contours can be downloaded for offline use as well, so a downloaded area still +shows why a link fails when there is no network. The terrain data comes from Mapterhorn and is credited on +the map. On **Google Play**, each downloaded region in the **Offline Manager** list has a **Download Terrain** +button and **Hillshade** and **Contours** switches; terrain attaches to that region and shares its storage limit, and an area +too large for terrain asks you to zoom in or pick a smaller region. On **F-Droid** and **Desktop**, the +layers sheet has an **Offline Terrain** section of its own: frame the area, tap **Start Download**, and the +hillshade and contours draw from the downloaded tiles from then on. ### Going offline @@ -181,8 +193,8 @@ An **Offline** pill appears over the map whenever the device has no network conn builds, if you've imported a local `.mbtiles` archive, the map switches to it automatically the moment the network drops — no toggle to remember — and switches back once you're reconnected, as long as you haven't picked a different base map yourself in between. On **F-Droid**, a downloaded offline area keeps rendering on -its own; the pill is purely informational there. **Desktop** has no offline downloads at all yet, so the pill -is informational there too. +its own; the pill is purely informational there. **Desktop** downloads offline terrain but no base-map +packs, so the pill is informational there too. ## Related Topics diff --git a/docs/sr-rLatn/user/messages-and-channels.md b/docs/sr-rLatn/user/messages-and-channels.md index 819d4a49c1..8aba80d167 100644 --- a/docs/sr-rLatn/user/messages-and-channels.md +++ b/docs/sr-rLatn/user/messages-and-channels.md @@ -2,7 +2,7 @@ title: Messages & Channels parent: User Guide nav_order: 3 -last_updated: 2026-08-30 +last_updated: 2026-09-14 description: Send and receive messages, manage channels, configure encryption, search conversations, and use quick chat, reactions, and message actions. aliases: - channels @@ -50,6 +50,21 @@ Key length alone does not change the icon: a 128-bit key and a 256-bit key both Tapping an existing channel opens the same editor, where you can change the name, the PSK, MQTT uplink and downlink, and position precision. Every edit on this screen — adding, editing, deleting, or dragging a channel into a new order — waits on **Send** the same way. +### Archived Channels + +A conversation belongs to the channel it was held on, not to the slot that channel occupied. When a channel leaves your radio — you scan a different channel QR, replace a channel's key, or switch the modem preset — its conversation is **archived** rather than deleted or merged into whatever takes the slot. + +An archived conversation: + +- stays in the **Channels** list under the name the channel had, below the channels currently on your radio, marked with a history icon; +- keeps its full message history, searchable as usual; +- is read-only — you cannot send, reply, or react, because the channel is no longer on the radio to send on, and it is not offered as a share target; +- comes back automatically if you re-add that exact channel, taking its messages back to the live slot. + +Changing the modem preset archives the channel too: the preset is part of a default channel's identity, so **LongFast** and **MediumFast** are different channels even though neither the name nor the key changed. Changing only the region does not archive anything. + +To remove an archived conversation for good, long-press it in the conversation list and delete it. + ## Директне поруке Direct messages (DMs) go to one specific node. When both radios hold each other's public keys, your radio encrypts the message to that node's public key, so no one else on the mesh can read it — not even nodes that share your channel. diff --git a/docs/sr-rLatn/user/mqtt.md b/docs/sr-rLatn/user/mqtt.md index bc6fe647c8..2658a070cf 100644 --- a/docs/sr-rLatn/user/mqtt.md +++ b/docs/sr-rLatn/user/mqtt.md @@ -2,7 +2,7 @@ title: MQTT parent: User Guide nav_order: 11 -last_updated: 2026-08-30 +last_updated: 2026-09-11 description: Bridge your mesh to the internet — MQTT broker setup, encryption layers, and map reporting. aliases: - mqtt @@ -25,7 +25,7 @@ The MQTT module connects your node to an MQTT broker, allowing: ## How It Works -``` +```text [Your Node] → Radio → [Gateway Node with Wi-Fi] → MQTT Broker → [Remote Gateway] → Radio → [Remote Node] ``` diff --git a/docs/sr-rLatn/user/node-metrics.md b/docs/sr-rLatn/user/node-metrics.md index 405185d420..d8d23e5b23 100644 --- a/docs/sr-rLatn/user/node-metrics.md +++ b/docs/sr-rLatn/user/node-metrics.md @@ -2,7 +2,7 @@ title: Node Metrics parent: User Guide nav_order: 5 -last_updated: 2026-08-30 +last_updated: 2026-09-09 description: Telemetry dashboards for each mesh node — device health, environment sensors, air quality, signal quality, power, traceroute, and position history. aliases: - metrics @@ -114,7 +114,7 @@ Radio signal quality information: ### Signal Quality Reference -Signal quality is rated from **SNR relative to the active LoRa modem preset's demodulation floor**, not from fixed thresholds — a given SNR means different things on different presets (e.g. −15 dB is fine on LongSlow but unusable on ShortFast). RSSI is shown but is not part of the rating. In the table, _limit_ is the preset's SNR limit. +Signal quality is rated from **SNR relative to the active LoRa modem preset's demodulation floor**, not from fixed thresholds — a given SNR means different things on different presets (e.g. −15 dB is fine on LongSlow but unusable on ShortFast). When RSSI and a noise-floor reading are both available, the app also rates their difference against the preset limit and uses the worse rating. Otherwise, RSSI is display-only. In the table, _limit_ is the preset's SNR limit. | Quality | Criteria | | ----------- | ---------------------------------------------------------------- | diff --git a/docs/sr-rLatn/user/nodes.md b/docs/sr-rLatn/user/nodes.md index c726b6c6d7..f5dfd7cacd 100644 --- a/docs/sr-rLatn/user/nodes.md +++ b/docs/sr-rLatn/user/nodes.md @@ -2,7 +2,7 @@ title: Чворови parent: User Guide nav_order: 4 -last_updated: 2026-09-04 +last_updated: 2026-09-11 description: Browse, filter, and sort mesh nodes — view details, signal quality, roles, and quick actions. aliases: - node-list @@ -74,19 +74,23 @@ Most users should keep the default **Client** role. Consider a different role wh > 💡 **Tip:** The mesh works best when most nodes are **Client** or **Router**. Too many Client Mute nodes reduce mesh resilience; too many Routers in a dense area can cause congestion. A good rule of thumb: one Router per 5–10 Clients in your area. -### Encryption Indicators +### Security indicators -Nodes display encryption status icons next to their name: +Each node carries one security icon beside its name in the node list. Tap it to read what it means, and choose **Show All Meanings** in that dialog for the full legend. The detail screen shows the same state in words, as a **Security** row that opens the same dialog. -| Иконица | Meaning | -| ----------- | ------------------------------------------------------------------------------------------------------------------------------------------------- | -| 🔒 Locked | Communication uses PKI (public key infrastructure) — end-to-end encrypted with verified identity | -| 🔓 Unlocked | No public key has been received for this node, so it cannot be direct messaged — use **Request User Info** on the node detail page to ask for one | -| ⚠️ Mismatch | Public key mismatch — the node's key has changed since last seen (investigate before trusting) | +| Иконица | Meaning | Shown for | +| --------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------- | +| Person with a shield check (green) | **Verified contact** — you verified this node's key in person by exchanging contact QR codes, so its identity is confirmed. The strongest trust the list shows. Your own connected node carries it too | Any firmware version | +| Nodes icon with a shield check (green) | **Signed node** — this node signs its broadcasts with its identity key, so its identity is consistent over time, but you have not verified it in person. On firmware 2.8 the icon appears from the version alone, before any signed broadcast has been heard | Firmware 2.8 or newer, and any node whose signed broadcast your node has verified | +| 🔒 Closed lock | A public key is on file and matches, so direct messages to this node are encrypted | Firmware before 2.8, or no reported version | +| 🔓 Open lock | No public key has been received for this node, so it cannot be direct messaged — use **Request User Info** on the node detail page to ask for one | Firmware before 2.8, or no reported version | +| ⚠️ Mismatch | **Public key mismatch** — a different key arrived for this node after one was stored. Investigate before trusting | Any firmware version | -> 💡 **Tip:** Direct messages always use PKI, so the radio needs the other node's public key before it can send one. It refuses the send rather than falling back to channel encryption. Keys arrive inside node info, which is why an open lock usually clears itself once that node is heard from properly. If you see a key mismatch warning, the node may have been reset or compromised. +Every node on firmware 2.8 or newer signs its broadcasts, so on that firmware the signed state is the baseline and the locks are not shown. -To clear a mismatch, first confirm through another trusted channel that the key change was intentional — a factory reset causes one. Then touch & hold the node, choose **Remove**, and let the two radios exchange keys again the next time yours hears it. +Direct messages always use public-key encryption, so your node needs the other node's public key before it can send one. It refuses the send rather than falling back to channel encryption. Keys arrive inside node info, which is why an open lock usually clears itself once that node is heard from properly. + +A mismatch never replaces the key you already hold. The app keeps the first key it recorded and refuses the new one, as the firmware does, so a stray or hostile node info cannot silently break encrypted messaging to a contact. To clear a mismatch, first confirm through another trusted channel that the key change was intentional — a factory reset causes one. Then touch & hold the node, choose **Remove**, and let the two nodes exchange keys again the next time yours hears it. ## Quick Actions @@ -108,7 +112,9 @@ radio is connected and running firmware 2.8 or newer — see ## Sharing a Contact -On a node's detail screen, tap **Share Contact** to produce a link and a QR code for that node. From the same dialog, **Write to NFC tag** saves the link to a writable NFC tag that anyone can tap to open. +On a node's detail screen, tap **Share Contact** to produce a link and a QR code for that node. From the same dialog, **Share link** opens the Android share sheet (on desktop it copies the link instead), **Write to NFC tag** saves it to a writable NFC tag, and **Copy** puts it on the clipboard. While that dialog is open and in front of you, the phone also offers the same link to any NFC reader, so someone can take the contact by tapping their phone against yours with no tag involved. + +Sharing your own contact this way marks it as verified in person, so whoever imports it sees the **Verified contact** icon rather than the signed one. Relaying someone else's contact passes on only what your app had already recorded about them. To add someone else's contact, use the import button on the node list and choose **Scan Shared Contact QR Code**, **Scan Shared Contact NFC**, or **Input Shared Contact URL**. The app asks you to confirm with **Import Shared Contact?**, and warns you when the contact is one you already have. @@ -120,14 +126,16 @@ Type in the search field to filter nodes by name or short name. The filter updat ### Filter Toggles -| Filter | Опис | -| --------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| **Hide offline nodes** | Show only nodes heard within the last 2 hours | -| **Only show direct nodes** | Show only nodes your radio heard directly, with no relay in between | -| **Include unknown** | Show nodes that haven't sent user info yet. **On by default**, so a node heard before its info arrives stays visible; these carry a badge marking them incomplete, and cannot be direct messaged until their user info brings a public key | -| **Exclude infrastructure** | Hide infrastructure-role nodes (Router, Router Late, Client Base, and legacy Repeater nodes) and any node that cannot be messaged, whatever its role | -| **Exclude MQTT** | Hide nodes heard only via MQTT internet bridge | -| **Only show ignored Nodes** | Replace the list with the nodes you have ignored. Every other node is hidden while this is on, and a banner appears at the top of the list to take you back | +| Filter | Опис | +| --------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **Hide offline nodes** | Show only nodes heard within the last 2 hours | +| **Only show direct nodes** | Show only nodes your radio heard directly, with no relay in between | +| **Include unknown** | Show nodes that haven't sent user info yet. **On by default**, so a node heard before its info arrives stays visible; these carry a badge marking them incomplete, and cannot be direct messaged until their user info brings a public key | +| **Exclude infrastructure** | Hide infrastructure-role nodes (Router, Router Late, Client Base, and legacy Repeater nodes) and any node that cannot be messaged, whatever its role | +| **Exclude MQTT** | Hide nodes heard only via MQTT internet bridge | +| **Signed only** | Show only nodes whose signed broadcasts your node has actually heard and verified. Stricter than the icon: a node on 2.8 shows as signed by its firmware version before any signed broadcast arrives, and a contact verified in person on older firmware is not signed at all | +| **Encrypted only** | Show nodes with a matching public key on file, the key an encrypted direct message needs. A node with a key mismatch is excluded | +| **Only show ignored Nodes** | Replace the list with the nodes you have ignored. Every other node is hidden while this is on, and a banner appears at the top of the list to take you back | ### Sort Options @@ -149,6 +157,8 @@ Tap the hop-histogram icon in the node list's app bar to open a bar chart of how Tapping a node opens the detail view with comprehensive information. See [Node Metrics](node-metrics) for full details on metrics and telemetry. +Signal quality is rated against your modem preset. The same SNR can be good on a long-range preset and poor on a faster one. Traceroute and neighbor-info SNR colors use that preset too. RSSI text has its own strength colors; it affects the quality rating only when a noise-floor reading is also available. + The Details card carries the node's short name, role, IDs, last heard time, hops away, uptime, and its SNR and RSSI: ![Node detail section](../../assets/screenshots/nodes_detail_section.png) diff --git a/docs/sr-rLatn/user/settings-module-admin.md b/docs/sr-rLatn/user/settings-module-admin.md index 9dd1536ed2..85797c1d3b 100644 --- a/docs/sr-rLatn/user/settings-module-admin.md +++ b/docs/sr-rLatn/user/settings-module-admin.md @@ -2,7 +2,7 @@ title: Settings — Modules & Admin parent: User Guide nav_order: 8 -last_updated: 2026-08-30 +last_updated: 2026-09-16 description: Configure optional feature modules (MQTT, telemetry, canned messages, TAK, and more) and perform device administration. aliases: - modules @@ -333,7 +333,7 @@ appear only when your own node is selected: ### O nama -**Settings → About** carries the app's own identity rather than the radio's: +**Settings → About** carries the app's own identity rather than the node's: Three sections: @@ -341,7 +341,11 @@ Three sections: - **Apps** — opens with **Need Hardware?**, a rotating carousel of popular devices that links out to where to buy one. It also lists the GitHub repository, the running app version, and **Acknowledgements** (see the next section). -- **Project information** — links to the website and to this documentation. +- **Project information** — links to the website, to this documentation, and to the GNU GPL v3 + license the app is released under. + +The footer states the copyright and that the app is free software with no warranty, which you +may redistribute under the same license. ### Acknowledgements diff --git a/docs/sr-rLatn/user/settings-radio-user.md b/docs/sr-rLatn/user/settings-radio-user.md index fdb9131d51..a9efd200ca 100644 --- a/docs/sr-rLatn/user/settings-radio-user.md +++ b/docs/sr-rLatn/user/settings-radio-user.md @@ -2,7 +2,7 @@ title: Settings — Radio & User parent: User Guide nav_order: 7 -last_updated: 2026-09-04 +last_updated: 2026-09-11 description: Configure your radio hardware, LoRa presets, user profile, position sharing, power management, and security. aliases: - подешавања @@ -51,7 +51,7 @@ The footer appears as soon as you change something. **Discard** throws the chang The status message is saved with the same **Save**, but it never reboots the node — and, like the rest of this screen, it can be edited on a remote node you administer. For your own radio there is a shortcut while it is connected: touch & hold your node in the [node list](nodes.md) and choose -**Update status**. Older firmware and a disconnected radio have no shortcut — the field above is +**Update status**. Older firmware and a disconnected node have no shortcut — the field itself is still the way in. ## Configuration @@ -77,13 +77,13 @@ On **Settings → LoRa**. | Setting | Опис | Подразумевано | | -------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------- | -| Регион | Regulatory region for frequency bands. You must set this before transmitting | Unset (must configure) | +| Регион | Regulatory region for frequency bands. You must set this before transmitting. On firmware 2.8 or newer, the first region set also creates the node's identity key and gives it a new node number | Unset (must configure) | | Унапред подешено | Speed/range tradeoff | LongFast | | Number of Hops | Maximum retransmit hops | 3 | | Transmit Power | Transmission power (dBm); 0 = max allowed for region | 0 (region max) | | Измена фреквенције | Overrides the computed operating frequency outright (MHz). It does not offset the calculated value — leave at 0 unless you know you need a specific frequency | 0 (use calculated) | | Користи предефинисано подешавање | On by default. Turn it off to set Spread Factor, Coding Rate and Bandwidth by hand instead of taking them from the modem preset | On | -| Фактор ширења | Manual mode only: 7–12. Higher spreads further but slower | From preset | +| Фактор ширења | Manual mode only: 5–12. Higher spreads further but slower. On SX127x (RF95) radios the firmware does not accept 5 or 6 and uses 11 instead | From preset | | Стопа кодирања | Manual mode only: 5–8. More redundancy costs airtime | From preset | | Проток | Manual mode only: the channel bandwidth in kHz, typed in directly. On the 2.4 GHz region the app offers a list of the bandwidths your radio supports instead, and a stored value that is not on that list shows as _Unsupported_ and blocks saving until you pick a supported one | From preset | | Фреквенцијски слот | Which slot within the region's band to use. 0 derives it from the primary channel name | 0 (automatic) | @@ -111,13 +111,13 @@ The Lite, Narrow, Medium Turbo, and Tiny presets need firmware 2.8 or newer — | Short Slow | ~5 km | 6.25 kbps | −10 dB | Suburban short-range; moderate building density | | Medium Fast | ~5 km | 3.52 kbps | −12.5 dB | Suburban areas; moderate building density | | Medium Slow | ~8 km | 1.95 kbps | −15 dB | Suburban/rural; moderate range with slower speed | -| Long Turbo | ~10 km | 1.34 kbps | −12.5 dB | Similar range to Long Fast but with 500 kHz bandwidth; faster throughput | +| Long Turbo | ~10 km | 1.34 kbps | −17.5 dB | Similar range to Long Fast but with 500 kHz bandwidth; faster throughput | | Long Fast | ~10 km | 1.1 kbps | −17.5 dB | **General use (default)** — balanced range and speed | | Long Moderate | ~20 km | 0.34 kbps | −17.5 dB | Rural with some terrain; occasional use | | Lite Fast | ~5 km | 1.76 kbps | −12.5 dB | EU 866 MHz SRD band (125 kHz BW); comparable to Medium Fast | | Lite Slow | ~10 km | 0.98 kbps | −15 dB | EU 866 MHz SRD band (125 kHz BW); comparable to Long Fast | -| Narrow Fast | ~5 km | 2.28 kbps | −10 dB | EU 868 MHz band (62.5 kHz BW); avoids interference with other devices | -| Narrow Slow | ~10 km | 1.30 kbps | −12.5 dB | EU 868 MHz band (62.5 kHz BW); comparable to Long Fast | +| Narrow Fast | ~5 km | 2.28 kbps | −7.5 dB | EU 868 MHz band (62.5 kHz BW); avoids interference with other devices | +| Narrow Slow | ~10 km | 1.30 kbps | −10 dB | EU 868 MHz band (62.5 kHz BW); comparable to Long Fast | | Medium Turbo | ~5 km | 7.0 kbps | −12.5 dB | Like Medium Fast but with 500 kHz bandwidth; not legal in every region. Needs firmware 2.8 or newer | | Tiny Fast | ~10 km | 0.68 kbps | −7.5 dB | Amateur bands that cap occupied bandwidth; these presets use 15.6 kHz. Needs firmware 2.8 or newer, an SX126x or SX127x radio, and a TCXO of ±5 ppm or better | | Tiny Slow | ~20 km | 0.33 kbps | −10 dB | Same band restrictions as Tiny Fast, longer range. Same firmware, radio, and TCXO requirements | diff --git a/docs/sr-rLatn/user/signal-meter.md b/docs/sr-rLatn/user/signal-meter.md index 26a55980df..71b3fb2879 100644 --- a/docs/sr-rLatn/user/signal-meter.md +++ b/docs/sr-rLatn/user/signal-meter.md @@ -2,7 +2,7 @@ title: How the Meshtastic Signal Meter Works parent: User Guide nav_order: 15 -last_updated: 2026-08-30 +last_updated: 2026-09-09 description: How the signal meter rates quality from SNR relative to the LoRa modem preset — spread spectrum, presets, and what the bars really mean. aliases: - signal @@ -32,7 +32,7 @@ Each modem preset has an SNR limit: the lowest SNR at which that preset can stil ## Rating Signal Quality -The app rates signal quality (None, Bad, Fair, or Good) from SNR alone, measured against the active preset's SNR limit. It does not factor in RSSI: without knowing the local noise floor, RSSI alone cannot say whether a signal is decodable. RSSI is still available — on the node detail screen and in the metrics charts. +The app rates signal quality (None, Bad, Fair, or Good) from SNR relative to the active preset's SNR limit. When RSSI and a noise-floor reading are both available, it also rates the difference between them against that limit and uses the worse rating. Otherwise, it uses SNR alone. Because the rating is relative to the preset, the same SNR rates differently on different presets. An SNR of −16 dB rates Good on Long Fast (SNR limit −17.5 dB) but None on Short Fast (SNR limit −7.5 dB). Let `limit` be the active preset's SNR limit: @@ -45,7 +45,7 @@ Because the rating is relative to the preset, the same SNR rates differently on The icon never goes blank, so count the bars carefully: a single bar means None, not a weak but usable link, and Good is a solid wedge rather than a set of bars. A gray three-bar icon labeled Unknown is a different state again — the packet carried no SNR measurement at all, which is not the same as measuring one and finding it too weak. -> ℹ️ **Note:** Traceroute hop colors use fixed thresholds (−7 dB / −15 dB); the per-node signal meter uses the preset-relative rating instead. +> ℹ️ **Note:** Traceroute and neighbor-info SNR colors use the active preset's limits too. RSSI text has separate strength colors: green above −115 dBm, yellow above −120 dBm, orange above −126 dBm, and red at or below −126 dBm. ## Diagnosing Local Interference diff --git a/docs/sr-rLatn/user/tak.md b/docs/sr-rLatn/user/tak.md index 461c9cd270..029bdfb879 100644 --- a/docs/sr-rLatn/user/tak.md +++ b/docs/sr-rLatn/user/tak.md @@ -2,7 +2,7 @@ title: TAK Integration parent: User Guide nav_order: 10 -last_updated: 2026-08-30 +last_updated: 2026-09-11 description: Interoperate with ATAK and WinTAK — CoT position sharing, TAK roles, and plugin setup. aliases: - tak @@ -36,7 +36,7 @@ The TAK module allows Meshtastic nodes to: > ⚠️ **Warning:** The old **Meshtastic ATAK Plugin** is no longer part of this path and cannot > work. It bridged through the cross-process AIDL API, which was removed in app 2.8.0; the mesh > service is now in-process only. Do not install it. Interop today runs over the app's own local -> TAK server plus the Mesh to CoT Converter, both described below, with stock ATAK/iTAK/WinTAK. +> TAK server plus the Mesh to CoT Converter, both described in the following sections, with stock ATAK/iTAK/WinTAK. ### Configuration diff --git a/docs/sr-rLatn/user/translate.md b/docs/sr-rLatn/user/translate.md index e7f535bfde..9516addc11 100644 --- a/docs/sr-rLatn/user/translate.md +++ b/docs/sr-rLatn/user/translate.md @@ -2,7 +2,7 @@ title: Translate the App parent: User Guide nav_order: 17 -last_updated: 2026-08-30 +last_updated: 2026-09-11 description: How the app and its documentation are translated via Crowdin, and guidelines for contributing translations. aliases: - translate @@ -45,7 +45,7 @@ If your language is not yet listed on Crowdin: The Android app uses **Compose Multiplatform resources** for all user-visible strings: -``` +```text core/resources/src/commonMain/composeResources/ ├── values/ ← English (default) │ └── strings.xml @@ -58,7 +58,7 @@ core/resources/src/commonMain/composeResources/ In-app documentation follows a similar pattern under `docs/`: -``` +```text docs/ ├── en/user/ ← English source (default) │ ├── onboarding.md diff --git a/docs/srp/user/desktop.md b/docs/srp/user/desktop.md index 3324b319e0..62e33b50e1 100644 --- a/docs/srp/user/desktop.md +++ b/docs/srp/user/desktop.md @@ -2,7 +2,7 @@ title: Desktop App parent: User Guide nav_order: 14 -last_updated: 2026-08-30 +last_updated: 2026-09-11 description: Install and use the Meshtastic Desktop app on Linux, macOS, and Windows — connections, feature parity, and keyboard shortcuts. aliases: - desktop @@ -58,20 +58,20 @@ Bluetooth Low Energy is supported on desktop via the [Kable](https://github.com/ ## Feature Parity -| Feature | Android | Desktop | Белешке | -| ----------------------------------------------------- | ------- | ------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Messaging | ✓ | ✓ | Full parity | -| Node List | ✓ | ✓ | Full parity | -| Мапа | ✓ | ✓ | Interactive MapLibre map, with base map and overlay pickers and custom tile sources. No offline downloads or local `.mbtiles` archives | -| Map layers (`.kml`/`.kmz`/GeoJSON) | ✓ | ✓ | Same layer store and sheet as Android; imported files draw on the desktop map | -| Site Planner | ✓ | ✓\* | \*Opens in your browser on desktop; the estimate is not drawn on the desktop map | -| Подешавања | ✓ | ✓ | Full parity | -| Bluetooth (BLE) | ✓ | ✓ | Via Kable on desktop | -| Firmware Update | ✓ | ✓ | In-app USB, BLE, and Wi-Fi (ESP32) update work the same as Android. The USB maintenance flow — nRF52/RP2040 factory erase and bootloader upgrade — is Android-only | -| Notifications | ✓ | ✓ | Native OS notifications | -| Widgets | ✓ | ✗ | Android-only | -| AI Assistant (Chirpy) | ✓\* | ✗ | Google flavor Android only | -| App Functions (system AI) | ✓† | ✗ | Google flavor Android only | +| Feature | Android | Desktop | Белешке | +| ----------------------------------------------------- | ------- | ------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Messaging | ✓ | ✓ | Full parity | +| Node List | ✓ | ✓ | Full parity | +| Мапа | ✓ | ✓ | Interactive MapLibre map, with base map and overlay pickers and custom tile sources. Offline terrain (hillshade and contours) can be downloaded; offline base-map packs and local `.mbtiles` archives cannot | +| Map layers (`.kml`/`.kmz`/GeoJSON) | ✓ | ✓ | Same layer store and sheet as Android; imported files draw on the desktop map | +| Site Planner | ✓ | ✓\* | \*Opens in your browser on desktop; the estimate is not drawn on the desktop map | +| Подешавања | ✓ | ✓ | Full parity | +| Bluetooth (BLE) | ✓ | ✓ | Via Kable on desktop | +| Firmware Update | ✓ | ✓ | In-app USB, BLE, and Wi-Fi (ESP32) update work the same as Android. The USB maintenance flow — nRF52/RP2040 factory erase and bootloader upgrade — is Android-only | +| Notifications | ✓ | ✓ | Native OS notifications | +| Widgets | ✓ | ✗ | Android-only | +| AI Assistant (Chirpy) | ✓\* | ✗ | Google flavor Android only | +| App Functions (system AI) | ✓† | ✗ | Google flavor Android only | \*Chirpy AI requires Android 14+ on Google flavor builds with supported hardware. @@ -122,7 +122,7 @@ Individual doc pages render with full formatting: ## Building from Source -```bash +```shell git clone https://github.com/meshtastic/Meshtastic-Android.git cd Meshtastic-Android ./gradlew :desktopApp:run @@ -135,7 +135,7 @@ Requirements: ## Known Limitations -- Offline tile downloads and local `.mbtiles` archives are not available on desktop. +- Offline base-map downloads and local `.mbtiles` archives are not available on desktop. Offline terrain is — see [Map & Waypoints](map-and-waypoints). - `.kml`/`.kmz`/GeoJSON layer import works — see [Map & Waypoints](map-and-waypoints#map-layers). Site Planner opens in your browser rather than in the app; to bring its coverage estimate onto the map, click the transmitter pin diff --git a/docs/srp/user/discovery.md b/docs/srp/user/discovery.md index 1dc7b393a2..e555756417 100644 --- a/docs/srp/user/discovery.md +++ b/docs/srp/user/discovery.md @@ -2,7 +2,7 @@ title: Local Mesh Discovery parent: User Guide nav_order: 12 -last_updated: 2026-08-30 +last_updated: 2026-09-11 description: Explore your mesh network — the Local Mesh Discovery scanner, traceroute paths, neighbor maps, and node discovery tools. aliases: - discovery @@ -97,7 +97,7 @@ Mesh Beacon lets nodes invite others to join their mesh. A beaconing node period Configure it under **Settings → Module configuration → Mesh Beacon**. The entry appears only on radios running firmware 2.8.0 or newer. A read-only **Region** row at the top of the screen shows the region the beacon advertises: that region, and the preset, are always the ones the radio itself uses, so a beacon cannot invite anyone onto settings your radio is not running. - **Listen for beacons** — receive invitations broadcast by other nodes. -- **Broadcast a beacon** — periodically advertise this mesh to nearby nodes, with an optional **Beacon message** of up to 100 bytes, a **Broadcast interval** picked from fixed intervals between 1 hour and 72 hours, and an **Offered channel** chosen from your radio's own channels. The offered channel is required, and defaults to your primary channel. +- **Broadcast a beacon** — periodically advertise this mesh to nearby nodes, with an optional **Beacon message** of up to 100 bytes, a **Broadcast interval** picked from fixed intervals between 1 hour and 72 hours, and an **Offered channel** chosen from your radio's own channels. The offered channel is required, and defaults to your primary channel. Over remote admin the picker offers the primary channel only. - **Broadcast targets** — optional extra destinations beyond the offered channel. **Add target** appends a row; each row picks a **Channel** and a **Transmit preset**, and **Remove target** deletes it. With no targets, the beacon goes out on the offered channel alone. Two conditions block beacon setup: diff --git a/docs/srp/user/help-and-docs.md b/docs/srp/user/help-and-docs.md index 8ec969318c..7848eeae23 100644 --- a/docs/srp/user/help-and-docs.md +++ b/docs/srp/user/help-and-docs.md @@ -2,7 +2,7 @@ title: Help & In-App Docs parent: User Guide nav_order: 21 -last_updated: 2026-08-30 +last_updated: 2026-09-11 description: Browse this documentation inside the app, search it, and ask Chirpy — the on-device AI assistant — questions about Meshtastic. aliases: - help @@ -23,7 +23,7 @@ The docs browser lists every user-guide page. Tap a page to read it; images and ### Претражи -The search box sits at the top of the docs browser. Type in it to filter pages by title and keywords — results update as you type, and the **Clear search** button (✕) empties the box. +Tap the search field at the top of the docs browser to open a full-screen search view. Type to filter pages by title and keywords — results update as you type, under the same field. The back arrow, or the keyboard's search action, closes the view. The field hides while you scroll down the page list and comes back when you scroll up. ![Searching the in-app documentation](../../assets/screenshots/docs-browser_search.png) diff --git a/docs/srp/user/map-and-waypoints.md b/docs/srp/user/map-and-waypoints.md index 6465c2419c..ff57857c6a 100644 --- a/docs/srp/user/map-and-waypoints.md +++ b/docs/srp/user/map-and-waypoints.md @@ -2,7 +2,7 @@ title: Map & Waypoints parent: User Guide nav_order: 6 -last_updated: 2026-09-01 +last_updated: 2026-09-11 description: View node positions on the map, create and share waypoints, manage map layers and Site Planner, and control position sharing and privacy. aliases: - map @@ -44,7 +44,7 @@ The floating toolbar provides quick access to the compass, the map type and laye ### Filtering the Map -Tap the filter button in the floating toolbar to open **Filter map**. **Display** controls what is drawn: **Only Favorites**, **Show Waypoints**, **Show Precision Circles**, and a slider that hides nodes not heard from recently. **Node roles** is a chip per device role, plus **All** to show every role; a selected chip means that role is shown. **Nodes** narrows the set further with **Hide offline nodes**, **Only show direct nodes**, **Exclude MQTT**, **Show ignored nodes**, and **Include unknown**. +Tap the filter button in the floating toolbar to open **Filter map**. **Display** controls what is drawn: **Only Favorites**, **Show Waypoints**, **Show Precision Circles**, and a slider that hides nodes not heard from recently. **Node roles** is a chip per device role, plus **All** to show every role; a selected chip means that role is shown. **Nodes** narrows the set further with **Hide offline nodes**, **Only show direct nodes**, **Signed only**, **Encrypted only**, **Exclude MQTT**, **Show ignored nodes**, and **Include unknown** — the same rules as the node list's [filter toggles](nodes). A dot on the filter button means at least one filter is hiding something — check it before concluding the mesh is quiet. Turning **Show Waypoints** off hides every waypoint, including your own. **Show ignored nodes** adds them to the map rather than showing only them — unlike the node list's **Only show ignored Nodes**. @@ -147,8 +147,9 @@ picker, all three offer the same raster base maps: Overlays can be toggled on top of any base map, from the layers sheet: - **Weather radar** — NOAA NEXRAD reflectivity (US coverage) -- **Hillshade** — terrain relief, on **F-Droid** and **Desktop** only. Useful for understanding why a - link fails, since LoRa range is limited by terrain +- **Hillshade** — terrain relief from an online tile source, on **F-Droid** and **Desktop** only; Google + Play draws hillshade from downloaded offline terrain instead. Useful for understanding why a link + fails, since LoRa range is limited by terrain ### Adding your own tile source @@ -156,7 +157,7 @@ Any XYZ tile endpoint can be added as a base map, on every flavor and on desktop Tile Sources** at the foot of the base map picker and paste a URL template using `{z}`, `{x}` and `{y}` — plus `{s}` if the provider uses rotating subdomains. A national mapping service, for example: -``` +```text https://wmts.geo.admin.ch/1.0.0/ch.swisstopo.pixelkarte-farbe/default/current/3857/{z}/{x}/{y}.jpeg ``` @@ -173,7 +174,18 @@ On **Google Play**, the layers sheet's **Offline Manager** section downloads its — water, roads and administrative boundaries extracted from the public Protomaps basemap dataset, drawn directly as map shapes rather than raster tiles. Frame the area you want, tap **Start Download**, and once it finishes flip **Show on map** to draw it (it's off by default so it never surprises you by covering up the -live map). You can still import a pre-made `.mbtiles` archive here too, as before. **Desktop** has neither. +live map). You can still import a pre-made `.mbtiles` archive here too, as before. **Desktop** has neither, though +the offline terrain described next does work there. + +### Offline terrain + +Hillshade and elevation contours can be downloaded for offline use as well, so a downloaded area still +shows why a link fails when there is no network. The terrain data comes from Mapterhorn and is credited on +the map. On **Google Play**, each downloaded region in the **Offline Manager** list has a **Download Terrain** +button and **Hillshade** and **Contours** switches; terrain attaches to that region and shares its storage limit, and an area +too large for terrain asks you to zoom in or pick a smaller region. On **F-Droid** and **Desktop**, the +layers sheet has an **Offline Terrain** section of its own: frame the area, tap **Start Download**, and the +hillshade and contours draw from the downloaded tiles from then on. ### Going offline @@ -181,8 +193,8 @@ An **Offline** pill appears over the map whenever the device has no network conn builds, if you've imported a local `.mbtiles` archive, the map switches to it automatically the moment the network drops — no toggle to remember — and switches back once you're reconnected, as long as you haven't picked a different base map yourself in between. On **F-Droid**, a downloaded offline area keeps rendering on -its own; the pill is purely informational there. **Desktop** has no offline downloads at all yet, so the pill -is informational there too. +its own; the pill is purely informational there. **Desktop** downloads offline terrain but no base-map +packs, so the pill is informational there too. ## Related Topics diff --git a/docs/srp/user/messages-and-channels.md b/docs/srp/user/messages-and-channels.md index 327ece4429..d6eec0c3aa 100644 --- a/docs/srp/user/messages-and-channels.md +++ b/docs/srp/user/messages-and-channels.md @@ -2,7 +2,7 @@ title: Messages & Channels parent: User Guide nav_order: 3 -last_updated: 2026-08-30 +last_updated: 2026-09-14 description: Send and receive messages, manage channels, configure encryption, search conversations, and use quick chat, reactions, and message actions. aliases: - channels @@ -50,6 +50,21 @@ Key length alone does not change the icon: a 128-bit key and a 256-bit key both Tapping an existing channel opens the same editor, where you can change the name, the PSK, MQTT uplink and downlink, and position precision. Every edit on this screen — adding, editing, deleting, or dragging a channel into a new order — waits on **Send** the same way. +### Archived Channels + +A conversation belongs to the channel it was held on, not to the slot that channel occupied. When a channel leaves your radio — you scan a different channel QR, replace a channel's key, or switch the modem preset — its conversation is **archived** rather than deleted or merged into whatever takes the slot. + +An archived conversation: + +- stays in the **Channels** list under the name the channel had, below the channels currently on your radio, marked with a history icon; +- keeps its full message history, searchable as usual; +- is read-only — you cannot send, reply, or react, because the channel is no longer on the radio to send on, and it is not offered as a share target; +- comes back automatically if you re-add that exact channel, taking its messages back to the live slot. + +Changing the modem preset archives the channel too: the preset is part of a default channel's identity, so **LongFast** and **MediumFast** are different channels even though neither the name nor the key changed. Changing only the region does not archive anything. + +To remove an archived conversation for good, long-press it in the conversation list and delete it. + ## Директне поруке Direct messages (DMs) go to one specific node. When both radios hold each other's public keys, your radio encrypts the message to that node's public key, so no one else on the mesh can read it — not even nodes that share your channel. diff --git a/docs/srp/user/mqtt.md b/docs/srp/user/mqtt.md index bc6fe647c8..2658a070cf 100644 --- a/docs/srp/user/mqtt.md +++ b/docs/srp/user/mqtt.md @@ -2,7 +2,7 @@ title: MQTT parent: User Guide nav_order: 11 -last_updated: 2026-08-30 +last_updated: 2026-09-11 description: Bridge your mesh to the internet — MQTT broker setup, encryption layers, and map reporting. aliases: - mqtt @@ -25,7 +25,7 @@ The MQTT module connects your node to an MQTT broker, allowing: ## How It Works -``` +```text [Your Node] → Radio → [Gateway Node with Wi-Fi] → MQTT Broker → [Remote Gateway] → Radio → [Remote Node] ``` diff --git a/docs/srp/user/node-metrics.md b/docs/srp/user/node-metrics.md index ff6f7bb348..2a3ec677e7 100644 --- a/docs/srp/user/node-metrics.md +++ b/docs/srp/user/node-metrics.md @@ -2,7 +2,7 @@ title: Node Metrics parent: User Guide nav_order: 5 -last_updated: 2026-08-30 +last_updated: 2026-09-09 description: Telemetry dashboards for each mesh node — device health, environment sensors, air quality, signal quality, power, traceroute, and position history. aliases: - metrics @@ -114,7 +114,7 @@ Radio signal quality information: ### Signal Quality Reference -Signal quality is rated from **SNR relative to the active LoRa modem preset's demodulation floor**, not from fixed thresholds — a given SNR means different things on different presets (e.g. −15 dB is fine on LongSlow but unusable on ShortFast). RSSI is shown but is not part of the rating. In the table, _limit_ is the preset's SNR limit. +Signal quality is rated from **SNR relative to the active LoRa modem preset's demodulation floor**, not from fixed thresholds — a given SNR means different things on different presets (e.g. −15 dB is fine on LongSlow but unusable on ShortFast). When RSSI and a noise-floor reading are both available, the app also rates their difference against the preset limit and uses the worse rating. Otherwise, RSSI is display-only. In the table, _limit_ is the preset's SNR limit. | Quality | Criteria | | ---------- | ---------------------------------------------------------------- | diff --git a/docs/srp/user/nodes.md b/docs/srp/user/nodes.md index 4f3bb21c8e..4d85cafb53 100644 --- a/docs/srp/user/nodes.md +++ b/docs/srp/user/nodes.md @@ -2,7 +2,7 @@ title: Чворови parent: User Guide nav_order: 4 -last_updated: 2026-09-04 +last_updated: 2026-09-11 description: Browse, filter, and sort mesh nodes — view details, signal quality, roles, and quick actions. aliases: - node-list @@ -74,19 +74,23 @@ Most users should keep the default **Client** role. Consider a different role wh > 💡 **Tip:** The mesh works best when most nodes are **Client** or **Router**. Too many Client Mute nodes reduce mesh resilience; too many Routers in a dense area can cause congestion. A good rule of thumb: one Router per 5–10 Clients in your area. -### Encryption Indicators +### Security indicators -Nodes display encryption status icons next to their name: +Each node carries one security icon beside its name in the node list. Tap it to read what it means, and choose **Show All Meanings** in that dialog for the full legend. The detail screen shows the same state in words, as a **Security** row that opens the same dialog. -| Иконица | Meaning | -| ----------- | ------------------------------------------------------------------------------------------------------------------------------------------------- | -| 🔒 Locked | Communication uses PKI (public key infrastructure) — end-to-end encrypted with verified identity | -| 🔓 Unlocked | No public key has been received for this node, so it cannot be direct messaged — use **Request User Info** on the node detail page to ask for one | -| ⚠️ Mismatch | Public key mismatch — the node's key has changed since last seen (investigate before trusting) | +| Иконица | Meaning | Shown for | +| --------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------- | +| Person with a shield check (green) | **Verified contact** — you verified this node's key in person by exchanging contact QR codes, so its identity is confirmed. The strongest trust the list shows. Your own connected node carries it too | Any firmware version | +| Nodes icon with a shield check (green) | **Signed node** — this node signs its broadcasts with its identity key, so its identity is consistent over time, but you have not verified it in person. On firmware 2.8 the icon appears from the version alone, before any signed broadcast has been heard | Firmware 2.8 or newer, and any node whose signed broadcast your node has verified | +| 🔒 Closed lock | A public key is on file and matches, so direct messages to this node are encrypted | Firmware before 2.8, or no reported version | +| 🔓 Open lock | No public key has been received for this node, so it cannot be direct messaged — use **Request User Info** on the node detail page to ask for one | Firmware before 2.8, or no reported version | +| ⚠️ Mismatch | **Public key mismatch** — a different key arrived for this node after one was stored. Investigate before trusting | Any firmware version | -> 💡 **Tip:** Direct messages always use PKI, so the radio needs the other node's public key before it can send one. It refuses the send rather than falling back to channel encryption. Keys arrive inside node info, which is why an open lock usually clears itself once that node is heard from properly. If you see a key mismatch warning, the node may have been reset or compromised. +Every node on firmware 2.8 or newer signs its broadcasts, so on that firmware the signed state is the baseline and the locks are not shown. -To clear a mismatch, first confirm through another trusted channel that the key change was intentional — a factory reset causes one. Then touch & hold the node, choose **Remove**, and let the two radios exchange keys again the next time yours hears it. +Direct messages always use public-key encryption, so your node needs the other node's public key before it can send one. It refuses the send rather than falling back to channel encryption. Keys arrive inside node info, which is why an open lock usually clears itself once that node is heard from properly. + +A mismatch never replaces the key you already hold. The app keeps the first key it recorded and refuses the new one, as the firmware does, so a stray or hostile node info cannot silently break encrypted messaging to a contact. To clear a mismatch, first confirm through another trusted channel that the key change was intentional — a factory reset causes one. Then touch & hold the node, choose **Remove**, and let the two nodes exchange keys again the next time yours hears it. ## Quick Actions @@ -108,7 +112,9 @@ radio is connected and running firmware 2.8 or newer — see ## Sharing a Contact -On a node's detail screen, tap **Share Contact** to produce a link and a QR code for that node. From the same dialog, **Write to NFC tag** saves the link to a writable NFC tag that anyone can tap to open. +On a node's detail screen, tap **Share Contact** to produce a link and a QR code for that node. From the same dialog, **Share link** opens the Android share sheet (on desktop it copies the link instead), **Write to NFC tag** saves it to a writable NFC tag, and **Copy** puts it on the clipboard. While that dialog is open and in front of you, the phone also offers the same link to any NFC reader, so someone can take the contact by tapping their phone against yours with no tag involved. + +Sharing your own contact this way marks it as verified in person, so whoever imports it sees the **Verified contact** icon rather than the signed one. Relaying someone else's contact passes on only what your app had already recorded about them. To add someone else's contact, use the import button on the node list and choose **Scan Shared Contact QR Code**, **Scan Shared Contact NFC**, or **Input Shared Contact URL**. The app asks you to confirm with **Import Shared Contact?**, and warns you when the contact is one you already have. @@ -120,14 +126,16 @@ Type in the search field to filter nodes by name or short name. The filter updat ### Filter Toggles -| Филтер | Опис | -| --------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| **Hide offline nodes** | Show only nodes heard within the last 2 hours | -| **Only show direct nodes** | Show only nodes your radio heard directly, with no relay in between | -| **Include unknown** | Show nodes that haven't sent user info yet. **On by default**, so a node heard before its info arrives stays visible; these carry a badge marking them incomplete, and cannot be direct messaged until their user info brings a public key | -| **Exclude infrastructure** | Hide infrastructure-role nodes (Router, Router Late, Client Base, and legacy Repeater nodes) and any node that cannot be messaged, whatever its role | -| **Exclude MQTT** | Hide nodes heard only via MQTT internet bridge | -| **Only show ignored Nodes** | Replace the list with the nodes you have ignored. Every other node is hidden while this is on, and a banner appears at the top of the list to take you back | +| Филтер | Опис | +| --------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **Hide offline nodes** | Show only nodes heard within the last 2 hours | +| **Only show direct nodes** | Show only nodes your radio heard directly, with no relay in between | +| **Include unknown** | Show nodes that haven't sent user info yet. **On by default**, so a node heard before its info arrives stays visible; these carry a badge marking them incomplete, and cannot be direct messaged until their user info brings a public key | +| **Exclude infrastructure** | Hide infrastructure-role nodes (Router, Router Late, Client Base, and legacy Repeater nodes) and any node that cannot be messaged, whatever its role | +| **Exclude MQTT** | Hide nodes heard only via MQTT internet bridge | +| **Signed only** | Show only nodes whose signed broadcasts your node has actually heard and verified. Stricter than the icon: a node on 2.8 shows as signed by its firmware version before any signed broadcast arrives, and a contact verified in person on older firmware is not signed at all | +| **Encrypted only** | Show nodes with a matching public key on file, the key an encrypted direct message needs. A node with a key mismatch is excluded | +| **Only show ignored Nodes** | Replace the list with the nodes you have ignored. Every other node is hidden while this is on, and a banner appears at the top of the list to take you back | ### Sort Options @@ -149,6 +157,8 @@ Tap the hop-histogram icon in the node list's app bar to open a bar chart of how Tapping a node opens the detail view with comprehensive information. See [Node Metrics](node-metrics) for full details on metrics and telemetry. +Signal quality is rated against your modem preset. The same SNR can be good on a long-range preset and poor on a faster one. Traceroute and neighbor-info SNR colors use that preset too. RSSI text has its own strength colors; it affects the quality rating only when a noise-floor reading is also available. + The Details card carries the node's short name, role, IDs, last heard time, hops away, uptime, and its SNR and RSSI: ![Node detail section](../../assets/screenshots/nodes_detail_section.png) diff --git a/docs/srp/user/settings-module-admin.md b/docs/srp/user/settings-module-admin.md index b5eaaa93ae..145cadf151 100644 --- a/docs/srp/user/settings-module-admin.md +++ b/docs/srp/user/settings-module-admin.md @@ -2,7 +2,7 @@ title: Settings — Modules & Admin parent: User Guide nav_order: 8 -last_updated: 2026-08-30 +last_updated: 2026-09-16 description: Configure optional feature modules (MQTT, telemetry, canned messages, TAK, and more) and perform device administration. aliases: - modules @@ -333,7 +333,7 @@ appear only when your own node is selected: ### О -**Settings → About** carries the app's own identity rather than the radio's: +**Settings → About** carries the app's own identity rather than the node's: Three sections: @@ -341,7 +341,11 @@ Three sections: - **Apps** — opens with **Need Hardware?**, a rotating carousel of popular devices that links out to where to buy one. It also lists the GitHub repository, the running app version, and **Acknowledgements** (see the next section). -- **Project information** — links to the website and to this documentation. +- **Project information** — links to the website, to this documentation, and to the GNU GPL v3 + license the app is released under. + +The footer states the copyright and that the app is free software with no warranty, which you +may redistribute under the same license. ### Acknowledgements diff --git a/docs/srp/user/settings-radio-user.md b/docs/srp/user/settings-radio-user.md index e2cc50dc32..2d11a33d82 100644 --- a/docs/srp/user/settings-radio-user.md +++ b/docs/srp/user/settings-radio-user.md @@ -2,7 +2,7 @@ title: Settings — Radio & User parent: User Guide nav_order: 7 -last_updated: 2026-09-04 +last_updated: 2026-09-11 description: Configure your radio hardware, LoRa presets, user profile, position sharing, power management, and security. aliases: - подешавања @@ -51,7 +51,7 @@ The footer appears as soon as you change something. **Discard** throws the chang The status message is saved with the same **Save**, but it never reboots the node — and, like the rest of this screen, it can be edited on a remote node you administer. For your own radio there is a shortcut while it is connected: touch & hold your node in the [node list](nodes.md) and choose -**Update status**. Older firmware and a disconnected radio have no shortcut — the field above is +**Update status**. Older firmware and a disconnected node have no shortcut — the field itself is still the way in. ## Configuration @@ -77,13 +77,13 @@ On **Settings → LoRa**. | Setting | Опис | Подразумевано | | -------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------- | -| Регион | Regulatory region for frequency bands. You must set this before transmitting | Unset (must configure) | +| Регион | Regulatory region for frequency bands. You must set this before transmitting. On firmware 2.8 or newer, the first region set also creates the node's identity key and gives it a new node number | Unset (must configure) | | Унапред подешено | Speed/range tradeoff | LongFast | | Number of Hops | Maximum retransmit hops | 3 | | Transmit Power | Transmission power (dBm); 0 = max allowed for region | 0 (region max) | | Измена фреквенције | Overrides the computed operating frequency outright (MHz). It does not offset the calculated value — leave at 0 unless you know you need a specific frequency | 0 (use calculated) | | Користи предефинисано подешавање | On by default. Turn it off to set Spread Factor, Coding Rate and Bandwidth by hand instead of taking them from the modem preset | On | -| Фактор ширења | Manual mode only: 7–12. Higher spreads further but slower | From preset | +| Фактор ширења | Manual mode only: 5–12. Higher spreads further but slower. On SX127x (RF95) radios the firmware does not accept 5 or 6 and uses 11 instead | From preset | | Стопа кодирања | Manual mode only: 5–8. More redundancy costs airtime | From preset | | Проток | Manual mode only: the channel bandwidth in kHz, typed in directly. On the 2.4 GHz region the app offers a list of the bandwidths your radio supports instead, and a stored value that is not on that list shows as _Unsupported_ and blocks saving until you pick a supported one | From preset | | Фреквенцијски слот | Which slot within the region's band to use. 0 derives it from the primary channel name | 0 (automatic) | @@ -111,13 +111,13 @@ The Lite, Narrow, Medium Turbo, and Tiny presets need firmware 2.8 or newer — | Short Slow | ~5 km | 6.25 kbps | −10 dB | Suburban short-range; moderate building density | | Medium Fast | ~5 km | 3.52 kbps | −12.5 dB | Suburban areas; moderate building density | | Medium Slow | ~8 km | 1.95 kbps | −15 dB | Suburban/rural; moderate range with slower speed | -| Long Turbo | ~10 km | 1.34 kbps | −12.5 dB | Similar range to Long Fast but with 500 kHz bandwidth; faster throughput | +| Long Turbo | ~10 km | 1.34 kbps | −17.5 dB | Similar range to Long Fast but with 500 kHz bandwidth; faster throughput | | Long Fast | ~10 km | 1.1 kbps | −17.5 dB | **General use (default)** — balanced range and speed | | Long Moderate | ~20 km | 0.34 kbps | −17.5 dB | Rural with some terrain; occasional use | | Lite Fast | ~5 km | 1.76 kbps | −12.5 dB | EU 866 MHz SRD band (125 kHz BW); comparable to Medium Fast | | Lite Slow | ~10 km | 0.98 kbps | −15 dB | EU 866 MHz SRD band (125 kHz BW); comparable to Long Fast | -| Narrow Fast | ~5 km | 2.28 kbps | −10 dB | EU 868 MHz band (62.5 kHz BW); avoids interference with other devices | -| Narrow Slow | ~10 km | 1.30 kbps | −12.5 dB | EU 868 MHz band (62.5 kHz BW); comparable to Long Fast | +| Narrow Fast | ~5 km | 2.28 kbps | −7.5 dB | EU 868 MHz band (62.5 kHz BW); avoids interference with other devices | +| Narrow Slow | ~10 km | 1.30 kbps | −10 dB | EU 868 MHz band (62.5 kHz BW); comparable to Long Fast | | Medium Turbo | ~5 km | 7.0 kbps | −12.5 dB | Like Medium Fast but with 500 kHz bandwidth; not legal in every region. Needs firmware 2.8 or newer | | Tiny Fast | ~10 km | 0.68 kbps | −7.5 dB | Amateur bands that cap occupied bandwidth; these presets use 15.6 kHz. Needs firmware 2.8 or newer, an SX126x or SX127x radio, and a TCXO of ±5 ppm or better | | Tiny Slow | ~20 km | 0.33 kbps | −10 dB | Same band restrictions as Tiny Fast, longer range. Same firmware, radio, and TCXO requirements | diff --git a/docs/srp/user/signal-meter.md b/docs/srp/user/signal-meter.md index 3d1a2ff7bf..6f9ee5f431 100644 --- a/docs/srp/user/signal-meter.md +++ b/docs/srp/user/signal-meter.md @@ -2,7 +2,7 @@ title: How the Meshtastic Signal Meter Works parent: User Guide nav_order: 15 -last_updated: 2026-08-30 +last_updated: 2026-09-09 description: How the signal meter rates quality from SNR relative to the LoRa modem preset — spread spectrum, presets, and what the bars really mean. aliases: - signal @@ -32,7 +32,7 @@ Each modem preset has an SNR limit: the lowest SNR at which that preset can stil ## Rating Signal Quality -The app rates signal quality (None, Bad, Fair, or Good) from SNR alone, measured against the active preset's SNR limit. It does not factor in RSSI: without knowing the local noise floor, RSSI alone cannot say whether a signal is decodable. RSSI is still available — on the node detail screen and in the metrics charts. +The app rates signal quality (None, Bad, Fair, or Good) from SNR relative to the active preset's SNR limit. When RSSI and a noise-floor reading are both available, it also rates the difference between them against that limit and uses the worse rating. Otherwise, it uses SNR alone. Because the rating is relative to the preset, the same SNR rates differently on different presets. An SNR of −16 dB rates Good on Long Fast (SNR limit −17.5 dB) but None on Short Fast (SNR limit −7.5 dB). Let `limit` be the active preset's SNR limit: @@ -45,7 +45,7 @@ Because the rating is relative to the preset, the same SNR rates differently on The icon never goes blank, so count the bars carefully: a single bar means None, not a weak but usable link, and Good is a solid wedge rather than a set of bars. A gray three-bar icon labeled Unknown is a different state again — the packet carried no SNR measurement at all, which is not the same as measuring one and finding it too weak. -> ℹ️ **Note:** Traceroute hop colors use fixed thresholds (−7 dB / −15 dB); the per-node signal meter uses the preset-relative rating instead. +> ℹ️ **Note:** Traceroute and neighbor-info SNR colors use the active preset's limits too. RSSI text has separate strength colors: green above −115 dBm, yellow above −120 dBm, orange above −126 dBm, and red at or below −126 dBm. ## Diagnosing Local Interference diff --git a/docs/srp/user/tak.md b/docs/srp/user/tak.md index 461c9cd270..029bdfb879 100644 --- a/docs/srp/user/tak.md +++ b/docs/srp/user/tak.md @@ -2,7 +2,7 @@ title: TAK Integration parent: User Guide nav_order: 10 -last_updated: 2026-08-30 +last_updated: 2026-09-11 description: Interoperate with ATAK and WinTAK — CoT position sharing, TAK roles, and plugin setup. aliases: - tak @@ -36,7 +36,7 @@ The TAK module allows Meshtastic nodes to: > ⚠️ **Warning:** The old **Meshtastic ATAK Plugin** is no longer part of this path and cannot > work. It bridged through the cross-process AIDL API, which was removed in app 2.8.0; the mesh > service is now in-process only. Do not install it. Interop today runs over the app's own local -> TAK server plus the Mesh to CoT Converter, both described below, with stock ATAK/iTAK/WinTAK. +> TAK server plus the Mesh to CoT Converter, both described in the following sections, with stock ATAK/iTAK/WinTAK. ### Configuration diff --git a/docs/srp/user/translate.md b/docs/srp/user/translate.md index e7f535bfde..9516addc11 100644 --- a/docs/srp/user/translate.md +++ b/docs/srp/user/translate.md @@ -2,7 +2,7 @@ title: Translate the App parent: User Guide nav_order: 17 -last_updated: 2026-08-30 +last_updated: 2026-09-11 description: How the app and its documentation are translated via Crowdin, and guidelines for contributing translations. aliases: - translate @@ -45,7 +45,7 @@ If your language is not yet listed on Crowdin: The Android app uses **Compose Multiplatform resources** for all user-visible strings: -``` +```text core/resources/src/commonMain/composeResources/ ├── values/ ← English (default) │ └── strings.xml @@ -58,7 +58,7 @@ core/resources/src/commonMain/composeResources/ In-app documentation follows a similar pattern under `docs/`: -``` +```text docs/ ├── en/user/ ← English source (default) │ ├── onboarding.md diff --git a/docs/sv-rSE/user/desktop.md b/docs/sv-rSE/user/desktop.md index 23c5010c0d..dc0638b7da 100644 --- a/docs/sv-rSE/user/desktop.md +++ b/docs/sv-rSE/user/desktop.md @@ -2,7 +2,7 @@ title: Desktop App parent: User Guide nav_order: 14 -last_updated: 2026-08-30 +last_updated: 2026-09-11 description: Install and use the Meshtastic Desktop app on Linux, macOS, and Windows — connections, feature parity, and keyboard shortcuts. aliases: - desktop @@ -58,20 +58,20 @@ Bluetooth Low Energy is supported on desktop via the [Kable](https://github.com/ ## Feature Parity -| Feature | Android | Desktop | Anteckningar | -| ----------------------------------------------------- | ------- | ------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Messaging | ✓ | ✓ | Full parity | -| Node List | ✓ | ✓ | Full parity | -| Karta | ✓ | ✓ | Interactive MapLibre map, with base map and overlay pickers and custom tile sources. No offline downloads or local `.mbtiles` archives | -| Map layers (`.kml`/`.kmz`/GeoJSON) | ✓ | ✓ | Same layer store and sheet as Android; imported files draw on the desktop map | -| Site Planner | ✓ | ✓\* | \*Opens in your browser on desktop; the estimate is not drawn on the desktop map | -| Inställningar | ✓ | ✓ | Full parity | -| Bluetooth (BLE) | ✓ | ✓ | Via Kable on desktop | -| Uppdatering av fast programvara | ✓ | ✓ | In-app USB, BLE, and Wi-Fi (ESP32) update work the same as Android. The USB maintenance flow — nRF52/RP2040 factory erase and bootloader upgrade — is Android-only | -| Notifications | ✓ | ✓ | Native OS notifications | -| Widgets | ✓ | ✗ | Android-only | -| AI Assistant (Chirpy) | ✓\* | ✗ | Google flavor Android only | -| App Functions (system AI) | ✓† | ✗ | Google flavor Android only | +| Feature | Android | Desktop | Anteckningar | +| ----------------------------------------------------- | ------- | ------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Messaging | ✓ | ✓ | Full parity | +| Node List | ✓ | ✓ | Full parity | +| Karta | ✓ | ✓ | Interactive MapLibre map, with base map and overlay pickers and custom tile sources. Offline terrain (hillshade and contours) can be downloaded; offline base-map packs and local `.mbtiles` archives cannot | +| Map layers (`.kml`/`.kmz`/GeoJSON) | ✓ | ✓ | Same layer store and sheet as Android; imported files draw on the desktop map | +| Site Planner | ✓ | ✓\* | \*Opens in your browser on desktop; the estimate is not drawn on the desktop map | +| Inställningar | ✓ | ✓ | Full parity | +| Bluetooth (BLE) | ✓ | ✓ | Via Kable on desktop | +| Uppdatering av fast programvara | ✓ | ✓ | In-app USB, BLE, and Wi-Fi (ESP32) update work the same as Android. The USB maintenance flow — nRF52/RP2040 factory erase and bootloader upgrade — is Android-only | +| Notifications | ✓ | ✓ | Native OS notifications | +| Widgets | ✓ | ✗ | Android-only | +| AI Assistant (Chirpy) | ✓\* | ✗ | Google flavor Android only | +| App Functions (system AI) | ✓† | ✗ | Google flavor Android only | \*Chirpy AI requires Android 14+ on Google flavor builds with supported hardware. @@ -122,7 +122,7 @@ Individual doc pages render with full formatting: ## Building from Source -```bash +```shell git clone https://github.com/meshtastic/Meshtastic-Android.git cd Meshtastic-Android ./gradlew :desktopApp:run @@ -135,7 +135,7 @@ Requirements: ## Known Limitations -- Offline tile downloads and local `.mbtiles` archives are not available on desktop. +- Offline base-map downloads and local `.mbtiles` archives are not available on desktop. Offline terrain is — see [Map & Waypoints](map-and-waypoints). - `.kml`/`.kmz`/GeoJSON layer import works — see [Map & Waypoints](map-and-waypoints#map-layers). Site Planner opens in your browser rather than in the app; to bring its coverage estimate onto the map, click the transmitter pin diff --git a/docs/sv-rSE/user/discovery.md b/docs/sv-rSE/user/discovery.md index 4103d348ac..fa6f012be3 100644 --- a/docs/sv-rSE/user/discovery.md +++ b/docs/sv-rSE/user/discovery.md @@ -2,7 +2,7 @@ title: Local Mesh Discovery parent: User Guide nav_order: 12 -last_updated: 2026-08-30 +last_updated: 2026-09-11 description: Explore your mesh network — the Local Mesh Discovery scanner, traceroute paths, neighbor maps, and node discovery tools. aliases: - discovery @@ -97,7 +97,7 @@ Mesh Beacon lets nodes invite others to join their mesh. A beaconing node period Configure it under **Settings → Module configuration → Mesh Beacon**. The entry appears only on radios running firmware 2.8.0 or newer. A read-only **Region** row at the top of the screen shows the region the beacon advertises: that region, and the preset, are always the ones the radio itself uses, so a beacon cannot invite anyone onto settings your radio is not running. - **Listen for beacons** — receive invitations broadcast by other nodes. -- **Broadcast a beacon** — periodically advertise this mesh to nearby nodes, with an optional **Beacon message** of up to 100 bytes, a **Broadcast interval** picked from fixed intervals between 1 hour and 72 hours, and an **Offered channel** chosen from your radio's own channels. The offered channel is required, and defaults to your primary channel. +- **Broadcast a beacon** — periodically advertise this mesh to nearby nodes, with an optional **Beacon message** of up to 100 bytes, a **Broadcast interval** picked from fixed intervals between 1 hour and 72 hours, and an **Offered channel** chosen from your radio's own channels. The offered channel is required, and defaults to your primary channel. Over remote admin the picker offers the primary channel only. - **Broadcast targets** — optional extra destinations beyond the offered channel. **Add target** appends a row; each row picks a **Channel** and a **Transmit preset**, and **Remove target** deletes it. With no targets, the beacon goes out on the offered channel alone. Two conditions block beacon setup: diff --git a/docs/sv-rSE/user/help-and-docs.md b/docs/sv-rSE/user/help-and-docs.md index e624a9783d..018dbc280f 100644 --- a/docs/sv-rSE/user/help-and-docs.md +++ b/docs/sv-rSE/user/help-and-docs.md @@ -2,7 +2,7 @@ title: Help & In-App Docs parent: Användarguide nav_order: 21 -last_updated: 2026-08-30 +last_updated: 2026-09-11 description: Browse this documentation inside the app, search it, and ask Chirpy — the on-device AI assistant — questions about Meshtastic. aliases: - help @@ -23,7 +23,7 @@ The docs browser lists every user-guide page. Tap a page to read it; images and ### Search -The search box sits at the top of the docs browser. Type in it to filter pages by title and keywords — results update as you type, and the **Clear search** button (✕) empties the box. +Tap the search field at the top of the docs browser to open a full-screen search view. Type to filter pages by title and keywords — results update as you type, under the same field. The back arrow, or the keyboard's search action, closes the view. The field hides while you scroll down the page list and comes back when you scroll up. ![Searching the in-app documentation](../../assets/screenshots/docs-browser_search.png) diff --git a/docs/sv-rSE/user/map-and-waypoints.md b/docs/sv-rSE/user/map-and-waypoints.md index 36f56858a9..0e25ea00b0 100644 --- a/docs/sv-rSE/user/map-and-waypoints.md +++ b/docs/sv-rSE/user/map-and-waypoints.md @@ -2,7 +2,7 @@ title: Map & Waypoints parent: User Guide nav_order: 6 -last_updated: 2026-09-01 +last_updated: 2026-09-11 description: View node positions on the map, create and share waypoints, manage map layers and Site Planner, and control position sharing and privacy. aliases: - map @@ -44,7 +44,7 @@ The floating toolbar provides quick access to the compass, the map type and laye ### Filtering the Map -Tap the filter button in the floating toolbar to open **Filter map**. **Display** controls what is drawn: **Only Favorites**, **Show Waypoints**, **Show Precision Circles**, and a slider that hides nodes not heard from recently. **Node roles** is a chip per device role, plus **All** to show every role; a selected chip means that role is shown. **Nodes** narrows the set further with **Hide offline nodes**, **Only show direct nodes**, **Exclude MQTT**, **Show ignored nodes**, and **Include unknown**. +Tap the filter button in the floating toolbar to open **Filter map**. **Display** controls what is drawn: **Only Favorites**, **Show Waypoints**, **Show Precision Circles**, and a slider that hides nodes not heard from recently. **Node roles** is a chip per device role, plus **All** to show every role; a selected chip means that role is shown. **Nodes** narrows the set further with **Hide offline nodes**, **Only show direct nodes**, **Signed only**, **Encrypted only**, **Exclude MQTT**, **Show ignored nodes**, and **Include unknown** — the same rules as the node list's [filter toggles](nodes). A dot on the filter button means at least one filter is hiding something — check it before concluding the mesh is quiet. Turning **Show Waypoints** off hides every waypoint, including your own. **Show ignored nodes** adds them to the map rather than showing only them — unlike the node list's **Only show ignored Nodes**. @@ -147,8 +147,9 @@ picker, all three offer the same raster base maps: Overlays can be toggled on top of any base map, from the layers sheet: - **Weather radar** — NOAA NEXRAD reflectivity (US coverage) -- **Hillshade** — terrain relief, on **F-Droid** and **Desktop** only. Useful for understanding why a - link fails, since LoRa range is limited by terrain +- **Hillshade** — terrain relief from an online tile source, on **F-Droid** and **Desktop** only; Google + Play draws hillshade from downloaded offline terrain instead. Useful for understanding why a link + fails, since LoRa range is limited by terrain ### Adding your own tile source @@ -156,7 +157,7 @@ Any XYZ tile endpoint can be added as a base map, on every flavor and on desktop Tile Sources** at the foot of the base map picker and paste a URL template using `{z}`, `{x}` and `{y}` — plus `{s}` if the provider uses rotating subdomains. A national mapping service, for example: -``` +```text https://wmts.geo.admin.ch/1.0.0/ch.swisstopo.pixelkarte-farbe/default/current/3857/{z}/{x}/{y}.jpeg ``` @@ -173,7 +174,18 @@ On **Google Play**, the layers sheet's **Offline Manager** section downloads its — water, roads and administrative boundaries extracted from the public Protomaps basemap dataset, drawn directly as map shapes rather than raster tiles. Frame the area you want, tap **Start Download**, and once it finishes flip **Show on map** to draw it (it's off by default so it never surprises you by covering up the -live map). You can still import a pre-made `.mbtiles` archive here too, as before. **Desktop** has neither. +live map). You can still import a pre-made `.mbtiles` archive here too, as before. **Desktop** has neither, though +the offline terrain described next does work there. + +### Offline terrain + +Hillshade and elevation contours can be downloaded for offline use as well, so a downloaded area still +shows why a link fails when there is no network. The terrain data comes from Mapterhorn and is credited on +the map. On **Google Play**, each downloaded region in the **Offline Manager** list has a **Download Terrain** +button and **Hillshade** and **Contours** switches; terrain attaches to that region and shares its storage limit, and an area +too large for terrain asks you to zoom in or pick a smaller region. On **F-Droid** and **Desktop**, the +layers sheet has an **Offline Terrain** section of its own: frame the area, tap **Start Download**, and the +hillshade and contours draw from the downloaded tiles from then on. ### Going offline @@ -181,8 +193,8 @@ An **Offline** pill appears over the map whenever the device has no network conn builds, if you've imported a local `.mbtiles` archive, the map switches to it automatically the moment the network drops — no toggle to remember — and switches back once you're reconnected, as long as you haven't picked a different base map yourself in between. On **F-Droid**, a downloaded offline area keeps rendering on -its own; the pill is purely informational there. **Desktop** has no offline downloads at all yet, so the pill -is informational there too. +its own; the pill is purely informational there. **Desktop** downloads offline terrain but no base-map +packs, so the pill is informational there too. ## Related Topics diff --git a/docs/sv-rSE/user/messages-and-channels.md b/docs/sv-rSE/user/messages-and-channels.md index e99de8bb47..a694325e21 100644 --- a/docs/sv-rSE/user/messages-and-channels.md +++ b/docs/sv-rSE/user/messages-and-channels.md @@ -2,7 +2,7 @@ title: Messages & Channels parent: User Guide nav_order: 3 -last_updated: 2026-08-30 +last_updated: 2026-09-14 description: Send and receive messages, manage channels, configure encryption, search conversations, and use quick chat, reactions, and message actions. aliases: - channels @@ -50,6 +50,21 @@ Key length alone does not change the icon: a 128-bit key and a 256-bit key both Tapping an existing channel opens the same editor, where you can change the name, the PSK, MQTT uplink and downlink, and position precision. Every edit on this screen — adding, editing, deleting, or dragging a channel into a new order — waits on **Send** the same way. +### Archived Channels + +A conversation belongs to the channel it was held on, not to the slot that channel occupied. When a channel leaves your radio — you scan a different channel QR, replace a channel's key, or switch the modem preset — its conversation is **archived** rather than deleted or merged into whatever takes the slot. + +An archived conversation: + +- stays in the **Channels** list under the name the channel had, below the channels currently on your radio, marked with a history icon; +- keeps its full message history, searchable as usual; +- is read-only — you cannot send, reply, or react, because the channel is no longer on the radio to send on, and it is not offered as a share target; +- comes back automatically if you re-add that exact channel, taking its messages back to the live slot. + +Changing the modem preset archives the channel too: the preset is part of a default channel's identity, so **LongFast** and **MediumFast** are different channels even though neither the name nor the key changed. Changing only the region does not archive anything. + +To remove an archived conversation for good, long-press it in the conversation list and delete it. + ## Direct Messages Direct messages (DMs) go to one specific node. When both radios hold each other's public keys, your radio encrypts the message to that node's public key, so no one else on the mesh can read it — not even nodes that share your channel. diff --git a/docs/sv-rSE/user/mqtt.md b/docs/sv-rSE/user/mqtt.md index 18708ce7ff..148410d389 100644 --- a/docs/sv-rSE/user/mqtt.md +++ b/docs/sv-rSE/user/mqtt.md @@ -2,7 +2,7 @@ title: MQTT parent: User Guide nav_order: 11 -last_updated: 2026-08-30 +last_updated: 2026-09-11 description: Bridge your mesh to the internet — MQTT broker setup, encryption layers, and map reporting. aliases: - mqtt @@ -25,7 +25,7 @@ The MQTT module connects your node to an MQTT broker, allowing: ## How It Works -``` +```text [Your Node] → Radio → [Gateway Node with Wi-Fi] → MQTT Broker → [Remote Gateway] → Radio → [Remote Node] ``` diff --git a/docs/sv-rSE/user/node-metrics.md b/docs/sv-rSE/user/node-metrics.md index dbed0fd55a..a6f517c441 100644 --- a/docs/sv-rSE/user/node-metrics.md +++ b/docs/sv-rSE/user/node-metrics.md @@ -2,7 +2,7 @@ title: Node Metrics parent: User Guide nav_order: 5 -last_updated: 2026-08-30 +last_updated: 2026-09-09 description: Telemetry dashboards for each mesh node — device health, environment sensors, air quality, signal quality, power, traceroute, and position history. aliases: - metrics @@ -114,7 +114,7 @@ Radio signal quality information: ### Signal Quality Reference -Signal quality is rated from **SNR relative to the active LoRa modem preset's demodulation floor**, not from fixed thresholds — a given SNR means different things on different presets (e.g. −15 dB is fine on LongSlow but unusable on ShortFast). RSSI is shown but is not part of the rating. In the table, _limit_ is the preset's SNR limit. +Signal quality is rated from **SNR relative to the active LoRa modem preset's demodulation floor**, not from fixed thresholds — a given SNR means different things on different presets (e.g. −15 dB is fine on LongSlow but unusable on ShortFast). When RSSI and a noise-floor reading are both available, the app also rates their difference against the preset limit and uses the worse rating. Otherwise, RSSI is display-only. In the table, _limit_ is the preset's SNR limit. | Quality | Criteria | | ------- | ---------------------------------------------------------------- | diff --git a/docs/sv-rSE/user/nodes.md b/docs/sv-rSE/user/nodes.md index 5306bc3abb..08e10d7134 100644 --- a/docs/sv-rSE/user/nodes.md +++ b/docs/sv-rSE/user/nodes.md @@ -2,7 +2,7 @@ title: Noder parent: User Guide nav_order: 4 -last_updated: 2026-09-04 +last_updated: 2026-09-11 description: Browse, filter, and sort mesh nodes — view details, signal quality, roles, and quick actions. aliases: - node-list @@ -74,19 +74,23 @@ Most users should keep the default **Client** role. Consider a different role wh > 💡 **Tip:** The mesh works best when most nodes are **Client** or **Router**. Too many Client Mute nodes reduce mesh resilience; too many Routers in a dense area can cause congestion. A good rule of thumb: one Router per 5–10 Clients in your area. -### Encryption Indicators +### Security indicators -Nodes display encryption status icons next to their name: +Each node carries one security icon beside its name in the node list. Tap it to read what it means, and choose **Show All Meanings** in that dialog for the full legend. The detail screen shows the same state in words, as a **Security** row that opens the same dialog. -| Icon | Meaning | -| ----------- | ------------------------------------------------------------------------------------------------------------------------------------------------- | -| 🔒 Locked | Communication uses PKI (public key infrastructure) — end-to-end encrypted with verified identity | -| 🔓 Unlocked | No public key has been received for this node, so it cannot be direct messaged — use **Request User Info** on the node detail page to ask for one | -| ⚠️ Mismatch | Public key mismatch — the node's key has changed since last seen (investigate before trusting) | +| Icon | Meaning | Shown for | +| --------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------- | +| Person with a shield check (green) | **Verified contact** — you verified this node's key in person by exchanging contact QR codes, so its identity is confirmed. The strongest trust the list shows. Your own connected node carries it too | Any firmware version | +| Nodes icon with a shield check (green) | **Signed node** — this node signs its broadcasts with its identity key, so its identity is consistent over time, but you have not verified it in person. On firmware 2.8 the icon appears from the version alone, before any signed broadcast has been heard | Firmware 2.8 or newer, and any node whose signed broadcast your node has verified | +| 🔒 Closed lock | A public key is on file and matches, so direct messages to this node are encrypted | Firmware before 2.8, or no reported version | +| 🔓 Open lock | No public key has been received for this node, so it cannot be direct messaged — use **Request User Info** on the node detail page to ask for one | Firmware before 2.8, or no reported version | +| ⚠️ Mismatch | **Public key mismatch** — a different key arrived for this node after one was stored. Investigate before trusting | Any firmware version | -> 💡 **Tip:** Direct messages always use PKI, so the radio needs the other node's public key before it can send one. It refuses the send rather than falling back to channel encryption. Keys arrive inside node info, which is why an open lock usually clears itself once that node is heard from properly. If you see a key mismatch warning, the node may have been reset or compromised. +Every node on firmware 2.8 or newer signs its broadcasts, so on that firmware the signed state is the baseline and the locks are not shown. -To clear a mismatch, first confirm through another trusted channel that the key change was intentional — a factory reset causes one. Then touch & hold the node, choose **Remove**, and let the two radios exchange keys again the next time yours hears it. +Direct messages always use public-key encryption, so your node needs the other node's public key before it can send one. It refuses the send rather than falling back to channel encryption. Keys arrive inside node info, which is why an open lock usually clears itself once that node is heard from properly. + +A mismatch never replaces the key you already hold. The app keeps the first key it recorded and refuses the new one, as the firmware does, so a stray or hostile node info cannot silently break encrypted messaging to a contact. To clear a mismatch, first confirm through another trusted channel that the key change was intentional — a factory reset causes one. Then touch & hold the node, choose **Remove**, and let the two nodes exchange keys again the next time yours hears it. ## Quick Actions @@ -108,7 +112,9 @@ radio is connected and running firmware 2.8 or newer — see ## Sharing a Contact -On a node's detail screen, tap **Share Contact** to produce a link and a QR code for that node. From the same dialog, **Write to NFC tag** saves the link to a writable NFC tag that anyone can tap to open. +On a node's detail screen, tap **Share Contact** to produce a link and a QR code for that node. From the same dialog, **Share link** opens the Android share sheet (on desktop it copies the link instead), **Write to NFC tag** saves it to a writable NFC tag, and **Copy** puts it on the clipboard. While that dialog is open and in front of you, the phone also offers the same link to any NFC reader, so someone can take the contact by tapping their phone against yours with no tag involved. + +Sharing your own contact this way marks it as verified in person, so whoever imports it sees the **Verified contact** icon rather than the signed one. Relaying someone else's contact passes on only what your app had already recorded about them. To add someone else's contact, use the import button on the node list and choose **Scan Shared Contact QR Code**, **Scan Shared Contact NFC**, or **Input Shared Contact URL**. The app asks you to confirm with **Import Shared Contact?**, and warns you when the contact is one you already have. @@ -120,14 +126,16 @@ Type in the search field to filter nodes by name or short name. The filter updat ### Filter Toggles -| Filter | Beskrivning | -| --------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| **Hide offline nodes** | Show only nodes heard within the last 2 hours | -| **Only show direct nodes** | Show only nodes your radio heard directly, with no relay in between | -| **Include unknown** | Show nodes that haven't sent user info yet. **On by default**, so a node heard before its info arrives stays visible; these carry a badge marking them incomplete, and cannot be direct messaged until their user info brings a public key | -| **Exclude infrastructure** | Hide infrastructure-role nodes (Router, Router Late, Client Base, and legacy Repeater nodes) and any node that cannot be messaged, whatever its role | -| **Exclude MQTT** | Hide nodes heard only via MQTT internet bridge | -| **Only show ignored Nodes** | Replace the list with the nodes you have ignored. Every other node is hidden while this is on, and a banner appears at the top of the list to take you back | +| Filter | Beskrivning | +| --------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **Hide offline nodes** | Show only nodes heard within the last 2 hours | +| **Only show direct nodes** | Show only nodes your radio heard directly, with no relay in between | +| **Include unknown** | Show nodes that haven't sent user info yet. **On by default**, so a node heard before its info arrives stays visible; these carry a badge marking them incomplete, and cannot be direct messaged until their user info brings a public key | +| **Exclude infrastructure** | Hide infrastructure-role nodes (Router, Router Late, Client Base, and legacy Repeater nodes) and any node that cannot be messaged, whatever its role | +| **Exclude MQTT** | Hide nodes heard only via MQTT internet bridge | +| **Signed only** | Show only nodes whose signed broadcasts your node has actually heard and verified. Stricter than the icon: a node on 2.8 shows as signed by its firmware version before any signed broadcast arrives, and a contact verified in person on older firmware is not signed at all | +| **Encrypted only** | Show nodes with a matching public key on file, the key an encrypted direct message needs. A node with a key mismatch is excluded | +| **Only show ignored Nodes** | Replace the list with the nodes you have ignored. Every other node is hidden while this is on, and a banner appears at the top of the list to take you back | ### Sort Options @@ -149,6 +157,8 @@ Tap the hop-histogram icon in the node list's app bar to open a bar chart of how Tapping a node opens the detail view with comprehensive information. See [Node Metrics](node-metrics) for full details on metrics and telemetry. +Signal quality is rated against your modem preset. The same SNR can be good on a long-range preset and poor on a faster one. Traceroute and neighbor-info SNR colors use that preset too. RSSI text has its own strength colors; it affects the quality rating only when a noise-floor reading is also available. + The Details card carries the node's short name, role, IDs, last heard time, hops away, uptime, and its SNR and RSSI: ![Node detail section](../../assets/screenshots/nodes_detail_section.png) diff --git a/docs/sv-rSE/user/settings-module-admin.md b/docs/sv-rSE/user/settings-module-admin.md index 79486f5665..739642b788 100644 --- a/docs/sv-rSE/user/settings-module-admin.md +++ b/docs/sv-rSE/user/settings-module-admin.md @@ -2,7 +2,7 @@ title: Settings — Modules & Admin parent: User Guide nav_order: 8 -last_updated: 2026-08-30 +last_updated: 2026-09-16 description: Configure optional feature modules (MQTT, telemetry, canned messages, TAK, and more) and perform device administration. aliases: - modules @@ -333,7 +333,7 @@ appear only when your own node is selected: ### Om -**Settings → About** carries the app's own identity rather than the radio's: +**Settings → About** carries the app's own identity rather than the node's: Three sections: @@ -341,7 +341,11 @@ Three sections: - **Apps** — opens with **Need Hardware?**, a rotating carousel of popular devices that links out to where to buy one. It also lists the GitHub repository, the running app version, and **Acknowledgements** (see the next section). -- **Project information** — links to the website and to this documentation. +- **Project information** — links to the website, to this documentation, and to the GNU GPL v3 + license the app is released under. + +The footer states the copyright and that the app is free software with no warranty, which you +may redistribute under the same license. ### Bekräftelser diff --git a/docs/sv-rSE/user/settings-radio-user.md b/docs/sv-rSE/user/settings-radio-user.md index 591a516163..f1b0cef74e 100644 --- a/docs/sv-rSE/user/settings-radio-user.md +++ b/docs/sv-rSE/user/settings-radio-user.md @@ -2,7 +2,7 @@ title: Settings — Radio & User parent: User Guide nav_order: 7 -last_updated: 2026-09-04 +last_updated: 2026-09-11 description: Configure your radio hardware, LoRa presets, user profile, position sharing, power management, and security. aliases: - inställningar @@ -51,7 +51,7 @@ The footer appears as soon as you change something. **Discard** throws the chang The status message is saved with the same **Save**, but it never reboots the node — and, like the rest of this screen, it can be edited on a remote node you administer. For your own radio there is a shortcut while it is connected: touch & hold your node in the [node list](nodes.md) and choose -**Update status**. Older firmware and a disconnected radio have no shortcut — the field above is +**Update status**. Older firmware and a disconnected node have no shortcut — the field itself is still the way in. ## Konfiguration @@ -77,13 +77,13 @@ On **Settings → LoRa**. | Setting | Beskrivning | Förvald | | ----------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------- | -| Region | Regulatory region for frequency bands. You must set this before transmitting | Unset (must configure) | +| Region | Regulatory region for frequency bands. You must set this before transmitting. On firmware 2.8 or newer, the first region set also creates the node's identity key and gives it a new node number | Unset (must configure) | | Förval | Speed/range tradeoff | LongFast | | Antal hopp | Maximum retransmit hops | 3 | | Sändningseffekt | Transmission power (dBm); 0 = max allowed for region | 0 (region max) | | Åsidosätt | Overrides the computed operating frequency outright (MHz). It does not offset the calculated value — leave at 0 unless you know you need a specific frequency | 0 (use calculated) | | Använd förinställning | On by default. Turn it off to set Spread Factor, Coding Rate and Bandwidth by hand instead of taking them from the modem preset | On | -| Spridningsfaktor | Manual mode only: 7–12. Higher spreads further but slower | From preset | +| Spridningsfaktor | Manual mode only: 5–12. Higher spreads further but slower. On SX127x (RF95) radios the firmware does not accept 5 or 6 and uses 11 instead | From preset | | Kodningshastighet | Manual mode only: 5–8. More redundancy costs airtime | From preset | | Bandbredd | Manual mode only: the channel bandwidth in kHz, typed in directly. On the 2.4 GHz region the app offers a list of the bandwidths your radio supports instead, and a stored value that is not on that list shows as _Unsupported_ and blocks saving until you pick a supported one | From preset | | Frekvens-slot | Which slot within the region's band to use. 0 derives it from the primary channel name | 0 (automatic) | @@ -111,13 +111,13 @@ The Lite, Narrow, Medium Turbo, and Tiny presets need firmware 2.8 or newer — | Short Slow | ~5 km | 6.25 kbps | −10 dB | Suburban short-range; moderate building density | | Medium Fast | ~5 km | 3.52 kbps | −12.5 dB | Suburban areas; moderate building density | | Medium Slow | ~8 km | 1.95 kbps | −15 dB | Suburban/rural; moderate range with slower speed | -| Long Turbo | ~10 km | 1.34 kbps | −12.5 dB | Similar range to Long Fast but with 500 kHz bandwidth; faster throughput | +| Long Turbo | ~10 km | 1.34 kbps | −17.5 dB | Similar range to Long Fast but with 500 kHz bandwidth; faster throughput | | Long Fast | ~10 km | 1.1 kbps | −17.5 dB | **General use (default)** — balanced range and speed | | Long Moderate | ~20 km | 0.34 kbps | −17.5 dB | Rural with some terrain; occasional use | | Lite Fast | ~5 km | 1.76 kbps | −12.5 dB | EU 866 MHz SRD band (125 kHz BW); comparable to Medium Fast | | Lite Slow | ~10 km | 0.98 kbps | −15 dB | EU 866 MHz SRD band (125 kHz BW); comparable to Long Fast | -| Narrow Fast | ~5 km | 2.28 kbps | −10 dB | EU 868 MHz band (62.5 kHz BW); avoids interference with other devices | -| Narrow Slow | ~10 km | 1.30 kbps | −12.5 dB | EU 868 MHz band (62.5 kHz BW); comparable to Long Fast | +| Narrow Fast | ~5 km | 2.28 kbps | −7.5 dB | EU 868 MHz band (62.5 kHz BW); avoids interference with other devices | +| Narrow Slow | ~10 km | 1.30 kbps | −10 dB | EU 868 MHz band (62.5 kHz BW); comparable to Long Fast | | Medium Turbo | ~5 km | 7.0 kbps | −12.5 dB | Like Medium Fast but with 500 kHz bandwidth; not legal in every region. Needs firmware 2.8 or newer | | Tiny Fast | ~10 km | 0.68 kbps | −7.5 dB | Amateur bands that cap occupied bandwidth; these presets use 15.6 kHz. Needs firmware 2.8 or newer, an SX126x or SX127x radio, and a TCXO of ±5 ppm or better | | Tiny Slow | ~20 km | 0.33 kbps | −10 dB | Same band restrictions as Tiny Fast, longer range. Same firmware, radio, and TCXO requirements | diff --git a/docs/sv-rSE/user/signal-meter.md b/docs/sv-rSE/user/signal-meter.md index df80608843..9a59ea2738 100644 --- a/docs/sv-rSE/user/signal-meter.md +++ b/docs/sv-rSE/user/signal-meter.md @@ -2,7 +2,7 @@ title: How the Meshtastic Signal Meter Works parent: User Guide nav_order: 15 -last_updated: 2026-08-30 +last_updated: 2026-09-09 description: How the signal meter rates quality from SNR relative to the LoRa modem preset — spread spectrum, presets, and what the bars really mean. aliases: - signal @@ -32,7 +32,7 @@ Each modem preset has an SNR limit: the lowest SNR at which that preset can stil ## Rating Signal Quality -The app rates signal quality (None, Bad, Fair, or Good) from SNR alone, measured against the active preset's SNR limit. It does not factor in RSSI: without knowing the local noise floor, RSSI alone cannot say whether a signal is decodable. RSSI is still available — on the node detail screen and in the metrics charts. +The app rates signal quality (None, Bad, Fair, or Good) from SNR relative to the active preset's SNR limit. When RSSI and a noise-floor reading are both available, it also rates the difference between them against that limit and uses the worse rating. Otherwise, it uses SNR alone. Because the rating is relative to the preset, the same SNR rates differently on different presets. An SNR of −16 dB rates Good on Long Fast (SNR limit −17.5 dB) but None on Short Fast (SNR limit −7.5 dB). Let `limit` be the active preset's SNR limit: @@ -45,7 +45,7 @@ Because the rating is relative to the preset, the same SNR rates differently on The icon never goes blank, so count the bars carefully: a single bar means None, not a weak but usable link, and Good is a solid wedge rather than a set of bars. A gray three-bar icon labeled Unknown is a different state again — the packet carried no SNR measurement at all, which is not the same as measuring one and finding it too weak. -> ℹ️ **Note:** Traceroute hop colors use fixed thresholds (−7 dB / −15 dB); the per-node signal meter uses the preset-relative rating instead. +> ℹ️ **Note:** Traceroute and neighbor-info SNR colors use the active preset's limits too. RSSI text has separate strength colors: green above −115 dBm, yellow above −120 dBm, orange above −126 dBm, and red at or below −126 dBm. ## Diagnosing Local Interference diff --git a/docs/sv-rSE/user/tak.md b/docs/sv-rSE/user/tak.md index 2383e29064..17ef124c6b 100644 --- a/docs/sv-rSE/user/tak.md +++ b/docs/sv-rSE/user/tak.md @@ -2,7 +2,7 @@ title: TAK Integration parent: User Guide nav_order: 10 -last_updated: 2026-08-30 +last_updated: 2026-09-11 description: Interoperate with ATAK and WinTAK — CoT position sharing, TAK roles, and plugin setup. aliases: - tak @@ -36,7 +36,7 @@ The TAK module allows Meshtastic nodes to: > ⚠️ **Warning:** The old **Meshtastic ATAK Plugin** is no longer part of this path and cannot > work. It bridged through the cross-process AIDL API, which was removed in app 2.8.0; the mesh > service is now in-process only. Do not install it. Interop today runs over the app's own local -> TAK server plus the Mesh to CoT Converter, both described below, with stock ATAK/iTAK/WinTAK. +> TAK server plus the Mesh to CoT Converter, both described in the following sections, with stock ATAK/iTAK/WinTAK. ### Konfiguration diff --git a/docs/sv-rSE/user/translate.md b/docs/sv-rSE/user/translate.md index 753bccbced..c97b681b63 100644 --- a/docs/sv-rSE/user/translate.md +++ b/docs/sv-rSE/user/translate.md @@ -2,7 +2,7 @@ title: Translate the App parent: User Guide nav_order: 17 -last_updated: 2026-08-30 +last_updated: 2026-09-11 description: How the app and its documentation are translated via Crowdin, and guidelines for contributing translations. aliases: - translate @@ -45,7 +45,7 @@ If your language is not yet listed on Crowdin: The Android app uses **Compose Multiplatform resources** for all user-visible strings: -``` +```text core/resources/src/commonMain/composeResources/ ├── values/ ← English (default) │ └── strings.xml @@ -58,7 +58,7 @@ core/resources/src/commonMain/composeResources/ In-app documentation follows a similar pattern under `docs/`: -``` +```text docs/ ├── en/user/ ← English source (default) │ ├── onboarding.md diff --git a/docs/tr-rTR/user/desktop.md b/docs/tr-rTR/user/desktop.md index 6d200ac304..b1e640369b 100644 --- a/docs/tr-rTR/user/desktop.md +++ b/docs/tr-rTR/user/desktop.md @@ -2,7 +2,7 @@ title: Desktop App parent: User Guide nav_order: 14 -last_updated: 2026-08-30 +last_updated: 2026-09-11 description: Install and use the Meshtastic Desktop app on Linux, macOS, and Windows — connections, feature parity, and keyboard shortcuts. aliases: - desktop @@ -58,20 +58,20 @@ Bluetooth Low Energy is supported on desktop via the [Kable](https://github.com/ ## Feature Parity -| Feature | Android | Desktop | Notes | -| ----------------------------------------------------- | ------- | ------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Messaging | ✓ | ✓ | Full parity | -| Node List | ✓ | ✓ | Full parity | -| Harita | ✓ | ✓ | Interactive MapLibre map, with base map and overlay pickers and custom tile sources. No offline downloads or local `.mbtiles` archives | -| Map layers (`.kml`/`.kmz`/GeoJSON) | ✓ | ✓ | Same layer store and sheet as Android; imported files draw on the desktop map | -| Site Planner | ✓ | ✓\* | \*Opens in your browser on desktop; the estimate is not drawn on the desktop map | -| Ayarlar | ✓ | ✓ | Full parity | -| Bluetooth (BLE) | ✓ | ✓ | Via Kable on desktop | -| Firmware Update | ✓ | ✓ | In-app USB, BLE, and Wi-Fi (ESP32) update work the same as Android. The USB maintenance flow — nRF52/RP2040 factory erase and bootloader upgrade — is Android-only | -| Notifications | ✓ | ✓ | Native OS notifications | -| Widgets | ✓ | ✗ | Android-only | -| AI Assistant (Chirpy) | ✓\* | ✗ | Google flavor Android only | -| App Functions (system AI) | ✓† | ✗ | Google flavor Android only | +| Feature | Android | Desktop | Notes | +| ----------------------------------------------------- | ------- | ------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Messaging | ✓ | ✓ | Full parity | +| Node List | ✓ | ✓ | Full parity | +| Harita | ✓ | ✓ | Interactive MapLibre map, with base map and overlay pickers and custom tile sources. Offline terrain (hillshade and contours) can be downloaded; offline base-map packs and local `.mbtiles` archives cannot | +| Map layers (`.kml`/`.kmz`/GeoJSON) | ✓ | ✓ | Same layer store and sheet as Android; imported files draw on the desktop map | +| Site Planner | ✓ | ✓\* | \*Opens in your browser on desktop; the estimate is not drawn on the desktop map | +| Ayarlar | ✓ | ✓ | Full parity | +| Bluetooth (BLE) | ✓ | ✓ | Via Kable on desktop | +| Firmware Update | ✓ | ✓ | In-app USB, BLE, and Wi-Fi (ESP32) update work the same as Android. The USB maintenance flow — nRF52/RP2040 factory erase and bootloader upgrade — is Android-only | +| Notifications | ✓ | ✓ | Native OS notifications | +| Widgets | ✓ | ✗ | Android-only | +| AI Assistant (Chirpy) | ✓\* | ✗ | Google flavor Android only | +| App Functions (system AI) | ✓† | ✗ | Google flavor Android only | \*Chirpy AI requires Android 14+ on Google flavor builds with supported hardware. @@ -122,7 +122,7 @@ Individual doc pages render with full formatting: ## Building from Source -```bash +```shell git clone https://github.com/meshtastic/Meshtastic-Android.git cd Meshtastic-Android ./gradlew :desktopApp:run @@ -135,7 +135,7 @@ Requirements: ## Known Limitations -- Offline tile downloads and local `.mbtiles` archives are not available on desktop. +- Offline base-map downloads and local `.mbtiles` archives are not available on desktop. Offline terrain is — see [Map & Waypoints](map-and-waypoints). - `.kml`/`.kmz`/GeoJSON layer import works — see [Map & Waypoints](map-and-waypoints#map-layers). Site Planner opens in your browser rather than in the app; to bring its coverage estimate onto the map, click the transmitter pin diff --git a/docs/tr-rTR/user/discovery.md b/docs/tr-rTR/user/discovery.md index faac9f83bd..f57056f030 100644 --- a/docs/tr-rTR/user/discovery.md +++ b/docs/tr-rTR/user/discovery.md @@ -2,7 +2,7 @@ title: Local Mesh Discovery parent: User Guide nav_order: 12 -last_updated: 2026-08-30 +last_updated: 2026-09-11 description: Explore your mesh network — the Local Mesh Discovery scanner, traceroute paths, neighbor maps, and node discovery tools. aliases: - discovery @@ -97,7 +97,7 @@ Mesh Beacon lets nodes invite others to join their mesh. A beaconing node period Configure it under **Settings → Module configuration → Mesh Beacon**. The entry appears only on radios running firmware 2.8.0 or newer. A read-only **Region** row at the top of the screen shows the region the beacon advertises: that region, and the preset, are always the ones the radio itself uses, so a beacon cannot invite anyone onto settings your radio is not running. - **Listen for beacons** — receive invitations broadcast by other nodes. -- **Broadcast a beacon** — periodically advertise this mesh to nearby nodes, with an optional **Beacon message** of up to 100 bytes, a **Broadcast interval** picked from fixed intervals between 1 hour and 72 hours, and an **Offered channel** chosen from your radio's own channels. The offered channel is required, and defaults to your primary channel. +- **Broadcast a beacon** — periodically advertise this mesh to nearby nodes, with an optional **Beacon message** of up to 100 bytes, a **Broadcast interval** picked from fixed intervals between 1 hour and 72 hours, and an **Offered channel** chosen from your radio's own channels. The offered channel is required, and defaults to your primary channel. Over remote admin the picker offers the primary channel only. - **Broadcast targets** — optional extra destinations beyond the offered channel. **Add target** appends a row; each row picks a **Channel** and a **Transmit preset**, and **Remove target** deletes it. With no targets, the beacon goes out on the offered channel alone. Two conditions block beacon setup: diff --git a/docs/tr-rTR/user/help-and-docs.md b/docs/tr-rTR/user/help-and-docs.md index f306cde7f7..c83898cea3 100644 --- a/docs/tr-rTR/user/help-and-docs.md +++ b/docs/tr-rTR/user/help-and-docs.md @@ -2,7 +2,7 @@ title: Help & In-App Docs parent: User Guide nav_order: 21 -last_updated: 2026-08-30 +last_updated: 2026-09-11 description: Browse this documentation inside the app, search it, and ask Chirpy — the on-device AI assistant — questions about Meshtastic. aliases: - help @@ -23,7 +23,7 @@ The docs browser lists every user-guide page. Tap a page to read it; images and ### Search -The search box sits at the top of the docs browser. Type in it to filter pages by title and keywords — results update as you type, and the **Clear search** button (✕) empties the box. +Tap the search field at the top of the docs browser to open a full-screen search view. Type to filter pages by title and keywords — results update as you type, under the same field. The back arrow, or the keyboard's search action, closes the view. The field hides while you scroll down the page list and comes back when you scroll up. ![Searching the in-app documentation](../../assets/screenshots/docs-browser_search.png) diff --git a/docs/tr-rTR/user/map-and-waypoints.md b/docs/tr-rTR/user/map-and-waypoints.md index 0485a416ab..5897d56932 100644 --- a/docs/tr-rTR/user/map-and-waypoints.md +++ b/docs/tr-rTR/user/map-and-waypoints.md @@ -2,7 +2,7 @@ title: Map & Waypoints parent: User Guide nav_order: 6 -last_updated: 2026-09-01 +last_updated: 2026-09-11 description: View node positions on the map, create and share waypoints, manage map layers and Site Planner, and control position sharing and privacy. aliases: - map @@ -44,7 +44,7 @@ The floating toolbar provides quick access to the compass, the map type and laye ### Filtering the Map -Tap the filter button in the floating toolbar to open **Filter map**. **Display** controls what is drawn: **Only Favorites**, **Show Waypoints**, **Show Precision Circles**, and a slider that hides nodes not heard from recently. **Node roles** is a chip per device role, plus **All** to show every role; a selected chip means that role is shown. **Nodes** narrows the set further with **Hide offline nodes**, **Only show direct nodes**, **Exclude MQTT**, **Show ignored nodes**, and **Include unknown**. +Tap the filter button in the floating toolbar to open **Filter map**. **Display** controls what is drawn: **Only Favorites**, **Show Waypoints**, **Show Precision Circles**, and a slider that hides nodes not heard from recently. **Node roles** is a chip per device role, plus **All** to show every role; a selected chip means that role is shown. **Nodes** narrows the set further with **Hide offline nodes**, **Only show direct nodes**, **Signed only**, **Encrypted only**, **Exclude MQTT**, **Show ignored nodes**, and **Include unknown** — the same rules as the node list's [filter toggles](nodes). A dot on the filter button means at least one filter is hiding something — check it before concluding the mesh is quiet. Turning **Show Waypoints** off hides every waypoint, including your own. **Show ignored nodes** adds them to the map rather than showing only them — unlike the node list's **Only show ignored Nodes**. @@ -147,8 +147,9 @@ picker, all three offer the same raster base maps: Overlays can be toggled on top of any base map, from the layers sheet: - **Weather radar** — NOAA NEXRAD reflectivity (US coverage) -- **Hillshade** — terrain relief, on **F-Droid** and **Desktop** only. Useful for understanding why a - link fails, since LoRa range is limited by terrain +- **Hillshade** — terrain relief from an online tile source, on **F-Droid** and **Desktop** only; Google + Play draws hillshade from downloaded offline terrain instead. Useful for understanding why a link + fails, since LoRa range is limited by terrain ### Adding your own tile source @@ -156,7 +157,7 @@ Any XYZ tile endpoint can be added as a base map, on every flavor and on desktop Tile Sources** at the foot of the base map picker and paste a URL template using `{z}`, `{x}` and `{y}` — plus `{s}` if the provider uses rotating subdomains. A national mapping service, for example: -``` +```text https://wmts.geo.admin.ch/1.0.0/ch.swisstopo.pixelkarte-farbe/default/current/3857/{z}/{x}/{y}.jpeg ``` @@ -173,7 +174,18 @@ On **Google Play**, the layers sheet's **Offline Manager** section downloads its — water, roads and administrative boundaries extracted from the public Protomaps basemap dataset, drawn directly as map shapes rather than raster tiles. Frame the area you want, tap **Start Download**, and once it finishes flip **Show on map** to draw it (it's off by default so it never surprises you by covering up the -live map). You can still import a pre-made `.mbtiles` archive here too, as before. **Desktop** has neither. +live map). You can still import a pre-made `.mbtiles` archive here too, as before. **Desktop** has neither, though +the offline terrain described next does work there. + +### Offline terrain + +Hillshade and elevation contours can be downloaded for offline use as well, so a downloaded area still +shows why a link fails when there is no network. The terrain data comes from Mapterhorn and is credited on +the map. On **Google Play**, each downloaded region in the **Offline Manager** list has a **Download Terrain** +button and **Hillshade** and **Contours** switches; terrain attaches to that region and shares its storage limit, and an area +too large for terrain asks you to zoom in or pick a smaller region. On **F-Droid** and **Desktop**, the +layers sheet has an **Offline Terrain** section of its own: frame the area, tap **Start Download**, and the +hillshade and contours draw from the downloaded tiles from then on. ### Going offline @@ -181,8 +193,8 @@ An **Offline** pill appears over the map whenever the device has no network conn builds, if you've imported a local `.mbtiles` archive, the map switches to it automatically the moment the network drops — no toggle to remember — and switches back once you're reconnected, as long as you haven't picked a different base map yourself in between. On **F-Droid**, a downloaded offline area keeps rendering on -its own; the pill is purely informational there. **Desktop** has no offline downloads at all yet, so the pill -is informational there too. +its own; the pill is purely informational there. **Desktop** downloads offline terrain but no base-map +packs, so the pill is informational there too. ## Related Topics diff --git a/docs/tr-rTR/user/messages-and-channels.md b/docs/tr-rTR/user/messages-and-channels.md index d6c9a4544b..cfb52a7b56 100644 --- a/docs/tr-rTR/user/messages-and-channels.md +++ b/docs/tr-rTR/user/messages-and-channels.md @@ -2,7 +2,7 @@ title: Messages & Channels parent: User Guide nav_order: 3 -last_updated: 2026-08-30 +last_updated: 2026-09-14 description: Send and receive messages, manage channels, configure encryption, search conversations, and use quick chat, reactions, and message actions. aliases: - channels @@ -50,6 +50,21 @@ Key length alone does not change the icon: a 128-bit key and a 256-bit key both Tapping an existing channel opens the same editor, where you can change the name, the PSK, MQTT uplink and downlink, and position precision. Every edit on this screen — adding, editing, deleting, or dragging a channel into a new order — waits on **Send** the same way. +### Archived Channels + +A conversation belongs to the channel it was held on, not to the slot that channel occupied. When a channel leaves your radio — you scan a different channel QR, replace a channel's key, or switch the modem preset — its conversation is **archived** rather than deleted or merged into whatever takes the slot. + +An archived conversation: + +- stays in the **Channels** list under the name the channel had, below the channels currently on your radio, marked with a history icon; +- keeps its full message history, searchable as usual; +- is read-only — you cannot send, reply, or react, because the channel is no longer on the radio to send on, and it is not offered as a share target; +- comes back automatically if you re-add that exact channel, taking its messages back to the live slot. + +Changing the modem preset archives the channel too: the preset is part of a default channel's identity, so **LongFast** and **MediumFast** are different channels even though neither the name nor the key changed. Changing only the region does not archive anything. + +To remove an archived conversation for good, long-press it in the conversation list and delete it. + ## Direct Messages Direct messages (DMs) go to one specific node. When both radios hold each other's public keys, your radio encrypts the message to that node's public key, so no one else on the mesh can read it — not even nodes that share your channel. diff --git a/docs/tr-rTR/user/mqtt.md b/docs/tr-rTR/user/mqtt.md index bb7e568755..eb3ec5e3fb 100644 --- a/docs/tr-rTR/user/mqtt.md +++ b/docs/tr-rTR/user/mqtt.md @@ -2,7 +2,7 @@ title: MQTT parent: User Guide nav_order: 11 -last_updated: 2026-08-30 +last_updated: 2026-09-11 description: Bridge your mesh to the internet — MQTT broker setup, encryption layers, and map reporting. aliases: - mqtt @@ -25,7 +25,7 @@ The MQTT module connects your node to an MQTT broker, allowing: ## How It Works -``` +```text [Your Node] → Radio → [Gateway Node with Wi-Fi] → MQTT Broker → [Remote Gateway] → Radio → [Remote Node] ``` diff --git a/docs/tr-rTR/user/node-metrics.md b/docs/tr-rTR/user/node-metrics.md index 54b9909fcc..59a3b4e802 100644 --- a/docs/tr-rTR/user/node-metrics.md +++ b/docs/tr-rTR/user/node-metrics.md @@ -2,7 +2,7 @@ title: Node Metrics parent: User Guide nav_order: 5 -last_updated: 2026-08-30 +last_updated: 2026-09-09 description: Telemetry dashboards for each mesh node — device health, environment sensors, air quality, signal quality, power, traceroute, and position history. aliases: - metrics @@ -114,7 +114,7 @@ Radio signal quality information: ### Signal Quality Reference -Signal quality is rated from **SNR relative to the active LoRa modem preset's demodulation floor**, not from fixed thresholds — a given SNR means different things on different presets (e.g. −15 dB is fine on LongSlow but unusable on ShortFast). RSSI is shown but is not part of the rating. In the table, _limit_ is the preset's SNR limit. +Signal quality is rated from **SNR relative to the active LoRa modem preset's demodulation floor**, not from fixed thresholds — a given SNR means different things on different presets (e.g. −15 dB is fine on LongSlow but unusable on ShortFast). When RSSI and a noise-floor reading are both available, the app also rates their difference against the preset limit and uses the worse rating. Otherwise, RSSI is display-only. In the table, _limit_ is the preset's SNR limit. | Quality | Criteria | | ---------- | ---------------------------------------------------------------- | diff --git a/docs/tr-rTR/user/nodes.md b/docs/tr-rTR/user/nodes.md index a548393d97..e04a38dc18 100644 --- a/docs/tr-rTR/user/nodes.md +++ b/docs/tr-rTR/user/nodes.md @@ -2,7 +2,7 @@ title: Nodelar parent: User Guide nav_order: 4 -last_updated: 2026-09-04 +last_updated: 2026-09-11 description: Browse, filter, and sort mesh nodes — view details, signal quality, roles, and quick actions. aliases: - node-list @@ -74,19 +74,23 @@ Most users should keep the default **Client** role. Consider a different role wh > 💡 **Tip:** The mesh works best when most nodes are **Client** or **Router**. Too many Client Mute nodes reduce mesh resilience; too many Routers in a dense area can cause congestion. A good rule of thumb: one Router per 5–10 Clients in your area. -### Encryption Indicators +### Security indicators -Nodes display encryption status icons next to their name: +Each node carries one security icon beside its name in the node list. Tap it to read what it means, and choose **Show All Meanings** in that dialog for the full legend. The detail screen shows the same state in words, as a **Security** row that opens the same dialog. -| Icon | Meaning | -| ----------- | ------------------------------------------------------------------------------------------------------------------------------------------------- | -| 🔒 Locked | Communication uses PKI (public key infrastructure) — end-to-end encrypted with verified identity | -| 🔓 Unlocked | No public key has been received for this node, so it cannot be direct messaged — use **Request User Info** on the node detail page to ask for one | -| ⚠️ Mismatch | Public key mismatch — the node's key has changed since last seen (investigate before trusting) | +| Icon | Meaning | Shown for | +| --------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------- | +| Person with a shield check (green) | **Verified contact** — you verified this node's key in person by exchanging contact QR codes, so its identity is confirmed. The strongest trust the list shows. Your own connected node carries it too | Any firmware version | +| Nodes icon with a shield check (green) | **Signed node** — this node signs its broadcasts with its identity key, so its identity is consistent over time, but you have not verified it in person. On firmware 2.8 the icon appears from the version alone, before any signed broadcast has been heard | Firmware 2.8 or newer, and any node whose signed broadcast your node has verified | +| 🔒 Closed lock | A public key is on file and matches, so direct messages to this node are encrypted | Firmware before 2.8, or no reported version | +| 🔓 Open lock | No public key has been received for this node, so it cannot be direct messaged — use **Request User Info** on the node detail page to ask for one | Firmware before 2.8, or no reported version | +| ⚠️ Mismatch | **Public key mismatch** — a different key arrived for this node after one was stored. Investigate before trusting | Any firmware version | -> 💡 **Tip:** Direct messages always use PKI, so the radio needs the other node's public key before it can send one. It refuses the send rather than falling back to channel encryption. Keys arrive inside node info, which is why an open lock usually clears itself once that node is heard from properly. If you see a key mismatch warning, the node may have been reset or compromised. +Every node on firmware 2.8 or newer signs its broadcasts, so on that firmware the signed state is the baseline and the locks are not shown. -To clear a mismatch, first confirm through another trusted channel that the key change was intentional — a factory reset causes one. Then touch & hold the node, choose **Remove**, and let the two radios exchange keys again the next time yours hears it. +Direct messages always use public-key encryption, so your node needs the other node's public key before it can send one. It refuses the send rather than falling back to channel encryption. Keys arrive inside node info, which is why an open lock usually clears itself once that node is heard from properly. + +A mismatch never replaces the key you already hold. The app keeps the first key it recorded and refuses the new one, as the firmware does, so a stray or hostile node info cannot silently break encrypted messaging to a contact. To clear a mismatch, first confirm through another trusted channel that the key change was intentional — a factory reset causes one. Then touch & hold the node, choose **Remove**, and let the two nodes exchange keys again the next time yours hears it. ## Quick Actions @@ -108,7 +112,9 @@ radio is connected and running firmware 2.8 or newer — see ## Sharing a Contact -On a node's detail screen, tap **Share Contact** to produce a link and a QR code for that node. From the same dialog, **Write to NFC tag** saves the link to a writable NFC tag that anyone can tap to open. +On a node's detail screen, tap **Share Contact** to produce a link and a QR code for that node. From the same dialog, **Share link** opens the Android share sheet (on desktop it copies the link instead), **Write to NFC tag** saves it to a writable NFC tag, and **Copy** puts it on the clipboard. While that dialog is open and in front of you, the phone also offers the same link to any NFC reader, so someone can take the contact by tapping their phone against yours with no tag involved. + +Sharing your own contact this way marks it as verified in person, so whoever imports it sees the **Verified contact** icon rather than the signed one. Relaying someone else's contact passes on only what your app had already recorded about them. To add someone else's contact, use the import button on the node list and choose **Scan Shared Contact QR Code**, **Scan Shared Contact NFC**, or **Input Shared Contact URL**. The app asks you to confirm with **Import Shared Contact?**, and warns you when the contact is one you already have. @@ -120,14 +126,16 @@ Type in the search field to filter nodes by name or short name. The filter updat ### Filter Toggles -| Filtre | Açıklaması | -| --------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| **Hide offline nodes** | Show only nodes heard within the last 2 hours | -| **Only show direct nodes** | Show only nodes your radio heard directly, with no relay in between | -| **Include unknown** | Show nodes that haven't sent user info yet. **On by default**, so a node heard before its info arrives stays visible; these carry a badge marking them incomplete, and cannot be direct messaged until their user info brings a public key | -| **Exclude infrastructure** | Hide infrastructure-role nodes (Router, Router Late, Client Base, and legacy Repeater nodes) and any node that cannot be messaged, whatever its role | -| **Exclude MQTT** | Hide nodes heard only via MQTT internet bridge | -| **Only show ignored Nodes** | Replace the list with the nodes you have ignored. Every other node is hidden while this is on, and a banner appears at the top of the list to take you back | +| Filtre | Açıklaması | +| --------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **Hide offline nodes** | Show only nodes heard within the last 2 hours | +| **Only show direct nodes** | Show only nodes your radio heard directly, with no relay in between | +| **Include unknown** | Show nodes that haven't sent user info yet. **On by default**, so a node heard before its info arrives stays visible; these carry a badge marking them incomplete, and cannot be direct messaged until their user info brings a public key | +| **Exclude infrastructure** | Hide infrastructure-role nodes (Router, Router Late, Client Base, and legacy Repeater nodes) and any node that cannot be messaged, whatever its role | +| **Exclude MQTT** | Hide nodes heard only via MQTT internet bridge | +| **Signed only** | Show only nodes whose signed broadcasts your node has actually heard and verified. Stricter than the icon: a node on 2.8 shows as signed by its firmware version before any signed broadcast arrives, and a contact verified in person on older firmware is not signed at all | +| **Encrypted only** | Show nodes with a matching public key on file, the key an encrypted direct message needs. A node with a key mismatch is excluded | +| **Only show ignored Nodes** | Replace the list with the nodes you have ignored. Every other node is hidden while this is on, and a banner appears at the top of the list to take you back | ### Sort Options @@ -149,6 +157,8 @@ Tap the hop-histogram icon in the node list's app bar to open a bar chart of how Tapping a node opens the detail view with comprehensive information. See [Node Metrics](node-metrics) for full details on metrics and telemetry. +Signal quality is rated against your modem preset. The same SNR can be good on a long-range preset and poor on a faster one. Traceroute and neighbor-info SNR colors use that preset too. RSSI text has its own strength colors; it affects the quality rating only when a noise-floor reading is also available. + The Details card carries the node's short name, role, IDs, last heard time, hops away, uptime, and its SNR and RSSI: ![Node detail section](../../assets/screenshots/nodes_detail_section.png) diff --git a/docs/tr-rTR/user/settings-module-admin.md b/docs/tr-rTR/user/settings-module-admin.md index ace666824e..0039b16442 100644 --- a/docs/tr-rTR/user/settings-module-admin.md +++ b/docs/tr-rTR/user/settings-module-admin.md @@ -2,7 +2,7 @@ title: Settings — Modules & Admin parent: User Guide nav_order: 8 -last_updated: 2026-08-30 +last_updated: 2026-09-16 description: Configure optional feature modules (MQTT, telemetry, canned messages, TAK, and more) and perform device administration. aliases: - modules @@ -333,7 +333,7 @@ appear only when your own node is selected: ### Hakkında -**Settings → About** carries the app's own identity rather than the radio's: +**Settings → About** carries the app's own identity rather than the node's: Three sections: @@ -341,7 +341,11 @@ Three sections: - **Apps** — opens with **Need Hardware?**, a rotating carousel of popular devices that links out to where to buy one. It also lists the GitHub repository, the running app version, and **Acknowledgements** (see the next section). -- **Project information** — links to the website and to this documentation. +- **Project information** — links to the website, to this documentation, and to the GNU GPL v3 + license the app is released under. + +The footer states the copyright and that the app is free software with no warranty, which you +may redistribute under the same license. ### Acknowledgements diff --git a/docs/tr-rTR/user/settings-radio-user.md b/docs/tr-rTR/user/settings-radio-user.md index a75bf3a736..6a9edee59f 100644 --- a/docs/tr-rTR/user/settings-radio-user.md +++ b/docs/tr-rTR/user/settings-radio-user.md @@ -2,7 +2,7 @@ title: Settings — Radio & User parent: User Guide nav_order: 7 -last_updated: 2026-09-04 +last_updated: 2026-09-11 description: Configure your radio hardware, LoRa presets, user profile, position sharing, power management, and security. aliases: - ayarlar @@ -51,7 +51,7 @@ The footer appears as soon as you change something. **Discard** throws the chang The status message is saved with the same **Save**, but it never reboots the node — and, like the rest of this screen, it can be edited on a remote node you administer. For your own radio there is a shortcut while it is connected: touch & hold your node in the [node list](nodes.md) and choose -**Update status**. Older firmware and a disconnected radio have no shortcut — the field above is +**Update status**. Older firmware and a disconnected node have no shortcut — the field itself is still the way in. ## Yapılandırma @@ -77,13 +77,13 @@ On **Settings → LoRa**. | Setting | Açıklaması | Varsayılan | | ---------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------- | -| Bölge | Regulatory region for frequency bands. You must set this before transmitting | Unset (must configure) | +| Bölge | Regulatory region for frequency bands. You must set this before transmitting. On firmware 2.8 or newer, the first region set also creates the node's identity key and gives it a new node number | Unset (must configure) | | Presets | Speed/range tradeoff | LongFast | | Number of Hops | Maximum retransmit hops | 3 | | Transmit Power | Transmission power (dBm); 0 = max allowed for region | 0 (region max) | | Frequency Override | Overrides the computed operating frequency outright (MHz). It does not offset the calculated value — leave at 0 unless you know you need a specific frequency | 0 (use calculated) | | Use Preset | On by default. Turn it off to set Spread Factor, Coding Rate and Bandwidth by hand instead of taking them from the modem preset | On | -| Spread Factor | Manual mode only: 7–12. Higher spreads further but slower | From preset | +| Spread Factor | Manual mode only: 5–12. Higher spreads further but slower. On SX127x (RF95) radios the firmware does not accept 5 or 6 and uses 11 instead | From preset | | Coding Rate | Manual mode only: 5–8. More redundancy costs airtime | From preset | | Bant genişliği | Manual mode only: the channel bandwidth in kHz, typed in directly. On the 2.4 GHz region the app offers a list of the bandwidths your radio supports instead, and a stored value that is not on that list shows as _Unsupported_ and blocks saving until you pick a supported one | From preset | | Frequency Slot | Which slot within the region's band to use. 0 derives it from the primary channel name | 0 (automatic) | @@ -111,13 +111,13 @@ The Lite, Narrow, Medium Turbo, and Tiny presets need firmware 2.8 or newer — | Short Slow | ~5 km | 6.25 kbps | −10 dB | Suburban short-range; moderate building density | | Medium Fast | ~5 km | 3.52 kbps | −12.5 dB | Suburban areas; moderate building density | | Medium Slow | ~8 km | 1.95 kbps | −15 dB | Suburban/rural; moderate range with slower speed | -| Long Turbo | ~10 km | 1.34 kbps | −12.5 dB | Similar range to Long Fast but with 500 kHz bandwidth; faster throughput | +| Long Turbo | ~10 km | 1.34 kbps | −17.5 dB | Similar range to Long Fast but with 500 kHz bandwidth; faster throughput | | Long Fast | ~10 km | 1.1 kbps | −17.5 dB | **General use (default)** — balanced range and speed | | Long Moderate | ~20 km | 0.34 kbps | −17.5 dB | Rural with some terrain; occasional use | | Lite Fast | ~5 km | 1.76 kbps | −12.5 dB | EU 866 MHz SRD band (125 kHz BW); comparable to Medium Fast | | Lite Slow | ~10 km | 0.98 kbps | −15 dB | EU 866 MHz SRD band (125 kHz BW); comparable to Long Fast | -| Narrow Fast | ~5 km | 2.28 kbps | −10 dB | EU 868 MHz band (62.5 kHz BW); avoids interference with other devices | -| Narrow Slow | ~10 km | 1.30 kbps | −12.5 dB | EU 868 MHz band (62.5 kHz BW); comparable to Long Fast | +| Narrow Fast | ~5 km | 2.28 kbps | −7.5 dB | EU 868 MHz band (62.5 kHz BW); avoids interference with other devices | +| Narrow Slow | ~10 km | 1.30 kbps | −10 dB | EU 868 MHz band (62.5 kHz BW); comparable to Long Fast | | Medium Turbo | ~5 km | 7.0 kbps | −12.5 dB | Like Medium Fast but with 500 kHz bandwidth; not legal in every region. Needs firmware 2.8 or newer | | Tiny Fast | ~10 km | 0.68 kbps | −7.5 dB | Amateur bands that cap occupied bandwidth; these presets use 15.6 kHz. Needs firmware 2.8 or newer, an SX126x or SX127x radio, and a TCXO of ±5 ppm or better | | Tiny Slow | ~20 km | 0.33 kbps | −10 dB | Same band restrictions as Tiny Fast, longer range. Same firmware, radio, and TCXO requirements | diff --git a/docs/tr-rTR/user/signal-meter.md b/docs/tr-rTR/user/signal-meter.md index db2e348deb..4e996f3620 100644 --- a/docs/tr-rTR/user/signal-meter.md +++ b/docs/tr-rTR/user/signal-meter.md @@ -2,7 +2,7 @@ title: How the Meshtastic Signal Meter Works parent: User Guide nav_order: 15 -last_updated: 2026-08-30 +last_updated: 2026-09-09 description: How the signal meter rates quality from SNR relative to the LoRa modem preset — spread spectrum, presets, and what the bars really mean. aliases: - signal @@ -32,7 +32,7 @@ Each modem preset has an SNR limit: the lowest SNR at which that preset can stil ## Rating Signal Quality -The app rates signal quality (None, Bad, Fair, or Good) from SNR alone, measured against the active preset's SNR limit. It does not factor in RSSI: without knowing the local noise floor, RSSI alone cannot say whether a signal is decodable. RSSI is still available — on the node detail screen and in the metrics charts. +The app rates signal quality (None, Bad, Fair, or Good) from SNR relative to the active preset's SNR limit. When RSSI and a noise-floor reading are both available, it also rates the difference between them against that limit and uses the worse rating. Otherwise, it uses SNR alone. Because the rating is relative to the preset, the same SNR rates differently on different presets. An SNR of −16 dB rates Good on Long Fast (SNR limit −17.5 dB) but None on Short Fast (SNR limit −7.5 dB). Let `limit` be the active preset's SNR limit: @@ -45,7 +45,7 @@ Because the rating is relative to the preset, the same SNR rates differently on The icon never goes blank, so count the bars carefully: a single bar means None, not a weak but usable link, and Good is a solid wedge rather than a set of bars. A gray three-bar icon labeled Unknown is a different state again — the packet carried no SNR measurement at all, which is not the same as measuring one and finding it too weak. -> ℹ️ **Note:** Traceroute hop colors use fixed thresholds (−7 dB / −15 dB); the per-node signal meter uses the preset-relative rating instead. +> ℹ️ **Note:** Traceroute and neighbor-info SNR colors use the active preset's limits too. RSSI text has separate strength colors: green above −115 dBm, yellow above −120 dBm, orange above −126 dBm, and red at or below −126 dBm. ## Diagnosing Local Interference diff --git a/docs/tr-rTR/user/tak.md b/docs/tr-rTR/user/tak.md index cac58abf14..3cf33e7e23 100644 --- a/docs/tr-rTR/user/tak.md +++ b/docs/tr-rTR/user/tak.md @@ -2,7 +2,7 @@ title: TAK Integration parent: User Guide nav_order: 10 -last_updated: 2026-08-30 +last_updated: 2026-09-11 description: Interoperate with ATAK and WinTAK — CoT position sharing, TAK roles, and plugin setup. aliases: - tak @@ -36,7 +36,7 @@ The TAK module allows Meshtastic nodes to: > ⚠️ **Warning:** The old **Meshtastic ATAK Plugin** is no longer part of this path and cannot > work. It bridged through the cross-process AIDL API, which was removed in app 2.8.0; the mesh > service is now in-process only. Do not install it. Interop today runs over the app's own local -> TAK server plus the Mesh to CoT Converter, both described below, with stock ATAK/iTAK/WinTAK. +> TAK server plus the Mesh to CoT Converter, both described in the following sections, with stock ATAK/iTAK/WinTAK. ### Yapılandırma diff --git a/docs/tr-rTR/user/translate.md b/docs/tr-rTR/user/translate.md index 3d6eb94657..7ec1c138d2 100644 --- a/docs/tr-rTR/user/translate.md +++ b/docs/tr-rTR/user/translate.md @@ -2,7 +2,7 @@ title: Translate the App parent: User Guide nav_order: 17 -last_updated: 2026-08-30 +last_updated: 2026-09-11 description: How the app and its documentation are translated via Crowdin, and guidelines for contributing translations. aliases: - translate @@ -45,7 +45,7 @@ If your language is not yet listed on Crowdin: The Android app uses **Compose Multiplatform resources** for all user-visible strings: -``` +```text core/resources/src/commonMain/composeResources/ ├── values/ ← English (default) │ └── strings.xml @@ -58,7 +58,7 @@ core/resources/src/commonMain/composeResources/ In-app documentation follows a similar pattern under `docs/`: -``` +```text docs/ ├── en/user/ ← English source (default) │ ├── onboarding.md diff --git a/docs/uk-rUA/user/desktop.md b/docs/uk-rUA/user/desktop.md index 2cf7b4cef2..22356fb409 100644 --- a/docs/uk-rUA/user/desktop.md +++ b/docs/uk-rUA/user/desktop.md @@ -2,7 +2,7 @@ title: Desktop App parent: User Guide nav_order: 14 -last_updated: 2026-08-30 +last_updated: 2026-09-11 description: Install and use the Meshtastic Desktop app on Linux, macOS, and Windows — connections, feature parity, and keyboard shortcuts. aliases: - desktop @@ -58,20 +58,20 @@ Bluetooth Low Energy is supported on desktop via the [Kable](https://github.com/ ## Feature Parity -| Feature | Android | Desktop | Нотатки | -| ----------------------------------------------------- | ------- | ------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Messaging | ✓ | ✓ | Full parity | -| Node List | ✓ | ✓ | Full parity | -| Мапа | ✓ | ✓ | Interactive MapLibre map, with base map and overlay pickers and custom tile sources. No offline downloads or local `.mbtiles` archives | -| Map layers (`.kml`/`.kmz`/GeoJSON) | ✓ | ✓ | Same layer store and sheet as Android; imported files draw on the desktop map | -| Site Planner | ✓ | ✓\* | \*Opens in your browser on desktop; the estimate is not drawn on the desktop map | -| Налаштування | ✓ | ✓ | Full parity | -| Bluetooth (BLE) | ✓ | ✓ | Via Kable on desktop | -| Оновити прошивку | ✓ | ✓ | In-app USB, BLE, and Wi-Fi (ESP32) update work the same as Android. The USB maintenance flow — nRF52/RP2040 factory erase and bootloader upgrade — is Android-only | -| Notifications | ✓ | ✓ | Native OS notifications | -| Widgets | ✓ | ✗ | Android-only | -| AI Assistant (Chirpy) | ✓\* | ✗ | Google flavor Android only | -| App Functions (system AI) | ✓† | ✗ | Google flavor Android only | +| Feature | Android | Desktop | Нотатки | +| ----------------------------------------------------- | ------- | ------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Messaging | ✓ | ✓ | Full parity | +| Node List | ✓ | ✓ | Full parity | +| Мапа | ✓ | ✓ | Interactive MapLibre map, with base map and overlay pickers and custom tile sources. Offline terrain (hillshade and contours) can be downloaded; offline base-map packs and local `.mbtiles` archives cannot | +| Map layers (`.kml`/`.kmz`/GeoJSON) | ✓ | ✓ | Same layer store and sheet as Android; imported files draw on the desktop map | +| Site Planner | ✓ | ✓\* | \*Opens in your browser on desktop; the estimate is not drawn on the desktop map | +| Налаштування | ✓ | ✓ | Full parity | +| Bluetooth (BLE) | ✓ | ✓ | Via Kable on desktop | +| Оновити прошивку | ✓ | ✓ | In-app USB, BLE, and Wi-Fi (ESP32) update work the same as Android. The USB maintenance flow — nRF52/RP2040 factory erase and bootloader upgrade — is Android-only | +| Notifications | ✓ | ✓ | Native OS notifications | +| Widgets | ✓ | ✗ | Android-only | +| AI Assistant (Chirpy) | ✓\* | ✗ | Google flavor Android only | +| App Functions (system AI) | ✓† | ✗ | Google flavor Android only | \*Chirpy AI requires Android 14+ on Google flavor builds with supported hardware. @@ -122,7 +122,7 @@ Individual doc pages render with full formatting: ## Building from Source -```bash +```shell git clone https://github.com/meshtastic/Meshtastic-Android.git cd Meshtastic-Android ./gradlew :desktopApp:run @@ -135,7 +135,7 @@ Requirements: ## Known Limitations -- Offline tile downloads and local `.mbtiles` archives are not available on desktop. +- Offline base-map downloads and local `.mbtiles` archives are not available on desktop. Offline terrain is — see [Map & Waypoints](map-and-waypoints). - `.kml`/`.kmz`/GeoJSON layer import works — see [Map & Waypoints](map-and-waypoints#map-layers). Site Planner opens in your browser rather than in the app; to bring its coverage estimate onto the map, click the transmitter pin diff --git a/docs/uk-rUA/user/discovery.md b/docs/uk-rUA/user/discovery.md index c49dae99df..02f2de3977 100644 --- a/docs/uk-rUA/user/discovery.md +++ b/docs/uk-rUA/user/discovery.md @@ -2,7 +2,7 @@ title: Local Mesh Discovery parent: User Guide nav_order: 12 -last_updated: 2026-08-30 +last_updated: 2026-09-11 description: Explore your mesh network — the Local Mesh Discovery scanner, traceroute paths, neighbor maps, and node discovery tools. aliases: - discovery @@ -97,7 +97,7 @@ Mesh Beacon lets nodes invite others to join their mesh. A beaconing node period Configure it under **Settings → Module configuration → Mesh Beacon**. The entry appears only on radios running firmware 2.8.0 or newer. A read-only **Region** row at the top of the screen shows the region the beacon advertises: that region, and the preset, are always the ones the radio itself uses, so a beacon cannot invite anyone onto settings your radio is not running. - **Listen for beacons** — receive invitations broadcast by other nodes. -- **Broadcast a beacon** — periodically advertise this mesh to nearby nodes, with an optional **Beacon message** of up to 100 bytes, a **Broadcast interval** picked from fixed intervals between 1 hour and 72 hours, and an **Offered channel** chosen from your radio's own channels. The offered channel is required, and defaults to your primary channel. +- **Broadcast a beacon** — periodically advertise this mesh to nearby nodes, with an optional **Beacon message** of up to 100 bytes, a **Broadcast interval** picked from fixed intervals between 1 hour and 72 hours, and an **Offered channel** chosen from your radio's own channels. The offered channel is required, and defaults to your primary channel. Over remote admin the picker offers the primary channel only. - **Broadcast targets** — optional extra destinations beyond the offered channel. **Add target** appends a row; each row picks a **Channel** and a **Transmit preset**, and **Remove target** deletes it. With no targets, the beacon goes out on the offered channel alone. Two conditions block beacon setup: diff --git a/docs/uk-rUA/user/help-and-docs.md b/docs/uk-rUA/user/help-and-docs.md index 3055f181ff..3853d2693a 100644 --- a/docs/uk-rUA/user/help-and-docs.md +++ b/docs/uk-rUA/user/help-and-docs.md @@ -2,7 +2,7 @@ title: Help & In-App Docs parent: Посібник користувача nav_order: 21 -last_updated: 2026-08-30 +last_updated: 2026-09-11 description: Browse this documentation inside the app, search it, and ask Chirpy — the on-device AI assistant — questions about Meshtastic. aliases: - help @@ -23,7 +23,7 @@ The docs browser lists every user-guide page. Tap a page to read it; images and ### Search -The search box sits at the top of the docs browser. Type in it to filter pages by title and keywords — results update as you type, and the **Clear search** button (✕) empties the box. +Tap the search field at the top of the docs browser to open a full-screen search view. Type to filter pages by title and keywords — results update as you type, under the same field. The back arrow, or the keyboard's search action, closes the view. The field hides while you scroll down the page list and comes back when you scroll up. ![Searching the in-app documentation](../../assets/screenshots/docs-browser_search.png) diff --git a/docs/uk-rUA/user/map-and-waypoints.md b/docs/uk-rUA/user/map-and-waypoints.md index 9f98441b80..e68b433616 100644 --- a/docs/uk-rUA/user/map-and-waypoints.md +++ b/docs/uk-rUA/user/map-and-waypoints.md @@ -2,7 +2,7 @@ title: Map & Waypoints parent: User Guide nav_order: 6 -last_updated: 2026-09-01 +last_updated: 2026-09-11 description: View node positions on the map, create and share waypoints, manage map layers and Site Planner, and control position sharing and privacy. aliases: - map @@ -44,7 +44,7 @@ The floating toolbar provides quick access to the compass, the map type and laye ### Filtering the Map -Tap the filter button in the floating toolbar to open **Filter map**. **Display** controls what is drawn: **Only Favorites**, **Show Waypoints**, **Show Precision Circles**, and a slider that hides nodes not heard from recently. **Node roles** is a chip per device role, plus **All** to show every role; a selected chip means that role is shown. **Nodes** narrows the set further with **Hide offline nodes**, **Only show direct nodes**, **Exclude MQTT**, **Show ignored nodes**, and **Include unknown**. +Tap the filter button in the floating toolbar to open **Filter map**. **Display** controls what is drawn: **Only Favorites**, **Show Waypoints**, **Show Precision Circles**, and a slider that hides nodes not heard from recently. **Node roles** is a chip per device role, plus **All** to show every role; a selected chip means that role is shown. **Nodes** narrows the set further with **Hide offline nodes**, **Only show direct nodes**, **Signed only**, **Encrypted only**, **Exclude MQTT**, **Show ignored nodes**, and **Include unknown** — the same rules as the node list's [filter toggles](nodes). A dot on the filter button means at least one filter is hiding something — check it before concluding the mesh is quiet. Turning **Show Waypoints** off hides every waypoint, including your own. **Show ignored nodes** adds them to the map rather than showing only them — unlike the node list's **Only show ignored Nodes**. @@ -147,8 +147,9 @@ picker, all three offer the same raster base maps: Overlays can be toggled on top of any base map, from the layers sheet: - **Weather radar** — NOAA NEXRAD reflectivity (US coverage) -- **Hillshade** — terrain relief, on **F-Droid** and **Desktop** only. Useful for understanding why a - link fails, since LoRa range is limited by terrain +- **Hillshade** — terrain relief from an online tile source, on **F-Droid** and **Desktop** only; Google + Play draws hillshade from downloaded offline terrain instead. Useful for understanding why a link + fails, since LoRa range is limited by terrain ### Adding your own tile source @@ -156,7 +157,7 @@ Any XYZ tile endpoint can be added as a base map, on every flavor and on desktop Tile Sources** at the foot of the base map picker and paste a URL template using `{z}`, `{x}` and `{y}` — plus `{s}` if the provider uses rotating subdomains. A national mapping service, for example: -``` +```text https://wmts.geo.admin.ch/1.0.0/ch.swisstopo.pixelkarte-farbe/default/current/3857/{z}/{x}/{y}.jpeg ``` @@ -173,7 +174,18 @@ On **Google Play**, the layers sheet's **Offline Manager** section downloads its — water, roads and administrative boundaries extracted from the public Protomaps basemap dataset, drawn directly as map shapes rather than raster tiles. Frame the area you want, tap **Start Download**, and once it finishes flip **Show on map** to draw it (it's off by default so it never surprises you by covering up the -live map). You can still import a pre-made `.mbtiles` archive here too, as before. **Desktop** has neither. +live map). You can still import a pre-made `.mbtiles` archive here too, as before. **Desktop** has neither, though +the offline terrain described next does work there. + +### Offline terrain + +Hillshade and elevation contours can be downloaded for offline use as well, so a downloaded area still +shows why a link fails when there is no network. The terrain data comes from Mapterhorn and is credited on +the map. On **Google Play**, each downloaded region in the **Offline Manager** list has a **Download Terrain** +button and **Hillshade** and **Contours** switches; terrain attaches to that region and shares its storage limit, and an area +too large for terrain asks you to zoom in or pick a smaller region. On **F-Droid** and **Desktop**, the +layers sheet has an **Offline Terrain** section of its own: frame the area, tap **Start Download**, and the +hillshade and contours draw from the downloaded tiles from then on. ### Going offline @@ -181,8 +193,8 @@ An **Offline** pill appears over the map whenever the device has no network conn builds, if you've imported a local `.mbtiles` archive, the map switches to it automatically the moment the network drops — no toggle to remember — and switches back once you're reconnected, as long as you haven't picked a different base map yourself in between. On **F-Droid**, a downloaded offline area keeps rendering on -its own; the pill is purely informational there. **Desktop** has no offline downloads at all yet, so the pill -is informational there too. +its own; the pill is purely informational there. **Desktop** downloads offline terrain but no base-map +packs, so the pill is informational there too. ## Related Topics diff --git a/docs/uk-rUA/user/messages-and-channels.md b/docs/uk-rUA/user/messages-and-channels.md index 4bef671a1b..d10c4ac7e9 100644 --- a/docs/uk-rUA/user/messages-and-channels.md +++ b/docs/uk-rUA/user/messages-and-channels.md @@ -2,7 +2,7 @@ title: Messages & Channels parent: User Guide nav_order: 3 -last_updated: 2026-08-30 +last_updated: 2026-09-14 description: Send and receive messages, manage channels, configure encryption, search conversations, and use quick chat, reactions, and message actions. aliases: - channels @@ -50,6 +50,21 @@ Key length alone does not change the icon: a 128-bit key and a 256-bit key both Tapping an existing channel opens the same editor, where you can change the name, the PSK, MQTT uplink and downlink, and position precision. Every edit on this screen — adding, editing, deleting, or dragging a channel into a new order — waits on **Send** the same way. +### Archived Channels + +A conversation belongs to the channel it was held on, not to the slot that channel occupied. When a channel leaves your radio — you scan a different channel QR, replace a channel's key, or switch the modem preset — its conversation is **archived** rather than deleted or merged into whatever takes the slot. + +An archived conversation: + +- stays in the **Channels** list under the name the channel had, below the channels currently on your radio, marked with a history icon; +- keeps its full message history, searchable as usual; +- is read-only — you cannot send, reply, or react, because the channel is no longer on the radio to send on, and it is not offered as a share target; +- comes back automatically if you re-add that exact channel, taking its messages back to the live slot. + +Changing the modem preset archives the channel too: the preset is part of a default channel's identity, so **LongFast** and **MediumFast** are different channels even though neither the name nor the key changed. Changing only the region does not archive anything. + +To remove an archived conversation for good, long-press it in the conversation list and delete it. + ## Direct Messages Direct messages (DMs) go to one specific node. When both radios hold each other's public keys, your radio encrypts the message to that node's public key, so no one else on the mesh can read it — not even nodes that share your channel. diff --git a/docs/uk-rUA/user/mqtt.md b/docs/uk-rUA/user/mqtt.md index e4117a5dc2..be1394305a 100644 --- a/docs/uk-rUA/user/mqtt.md +++ b/docs/uk-rUA/user/mqtt.md @@ -2,7 +2,7 @@ title: MQTT parent: User Guide nav_order: 11 -last_updated: 2026-08-30 +last_updated: 2026-09-11 description: Bridge your mesh to the internet — MQTT broker setup, encryption layers, and map reporting. aliases: - mqtt @@ -25,7 +25,7 @@ The MQTT module connects your node to an MQTT broker, allowing: ## How It Works -``` +```text [Your Node] → Radio → [Gateway Node with Wi-Fi] → MQTT Broker → [Remote Gateway] → Radio → [Remote Node] ``` diff --git a/docs/uk-rUA/user/node-metrics.md b/docs/uk-rUA/user/node-metrics.md index ebe1cc5f76..06ab607db1 100644 --- a/docs/uk-rUA/user/node-metrics.md +++ b/docs/uk-rUA/user/node-metrics.md @@ -2,7 +2,7 @@ title: Node Metrics parent: User Guide nav_order: 5 -last_updated: 2026-08-30 +last_updated: 2026-09-09 description: Telemetry dashboards for each mesh node — device health, environment sensors, air quality, signal quality, power, traceroute, and position history. aliases: - metrics @@ -114,7 +114,7 @@ Radio signal quality information: ### Signal Quality Reference -Signal quality is rated from **SNR relative to the active LoRa modem preset's demodulation floor**, not from fixed thresholds — a given SNR means different things on different presets (e.g. −15 dB is fine on LongSlow but unusable on ShortFast). RSSI is shown but is not part of the rating. In the table, _limit_ is the preset's SNR limit. +Signal quality is rated from **SNR relative to the active LoRa modem preset's demodulation floor**, not from fixed thresholds — a given SNR means different things on different presets (e.g. −15 dB is fine on LongSlow but unusable on ShortFast). When RSSI and a noise-floor reading are both available, the app also rates their difference against the preset limit and uses the worse rating. Otherwise, RSSI is display-only. In the table, _limit_ is the preset's SNR limit. | Quality | Criteria | | ----------- | ---------------------------------------------------------------- | diff --git a/docs/uk-rUA/user/nodes.md b/docs/uk-rUA/user/nodes.md index c1c8385ad3..dc1e585d2f 100644 --- a/docs/uk-rUA/user/nodes.md +++ b/docs/uk-rUA/user/nodes.md @@ -2,7 +2,7 @@ title: Вузли parent: User Guide nav_order: 4 -last_updated: 2026-09-04 +last_updated: 2026-09-11 description: Browse, filter, and sort mesh nodes — view details, signal quality, roles, and quick actions. aliases: - node-list @@ -74,19 +74,23 @@ Most users should keep the default **Client** role. Consider a different role wh > 💡 **Tip:** The mesh works best when most nodes are **Client** or **Router**. Too many Client Mute nodes reduce mesh resilience; too many Routers in a dense area can cause congestion. A good rule of thumb: one Router per 5–10 Clients in your area. -### Encryption Indicators +### Security indicators -Nodes display encryption status icons next to their name: +Each node carries one security icon beside its name in the node list. Tap it to read what it means, and choose **Show All Meanings** in that dialog for the full legend. The detail screen shows the same state in words, as a **Security** row that opens the same dialog. -| Icon | Meaning | -| ----------- | ------------------------------------------------------------------------------------------------------------------------------------------------- | -| 🔒 Locked | Communication uses PKI (public key infrastructure) — end-to-end encrypted with verified identity | -| 🔓 Unlocked | No public key has been received for this node, so it cannot be direct messaged — use **Request User Info** on the node detail page to ask for one | -| ⚠️ Mismatch | Public key mismatch — the node's key has changed since last seen (investigate before trusting) | +| Icon | Meaning | Shown for | +| --------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------- | +| Person with a shield check (green) | **Verified contact** — you verified this node's key in person by exchanging contact QR codes, so its identity is confirmed. The strongest trust the list shows. Your own connected node carries it too | Any firmware version | +| Nodes icon with a shield check (green) | **Signed node** — this node signs its broadcasts with its identity key, so its identity is consistent over time, but you have not verified it in person. On firmware 2.8 the icon appears from the version alone, before any signed broadcast has been heard | Firmware 2.8 or newer, and any node whose signed broadcast your node has verified | +| 🔒 Closed lock | A public key is on file and matches, so direct messages to this node are encrypted | Firmware before 2.8, or no reported version | +| 🔓 Open lock | No public key has been received for this node, so it cannot be direct messaged — use **Request User Info** on the node detail page to ask for one | Firmware before 2.8, or no reported version | +| ⚠️ Mismatch | **Public key mismatch** — a different key arrived for this node after one was stored. Investigate before trusting | Any firmware version | -> 💡 **Tip:** Direct messages always use PKI, so the radio needs the other node's public key before it can send one. It refuses the send rather than falling back to channel encryption. Keys arrive inside node info, which is why an open lock usually clears itself once that node is heard from properly. If you see a key mismatch warning, the node may have been reset or compromised. +Every node on firmware 2.8 or newer signs its broadcasts, so on that firmware the signed state is the baseline and the locks are not shown. -To clear a mismatch, first confirm through another trusted channel that the key change was intentional — a factory reset causes one. Then touch & hold the node, choose **Remove**, and let the two radios exchange keys again the next time yours hears it. +Direct messages always use public-key encryption, so your node needs the other node's public key before it can send one. It refuses the send rather than falling back to channel encryption. Keys arrive inside node info, which is why an open lock usually clears itself once that node is heard from properly. + +A mismatch never replaces the key you already hold. The app keeps the first key it recorded and refuses the new one, as the firmware does, so a stray or hostile node info cannot silently break encrypted messaging to a contact. To clear a mismatch, first confirm through another trusted channel that the key change was intentional — a factory reset causes one. Then touch & hold the node, choose **Remove**, and let the two nodes exchange keys again the next time yours hears it. ## Quick Actions @@ -108,7 +112,9 @@ radio is connected and running firmware 2.8 or newer — see ## Sharing a Contact -On a node's detail screen, tap **Share Contact** to produce a link and a QR code for that node. From the same dialog, **Write to NFC tag** saves the link to a writable NFC tag that anyone can tap to open. +On a node's detail screen, tap **Share Contact** to produce a link and a QR code for that node. From the same dialog, **Share link** opens the Android share sheet (on desktop it copies the link instead), **Write to NFC tag** saves it to a writable NFC tag, and **Copy** puts it on the clipboard. While that dialog is open and in front of you, the phone also offers the same link to any NFC reader, so someone can take the contact by tapping their phone against yours with no tag involved. + +Sharing your own contact this way marks it as verified in person, so whoever imports it sees the **Verified contact** icon rather than the signed one. Relaying someone else's contact passes on only what your app had already recorded about them. To add someone else's contact, use the import button on the node list and choose **Scan Shared Contact QR Code**, **Scan Shared Contact NFC**, or **Input Shared Contact URL**. The app asks you to confirm with **Import Shared Contact?**, and warns you when the contact is one you already have. @@ -120,14 +126,16 @@ Type in the search field to filter nodes by name or short name. The filter updat ### Filter Toggles -| Фільтри | Опис | -| --------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| **Hide offline nodes** | Show only nodes heard within the last 2 hours | -| **Only show direct nodes** | Show only nodes your radio heard directly, with no relay in between | -| **Include unknown** | Show nodes that haven't sent user info yet. **On by default**, so a node heard before its info arrives stays visible; these carry a badge marking them incomplete, and cannot be direct messaged until their user info brings a public key | -| **Exclude infrastructure** | Hide infrastructure-role nodes (Router, Router Late, Client Base, and legacy Repeater nodes) and any node that cannot be messaged, whatever its role | -| **Exclude MQTT** | Hide nodes heard only via MQTT internet bridge | -| **Only show ignored Nodes** | Replace the list with the nodes you have ignored. Every other node is hidden while this is on, and a banner appears at the top of the list to take you back | +| Фільтри | Опис | +| --------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **Hide offline nodes** | Show only nodes heard within the last 2 hours | +| **Only show direct nodes** | Show only nodes your radio heard directly, with no relay in between | +| **Include unknown** | Show nodes that haven't sent user info yet. **On by default**, so a node heard before its info arrives stays visible; these carry a badge marking them incomplete, and cannot be direct messaged until their user info brings a public key | +| **Exclude infrastructure** | Hide infrastructure-role nodes (Router, Router Late, Client Base, and legacy Repeater nodes) and any node that cannot be messaged, whatever its role | +| **Exclude MQTT** | Hide nodes heard only via MQTT internet bridge | +| **Signed only** | Show only nodes whose signed broadcasts your node has actually heard and verified. Stricter than the icon: a node on 2.8 shows as signed by its firmware version before any signed broadcast arrives, and a contact verified in person on older firmware is not signed at all | +| **Encrypted only** | Show nodes with a matching public key on file, the key an encrypted direct message needs. A node with a key mismatch is excluded | +| **Only show ignored Nodes** | Replace the list with the nodes you have ignored. Every other node is hidden while this is on, and a banner appears at the top of the list to take you back | ### Sort Options @@ -149,6 +157,8 @@ Tap the hop-histogram icon in the node list's app bar to open a bar chart of how Tapping a node opens the detail view with comprehensive information. See [Node Metrics](node-metrics) for full details on metrics and telemetry. +Signal quality is rated against your modem preset. The same SNR can be good on a long-range preset and poor on a faster one. Traceroute and neighbor-info SNR colors use that preset too. RSSI text has its own strength colors; it affects the quality rating only when a noise-floor reading is also available. + The Details card carries the node's short name, role, IDs, last heard time, hops away, uptime, and its SNR and RSSI: ![Node detail section](../../assets/screenshots/nodes_detail_section.png) diff --git a/docs/uk-rUA/user/settings-module-admin.md b/docs/uk-rUA/user/settings-module-admin.md index 69ce04c4ca..4f0fb279fc 100644 --- a/docs/uk-rUA/user/settings-module-admin.md +++ b/docs/uk-rUA/user/settings-module-admin.md @@ -2,7 +2,7 @@ title: Settings — Modules & Admin parent: User Guide nav_order: 8 -last_updated: 2026-08-30 +last_updated: 2026-09-16 description: Configure optional feature modules (MQTT, telemetry, canned messages, TAK, and more) and perform device administration. aliases: - modules @@ -333,7 +333,7 @@ appear only when your own node is selected: ### Про -**Settings → About** carries the app's own identity rather than the radio's: +**Settings → About** carries the app's own identity rather than the node's: Three sections: @@ -341,7 +341,11 @@ Three sections: - **Apps** — opens with **Need Hardware?**, a rotating carousel of popular devices that links out to where to buy one. It also lists the GitHub repository, the running app version, and **Acknowledgements** (see the next section). -- **Project information** — links to the website and to this documentation. +- **Project information** — links to the website, to this documentation, and to the GNU GPL v3 + license the app is released under. + +The footer states the copyright and that the app is free software with no warranty, which you +may redistribute under the same license. ### Подяки diff --git a/docs/uk-rUA/user/settings-radio-user.md b/docs/uk-rUA/user/settings-radio-user.md index 547210287e..9d00537043 100644 --- a/docs/uk-rUA/user/settings-radio-user.md +++ b/docs/uk-rUA/user/settings-radio-user.md @@ -2,7 +2,7 @@ title: Settings — Radio & User parent: User Guide nav_order: 7 -last_updated: 2026-09-04 +last_updated: 2026-09-11 description: Configure your radio hardware, LoRa presets, user profile, position sharing, power management, and security. aliases: - налаштування @@ -51,7 +51,7 @@ The footer appears as soon as you change something. **Discard** throws the chang The status message is saved with the same **Save**, but it never reboots the node — and, like the rest of this screen, it can be edited on a remote node you administer. For your own radio there is a shortcut while it is connected: touch & hold your node in the [node list](nodes.md) and choose -**Update status**. Older firmware and a disconnected radio have no shortcut — the field above is +**Update status**. Older firmware and a disconnected node have no shortcut — the field itself is still the way in. ## Налаштування @@ -77,13 +77,13 @@ On **Settings → LoRa**. | Setting | Опис | За замовчуванням | | ------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------- | -| Регіон | Regulatory region for frequency bands. You must set this before transmitting | Unset (must configure) | +| Регіон | Regulatory region for frequency bands. You must set this before transmitting. On firmware 2.8 or newer, the first region set also creates the node's identity key and gives it a new node number | Unset (must configure) | | Пресети | Speed/range tradeoff | LongFast | | К-ть стрибків | Maximum retransmit hops | 3 | | Потужність передачі | Transmission power (dBm); 0 = max allowed for region | 0 (region max) | | Перевизначити частоту | Overrides the computed operating frequency outright (MHz). It does not offset the calculated value — leave at 0 unless you know you need a specific frequency | 0 (use calculated) | | Використовувати пресет | On by default. Turn it off to set Spread Factor, Coding Rate and Bandwidth by hand instead of taking them from the modem preset | On | -| Показник розширення сигналу | Manual mode only: 7–12. Higher spreads further but slower | From preset | +| Показник розширення сигналу | Manual mode only: 5–12. Higher spreads further but slower. On SX127x (RF95) radios the firmware does not accept 5 or 6 and uses 11 instead | From preset | | Швидкість кодування | Manual mode only: 5–8. More redundancy costs airtime | From preset | | Ширина смуги пропускання | Manual mode only: the channel bandwidth in kHz, typed in directly. On the 2.4 GHz region the app offers a list of the bandwidths your radio supports instead, and a stored value that is not on that list shows as _Unsupported_ and blocks saving until you pick a supported one | From preset | | Слот частоти | Which slot within the region's band to use. 0 derives it from the primary channel name | 0 (automatic) | @@ -111,13 +111,13 @@ The Lite, Narrow, Medium Turbo, and Tiny presets need firmware 2.8 or newer — | Short Slow | ~5 km | 6.25 kbps | −10 dB | Suburban short-range; moderate building density | | Medium Fast | ~5 km | 3.52 kbps | −12.5 dB | Suburban areas; moderate building density | | Medium Slow | ~8 km | 1.95 kbps | −15 dB | Suburban/rural; moderate range with slower speed | -| Long Turbo | ~10 km | 1.34 kbps | −12.5 dB | Similar range to Long Fast but with 500 kHz bandwidth; faster throughput | +| Long Turbo | ~10 km | 1.34 kbps | −17.5 dB | Similar range to Long Fast but with 500 kHz bandwidth; faster throughput | | Long Fast | ~10 km | 1.1 kbps | −17.5 dB | **General use (default)** — balanced range and speed | | Long Moderate | ~20 km | 0.34 kbps | −17.5 dB | Rural with some terrain; occasional use | | Lite Fast | ~5 km | 1.76 kbps | −12.5 dB | EU 866 MHz SRD band (125 kHz BW); comparable to Medium Fast | | Lite Slow | ~10 km | 0.98 kbps | −15 dB | EU 866 MHz SRD band (125 kHz BW); comparable to Long Fast | -| Narrow Fast | ~5 km | 2.28 kbps | −10 dB | EU 868 MHz band (62.5 kHz BW); avoids interference with other devices | -| Narrow Slow | ~10 km | 1.30 kbps | −12.5 dB | EU 868 MHz band (62.5 kHz BW); comparable to Long Fast | +| Narrow Fast | ~5 km | 2.28 kbps | −7.5 dB | EU 868 MHz band (62.5 kHz BW); avoids interference with other devices | +| Narrow Slow | ~10 km | 1.30 kbps | −10 dB | EU 868 MHz band (62.5 kHz BW); comparable to Long Fast | | Medium Turbo | ~5 km | 7.0 kbps | −12.5 dB | Like Medium Fast but with 500 kHz bandwidth; not legal in every region. Needs firmware 2.8 or newer | | Tiny Fast | ~10 km | 0.68 kbps | −7.5 dB | Amateur bands that cap occupied bandwidth; these presets use 15.6 kHz. Needs firmware 2.8 or newer, an SX126x or SX127x radio, and a TCXO of ±5 ppm or better | | Tiny Slow | ~20 km | 0.33 kbps | −10 dB | Same band restrictions as Tiny Fast, longer range. Same firmware, radio, and TCXO requirements | diff --git a/docs/uk-rUA/user/signal-meter.md b/docs/uk-rUA/user/signal-meter.md index f3d37ee73c..b3360d71bd 100644 --- a/docs/uk-rUA/user/signal-meter.md +++ b/docs/uk-rUA/user/signal-meter.md @@ -2,7 +2,7 @@ title: How the Meshtastic Signal Meter Works parent: User Guide nav_order: 15 -last_updated: 2026-08-30 +last_updated: 2026-09-09 description: How the signal meter rates quality from SNR relative to the LoRa modem preset — spread spectrum, presets, and what the bars really mean. aliases: - signal @@ -32,7 +32,7 @@ Each modem preset has an SNR limit: the lowest SNR at which that preset can stil ## Rating Signal Quality -The app rates signal quality (None, Bad, Fair, or Good) from SNR alone, measured against the active preset's SNR limit. It does not factor in RSSI: without knowing the local noise floor, RSSI alone cannot say whether a signal is decodable. RSSI is still available — on the node detail screen and in the metrics charts. +The app rates signal quality (None, Bad, Fair, or Good) from SNR relative to the active preset's SNR limit. When RSSI and a noise-floor reading are both available, it also rates the difference between them against that limit and uses the worse rating. Otherwise, it uses SNR alone. Because the rating is relative to the preset, the same SNR rates differently on different presets. An SNR of −16 dB rates Good on Long Fast (SNR limit −17.5 dB) but None on Short Fast (SNR limit −7.5 dB). Let `limit` be the active preset's SNR limit: @@ -45,7 +45,7 @@ Because the rating is relative to the preset, the same SNR rates differently on The icon never goes blank, so count the bars carefully: a single bar means None, not a weak but usable link, and Good is a solid wedge rather than a set of bars. A gray three-bar icon labeled Unknown is a different state again — the packet carried no SNR measurement at all, which is not the same as measuring one and finding it too weak. -> ℹ️ **Note:** Traceroute hop colors use fixed thresholds (−7 dB / −15 dB); the per-node signal meter uses the preset-relative rating instead. +> ℹ️ **Note:** Traceroute and neighbor-info SNR colors use the active preset's limits too. RSSI text has separate strength colors: green above −115 dBm, yellow above −120 dBm, orange above −126 dBm, and red at or below −126 dBm. ## Diagnosing Local Interference diff --git a/docs/uk-rUA/user/tak.md b/docs/uk-rUA/user/tak.md index eeabaccafa..5f0a831f86 100644 --- a/docs/uk-rUA/user/tak.md +++ b/docs/uk-rUA/user/tak.md @@ -2,7 +2,7 @@ title: TAK Integration parent: User Guide nav_order: 10 -last_updated: 2026-08-30 +last_updated: 2026-09-11 description: Interoperate with ATAK and WinTAK — CoT position sharing, TAK roles, and plugin setup. aliases: - tak @@ -36,7 +36,7 @@ The TAK module allows Meshtastic nodes to: > ⚠️ **Warning:** The old **Meshtastic ATAK Plugin** is no longer part of this path and cannot > work. It bridged through the cross-process AIDL API, which was removed in app 2.8.0; the mesh > service is now in-process only. Do not install it. Interop today runs over the app's own local -> TAK server plus the Mesh to CoT Converter, both described below, with stock ATAK/iTAK/WinTAK. +> TAK server plus the Mesh to CoT Converter, both described in the following sections, with stock ATAK/iTAK/WinTAK. ### Налаштування diff --git a/docs/uk-rUA/user/translate.md b/docs/uk-rUA/user/translate.md index 77e41d015a..12281d56c9 100644 --- a/docs/uk-rUA/user/translate.md +++ b/docs/uk-rUA/user/translate.md @@ -2,7 +2,7 @@ title: Translate the App parent: User Guide nav_order: 17 -last_updated: 2026-08-30 +last_updated: 2026-09-11 description: How the app and its documentation are translated via Crowdin, and guidelines for contributing translations. aliases: - translate @@ -45,7 +45,7 @@ If your language is not yet listed on Crowdin: The Android app uses **Compose Multiplatform resources** for all user-visible strings: -``` +```text core/resources/src/commonMain/composeResources/ ├── values/ ← English (default) │ └── strings.xml @@ -58,7 +58,7 @@ core/resources/src/commonMain/composeResources/ In-app documentation follows a similar pattern under `docs/`: -``` +```text docs/ ├── en/user/ ← English source (default) │ ├── onboarding.md diff --git a/docs/zh-rCN/user/desktop.md b/docs/zh-rCN/user/desktop.md index 3b098efba0..8effc0f051 100644 --- a/docs/zh-rCN/user/desktop.md +++ b/docs/zh-rCN/user/desktop.md @@ -2,7 +2,7 @@ title: Desktop App parent: User Guide nav_order: 14 -last_updated: 2026-08-30 +last_updated: 2026-09-11 description: Install and use the Meshtastic Desktop app on Linux, macOS, and Windows — connections, feature parity, and keyboard shortcuts. aliases: - desktop @@ -58,20 +58,20 @@ Bluetooth Low Energy is supported on desktop via the [Kable](https://github.com/ ## Feature Parity -| Feature | Android | Desktop | 注 | -| ----------------------------------------------------- | ------- | ------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Messaging | ✓ | ✓ | Full parity | -| Node List | ✓ | ✓ | Full parity | -| 地图 | ✓ | ✓ | Interactive MapLibre map, with base map and overlay pickers and custom tile sources. No offline downloads or local `.mbtiles` archives | -| Map layers (`.kml`/`.kmz`/GeoJSON) | ✓ | ✓ | Same layer store and sheet as Android; imported files draw on the desktop map | -| 站点规划器 | ✓ | ✓\* | \*Opens in your browser on desktop; the estimate is not drawn on the desktop map | -| 设置 | ✓ | ✓ | Full parity | -| Bluetooth (BLE) | ✓ | ✓ | Via Kable on desktop | -| 固件更新 | ✓ | ✓ | In-app USB, BLE, and Wi-Fi (ESP32) update work the same as Android. The USB maintenance flow — nRF52/RP2040 factory erase and bootloader upgrade — is Android-only | -| Notifications | ✓ | ✓ | Native OS notifications | -| Widgets | ✓ | ✗ | Android-only | -| AI Assistant (Chirpy) | ✓\* | ✗ | Google flavor Android only | -| App Functions (system AI) | ✓† | ✗ | Google flavor Android only | +| Feature | Android | Desktop | 注 | +| ----------------------------------------------------- | ------- | ------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Messaging | ✓ | ✓ | Full parity | +| Node List | ✓ | ✓ | Full parity | +| 地图 | ✓ | ✓ | Interactive MapLibre map, with base map and overlay pickers and custom tile sources. Offline terrain (hillshade and contours) can be downloaded; offline base-map packs and local `.mbtiles` archives cannot | +| Map layers (`.kml`/`.kmz`/GeoJSON) | ✓ | ✓ | Same layer store and sheet as Android; imported files draw on the desktop map | +| 站点规划器 | ✓ | ✓\* | \*Opens in your browser on desktop; the estimate is not drawn on the desktop map | +| 设置 | ✓ | ✓ | Full parity | +| Bluetooth (BLE) | ✓ | ✓ | Via Kable on desktop | +| 固件更新 | ✓ | ✓ | In-app USB, BLE, and Wi-Fi (ESP32) update work the same as Android. The USB maintenance flow — nRF52/RP2040 factory erase and bootloader upgrade — is Android-only | +| Notifications | ✓ | ✓ | Native OS notifications | +| Widgets | ✓ | ✗ | Android-only | +| AI Assistant (Chirpy) | ✓\* | ✗ | Google flavor Android only | +| App Functions (system AI) | ✓† | ✗ | Google flavor Android only | \*Chirpy AI requires Android 14+ on Google flavor builds with supported hardware. @@ -122,7 +122,7 @@ Individual doc pages render with full formatting: ## Building from Source -```bash +```shell git clone https://github.com/meshtastic/Meshtastic-Android.git cd Meshtastic-Android ./gradlew :desktopApp:run @@ -135,7 +135,7 @@ Requirements: ## Known Limitations -- Offline tile downloads and local `.mbtiles` archives are not available on desktop. +- Offline base-map downloads and local `.mbtiles` archives are not available on desktop. Offline terrain is — see [Map & Waypoints](map-and-waypoints). - `.kml`/`.kmz`/GeoJSON layer import works — see [Map & Waypoints](map-and-waypoints#map-layers). Site Planner opens in your browser rather than in the app; to bring its coverage estimate onto the map, click the transmitter pin diff --git a/docs/zh-rCN/user/discovery.md b/docs/zh-rCN/user/discovery.md index b4701cf338..ca2b452dbb 100644 --- a/docs/zh-rCN/user/discovery.md +++ b/docs/zh-rCN/user/discovery.md @@ -2,7 +2,7 @@ title: 本地网状网络发现 parent: User Guide nav_order: 12 -last_updated: 2026-08-30 +last_updated: 2026-09-11 description: Explore your mesh network — the Local Mesh Discovery scanner, traceroute paths, neighbor maps, and node discovery tools. aliases: - discovery @@ -97,7 +97,7 @@ Mesh Beacon lets nodes invite others to join their mesh. A beaconing node period Configure it under **Settings → Module configuration → Mesh Beacon**. The entry appears only on radios running firmware 2.8.0 or newer. A read-only **Region** row at the top of the screen shows the region the beacon advertises: that region, and the preset, are always the ones the radio itself uses, so a beacon cannot invite anyone onto settings your radio is not running. - **Listen for beacons** — receive invitations broadcast by other nodes. -- **Broadcast a beacon** — periodically advertise this mesh to nearby nodes, with an optional **Beacon message** of up to 100 bytes, a **Broadcast interval** picked from fixed intervals between 1 hour and 72 hours, and an **Offered channel** chosen from your radio's own channels. The offered channel is required, and defaults to your primary channel. +- **Broadcast a beacon** — periodically advertise this mesh to nearby nodes, with an optional **Beacon message** of up to 100 bytes, a **Broadcast interval** picked from fixed intervals between 1 hour and 72 hours, and an **Offered channel** chosen from your radio's own channels. The offered channel is required, and defaults to your primary channel. Over remote admin the picker offers the primary channel only. - **Broadcast targets** — optional extra destinations beyond the offered channel. **Add target** appends a row; each row picks a **Channel** and a **Transmit preset**, and **Remove target** deletes it. With no targets, the beacon goes out on the offered channel alone. Two conditions block beacon setup: diff --git a/docs/zh-rCN/user/help-and-docs.md b/docs/zh-rCN/user/help-and-docs.md index f306cde7f7..c83898cea3 100644 --- a/docs/zh-rCN/user/help-and-docs.md +++ b/docs/zh-rCN/user/help-and-docs.md @@ -2,7 +2,7 @@ title: Help & In-App Docs parent: User Guide nav_order: 21 -last_updated: 2026-08-30 +last_updated: 2026-09-11 description: Browse this documentation inside the app, search it, and ask Chirpy — the on-device AI assistant — questions about Meshtastic. aliases: - help @@ -23,7 +23,7 @@ The docs browser lists every user-guide page. Tap a page to read it; images and ### Search -The search box sits at the top of the docs browser. Type in it to filter pages by title and keywords — results update as you type, and the **Clear search** button (✕) empties the box. +Tap the search field at the top of the docs browser to open a full-screen search view. Type to filter pages by title and keywords — results update as you type, under the same field. The back arrow, or the keyboard's search action, closes the view. The field hides while you scroll down the page list and comes back when you scroll up. ![Searching the in-app documentation](../../assets/screenshots/docs-browser_search.png) diff --git a/docs/zh-rCN/user/map-and-waypoints.md b/docs/zh-rCN/user/map-and-waypoints.md index b0d1ecbcbe..0b1101c507 100644 --- a/docs/zh-rCN/user/map-and-waypoints.md +++ b/docs/zh-rCN/user/map-and-waypoints.md @@ -2,7 +2,7 @@ title: Map & Waypoints parent: User Guide nav_order: 6 -last_updated: 2026-09-01 +last_updated: 2026-09-11 description: View node positions on the map, create and share waypoints, manage map layers and Site Planner, and control position sharing and privacy. aliases: - map @@ -44,7 +44,7 @@ The floating toolbar provides quick access to the compass, the map type and laye ### Filtering the Map -Tap the filter button in the floating toolbar to open **Filter map**. **Display** controls what is drawn: **Only Favorites**, **Show Waypoints**, **Show Precision Circles**, and a slider that hides nodes not heard from recently. **Node roles** is a chip per device role, plus **All** to show every role; a selected chip means that role is shown. **Nodes** narrows the set further with **Hide offline nodes**, **Only show direct nodes**, **Exclude MQTT**, **Show ignored nodes**, and **Include unknown**. +Tap the filter button in the floating toolbar to open **Filter map**. **Display** controls what is drawn: **Only Favorites**, **Show Waypoints**, **Show Precision Circles**, and a slider that hides nodes not heard from recently. **Node roles** is a chip per device role, plus **All** to show every role; a selected chip means that role is shown. **Nodes** narrows the set further with **Hide offline nodes**, **Only show direct nodes**, **Signed only**, **Encrypted only**, **Exclude MQTT**, **Show ignored nodes**, and **Include unknown** — the same rules as the node list's [filter toggles](nodes). A dot on the filter button means at least one filter is hiding something — check it before concluding the mesh is quiet. Turning **Show Waypoints** off hides every waypoint, including your own. **Show ignored nodes** adds them to the map rather than showing only them — unlike the node list's **Only show ignored Nodes**. @@ -147,8 +147,9 @@ picker, all three offer the same raster base maps: Overlays can be toggled on top of any base map, from the layers sheet: - **Weather radar** — NOAA NEXRAD reflectivity (US coverage) -- **Hillshade** — terrain relief, on **F-Droid** and **Desktop** only. Useful for understanding why a - link fails, since LoRa range is limited by terrain +- **Hillshade** — terrain relief from an online tile source, on **F-Droid** and **Desktop** only; Google + Play draws hillshade from downloaded offline terrain instead. Useful for understanding why a link + fails, since LoRa range is limited by terrain ### Adding your own tile source @@ -156,7 +157,7 @@ Any XYZ tile endpoint can be added as a base map, on every flavor and on desktop Tile Sources** at the foot of the base map picker and paste a URL template using `{z}`, `{x}` and `{y}` — plus `{s}` if the provider uses rotating subdomains. A national mapping service, for example: -``` +```text https://wmts.geo.admin.ch/1.0.0/ch.swisstopo.pixelkarte-farbe/default/current/3857/{z}/{x}/{y}.jpeg ``` @@ -173,7 +174,18 @@ On **Google Play**, the layers sheet's **Offline Manager** section downloads its — water, roads and administrative boundaries extracted from the public Protomaps basemap dataset, drawn directly as map shapes rather than raster tiles. Frame the area you want, tap **Start Download**, and once it finishes flip **Show on map** to draw it (it's off by default so it never surprises you by covering up the -live map). You can still import a pre-made `.mbtiles` archive here too, as before. **Desktop** has neither. +live map). You can still import a pre-made `.mbtiles` archive here too, as before. **Desktop** has neither, though +the offline terrain described next does work there. + +### Offline terrain + +Hillshade and elevation contours can be downloaded for offline use as well, so a downloaded area still +shows why a link fails when there is no network. The terrain data comes from Mapterhorn and is credited on +the map. On **Google Play**, each downloaded region in the **Offline Manager** list has a **Download Terrain** +button and **Hillshade** and **Contours** switches; terrain attaches to that region and shares its storage limit, and an area +too large for terrain asks you to zoom in or pick a smaller region. On **F-Droid** and **Desktop**, the +layers sheet has an **Offline Terrain** section of its own: frame the area, tap **Start Download**, and the +hillshade and contours draw from the downloaded tiles from then on. ### Going offline @@ -181,8 +193,8 @@ An **Offline** pill appears over the map whenever the device has no network conn builds, if you've imported a local `.mbtiles` archive, the map switches to it automatically the moment the network drops — no toggle to remember — and switches back once you're reconnected, as long as you haven't picked a different base map yourself in between. On **F-Droid**, a downloaded offline area keeps rendering on -its own; the pill is purely informational there. **Desktop** has no offline downloads at all yet, so the pill -is informational there too. +its own; the pill is purely informational there. **Desktop** downloads offline terrain but no base-map +packs, so the pill is informational there too. ## Related Topics diff --git a/docs/zh-rCN/user/messages-and-channels.md b/docs/zh-rCN/user/messages-and-channels.md index 358f14d7fb..8e2fdbf528 100644 --- a/docs/zh-rCN/user/messages-and-channels.md +++ b/docs/zh-rCN/user/messages-and-channels.md @@ -2,7 +2,7 @@ title: Messages & Channels parent: User Guide nav_order: 3 -last_updated: 2026-08-30 +last_updated: 2026-09-14 description: Send and receive messages, manage channels, configure encryption, search conversations, and use quick chat, reactions, and message actions. aliases: - channels @@ -50,6 +50,21 @@ Key length alone does not change the icon: a 128-bit key and a 256-bit key both Tapping an existing channel opens the same editor, where you can change the name, the PSK, MQTT uplink and downlink, and position precision. Every edit on this screen — adding, editing, deleting, or dragging a channel into a new order — waits on **Send** the same way. +### Archived Channels + +A conversation belongs to the channel it was held on, not to the slot that channel occupied. When a channel leaves your radio — you scan a different channel QR, replace a channel's key, or switch the modem preset — its conversation is **archived** rather than deleted or merged into whatever takes the slot. + +An archived conversation: + +- stays in the **Channels** list under the name the channel had, below the channels currently on your radio, marked with a history icon; +- keeps its full message history, searchable as usual; +- is read-only — you cannot send, reply, or react, because the channel is no longer on the radio to send on, and it is not offered as a share target; +- comes back automatically if you re-add that exact channel, taking its messages back to the live slot. + +Changing the modem preset archives the channel too: the preset is part of a default channel's identity, so **LongFast** and **MediumFast** are different channels even though neither the name nor the key changed. Changing only the region does not archive anything. + +To remove an archived conversation for good, long-press it in the conversation list and delete it. + ## 私聊 Direct messages (DMs) go to one specific node. When both radios hold each other's public keys, your radio encrypts the message to that node's public key, so no one else on the mesh can read it — not even nodes that share your channel. diff --git a/docs/zh-rCN/user/mqtt.md b/docs/zh-rCN/user/mqtt.md index c8061e1838..56d99b58b0 100644 --- a/docs/zh-rCN/user/mqtt.md +++ b/docs/zh-rCN/user/mqtt.md @@ -2,7 +2,7 @@ title: MQTT parent: User Guide nav_order: 11 -last_updated: 2026-08-30 +last_updated: 2026-09-11 description: Bridge your mesh to the internet — MQTT broker setup, encryption layers, and map reporting. aliases: - mqtt @@ -25,7 +25,7 @@ The MQTT module connects your node to an MQTT broker, allowing: ## How It Works -``` +```text [Your Node] → Radio → [Gateway Node with Wi-Fi] → MQTT Broker → [Remote Gateway] → Radio → [Remote Node] ``` diff --git a/docs/zh-rCN/user/node-metrics.md b/docs/zh-rCN/user/node-metrics.md index 4a175d4b49..489fa5a70b 100644 --- a/docs/zh-rCN/user/node-metrics.md +++ b/docs/zh-rCN/user/node-metrics.md @@ -2,7 +2,7 @@ title: Node Metrics parent: User Guide nav_order: 5 -last_updated: 2026-08-30 +last_updated: 2026-09-09 description: Telemetry dashboards for each mesh node — device health, environment sensors, air quality, signal quality, power, traceroute, and position history. aliases: - metrics @@ -114,7 +114,7 @@ Radio signal quality information: ### Signal Quality Reference -Signal quality is rated from **SNR relative to the active LoRa modem preset's demodulation floor**, not from fixed thresholds — a given SNR means different things on different presets (e.g. −15 dB is fine on LongSlow but unusable on ShortFast). RSSI is shown but is not part of the rating. In the table, _limit_ is the preset's SNR limit. +Signal quality is rated from **SNR relative to the active LoRa modem preset's demodulation floor**, not from fixed thresholds — a given SNR means different things on different presets (e.g. −15 dB is fine on LongSlow but unusable on ShortFast). When RSSI and a noise-floor reading are both available, the app also rates their difference against the preset limit and uses the worse rating. Otherwise, RSSI is display-only. In the table, _limit_ is the preset's SNR limit. | Quality | Criteria | | ------- | ---------------------------------------------------------------- | diff --git a/docs/zh-rCN/user/nodes.md b/docs/zh-rCN/user/nodes.md index 25a70e6264..395f0007a5 100644 --- a/docs/zh-rCN/user/nodes.md +++ b/docs/zh-rCN/user/nodes.md @@ -2,7 +2,7 @@ title: 节点 parent: User Guide nav_order: 4 -last_updated: 2026-09-04 +last_updated: 2026-09-11 description: Browse, filter, and sort mesh nodes — view details, signal quality, roles, and quick actions. aliases: - node-list @@ -74,19 +74,23 @@ Most users should keep the default **Client** role. Consider a different role wh > 💡 **Tip:** The mesh works best when most nodes are **Client** or **Router**. Too many Client Mute nodes reduce mesh resilience; too many Routers in a dense area can cause congestion. A good rule of thumb: one Router per 5–10 Clients in your area. -### Encryption Indicators +### Security indicators -Nodes display encryption status icons next to their name: +Each node carries one security icon beside its name in the node list. Tap it to read what it means, and choose **Show All Meanings** in that dialog for the full legend. The detail screen shows the same state in words, as a **Security** row that opens the same dialog. -| 图标 | Meaning | -| ----------- | ------------------------------------------------------------------------------------------------------------------------------------------------- | -| 🔒 Locked | Communication uses PKI (public key infrastructure) — end-to-end encrypted with verified identity | -| 🔓 Unlocked | No public key has been received for this node, so it cannot be direct messaged — use **Request User Info** on the node detail page to ask for one | -| ⚠️ Mismatch | Public key mismatch — the node's key has changed since last seen (investigate before trusting) | +| 图标 | Meaning | Shown for | +| --------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------- | +| Person with a shield check (green) | **Verified contact** — you verified this node's key in person by exchanging contact QR codes, so its identity is confirmed. The strongest trust the list shows. Your own connected node carries it too | Any firmware version | +| Nodes icon with a shield check (green) | **Signed node** — this node signs its broadcasts with its identity key, so its identity is consistent over time, but you have not verified it in person. On firmware 2.8 the icon appears from the version alone, before any signed broadcast has been heard | Firmware 2.8 or newer, and any node whose signed broadcast your node has verified | +| 🔒 Closed lock | A public key is on file and matches, so direct messages to this node are encrypted | Firmware before 2.8, or no reported version | +| 🔓 Open lock | No public key has been received for this node, so it cannot be direct messaged — use **Request User Info** on the node detail page to ask for one | Firmware before 2.8, or no reported version | +| ⚠️ Mismatch | **Public key mismatch** — a different key arrived for this node after one was stored. Investigate before trusting | Any firmware version | -> 💡 **Tip:** Direct messages always use PKI, so the radio needs the other node's public key before it can send one. It refuses the send rather than falling back to channel encryption. Keys arrive inside node info, which is why an open lock usually clears itself once that node is heard from properly. If you see a key mismatch warning, the node may have been reset or compromised. +Every node on firmware 2.8 or newer signs its broadcasts, so on that firmware the signed state is the baseline and the locks are not shown. -To clear a mismatch, first confirm through another trusted channel that the key change was intentional — a factory reset causes one. Then touch & hold the node, choose **Remove**, and let the two radios exchange keys again the next time yours hears it. +Direct messages always use public-key encryption, so your node needs the other node's public key before it can send one. It refuses the send rather than falling back to channel encryption. Keys arrive inside node info, which is why an open lock usually clears itself once that node is heard from properly. + +A mismatch never replaces the key you already hold. The app keeps the first key it recorded and refuses the new one, as the firmware does, so a stray or hostile node info cannot silently break encrypted messaging to a contact. To clear a mismatch, first confirm through another trusted channel that the key change was intentional — a factory reset causes one. Then touch & hold the node, choose **Remove**, and let the two nodes exchange keys again the next time yours hears it. ## Quick Actions @@ -108,7 +112,9 @@ radio is connected and running firmware 2.8 or newer — see ## Sharing a Contact -On a node's detail screen, tap **Share Contact** to produce a link and a QR code for that node. From the same dialog, **Write to NFC tag** saves the link to a writable NFC tag that anyone can tap to open. +On a node's detail screen, tap **Share Contact** to produce a link and a QR code for that node. From the same dialog, **Share link** opens the Android share sheet (on desktop it copies the link instead), **Write to NFC tag** saves it to a writable NFC tag, and **Copy** puts it on the clipboard. While that dialog is open and in front of you, the phone also offers the same link to any NFC reader, so someone can take the contact by tapping their phone against yours with no tag involved. + +Sharing your own contact this way marks it as verified in person, so whoever imports it sees the **Verified contact** icon rather than the signed one. Relaying someone else's contact passes on only what your app had already recorded about them. To add someone else's contact, use the import button on the node list and choose **Scan Shared Contact QR Code**, **Scan Shared Contact NFC**, or **Input Shared Contact URL**. The app asks you to confirm with **Import Shared Contact?**, and warns you when the contact is one you already have. @@ -120,14 +126,16 @@ Type in the search field to filter nodes by name or short name. The filter updat ### Filter Toggles -| 搜索节点 | 说明 | -| --------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| **Hide offline nodes** | Show only nodes heard within the last 2 hours | -| **Only show direct nodes** | Show only nodes your radio heard directly, with no relay in between | -| **Include unknown** | Show nodes that haven't sent user info yet. **On by default**, so a node heard before its info arrives stays visible; these carry a badge marking them incomplete, and cannot be direct messaged until their user info brings a public key | -| **Exclude infrastructure** | Hide infrastructure-role nodes (Router, Router Late, Client Base, and legacy Repeater nodes) and any node that cannot be messaged, whatever its role | -| **Exclude MQTT** | Hide nodes heard only via MQTT internet bridge | -| **Only show ignored Nodes** | Replace the list with the nodes you have ignored. Every other node is hidden while this is on, and a banner appears at the top of the list to take you back | +| 搜索节点 | 说明 | +| --------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **Hide offline nodes** | Show only nodes heard within the last 2 hours | +| **Only show direct nodes** | Show only nodes your radio heard directly, with no relay in between | +| **Include unknown** | Show nodes that haven't sent user info yet. **On by default**, so a node heard before its info arrives stays visible; these carry a badge marking them incomplete, and cannot be direct messaged until their user info brings a public key | +| **Exclude infrastructure** | Hide infrastructure-role nodes (Router, Router Late, Client Base, and legacy Repeater nodes) and any node that cannot be messaged, whatever its role | +| **Exclude MQTT** | Hide nodes heard only via MQTT internet bridge | +| **Signed only** | Show only nodes whose signed broadcasts your node has actually heard and verified. Stricter than the icon: a node on 2.8 shows as signed by its firmware version before any signed broadcast arrives, and a contact verified in person on older firmware is not signed at all | +| **Encrypted only** | Show nodes with a matching public key on file, the key an encrypted direct message needs. A node with a key mismatch is excluded | +| **Only show ignored Nodes** | Replace the list with the nodes you have ignored. Every other node is hidden while this is on, and a banner appears at the top of the list to take you back | ### Sort Options @@ -149,6 +157,8 @@ Tap the hop-histogram icon in the node list's app bar to open a bar chart of how Tapping a node opens the detail view with comprehensive information. See [Node Metrics](node-metrics) for full details on metrics and telemetry. +Signal quality is rated against your modem preset. The same SNR can be good on a long-range preset and poor on a faster one. Traceroute and neighbor-info SNR colors use that preset too. RSSI text has its own strength colors; it affects the quality rating only when a noise-floor reading is also available. + The Details card carries the node's short name, role, IDs, last heard time, hops away, uptime, and its SNR and RSSI: ![Node detail section](../../assets/screenshots/nodes_detail_section.png) diff --git a/docs/zh-rCN/user/settings-module-admin.md b/docs/zh-rCN/user/settings-module-admin.md index 286905eee8..595f1d46f5 100644 --- a/docs/zh-rCN/user/settings-module-admin.md +++ b/docs/zh-rCN/user/settings-module-admin.md @@ -2,7 +2,7 @@ title: Settings — Modules & Admin parent: User Guide nav_order: 8 -last_updated: 2026-08-30 +last_updated: 2026-09-16 description: Configure optional feature modules (MQTT, telemetry, canned messages, TAK, and more) and perform device administration. aliases: - modules @@ -333,7 +333,7 @@ appear only when your own node is selected: ### 关于 -**Settings → About** carries the app's own identity rather than the radio's: +**Settings → About** carries the app's own identity rather than the node's: Three sections: @@ -341,7 +341,11 @@ Three sections: - **Apps** — opens with **Need Hardware?**, a rotating carousel of popular devices that links out to where to buy one. It also lists the GitHub repository, the running app version, and **Acknowledgements** (see the next section). -- **Project information** — links to the website and to this documentation. +- **Project information** — links to the website, to this documentation, and to the GNU GPL v3 + license the app is released under. + +The footer states the copyright and that the app is free software with no warranty, which you +may redistribute under the same license. ### 开源 diff --git a/docs/zh-rCN/user/settings-radio-user.md b/docs/zh-rCN/user/settings-radio-user.md index c10ee66415..303d8c42aa 100644 --- a/docs/zh-rCN/user/settings-radio-user.md +++ b/docs/zh-rCN/user/settings-radio-user.md @@ -2,7 +2,7 @@ title: Settings — Radio & User parent: User Guide nav_order: 7 -last_updated: 2026-09-04 +last_updated: 2026-09-11 description: Configure your radio hardware, LoRa presets, user profile, position sharing, power management, and security. aliases: - 设置 @@ -51,7 +51,7 @@ The footer appears as soon as you change something. **Discard** throws the chang The status message is saved with the same **Save**, but it never reboots the node — and, like the rest of this screen, it can be edited on a remote node you administer. For your own radio there is a shortcut while it is connected: touch & hold your node in the [node list](nodes.md) and choose -**Update status**. Older firmware and a disconnected radio have no shortcut — the field above is +**Update status**. Older firmware and a disconnected node have no shortcut — the field itself is still the way in. ## Configuration @@ -77,13 +77,13 @@ On **Settings → LoRa**. | Setting | 说明 | 默认 | | ------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------- | -| 区域 | Regulatory region for frequency bands. You must set this before transmitting | Unset (must configure) | +| 区域 | Regulatory region for frequency bands. You must set this before transmitting. On firmware 2.8 or newer, the first region set also creates the node's identity key and gives it a new node number | Unset (must configure) | | 预设 | Speed/range tradeoff | LongFast | | 节点数 | Maximum retransmit hops | 3 | | 发送强度 | Transmission power (dBm); 0 = max allowed for region | 0 (region max) | | 频率覆盖 | Overrides the computed operating frequency outright (MHz). It does not offset the calculated value — leave at 0 unless you know you need a specific frequency | 0 (use calculated) | | 使用预设 | On by default. Turn it off to set Spread Factor, Coding Rate and Bandwidth by hand instead of taking them from the modem preset | On | -| 扩散因子 | Manual mode only: 7–12. Higher spreads further but slower | From preset | +| 扩散因子 | Manual mode only: 5–12. Higher spreads further but slower. On SX127x (RF95) radios the firmware does not accept 5 or 6 and uses 11 instead | From preset | | 编码率 | Manual mode only: 5–8. More redundancy costs airtime | From preset | | 带宽 | Manual mode only: the channel bandwidth in kHz, typed in directly. On the 2.4 GHz region the app offers a list of the bandwidths your radio supports instead, and a stored value that is not on that list shows as _Unsupported_ and blocks saving until you pick a supported one | From preset | | 频率时隙 | Which slot within the region's band to use. 0 derives it from the primary channel name | 0 (automatic) | @@ -111,13 +111,13 @@ The Lite, Narrow, Medium Turbo, and Tiny presets need firmware 2.8 or newer — | Short Slow | ~5 km | 6.25 kbps | −10 dB | Suburban short-range; moderate building density | | Medium Fast | ~5 km | 3.52 kbps | −12.5 dB | Suburban areas; moderate building density | | Medium Slow | ~8 km | 1.95 kbps | −15 dB | Suburban/rural; moderate range with slower speed | -| Long Turbo | ~10 km | 1.34 kbps | −12.5 dB | Similar range to Long Fast but with 500 kHz bandwidth; faster throughput | +| Long Turbo | ~10 km | 1.34 kbps | −17.5 dB | Similar range to Long Fast but with 500 kHz bandwidth; faster throughput | | Long Fast | ~10 km | 1.1 kbps | −17.5 dB | **General use (default)** — balanced range and speed | | Long Moderate | ~20 km | 0.34 kbps | −17.5 dB | Rural with some terrain; occasional use | | Lite Fast | ~5 km | 1.76 kbps | −12.5 dB | EU 866 MHz SRD band (125 kHz BW); comparable to Medium Fast | | Lite Slow | ~10 km | 0.98 kbps | −15 dB | EU 866 MHz SRD band (125 kHz BW); comparable to Long Fast | -| Narrow Fast | ~5 km | 2.28 kbps | −10 dB | EU 868 MHz band (62.5 kHz BW); avoids interference with other devices | -| Narrow Slow | ~10 km | 1.30 kbps | −12.5 dB | EU 868 MHz band (62.5 kHz BW); comparable to Long Fast | +| Narrow Fast | ~5 km | 2.28 kbps | −7.5 dB | EU 868 MHz band (62.5 kHz BW); avoids interference with other devices | +| Narrow Slow | ~10 km | 1.30 kbps | −10 dB | EU 868 MHz band (62.5 kHz BW); comparable to Long Fast | | Medium Turbo | ~5 km | 7.0 kbps | −12.5 dB | Like Medium Fast but with 500 kHz bandwidth; not legal in every region. Needs firmware 2.8 or newer | | Tiny Fast | ~10 km | 0.68 kbps | −7.5 dB | Amateur bands that cap occupied bandwidth; these presets use 15.6 kHz. Needs firmware 2.8 or newer, an SX126x or SX127x radio, and a TCXO of ±5 ppm or better | | Tiny Slow | ~20 km | 0.33 kbps | −10 dB | Same band restrictions as Tiny Fast, longer range. Same firmware, radio, and TCXO requirements | diff --git a/docs/zh-rCN/user/signal-meter.md b/docs/zh-rCN/user/signal-meter.md index 7985012e2e..54874bb8ee 100644 --- a/docs/zh-rCN/user/signal-meter.md +++ b/docs/zh-rCN/user/signal-meter.md @@ -2,7 +2,7 @@ title: How the Meshtastic Signal Meter Works parent: User Guide nav_order: 15 -last_updated: 2026-08-30 +last_updated: 2026-09-09 description: How the signal meter rates quality from SNR relative to the LoRa modem preset — spread spectrum, presets, and what the bars really mean. aliases: - signal @@ -32,7 +32,7 @@ Each modem preset has an SNR limit: the lowest SNR at which that preset can stil ## Rating Signal Quality -The app rates signal quality (None, Bad, Fair, or Good) from SNR alone, measured against the active preset's SNR limit. It does not factor in RSSI: without knowing the local noise floor, RSSI alone cannot say whether a signal is decodable. RSSI is still available — on the node detail screen and in the metrics charts. +The app rates signal quality (None, Bad, Fair, or Good) from SNR relative to the active preset's SNR limit. When RSSI and a noise-floor reading are both available, it also rates the difference between them against that limit and uses the worse rating. Otherwise, it uses SNR alone. Because the rating is relative to the preset, the same SNR rates differently on different presets. An SNR of −16 dB rates Good on Long Fast (SNR limit −17.5 dB) but None on Short Fast (SNR limit −7.5 dB). Let `limit` be the active preset's SNR limit: @@ -45,7 +45,7 @@ Because the rating is relative to the preset, the same SNR rates differently on The icon never goes blank, so count the bars carefully: a single bar means None, not a weak but usable link, and Good is a solid wedge rather than a set of bars. A gray three-bar icon labeled Unknown is a different state again — the packet carried no SNR measurement at all, which is not the same as measuring one and finding it too weak. -> ℹ️ **Note:** Traceroute hop colors use fixed thresholds (−7 dB / −15 dB); the per-node signal meter uses the preset-relative rating instead. +> ℹ️ **Note:** Traceroute and neighbor-info SNR colors use the active preset's limits too. RSSI text has separate strength colors: green above −115 dBm, yellow above −120 dBm, orange above −126 dBm, and red at or below −126 dBm. ## Diagnosing Local Interference diff --git a/docs/zh-rCN/user/tak.md b/docs/zh-rCN/user/tak.md index 1f7f8c6e6d..509ab35ed4 100644 --- a/docs/zh-rCN/user/tak.md +++ b/docs/zh-rCN/user/tak.md @@ -2,7 +2,7 @@ title: TAK Integration parent: User Guide nav_order: 10 -last_updated: 2026-08-30 +last_updated: 2026-09-11 description: Interoperate with ATAK and WinTAK — CoT position sharing, TAK roles, and plugin setup. aliases: - tak @@ -36,7 +36,7 @@ The TAK module allows Meshtastic nodes to: > ⚠️ **Warning:** The old **Meshtastic ATAK Plugin** is no longer part of this path and cannot > work. It bridged through the cross-process AIDL API, which was removed in app 2.8.0; the mesh > service is now in-process only. Do not install it. Interop today runs over the app's own local -> TAK server plus the Mesh to CoT Converter, both described below, with stock ATAK/iTAK/WinTAK. +> TAK server plus the Mesh to CoT Converter, both described in the following sections, with stock ATAK/iTAK/WinTAK. ### Configuration diff --git a/docs/zh-rCN/user/translate.md b/docs/zh-rCN/user/translate.md index 7965b41912..481c48bac2 100644 --- a/docs/zh-rCN/user/translate.md +++ b/docs/zh-rCN/user/translate.md @@ -2,7 +2,7 @@ title: Translate the App parent: User Guide nav_order: 17 -last_updated: 2026-08-30 +last_updated: 2026-09-11 description: How the app and its documentation are translated via Crowdin, and guidelines for contributing translations. aliases: - translate @@ -45,7 +45,7 @@ If your language is not yet listed on Crowdin: The Android app uses **Compose Multiplatform resources** for all user-visible strings: -``` +```text core/resources/src/commonMain/composeResources/ ├── values/ ← English (default) │ └── strings.xml @@ -58,7 +58,7 @@ core/resources/src/commonMain/composeResources/ In-app documentation follows a similar pattern under `docs/`: -``` +```text docs/ ├── en/user/ ← English source (default) │ ├── onboarding.md diff --git a/docs/zh-rTW/user/desktop.md b/docs/zh-rTW/user/desktop.md index 6587f58ae2..9ca528b242 100644 --- a/docs/zh-rTW/user/desktop.md +++ b/docs/zh-rTW/user/desktop.md @@ -2,7 +2,7 @@ title: 桌面版應用程式 parent: 使用者指南 nav_order: 14 -last_updated: 2026-08-30 +last_updated: 2026-09-11 description: 在 Linux、macOS 及 Windows 上安裝並使用 Meshtastic 桌面版應用程式——涵蓋連線方式、功能對等性與鍵盤快速鍵。 aliases: - 桌面 @@ -58,20 +58,20 @@ Bluetooth Low Energy is supported on desktop via the [Kable](https://github.com/ ## 功能對等性 -| 功能 | Android | 桌面版 | 備註 | -| ----------------------------------------------------- | ------- | --- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| 訊息傳送 | ✓ | ✓ | 完全對等 | -| 節點清單 | ✓ | ✓ | 完全對等 | -| 地圖 | ✓ | ✓ | Interactive MapLibre map, with base map and overlay pickers and custom tile sources. No offline downloads or local `.mbtiles` archives | -| Map layers (`.kml`/`.kmz`/GeoJSON) | ✓ | ✓ | Same layer store and sheet as Android; imported files draw on the desktop map | -| Site Planner | ✓ | ✓\* | \*Opens in your browser on desktop; the estimate is not drawn on the desktop map | -| 設定 | ✓ | ✓ | 完全對等 | -| 藍牙(BLE) | ✓ | ✓ | 桌面版透過 Kable 支援 | -| 韌體更新 | ✓ | ✓ | In-app USB, BLE, and Wi-Fi (ESP32) update work the same as Android. The USB maintenance flow — nRF52/RP2040 factory erase and bootloader upgrade — is Android-only | -| 通知 | ✓ | ✓ | 原生作業系統通知 | -| 小工具 | ✓ | ✗ | 僅限 Android | -| AI 助理(Chirpy) | ✓\* | ✗ | 僅限 Google 版 Android | -| App Functions (system AI) | ✓† | ✗ | 僅限 Google 版 Android | +| 功能 | Android | 桌面版 | 備註 | +| ----------------------------------------------------- | ------- | --- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| 訊息傳送 | ✓ | ✓ | 完全對等 | +| 節點清單 | ✓ | ✓ | 完全對等 | +| 地圖 | ✓ | ✓ | Interactive MapLibre map, with base map and overlay pickers and custom tile sources. Offline terrain (hillshade and contours) can be downloaded; offline base-map packs and local `.mbtiles` archives cannot | +| Map layers (`.kml`/`.kmz`/GeoJSON) | ✓ | ✓ | Same layer store and sheet as Android; imported files draw on the desktop map | +| Site Planner | ✓ | ✓\* | \*Opens in your browser on desktop; the estimate is not drawn on the desktop map | +| 設定 | ✓ | ✓ | 完全對等 | +| 藍牙(BLE) | ✓ | ✓ | 桌面版透過 Kable 支援 | +| 韌體更新 | ✓ | ✓ | In-app USB, BLE, and Wi-Fi (ESP32) update work the same as Android. The USB maintenance flow — nRF52/RP2040 factory erase and bootloader upgrade — is Android-only | +| 通知 | ✓ | ✓ | 原生作業系統通知 | +| 小工具 | ✓ | ✗ | 僅限 Android | +| AI 助理(Chirpy) | ✓\* | ✗ | 僅限 Google 版 Android | +| App Functions (system AI) | ✓† | ✗ | 僅限 Google 版 Android | \*Chirpy AI 需要 Google 版 Android 14 以上版本,且須搭配支援的硬體。 @@ -122,7 +122,7 @@ The desktop app includes a built-in documentation browser for quick access to he ## 從原始碼建置 -```bash +```shell git clone https://github.com/meshtastic/Meshtastic-Android.git cd Meshtastic-Android ./gradlew :desktopApp:run @@ -135,7 +135,7 @@ cd Meshtastic-Android ## 已知限制 -- Offline tile downloads and local `.mbtiles` archives are not available on desktop. +- Offline base-map downloads and local `.mbtiles` archives are not available on desktop. Offline terrain is — see [Map & Waypoints](map-and-waypoints). - `.kml`/`.kmz`/GeoJSON layer import works — see [Map & Waypoints](map-and-waypoints#map-layers). Site Planner opens in your browser rather than in the app; to bring its coverage estimate onto the map, click the transmitter pin diff --git a/docs/zh-rTW/user/discovery.md b/docs/zh-rTW/user/discovery.md index b36e93c161..5548163d10 100644 --- a/docs/zh-rTW/user/discovery.md +++ b/docs/zh-rTW/user/discovery.md @@ -2,7 +2,7 @@ title: Local Mesh Discovery parent: 使用者指南 nav_order: 12 -last_updated: 2026-08-30 +last_updated: 2026-09-11 description: Explore your mesh network — the Local Mesh Discovery scanner, traceroute paths, neighbor maps, and node discovery tools. aliases: - discovery @@ -97,7 +97,7 @@ Mesh Beacon lets nodes invite others to join their mesh. A beaconing node period Configure it under **Settings → Module configuration → Mesh Beacon**. The entry appears only on radios running firmware 2.8.0 or newer. A read-only **Region** row at the top of the screen shows the region the beacon advertises: that region, and the preset, are always the ones the radio itself uses, so a beacon cannot invite anyone onto settings your radio is not running. - **Listen for beacons** — receive invitations broadcast by other nodes. -- **Broadcast a beacon** — periodically advertise this mesh to nearby nodes, with an optional **Beacon message** of up to 100 bytes, a **Broadcast interval** picked from fixed intervals between 1 hour and 72 hours, and an **Offered channel** chosen from your radio's own channels. The offered channel is required, and defaults to your primary channel. +- **Broadcast a beacon** — periodically advertise this mesh to nearby nodes, with an optional **Beacon message** of up to 100 bytes, a **Broadcast interval** picked from fixed intervals between 1 hour and 72 hours, and an **Offered channel** chosen from your radio's own channels. The offered channel is required, and defaults to your primary channel. Over remote admin the picker offers the primary channel only. - **Broadcast targets** — optional extra destinations beyond the offered channel. **Add target** appends a row; each row picks a **Channel** and a **Transmit preset**, and **Remove target** deletes it. With no targets, the beacon goes out on the offered channel alone. Two conditions block beacon setup: diff --git a/docs/zh-rTW/user/help-and-docs.md b/docs/zh-rTW/user/help-and-docs.md index c21e24c457..559bbb157c 100644 --- a/docs/zh-rTW/user/help-and-docs.md +++ b/docs/zh-rTW/user/help-and-docs.md @@ -2,7 +2,7 @@ title: Help & In-App Docs parent: User Guide nav_order: 21 -last_updated: 2026-08-30 +last_updated: 2026-09-11 description: Browse this documentation inside the app, search it, and ask Chirpy — the on-device AI assistant — questions about Meshtastic. aliases: - help @@ -23,7 +23,7 @@ The docs browser lists every user-guide page. Tap a page to read it; images and ### 搜尋 -The search box sits at the top of the docs browser. Type in it to filter pages by title and keywords — results update as you type, and the **Clear search** button (✕) empties the box. +Tap the search field at the top of the docs browser to open a full-screen search view. Type to filter pages by title and keywords — results update as you type, under the same field. The back arrow, or the keyboard's search action, closes the view. The field hides while you scroll down the page list and comes back when you scroll up. ![Searching the in-app documentation](../../assets/screenshots/docs-browser_search.png) diff --git a/docs/zh-rTW/user/map-and-waypoints.md b/docs/zh-rTW/user/map-and-waypoints.md index 427b803359..8c76a667f4 100644 --- a/docs/zh-rTW/user/map-and-waypoints.md +++ b/docs/zh-rTW/user/map-and-waypoints.md @@ -2,7 +2,7 @@ title: 地圖與航點 parent: 使用者指南 nav_order: 6 -last_updated: 2026-09-01 +last_updated: 2026-09-11 description: View node positions on the map, create and share waypoints, manage map layers and Site Planner, and control position sharing and privacy. aliases: - map @@ -44,7 +44,7 @@ The floating toolbar provides quick access to the compass, the map type and laye ### Filtering the Map -Tap the filter button in the floating toolbar to open **Filter map**. **Display** controls what is drawn: **Only Favorites**, **Show Waypoints**, **Show Precision Circles**, and a slider that hides nodes not heard from recently. **Node roles** is a chip per device role, plus **All** to show every role; a selected chip means that role is shown. **Nodes** narrows the set further with **Hide offline nodes**, **Only show direct nodes**, **Exclude MQTT**, **Show ignored nodes**, and **Include unknown**. +Tap the filter button in the floating toolbar to open **Filter map**. **Display** controls what is drawn: **Only Favorites**, **Show Waypoints**, **Show Precision Circles**, and a slider that hides nodes not heard from recently. **Node roles** is a chip per device role, plus **All** to show every role; a selected chip means that role is shown. **Nodes** narrows the set further with **Hide offline nodes**, **Only show direct nodes**, **Signed only**, **Encrypted only**, **Exclude MQTT**, **Show ignored nodes**, and **Include unknown** — the same rules as the node list's [filter toggles](nodes). A dot on the filter button means at least one filter is hiding something — check it before concluding the mesh is quiet. Turning **Show Waypoints** off hides every waypoint, including your own. **Show ignored nodes** adds them to the map rather than showing only them — unlike the node list's **Only show ignored Nodes**. @@ -147,8 +147,9 @@ picker, all three offer the same raster base maps: Overlays can be toggled on top of any base map, from the layers sheet: - **Weather radar** — NOAA NEXRAD reflectivity (US coverage) -- **Hillshade** — terrain relief, on **F-Droid** and **Desktop** only. Useful for understanding why a - link fails, since LoRa range is limited by terrain +- **Hillshade** — terrain relief from an online tile source, on **F-Droid** and **Desktop** only; Google + Play draws hillshade from downloaded offline terrain instead. Useful for understanding why a link + fails, since LoRa range is limited by terrain ### Adding your own tile source @@ -156,7 +157,7 @@ Any XYZ tile endpoint can be added as a base map, on every flavor and on desktop Tile Sources** at the foot of the base map picker and paste a URL template using `{z}`, `{x}` and `{y}` — plus `{s}` if the provider uses rotating subdomains. A national mapping service, for example: -``` +```text https://wmts.geo.admin.ch/1.0.0/ch.swisstopo.pixelkarte-farbe/default/current/3857/{z}/{x}/{y}.jpeg ``` @@ -173,7 +174,18 @@ On **Google Play**, the layers sheet's **Offline Manager** section downloads its — water, roads and administrative boundaries extracted from the public Protomaps basemap dataset, drawn directly as map shapes rather than raster tiles. Frame the area you want, tap **Start Download**, and once it finishes flip **Show on map** to draw it (it's off by default so it never surprises you by covering up the -live map). You can still import a pre-made `.mbtiles` archive here too, as before. **Desktop** has neither. +live map). You can still import a pre-made `.mbtiles` archive here too, as before. **Desktop** has neither, though +the offline terrain described next does work there. + +### Offline terrain + +Hillshade and elevation contours can be downloaded for offline use as well, so a downloaded area still +shows why a link fails when there is no network. The terrain data comes from Mapterhorn and is credited on +the map. On **Google Play**, each downloaded region in the **Offline Manager** list has a **Download Terrain** +button and **Hillshade** and **Contours** switches; terrain attaches to that region and shares its storage limit, and an area +too large for terrain asks you to zoom in or pick a smaller region. On **F-Droid** and **Desktop**, the +layers sheet has an **Offline Terrain** section of its own: frame the area, tap **Start Download**, and the +hillshade and contours draw from the downloaded tiles from then on. ### Going offline @@ -181,8 +193,8 @@ An **Offline** pill appears over the map whenever the device has no network conn builds, if you've imported a local `.mbtiles` archive, the map switches to it automatically the moment the network drops — no toggle to remember — and switches back once you're reconnected, as long as you haven't picked a different base map yourself in between. On **F-Droid**, a downloaded offline area keeps rendering on -its own; the pill is purely informational there. **Desktop** has no offline downloads at all yet, so the pill -is informational there too. +its own; the pill is purely informational there. **Desktop** downloads offline terrain but no base-map +packs, so the pill is informational there too. ## 相關主題 diff --git a/docs/zh-rTW/user/messages-and-channels.md b/docs/zh-rTW/user/messages-and-channels.md index 103b27eed9..c86f1d4f08 100644 --- a/docs/zh-rTW/user/messages-and-channels.md +++ b/docs/zh-rTW/user/messages-and-channels.md @@ -2,7 +2,7 @@ title: 訊息與頻道 parent: 使用者指南 nav_order: 3 -last_updated: 2026-08-30 +last_updated: 2026-09-14 description: Send and receive messages, manage channels, configure encryption, search conversations, and use quick chat, reactions, and message actions. aliases: - 頻道 @@ -50,6 +50,21 @@ Key length alone does not change the icon: a 128-bit key and a 256-bit key both Tapping an existing channel opens the same editor, where you can change the name, the PSK, MQTT uplink and downlink, and position precision. Every edit on this screen — adding, editing, deleting, or dragging a channel into a new order — waits on **Send** the same way. +### Archived Channels + +A conversation belongs to the channel it was held on, not to the slot that channel occupied. When a channel leaves your radio — you scan a different channel QR, replace a channel's key, or switch the modem preset — its conversation is **archived** rather than deleted or merged into whatever takes the slot. + +An archived conversation: + +- stays in the **Channels** list under the name the channel had, below the channels currently on your radio, marked with a history icon; +- keeps its full message history, searchable as usual; +- is read-only — you cannot send, reply, or react, because the channel is no longer on the radio to send on, and it is not offered as a share target; +- comes back automatically if you re-add that exact channel, taking its messages back to the live slot. + +Changing the modem preset archives the channel too: the preset is part of a default channel's identity, so **LongFast** and **MediumFast** are different channels even though neither the name nor the key changed. Changing only the region does not archive anything. + +To remove an archived conversation for good, long-press it in the conversation list and delete it. + ## 私訊 Direct messages (DMs) go to one specific node. When both radios hold each other's public keys, your radio encrypts the message to that node's public key, so no one else on the mesh can read it — not even nodes that share your channel. diff --git a/docs/zh-rTW/user/mqtt.md b/docs/zh-rTW/user/mqtt.md index a9653a48ce..a722757fc9 100644 --- a/docs/zh-rTW/user/mqtt.md +++ b/docs/zh-rTW/user/mqtt.md @@ -2,7 +2,7 @@ title: MQTT parent: 使用者指南 nav_order: 11 -last_updated: 2026-08-30 +last_updated: 2026-09-11 description: 將您的 mesh 網路橋接至網際網路 — MQTT 代理伺服器設定、加密層級與地圖回報。 aliases: - MQTT @@ -25,7 +25,7 @@ MQTT 模組可將您的節點連接至 MQTT 代理伺服器,實現以下功能 ## 運作方式 -``` +```text [Your Node] → Radio → [Gateway Node with Wi-Fi] → MQTT Broker → [Remote Gateway] → Radio → [Remote Node] ``` diff --git a/docs/zh-rTW/user/node-metrics.md b/docs/zh-rTW/user/node-metrics.md index 25acf11bb9..2ab49c01a5 100644 --- a/docs/zh-rTW/user/node-metrics.md +++ b/docs/zh-rTW/user/node-metrics.md @@ -2,7 +2,7 @@ title: Node Metrics parent: User Guide nav_order: 5 -last_updated: 2026-08-30 +last_updated: 2026-09-09 description: Telemetry dashboards for each mesh node — device health, environment sensors, air quality, signal quality, power, traceroute, and position history. aliases: - metrics @@ -114,7 +114,7 @@ Radio signal quality information: ### Signal Quality Reference -Signal quality is rated from **SNR relative to the active LoRa modem preset's demodulation floor**, not from fixed thresholds — a given SNR means different things on different presets (e.g. −15 dB is fine on LongSlow but unusable on ShortFast). RSSI is shown but is not part of the rating. In the table, _limit_ is the preset's SNR limit. +Signal quality is rated from **SNR relative to the active LoRa modem preset's demodulation floor**, not from fixed thresholds — a given SNR means different things on different presets (e.g. −15 dB is fine on LongSlow but unusable on ShortFast). When RSSI and a noise-floor reading are both available, the app also rates their difference against the preset limit and uses the worse rating. Otherwise, RSSI is display-only. In the table, _limit_ is the preset's SNR limit. | Quality | Criteria | | ------- | ---------------------------------------------------------------- | diff --git a/docs/zh-rTW/user/nodes.md b/docs/zh-rTW/user/nodes.md index 52d3caada6..31c953b696 100644 --- a/docs/zh-rTW/user/nodes.md +++ b/docs/zh-rTW/user/nodes.md @@ -2,7 +2,7 @@ title: 節點 parent: User Guide nav_order: 4 -last_updated: 2026-09-04 +last_updated: 2026-09-11 description: Browse, filter, and sort mesh nodes — view details, signal quality, roles, and quick actions. aliases: - node-list @@ -74,19 +74,23 @@ Most users should keep the default **Client** role. Consider a different role wh > 💡 **Tip:** The mesh works best when most nodes are **Client** or **Router**. Too many Client Mute nodes reduce mesh resilience; too many Routers in a dense area can cause congestion. A good rule of thumb: one Router per 5–10 Clients in your area. -### Encryption Indicators +### Security indicators -Nodes display encryption status icons next to their name: +Each node carries one security icon beside its name in the node list. Tap it to read what it means, and choose **Show All Meanings** in that dialog for the full legend. The detail screen shows the same state in words, as a **Security** row that opens the same dialog. -| Icon | Meaning | -| ----------- | ------------------------------------------------------------------------------------------------------------------------------------------------- | -| 🔒 Locked | Communication uses PKI (public key infrastructure) — end-to-end encrypted with verified identity | -| 🔓 Unlocked | No public key has been received for this node, so it cannot be direct messaged — use **Request User Info** on the node detail page to ask for one | -| ⚠️ Mismatch | Public key mismatch — the node's key has changed since last seen (investigate before trusting) | +| Icon | Meaning | Shown for | +| --------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------- | +| Person with a shield check (green) | **Verified contact** — you verified this node's key in person by exchanging contact QR codes, so its identity is confirmed. The strongest trust the list shows. Your own connected node carries it too | Any firmware version | +| Nodes icon with a shield check (green) | **Signed node** — this node signs its broadcasts with its identity key, so its identity is consistent over time, but you have not verified it in person. On firmware 2.8 the icon appears from the version alone, before any signed broadcast has been heard | Firmware 2.8 or newer, and any node whose signed broadcast your node has verified | +| 🔒 Closed lock | A public key is on file and matches, so direct messages to this node are encrypted | Firmware before 2.8, or no reported version | +| 🔓 Open lock | No public key has been received for this node, so it cannot be direct messaged — use **Request User Info** on the node detail page to ask for one | Firmware before 2.8, or no reported version | +| ⚠️ Mismatch | **Public key mismatch** — a different key arrived for this node after one was stored. Investigate before trusting | Any firmware version | -> 💡 **Tip:** Direct messages always use PKI, so the radio needs the other node's public key before it can send one. It refuses the send rather than falling back to channel encryption. Keys arrive inside node info, which is why an open lock usually clears itself once that node is heard from properly. If you see a key mismatch warning, the node may have been reset or compromised. +Every node on firmware 2.8 or newer signs its broadcasts, so on that firmware the signed state is the baseline and the locks are not shown. -To clear a mismatch, first confirm through another trusted channel that the key change was intentional — a factory reset causes one. Then touch & hold the node, choose **Remove**, and let the two radios exchange keys again the next time yours hears it. +Direct messages always use public-key encryption, so your node needs the other node's public key before it can send one. It refuses the send rather than falling back to channel encryption. Keys arrive inside node info, which is why an open lock usually clears itself once that node is heard from properly. + +A mismatch never replaces the key you already hold. The app keeps the first key it recorded and refuses the new one, as the firmware does, so a stray or hostile node info cannot silently break encrypted messaging to a contact. To clear a mismatch, first confirm through another trusted channel that the key change was intentional — a factory reset causes one. Then touch & hold the node, choose **Remove**, and let the two nodes exchange keys again the next time yours hears it. ## Quick Actions @@ -108,7 +112,9 @@ radio is connected and running firmware 2.8 or newer — see ## Sharing a Contact -On a node's detail screen, tap **Share Contact** to produce a link and a QR code for that node. From the same dialog, **Write to NFC tag** saves the link to a writable NFC tag that anyone can tap to open. +On a node's detail screen, tap **Share Contact** to produce a link and a QR code for that node. From the same dialog, **Share link** opens the Android share sheet (on desktop it copies the link instead), **Write to NFC tag** saves it to a writable NFC tag, and **Copy** puts it on the clipboard. While that dialog is open and in front of you, the phone also offers the same link to any NFC reader, so someone can take the contact by tapping their phone against yours with no tag involved. + +Sharing your own contact this way marks it as verified in person, so whoever imports it sees the **Verified contact** icon rather than the signed one. Relaying someone else's contact passes on only what your app had already recorded about them. To add someone else's contact, use the import button on the node list and choose **Scan Shared Contact QR Code**, **Scan Shared Contact NFC**, or **Input Shared Contact URL**. The app asks you to confirm with **Import Shared Contact?**, and warns you when the contact is one you already have. @@ -120,14 +126,16 @@ Type in the search field to filter nodes by name or short name. The filter updat ### Filter Toggles -| 過濾器 | 描述說明 | -| --------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| **Hide offline nodes** | Show only nodes heard within the last 2 hours | -| **Only show direct nodes** | Show only nodes your radio heard directly, with no relay in between | -| **Include unknown** | Show nodes that haven't sent user info yet. **On by default**, so a node heard before its info arrives stays visible; these carry a badge marking them incomplete, and cannot be direct messaged until their user info brings a public key | -| **Exclude infrastructure** | Hide infrastructure-role nodes (Router, Router Late, Client Base, and legacy Repeater nodes) and any node that cannot be messaged, whatever its role | -| **Exclude MQTT** | Hide nodes heard only via MQTT internet bridge | -| **Only show ignored Nodes** | Replace the list with the nodes you have ignored. Every other node is hidden while this is on, and a banner appears at the top of the list to take you back | +| 過濾器 | 描述說明 | +| --------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **Hide offline nodes** | Show only nodes heard within the last 2 hours | +| **Only show direct nodes** | Show only nodes your radio heard directly, with no relay in between | +| **Include unknown** | Show nodes that haven't sent user info yet. **On by default**, so a node heard before its info arrives stays visible; these carry a badge marking them incomplete, and cannot be direct messaged until their user info brings a public key | +| **Exclude infrastructure** | Hide infrastructure-role nodes (Router, Router Late, Client Base, and legacy Repeater nodes) and any node that cannot be messaged, whatever its role | +| **Exclude MQTT** | Hide nodes heard only via MQTT internet bridge | +| **Signed only** | Show only nodes whose signed broadcasts your node has actually heard and verified. Stricter than the icon: a node on 2.8 shows as signed by its firmware version before any signed broadcast arrives, and a contact verified in person on older firmware is not signed at all | +| **Encrypted only** | Show nodes with a matching public key on file, the key an encrypted direct message needs. A node with a key mismatch is excluded | +| **Only show ignored Nodes** | Replace the list with the nodes you have ignored. Every other node is hidden while this is on, and a banner appears at the top of the list to take you back | ### Sort Options @@ -149,6 +157,8 @@ Tap the hop-histogram icon in the node list's app bar to open a bar chart of how Tapping a node opens the detail view with comprehensive information. See [Node Metrics](node-metrics) for full details on metrics and telemetry. +Signal quality is rated against your modem preset. The same SNR can be good on a long-range preset and poor on a faster one. Traceroute and neighbor-info SNR colors use that preset too. RSSI text has its own strength colors; it affects the quality rating only when a noise-floor reading is also available. + The Details card carries the node's short name, role, IDs, last heard time, hops away, uptime, and its SNR and RSSI: ![Node detail section](../../assets/screenshots/nodes_detail_section.png) diff --git a/docs/zh-rTW/user/settings-module-admin.md b/docs/zh-rTW/user/settings-module-admin.md index 4bf1393bd5..d256b9f5fc 100644 --- a/docs/zh-rTW/user/settings-module-admin.md +++ b/docs/zh-rTW/user/settings-module-admin.md @@ -2,7 +2,7 @@ title: Settings — Modules & Admin parent: User Guide nav_order: 8 -last_updated: 2026-08-30 +last_updated: 2026-09-16 description: Configure optional feature modules (MQTT, telemetry, canned messages, TAK, and more) and perform device administration. aliases: - modules @@ -333,7 +333,7 @@ appear only when your own node is selected: ### About(關於) -**Settings → About** carries the app's own identity rather than the radio's: +**Settings → About** carries the app's own identity rather than the node's: Three sections: @@ -341,7 +341,11 @@ Three sections: - **Apps** — opens with **Need Hardware?**, a rotating carousel of popular devices that links out to where to buy one. It also lists the GitHub repository, the running app version, and **Acknowledgements** (see the next section). -- **Project information** — links to the website and to this documentation. +- **Project information** — links to the website, to this documentation, and to the GNU GPL v3 + license the app is released under. + +The footer states the copyright and that the app is free software with no warranty, which you +may redistribute under the same license. ### 致謝 diff --git a/docs/zh-rTW/user/settings-radio-user.md b/docs/zh-rTW/user/settings-radio-user.md index 0b37eb53b4..21f56f6a8c 100644 --- a/docs/zh-rTW/user/settings-radio-user.md +++ b/docs/zh-rTW/user/settings-radio-user.md @@ -2,7 +2,7 @@ title: Settings — Radio & User parent: User Guide nav_order: 7 -last_updated: 2026-09-04 +last_updated: 2026-09-11 description: Configure your radio hardware, LoRa presets, user profile, position sharing, power management, and security. aliases: - 設定 @@ -51,7 +51,7 @@ The footer appears as soon as you change something. **Discard** throws the chang The status message is saved with the same **Save**, but it never reboots the node — and, like the rest of this screen, it can be edited on a remote node you administer. For your own radio there is a shortcut while it is connected: touch & hold your node in the [node list](nodes.md) and choose -**Update status**. Older firmware and a disconnected radio have no shortcut — the field above is +**Update status**. Older firmware and a disconnected node have no shortcut — the field itself is still the way in. ## 設定 @@ -77,13 +77,13 @@ On **Settings → LoRa**. | 設定 | 描述說明 | 默認 | | ---------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------- | -| 地區 | Regulatory region for frequency bands. You must set this before transmitting | Unset (must configure) | +| 地區 | Regulatory region for frequency bands. You must set this before transmitting. On firmware 2.8 or newer, the first region set also creates the node's identity key and gives it a new node number | Unset (must configure) | | 預設配置 | Speed/range tradeoff | LongFast | | 中繼次數 | Maximum retransmit hops | 3 | | 傳輸功率 | Transmission power (dBm); 0 = max allowed for region | 0 (region max) | | 手動設定頻率 | Overrides the computed operating frequency outright (MHz). It does not offset the calculated value — leave at 0 unless you know you need a specific frequency | 0 (use calculated) | | 使用預設值 | On by default. Turn it off to set Spread Factor, Coding Rate and Bandwidth by hand instead of taking them from the modem preset | On | -| 擴頻因子 | Manual mode only: 7–12. Higher spreads further but slower | From preset | +| 擴頻因子 | Manual mode only: 5–12. Higher spreads further but slower. On SX127x (RF95) radios the firmware does not accept 5 or 6 and uses 11 instead | From preset | | 編碼率 | Manual mode only: 5–8. More redundancy costs airtime | From preset | | 頻寬 | Manual mode only: the channel bandwidth in kHz, typed in directly. On the 2.4 GHz region the app offers a list of the bandwidths your radio supports instead, and a stored value that is not on that list shows as _Unsupported_ and blocks saving until you pick a supported one | From preset | | 頻率槽 | Which slot within the region's band to use. 0 derives it from the primary channel name | 0 (automatic) | @@ -111,13 +111,13 @@ The Lite, Narrow, Medium Turbo, and Tiny presets need firmware 2.8 or newer — | 短 慢 | ~5 km | 6.25 kbps | −10 dB | Suburban short-range; moderate building density | | 中等快 | ~5 km | 3.52 kbps | −12.5 dB | Suburban areas; moderate building density | | 中等慢 | ~8 km | 1.95 kbps | −15 dB | Suburban/rural; moderate range with slower speed | -| Long Turbo | ~10 km | 1.34 kbps | −12.5 dB | Similar range to Long Fast but with 500 kHz bandwidth; faster throughput | +| Long Turbo | ~10 km | 1.34 kbps | −17.5 dB | Similar range to Long Fast but with 500 kHz bandwidth; faster throughput | | Long Fast | ~10 km | 1.1 kbps | −17.5 dB | **General use (default)** — balanced range and speed | | 長度中等的 | ~20 km | 0.34 kbps | −17.5 dB | Rural with some terrain; occasional use | | Lite Fast | ~5 km | 1.76 kbps | −12.5 dB | EU 866 MHz SRD band (125 kHz BW); comparable to Medium Fast | | Lite Slow | ~10 km | 0.98 kbps | −15 dB | EU 866 MHz SRD band (125 kHz BW); comparable to Long Fast | -| Narrow Fast | ~5 km | 2.28 kbps | −10 dB | EU 868 MHz band (62.5 kHz BW); avoids interference with other devices | -| Narrow Slow | ~10 km | 1.30 kbps | −12.5 dB | EU 868 MHz band (62.5 kHz BW); comparable to Long Fast | +| Narrow Fast | ~5 km | 2.28 kbps | −7.5 dB | EU 868 MHz band (62.5 kHz BW); avoids interference with other devices | +| Narrow Slow | ~10 km | 1.30 kbps | −10 dB | EU 868 MHz band (62.5 kHz BW); comparable to Long Fast | | Medium Turbo | ~5 km | 7.0 kbps | −12.5 dB | Like Medium Fast but with 500 kHz bandwidth; not legal in every region. Needs firmware 2.8 or newer | | Tiny Fast | ~10 km | 0.68 kbps | −7.5 dB | Amateur bands that cap occupied bandwidth; these presets use 15.6 kHz. Needs firmware 2.8 or newer, an SX126x or SX127x radio, and a TCXO of ±5 ppm or better | | Tiny Slow | ~20 km | 0.33 kbps | −10 dB | Same band restrictions as Tiny Fast, longer range. Same firmware, radio, and TCXO requirements | diff --git a/docs/zh-rTW/user/signal-meter.md b/docs/zh-rTW/user/signal-meter.md index 9d0ff38d7a..1573b18299 100644 --- a/docs/zh-rTW/user/signal-meter.md +++ b/docs/zh-rTW/user/signal-meter.md @@ -2,7 +2,7 @@ title: How the Meshtastic Signal Meter Works parent: User Guide nav_order: 15 -last_updated: 2026-08-30 +last_updated: 2026-09-09 description: How the signal meter rates quality from SNR relative to the LoRa modem preset — spread spectrum, presets, and what the bars really mean. aliases: - signal @@ -32,7 +32,7 @@ Each modem preset has an SNR limit: the lowest SNR at which that preset can stil ## Rating Signal Quality -The app rates signal quality (None, Bad, Fair, or Good) from SNR alone, measured against the active preset's SNR limit. It does not factor in RSSI: without knowing the local noise floor, RSSI alone cannot say whether a signal is decodable. RSSI is still available — on the node detail screen and in the metrics charts. +The app rates signal quality (None, Bad, Fair, or Good) from SNR relative to the active preset's SNR limit. When RSSI and a noise-floor reading are both available, it also rates the difference between them against that limit and uses the worse rating. Otherwise, it uses SNR alone. Because the rating is relative to the preset, the same SNR rates differently on different presets. An SNR of −16 dB rates Good on Long Fast (SNR limit −17.5 dB) but None on Short Fast (SNR limit −7.5 dB). Let `limit` be the active preset's SNR limit: @@ -163,7 +163,7 @@ Meshtastic is a project that allows you to use inexpensive LoRa radios as a long The icon never goes blank, so count the bars carefully: a single bar means None, not a weak but usable link, and Good is a solid wedge rather than a set of bars. A gray three-bar icon labeled Unknown is a different state again — the packet carried no SNR measurement at all, which is not the same as measuring one and finding it too weak. -> ℹ️ **Note:** Traceroute hop colors use fixed thresholds (−7 dB / −15 dB); the per-node signal meter uses the preset-relative rating instead. +> ℹ️ **Note:** Traceroute and neighbor-info SNR colors use the active preset's limits too. RSSI text has separate strength colors: green above −115 dBm, yellow above −120 dBm, orange above −126 dBm, and red at or below −126 dBm. ## Diagnosing Local Interference diff --git a/docs/zh-rTW/user/tak.md b/docs/zh-rTW/user/tak.md index ef7f249fa9..b3f7292e92 100644 --- a/docs/zh-rTW/user/tak.md +++ b/docs/zh-rTW/user/tak.md @@ -2,7 +2,7 @@ title: TAK Integration parent: User Guide nav_order: 10 -last_updated: 2026-08-30 +last_updated: 2026-09-11 description: Interoperate with ATAK and WinTAK — CoT position sharing, TAK roles, and plugin setup. aliases: - tak @@ -36,7 +36,7 @@ The TAK module allows Meshtastic nodes to: > ⚠️ **Warning:** The old **Meshtastic ATAK Plugin** is no longer part of this path and cannot > work. It bridged through the cross-process AIDL API, which was removed in app 2.8.0; the mesh > service is now in-process only. Do not install it. Interop today runs over the app's own local -> TAK server plus the Mesh to CoT Converter, both described below, with stock ATAK/iTAK/WinTAK. +> TAK server plus the Mesh to CoT Converter, both described in the following sections, with stock ATAK/iTAK/WinTAK. ### 設定 diff --git a/docs/zh-rTW/user/translate.md b/docs/zh-rTW/user/translate.md index ef4aef069d..ffe591eef6 100644 --- a/docs/zh-rTW/user/translate.md +++ b/docs/zh-rTW/user/translate.md @@ -2,7 +2,7 @@ title: Translate the App parent: User Guide nav_order: 17 -last_updated: 2026-08-30 +last_updated: 2026-09-11 description: How the app and its documentation are translated via Crowdin, and guidelines for contributing translations. aliases: - translate @@ -45,7 +45,7 @@ If your language is not yet listed on Crowdin: The Android app uses **Compose Multiplatform resources** for all user-visible strings: -``` +```text core/resources/src/commonMain/composeResources/ ├── values/ ← English (default) │ └── strings.xml @@ -58,7 +58,7 @@ core/resources/src/commonMain/composeResources/ In-app documentation follows a similar pattern under `docs/`: -``` +```text docs/ ├── en/user/ ← English source (default) │ ├── onboarding.md diff --git a/fastlane/Fastfile b/fastlane/Fastfile index 3e3a410c39..30b59945c8 100644 --- a/fastlane/Fastfile +++ b/fastlane/Fastfile @@ -1,17 +1,7 @@ -# This file contains the fastlane.tools configuration -# You can find the documentation at https://docs.fastlane.tools -# -# For a list of all available actions, check out -# -# https://docs.fastlane.tools/actions -# -# For a list of all available plugins, check out -# -# https://docs.fastlane.tools/plugins/available-plugins -# - -# Uncomment the line if you want fastlane to automatically update itself -# update_fastlane +# Lanes are invoked by the workflows in .github/workflows (release.yml, +# promote.yml, play-listing.yml); `bundle exec fastlane lanes` lists them. +# Play credentials come from fastlane/play-store-credentials.json, written by +# the workflow from the GOOGLE_PLAY_JSON_KEY secret and deleted afterwards. default_platform(:android) @@ -19,7 +9,7 @@ platform :android do desc "Deploy a new version to the internal track on Google Play" lane :internal do |options| aab_path = build_google_release - + upload_to_play_store( track: 'internal', aab: aab_path, @@ -32,6 +22,33 @@ platform :android do ) end + desc "Upload the store listing - title, descriptions, feature graphic, icon and screenshots - for every locale under fastlane/metadata/android. Touches no build or track. Dry-runs unless validate_only:false" + lane :play_listing do |options| + # Anything but an explicit `validate_only:false` stays a dry run, so a typo + # in the dispatch input can only ever under-publish. + validate_only = options[:validate_only].to_s != 'false' + + upload_to_play_store( + # Only changelogs are per-track, and those are skipped below; the track + # is named so supply has a release to resolve, nothing on it changes. + track: 'production', + skip_upload_apk: true, + skip_upload_aab: true, + skip_upload_metadata: false, + skip_upload_changelogs: true, + skip_upload_images: false, + skip_upload_screenshots: false, + # sha256-compare images and screenshots so an unchanged asset is not + # re-uploaded, and a run with nothing to change produces no edit. + sync_image_upload: true, + # A committed listing that is sent for review cancels and restarts any + # release review in flight (see promote.yml). Hold it; a human sends it + # for review from the Play Console. + changes_not_sent_for_review: true, + validate_only: validate_only, + ) + end + desc "Build the F-Droid release" lane :fdroid_build do gradle( diff --git a/fastlane/README.md b/fastlane/README.md index 6f22d168f3..f248380e91 100644 --- a/fastlane/README.md +++ b/fastlane/README.md @@ -23,6 +23,14 @@ For _fastlane_ installation instructions, see [Installing _fastlane_](https://do Deploy a new version to the internal track on Google Play +### android play_listing + +```sh +[bundle exec] fastlane android play_listing +``` + +Upload the store listing - title, descriptions, feature graphic, icon and screenshots - for every locale under fastlane/metadata/android. Touches no build or track. Dry-runs unless validate_only:false + ### android fdroid_build ```sh diff --git a/fastlane/metadata/android/ar-SA/changelogs/default.txt b/fastlane/metadata/android/ar-SA/changelogs/default.txt deleted file mode 100644 index 0553de284f..0000000000 --- a/fastlane/metadata/android/ar-SA/changelogs/default.txt +++ /dev/null @@ -1 +0,0 @@ -For detailed release notes, please visit: https://github.com/meshtastic/Meshtastic-Android/releases/ \ No newline at end of file diff --git a/fastlane/metadata/android/ar/changelogs/default.txt b/fastlane/metadata/android/ar/changelogs/default.txt new file mode 100644 index 0000000000..76874cc530 --- /dev/null +++ b/fastlane/metadata/android/ar/changelogs/default.txt @@ -0,0 +1,3 @@ +Stability and reliability fixes. + +Full release notes: https://github.com/meshtastic/Meshtastic-Android/releases diff --git a/fastlane/metadata/android/ar-SA/full_description.txt b/fastlane/metadata/android/ar/full_description.txt similarity index 100% rename from fastlane/metadata/android/ar-SA/full_description.txt rename to fastlane/metadata/android/ar/full_description.txt diff --git a/fastlane/metadata/android/ar-SA/short_description.txt b/fastlane/metadata/android/ar/short_description.txt similarity index 100% rename from fastlane/metadata/android/ar-SA/short_description.txt rename to fastlane/metadata/android/ar/short_description.txt diff --git a/fastlane/metadata/android/ar-SA/title.txt b/fastlane/metadata/android/ar/title.txt similarity index 100% rename from fastlane/metadata/android/ar-SA/title.txt rename to fastlane/metadata/android/ar/title.txt diff --git a/fastlane/metadata/android/be-BY/changelogs/default.txt b/fastlane/metadata/android/be-BY/changelogs/default.txt deleted file mode 100644 index 0553de284f..0000000000 --- a/fastlane/metadata/android/be-BY/changelogs/default.txt +++ /dev/null @@ -1 +0,0 @@ -For detailed release notes, please visit: https://github.com/meshtastic/Meshtastic-Android/releases/ \ No newline at end of file diff --git a/fastlane/metadata/android/be/changelogs/default.txt b/fastlane/metadata/android/be/changelogs/default.txt new file mode 100644 index 0000000000..76874cc530 --- /dev/null +++ b/fastlane/metadata/android/be/changelogs/default.txt @@ -0,0 +1,3 @@ +Stability and reliability fixes. + +Full release notes: https://github.com/meshtastic/Meshtastic-Android/releases diff --git a/fastlane/metadata/android/be-BY/full_description.txt b/fastlane/metadata/android/be/full_description.txt similarity index 100% rename from fastlane/metadata/android/be-BY/full_description.txt rename to fastlane/metadata/android/be/full_description.txt diff --git a/fastlane/metadata/android/be-BY/short_description.txt b/fastlane/metadata/android/be/short_description.txt similarity index 100% rename from fastlane/metadata/android/be-BY/short_description.txt rename to fastlane/metadata/android/be/short_description.txt diff --git a/fastlane/metadata/android/be-BY/title.txt b/fastlane/metadata/android/be/title.txt similarity index 100% rename from fastlane/metadata/android/be-BY/title.txt rename to fastlane/metadata/android/be/title.txt diff --git a/fastlane/metadata/android/bg-BG/changelogs/default.txt b/fastlane/metadata/android/bg-BG/changelogs/default.txt deleted file mode 100644 index 110f893ee0..0000000000 --- a/fastlane/metadata/android/bg-BG/changelogs/default.txt +++ /dev/null @@ -1 +0,0 @@ -За подробни бележки за изданието, моля, посетете: https://github.com/meshtastic/Meshtastic-Android/releases/ \ No newline at end of file diff --git a/fastlane/metadata/android/bg/changelogs/default.txt b/fastlane/metadata/android/bg/changelogs/default.txt new file mode 100644 index 0000000000..76874cc530 --- /dev/null +++ b/fastlane/metadata/android/bg/changelogs/default.txt @@ -0,0 +1,3 @@ +Stability and reliability fixes. + +Full release notes: https://github.com/meshtastic/Meshtastic-Android/releases diff --git a/fastlane/metadata/android/bg-BG/full_description.txt b/fastlane/metadata/android/bg/full_description.txt similarity index 100% rename from fastlane/metadata/android/bg-BG/full_description.txt rename to fastlane/metadata/android/bg/full_description.txt diff --git a/fastlane/metadata/android/bg-BG/short_description.txt b/fastlane/metadata/android/bg/short_description.txt similarity index 100% rename from fastlane/metadata/android/bg-BG/short_description.txt rename to fastlane/metadata/android/bg/short_description.txt diff --git a/fastlane/metadata/android/bg-BG/title.txt b/fastlane/metadata/android/bg/title.txt similarity index 100% rename from fastlane/metadata/android/bg-BG/title.txt rename to fastlane/metadata/android/bg/title.txt diff --git a/fastlane/metadata/android/ca-ES/changelogs/default.txt b/fastlane/metadata/android/ca-ES/changelogs/default.txt deleted file mode 100644 index 0553de284f..0000000000 --- a/fastlane/metadata/android/ca-ES/changelogs/default.txt +++ /dev/null @@ -1 +0,0 @@ -For detailed release notes, please visit: https://github.com/meshtastic/Meshtastic-Android/releases/ \ No newline at end of file diff --git a/fastlane/metadata/android/ca/changelogs/default.txt b/fastlane/metadata/android/ca/changelogs/default.txt new file mode 100644 index 0000000000..76874cc530 --- /dev/null +++ b/fastlane/metadata/android/ca/changelogs/default.txt @@ -0,0 +1,3 @@ +Stability and reliability fixes. + +Full release notes: https://github.com/meshtastic/Meshtastic-Android/releases diff --git a/fastlane/metadata/android/ca-ES/full_description.txt b/fastlane/metadata/android/ca/full_description.txt similarity index 100% rename from fastlane/metadata/android/ca-ES/full_description.txt rename to fastlane/metadata/android/ca/full_description.txt diff --git a/fastlane/metadata/android/ca-ES/short_description.txt b/fastlane/metadata/android/ca/short_description.txt similarity index 100% rename from fastlane/metadata/android/ca-ES/short_description.txt rename to fastlane/metadata/android/ca/short_description.txt diff --git a/fastlane/metadata/android/ca-ES/title.txt b/fastlane/metadata/android/ca/title.txt similarity index 100% rename from fastlane/metadata/android/ca-ES/title.txt rename to fastlane/metadata/android/ca/title.txt diff --git a/fastlane/metadata/android/cs-CZ/changelogs/default.txt b/fastlane/metadata/android/cs-CZ/changelogs/default.txt index 0553de284f..76874cc530 100644 --- a/fastlane/metadata/android/cs-CZ/changelogs/default.txt +++ b/fastlane/metadata/android/cs-CZ/changelogs/default.txt @@ -1 +1,3 @@ -For detailed release notes, please visit: https://github.com/meshtastic/Meshtastic-Android/releases/ \ No newline at end of file +Stability and reliability fixes. + +Full release notes: https://github.com/meshtastic/Meshtastic-Android/releases diff --git a/fastlane/metadata/android/cs-CZ/full_description.txt b/fastlane/metadata/android/cs-CZ/full_description.txt index 4ade9c5360..65f3298610 100644 --- a/fastlane/metadata/android/cs-CZ/full_description.txt +++ b/fastlane/metadata/android/cs-CZ/full_description.txt @@ -1,21 +1,21 @@ -Meshtastic is a tool for using Android devices with open-source, off-grid mesh radios. This app is the main client for the Meshtastic project, allowing you to manage your mesh devices and communicate with other users. +Meshtastic je nástroj, umožňující používat zařízení Android s open source mesh rádii pro komunikaci mimo běžnou infrastrukturu. Tato aplikace je hlavním klientem projektu Meshtastic a umožňuje spravovat vaše mesh zařízení a komunikovat s ostatními uživateli. -For more information about the Meshtastic project, please visit our website: meshtastic.org. The firmware that runs on the radio devices is a separate open-source project, which you can find here: https://github.com/meshtastic/Meshtastic-device. +Další informace o projektu Meshtastic najdete na našich webových stránkách: meshtastic.org. Firmware, který běží na rádiových zařízeních, je samostatný open source projekt, který najdete zde: https://github.com/meshtastic/Meshtastic-device. -Community and Support +Komunita a podpora -This project is currently in beta. We would love to hear from you! If you have questions, feedback, or encounter any problems, please join our friendly and active community: +Tento projekt je nyní v beta verzi. Budeme rádi, když nám dáte vědět! Máte-li dotazy, připomínky nebo narazíte na nějaký problém, připojte se k naší přátelské a aktivní komunitě: -• Discussion Forum: https://github.com/orgs/meshtastic/discussions +• Diskusní fórum: https://github.com/orgs/meshtastic/discussions • Discord: https://discord.gg/meshtastic -• Report an Issue: https://github.com/meshtastic/Meshtastic-Android/issues +• Nahlásit problém: https://github.com/meshtastic/Meshtastic-Android/issues -Documentation +Dokumentace -To learn more about the features and capabilities of this app and Meshtastic, please view our official documentation: -View Documentation +Další informace o funkcích a možnostech této aplikace a systému Meshtastic najdete v naší oficiální dokumentaci: +Zobrazit dokumentaci -Translations +Překlady -You can help translate the app into your native language using Crowdin: +Můžete pomoci přeložit aplikaci do vašeho rodného jazyka pomocí Crowdinu: https://crowdin.meshtastic.org/android diff --git a/fastlane/metadata/android/de-DE/changelogs/default.txt b/fastlane/metadata/android/de-DE/changelogs/default.txt index f16767ea68..76874cc530 100644 --- a/fastlane/metadata/android/de-DE/changelogs/default.txt +++ b/fastlane/metadata/android/de-DE/changelogs/default.txt @@ -1 +1,3 @@ -Für detaillierte Versionshinweise besuchen Sie bitte: https://github.com/meshtastic/Meshtastic-Android/releases/ \ No newline at end of file +Stability and reliability fixes. + +Full release notes: https://github.com/meshtastic/Meshtastic-Android/releases diff --git a/fastlane/metadata/android/el-GR/changelogs/default.txt b/fastlane/metadata/android/el-GR/changelogs/default.txt index 0553de284f..76874cc530 100644 --- a/fastlane/metadata/android/el-GR/changelogs/default.txt +++ b/fastlane/metadata/android/el-GR/changelogs/default.txt @@ -1 +1,3 @@ -For detailed release notes, please visit: https://github.com/meshtastic/Meshtastic-Android/releases/ \ No newline at end of file +Stability and reliability fixes. + +Full release notes: https://github.com/meshtastic/Meshtastic-Android/releases diff --git a/fastlane/metadata/android/en-US/changelogs/default.txt b/fastlane/metadata/android/en-US/changelogs/default.txt index 0553de284f..76874cc530 100644 --- a/fastlane/metadata/android/en-US/changelogs/default.txt +++ b/fastlane/metadata/android/en-US/changelogs/default.txt @@ -1 +1,3 @@ -For detailed release notes, please visit: https://github.com/meshtastic/Meshtastic-Android/releases/ \ No newline at end of file +Stability and reliability fixes. + +Full release notes: https://github.com/meshtastic/Meshtastic-Android/releases diff --git a/fastlane/metadata/android/en-US/images/chromebookScreenshots/1_messages.png b/fastlane/metadata/android/en-US/images/chromebookScreenshots/1_messages.png new file mode 100644 index 0000000000..4bbd2bb566 Binary files /dev/null and b/fastlane/metadata/android/en-US/images/chromebookScreenshots/1_messages.png differ diff --git a/fastlane/metadata/android/en-US/images/chromebookScreenshots/2_nodes.png b/fastlane/metadata/android/en-US/images/chromebookScreenshots/2_nodes.png new file mode 100644 index 0000000000..303ad5f300 Binary files /dev/null and b/fastlane/metadata/android/en-US/images/chromebookScreenshots/2_nodes.png differ diff --git a/fastlane/metadata/android/en-US/images/chromebookScreenshots/3_map.png b/fastlane/metadata/android/en-US/images/chromebookScreenshots/3_map.png new file mode 100644 index 0000000000..2517d4ec63 Binary files /dev/null and b/fastlane/metadata/android/en-US/images/chromebookScreenshots/3_map.png differ diff --git a/fastlane/metadata/android/en-US/images/chromebookScreenshots/4_node_detail.png b/fastlane/metadata/android/en-US/images/chromebookScreenshots/4_node_detail.png new file mode 100644 index 0000000000..434fda6c49 Binary files /dev/null and b/fastlane/metadata/android/en-US/images/chromebookScreenshots/4_node_detail.png differ diff --git a/fastlane/metadata/android/en-US/images/chromebookScreenshots/5_channels.png b/fastlane/metadata/android/en-US/images/chromebookScreenshots/5_channels.png new file mode 100644 index 0000000000..feccd2011c Binary files /dev/null and b/fastlane/metadata/android/en-US/images/chromebookScreenshots/5_channels.png differ diff --git a/fastlane/metadata/android/en-US/images/phoneScreenshots/1_en-US.png b/fastlane/metadata/android/en-US/images/phoneScreenshots/1_en-US.png deleted file mode 100644 index 954a99fd9e..0000000000 Binary files a/fastlane/metadata/android/en-US/images/phoneScreenshots/1_en-US.png and /dev/null differ diff --git a/fastlane/metadata/android/en-US/images/phoneScreenshots/1_messages.png b/fastlane/metadata/android/en-US/images/phoneScreenshots/1_messages.png new file mode 100644 index 0000000000..014ae9150f Binary files /dev/null and b/fastlane/metadata/android/en-US/images/phoneScreenshots/1_messages.png differ diff --git a/fastlane/metadata/android/en-US/images/phoneScreenshots/2_en-US.png b/fastlane/metadata/android/en-US/images/phoneScreenshots/2_en-US.png deleted file mode 100644 index b36367c627..0000000000 Binary files a/fastlane/metadata/android/en-US/images/phoneScreenshots/2_en-US.png and /dev/null differ diff --git a/fastlane/metadata/android/en-US/images/phoneScreenshots/2_nodes.png b/fastlane/metadata/android/en-US/images/phoneScreenshots/2_nodes.png new file mode 100644 index 0000000000..b984ec5a8e Binary files /dev/null and b/fastlane/metadata/android/en-US/images/phoneScreenshots/2_nodes.png differ diff --git a/fastlane/metadata/android/en-US/images/phoneScreenshots/3_en-US.png b/fastlane/metadata/android/en-US/images/phoneScreenshots/3_en-US.png deleted file mode 100644 index 3de7c392ca..0000000000 Binary files a/fastlane/metadata/android/en-US/images/phoneScreenshots/3_en-US.png and /dev/null differ diff --git a/fastlane/metadata/android/en-US/images/phoneScreenshots/3_map.png b/fastlane/metadata/android/en-US/images/phoneScreenshots/3_map.png new file mode 100644 index 0000000000..3c2be8642c Binary files /dev/null and b/fastlane/metadata/android/en-US/images/phoneScreenshots/3_map.png differ diff --git a/fastlane/metadata/android/en-US/images/phoneScreenshots/4_en-US.png b/fastlane/metadata/android/en-US/images/phoneScreenshots/4_en-US.png deleted file mode 100644 index d44186ed9d..0000000000 Binary files a/fastlane/metadata/android/en-US/images/phoneScreenshots/4_en-US.png and /dev/null differ diff --git a/fastlane/metadata/android/en-US/images/phoneScreenshots/4_node_detail.png b/fastlane/metadata/android/en-US/images/phoneScreenshots/4_node_detail.png new file mode 100644 index 0000000000..3b02938ea9 Binary files /dev/null and b/fastlane/metadata/android/en-US/images/phoneScreenshots/4_node_detail.png differ diff --git a/fastlane/metadata/android/en-US/images/phoneScreenshots/5_channels.png b/fastlane/metadata/android/en-US/images/phoneScreenshots/5_channels.png new file mode 100644 index 0000000000..777cc6edb1 Binary files /dev/null and b/fastlane/metadata/android/en-US/images/phoneScreenshots/5_channels.png differ diff --git a/fastlane/metadata/android/en-US/images/phoneScreenshots/5_en-US.png b/fastlane/metadata/android/en-US/images/phoneScreenshots/5_en-US.png deleted file mode 100644 index eab323cf87..0000000000 Binary files a/fastlane/metadata/android/en-US/images/phoneScreenshots/5_en-US.png and /dev/null differ diff --git a/fastlane/metadata/android/en-US/images/sevenInchScreenshots/1_messages.png b/fastlane/metadata/android/en-US/images/sevenInchScreenshots/1_messages.png new file mode 100644 index 0000000000..d40a27b1a5 Binary files /dev/null and b/fastlane/metadata/android/en-US/images/sevenInchScreenshots/1_messages.png differ diff --git a/fastlane/metadata/android/en-US/images/sevenInchScreenshots/2_nodes.png b/fastlane/metadata/android/en-US/images/sevenInchScreenshots/2_nodes.png new file mode 100644 index 0000000000..4363f09f85 Binary files /dev/null and b/fastlane/metadata/android/en-US/images/sevenInchScreenshots/2_nodes.png differ diff --git a/fastlane/metadata/android/en-US/images/sevenInchScreenshots/3_map.png b/fastlane/metadata/android/en-US/images/sevenInchScreenshots/3_map.png new file mode 100644 index 0000000000..8a6b72dbdf Binary files /dev/null and b/fastlane/metadata/android/en-US/images/sevenInchScreenshots/3_map.png differ diff --git a/fastlane/metadata/android/en-US/images/sevenInchScreenshots/4_node_detail.png b/fastlane/metadata/android/en-US/images/sevenInchScreenshots/4_node_detail.png new file mode 100644 index 0000000000..3285efbe91 Binary files /dev/null and b/fastlane/metadata/android/en-US/images/sevenInchScreenshots/4_node_detail.png differ diff --git a/fastlane/metadata/android/en-US/images/sevenInchScreenshots/5_channels.png b/fastlane/metadata/android/en-US/images/sevenInchScreenshots/5_channels.png new file mode 100644 index 0000000000..af089b648a Binary files /dev/null and b/fastlane/metadata/android/en-US/images/sevenInchScreenshots/5_channels.png differ diff --git a/fastlane/metadata/android/en-US/images/tenInchScreenshots/1_messages.png b/fastlane/metadata/android/en-US/images/tenInchScreenshots/1_messages.png new file mode 100644 index 0000000000..b0e11455a9 Binary files /dev/null and b/fastlane/metadata/android/en-US/images/tenInchScreenshots/1_messages.png differ diff --git a/fastlane/metadata/android/en-US/images/tenInchScreenshots/2_nodes.png b/fastlane/metadata/android/en-US/images/tenInchScreenshots/2_nodes.png new file mode 100644 index 0000000000..ac4b5ad758 Binary files /dev/null and b/fastlane/metadata/android/en-US/images/tenInchScreenshots/2_nodes.png differ diff --git a/fastlane/metadata/android/en-US/images/tenInchScreenshots/3_map.png b/fastlane/metadata/android/en-US/images/tenInchScreenshots/3_map.png new file mode 100644 index 0000000000..7bfa40af8a Binary files /dev/null and b/fastlane/metadata/android/en-US/images/tenInchScreenshots/3_map.png differ diff --git a/fastlane/metadata/android/en-US/images/tenInchScreenshots/4_node_detail.png b/fastlane/metadata/android/en-US/images/tenInchScreenshots/4_node_detail.png new file mode 100644 index 0000000000..9be941ba86 Binary files /dev/null and b/fastlane/metadata/android/en-US/images/tenInchScreenshots/4_node_detail.png differ diff --git a/fastlane/metadata/android/en-US/images/tenInchScreenshots/5_channels.png b/fastlane/metadata/android/en-US/images/tenInchScreenshots/5_channels.png new file mode 100644 index 0000000000..dfd7e70ae1 Binary files /dev/null and b/fastlane/metadata/android/en-US/images/tenInchScreenshots/5_channels.png differ diff --git a/fastlane/metadata/android/en-US/images/xrScreenshots/1_messages.png b/fastlane/metadata/android/en-US/images/xrScreenshots/1_messages.png new file mode 100644 index 0000000000..b9c8c9c06c Binary files /dev/null and b/fastlane/metadata/android/en-US/images/xrScreenshots/1_messages.png differ diff --git a/fastlane/metadata/android/en-US/images/xrScreenshots/2_nodes.png b/fastlane/metadata/android/en-US/images/xrScreenshots/2_nodes.png new file mode 100644 index 0000000000..a9489adfcd Binary files /dev/null and b/fastlane/metadata/android/en-US/images/xrScreenshots/2_nodes.png differ diff --git a/fastlane/metadata/android/en-US/images/xrScreenshots/3_map.png b/fastlane/metadata/android/en-US/images/xrScreenshots/3_map.png new file mode 100644 index 0000000000..a74cd7f90e Binary files /dev/null and b/fastlane/metadata/android/en-US/images/xrScreenshots/3_map.png differ diff --git a/fastlane/metadata/android/en-US/images/xrScreenshots/4_node_detail.png b/fastlane/metadata/android/en-US/images/xrScreenshots/4_node_detail.png new file mode 100644 index 0000000000..1215787929 Binary files /dev/null and b/fastlane/metadata/android/en-US/images/xrScreenshots/4_node_detail.png differ diff --git a/fastlane/metadata/android/en-US/images/xrScreenshots/5_channels.png b/fastlane/metadata/android/en-US/images/xrScreenshots/5_channels.png new file mode 100644 index 0000000000..ba7ccfbc5e Binary files /dev/null and b/fastlane/metadata/android/en-US/images/xrScreenshots/5_channels.png differ diff --git a/fastlane/metadata/android/es-ES/changelogs/default.txt b/fastlane/metadata/android/es-ES/changelogs/default.txt index 0553de284f..76874cc530 100644 --- a/fastlane/metadata/android/es-ES/changelogs/default.txt +++ b/fastlane/metadata/android/es-ES/changelogs/default.txt @@ -1 +1,3 @@ -For detailed release notes, please visit: https://github.com/meshtastic/Meshtastic-Android/releases/ \ No newline at end of file +Stability and reliability fixes. + +Full release notes: https://github.com/meshtastic/Meshtastic-Android/releases diff --git a/fastlane/metadata/android/et-EE/changelogs/default.txt b/fastlane/metadata/android/et-EE/changelogs/default.txt deleted file mode 100644 index 227e766cd6..0000000000 --- a/fastlane/metadata/android/et-EE/changelogs/default.txt +++ /dev/null @@ -1 +0,0 @@ -Täpsemad väljalaskemärkmed: https://github.com/meshtastic/Meshtastic-Android/releases/ \ No newline at end of file diff --git a/fastlane/metadata/android/et/changelogs/default.txt b/fastlane/metadata/android/et/changelogs/default.txt new file mode 100644 index 0000000000..76874cc530 --- /dev/null +++ b/fastlane/metadata/android/et/changelogs/default.txt @@ -0,0 +1,3 @@ +Stability and reliability fixes. + +Full release notes: https://github.com/meshtastic/Meshtastic-Android/releases diff --git a/fastlane/metadata/android/et-EE/full_description.txt b/fastlane/metadata/android/et/full_description.txt similarity index 100% rename from fastlane/metadata/android/et-EE/full_description.txt rename to fastlane/metadata/android/et/full_description.txt diff --git a/fastlane/metadata/android/et-EE/short_description.txt b/fastlane/metadata/android/et/short_description.txt similarity index 100% rename from fastlane/metadata/android/et-EE/short_description.txt rename to fastlane/metadata/android/et/short_description.txt diff --git a/fastlane/metadata/android/et-EE/title.txt b/fastlane/metadata/android/et/title.txt similarity index 100% rename from fastlane/metadata/android/et-EE/title.txt rename to fastlane/metadata/android/et/title.txt diff --git a/fastlane/metadata/android/fi-FI/changelogs/default.txt b/fastlane/metadata/android/fi-FI/changelogs/default.txt index 6c9f3887f9..76874cc530 100644 --- a/fastlane/metadata/android/fi-FI/changelogs/default.txt +++ b/fastlane/metadata/android/fi-FI/changelogs/default.txt @@ -1 +1,3 @@ -Yksityiskohtaiset julkaisutiedot löydät osoitteesta: https://github.com/meshtastic/Meshtastic-Android/releases/ \ No newline at end of file +Stability and reliability fixes. + +Full release notes: https://github.com/meshtastic/Meshtastic-Android/releases diff --git a/fastlane/metadata/android/fr-CA/changelogs/default.txt b/fastlane/metadata/android/fr-CA/changelogs/default.txt index a322da0203..76874cc530 100644 --- a/fastlane/metadata/android/fr-CA/changelogs/default.txt +++ b/fastlane/metadata/android/fr-CA/changelogs/default.txt @@ -1 +1,3 @@ -Pour des notes de version détaillées, veuillez visiter : https://github.com/meshtastic/Meshtastic-Android/releases/ \ No newline at end of file +Stability and reliability fixes. + +Full release notes: https://github.com/meshtastic/Meshtastic-Android/releases diff --git a/fastlane/metadata/android/fr-FR/changelogs/default.txt b/fastlane/metadata/android/fr-FR/changelogs/default.txt index a322da0203..76874cc530 100644 --- a/fastlane/metadata/android/fr-FR/changelogs/default.txt +++ b/fastlane/metadata/android/fr-FR/changelogs/default.txt @@ -1 +1,3 @@ -Pour des notes de version détaillées, veuillez visiter : https://github.com/meshtastic/Meshtastic-Android/releases/ \ No newline at end of file +Stability and reliability fixes. + +Full release notes: https://github.com/meshtastic/Meshtastic-Android/releases diff --git a/fastlane/metadata/android/ga-IE/changelogs/default.txt b/fastlane/metadata/android/ga-IE/changelogs/default.txt deleted file mode 100644 index 0553de284f..0000000000 --- a/fastlane/metadata/android/ga-IE/changelogs/default.txt +++ /dev/null @@ -1 +0,0 @@ -For detailed release notes, please visit: https://github.com/meshtastic/Meshtastic-Android/releases/ \ No newline at end of file diff --git a/fastlane/metadata/android/gl-ES/changelogs/default.txt b/fastlane/metadata/android/gl-ES/changelogs/default.txt index 0553de284f..76874cc530 100644 --- a/fastlane/metadata/android/gl-ES/changelogs/default.txt +++ b/fastlane/metadata/android/gl-ES/changelogs/default.txt @@ -1 +1,3 @@ -For detailed release notes, please visit: https://github.com/meshtastic/Meshtastic-Android/releases/ \ No newline at end of file +Stability and reliability fixes. + +Full release notes: https://github.com/meshtastic/Meshtastic-Android/releases diff --git a/fastlane/metadata/android/he-IL/changelogs/default.txt b/fastlane/metadata/android/he-IL/changelogs/default.txt deleted file mode 100644 index 0553de284f..0000000000 --- a/fastlane/metadata/android/he-IL/changelogs/default.txt +++ /dev/null @@ -1 +0,0 @@ -For detailed release notes, please visit: https://github.com/meshtastic/Meshtastic-Android/releases/ \ No newline at end of file diff --git a/fastlane/metadata/android/hr-HR/changelogs/default.txt b/fastlane/metadata/android/hr-HR/changelogs/default.txt deleted file mode 100644 index 0553de284f..0000000000 --- a/fastlane/metadata/android/hr-HR/changelogs/default.txt +++ /dev/null @@ -1 +0,0 @@ -For detailed release notes, please visit: https://github.com/meshtastic/Meshtastic-Android/releases/ \ No newline at end of file diff --git a/fastlane/metadata/android/hr/changelogs/default.txt b/fastlane/metadata/android/hr/changelogs/default.txt new file mode 100644 index 0000000000..76874cc530 --- /dev/null +++ b/fastlane/metadata/android/hr/changelogs/default.txt @@ -0,0 +1,3 @@ +Stability and reliability fixes. + +Full release notes: https://github.com/meshtastic/Meshtastic-Android/releases diff --git a/fastlane/metadata/android/hr-HR/full_description.txt b/fastlane/metadata/android/hr/full_description.txt similarity index 100% rename from fastlane/metadata/android/hr-HR/full_description.txt rename to fastlane/metadata/android/hr/full_description.txt diff --git a/fastlane/metadata/android/ga-IE/short_description.txt b/fastlane/metadata/android/hr/short_description.txt similarity index 100% rename from fastlane/metadata/android/ga-IE/short_description.txt rename to fastlane/metadata/android/hr/short_description.txt diff --git a/fastlane/metadata/android/ga-IE/title.txt b/fastlane/metadata/android/hr/title.txt similarity index 100% rename from fastlane/metadata/android/ga-IE/title.txt rename to fastlane/metadata/android/hr/title.txt diff --git a/fastlane/metadata/android/ht-HT/changelogs/default.txt b/fastlane/metadata/android/ht-HT/changelogs/default.txt deleted file mode 100644 index 0553de284f..0000000000 --- a/fastlane/metadata/android/ht-HT/changelogs/default.txt +++ /dev/null @@ -1 +0,0 @@ -For detailed release notes, please visit: https://github.com/meshtastic/Meshtastic-Android/releases/ \ No newline at end of file diff --git a/fastlane/metadata/android/hu-HU/changelogs/default.txt b/fastlane/metadata/android/hu-HU/changelogs/default.txt index dde6b121c2..76874cc530 100644 --- a/fastlane/metadata/android/hu-HU/changelogs/default.txt +++ b/fastlane/metadata/android/hu-HU/changelogs/default.txt @@ -1 +1,3 @@ -A részletes kiadási megjegyzésekért látogasd meg az alábbi oldalt: https://github.com/meshtastic/Meshtastic-Android/releases/ \ No newline at end of file +Stability and reliability fixes. + +Full release notes: https://github.com/meshtastic/Meshtastic-Android/releases diff --git a/fastlane/metadata/android/is-IS/changelogs/default.txt b/fastlane/metadata/android/is-IS/changelogs/default.txt index 0553de284f..76874cc530 100644 --- a/fastlane/metadata/android/is-IS/changelogs/default.txt +++ b/fastlane/metadata/android/is-IS/changelogs/default.txt @@ -1 +1,3 @@ -For detailed release notes, please visit: https://github.com/meshtastic/Meshtastic-Android/releases/ \ No newline at end of file +Stability and reliability fixes. + +Full release notes: https://github.com/meshtastic/Meshtastic-Android/releases diff --git a/fastlane/metadata/android/it-IT/changelogs/default.txt b/fastlane/metadata/android/it-IT/changelogs/default.txt index 88ed9eb9b4..76874cc530 100644 --- a/fastlane/metadata/android/it-IT/changelogs/default.txt +++ b/fastlane/metadata/android/it-IT/changelogs/default.txt @@ -1 +1,3 @@ -Per le note di rilascio dettagliate, visita: https://github.com/meshtastic/Meshtastic-Android/releases/ \ No newline at end of file +Stability and reliability fixes. + +Full release notes: https://github.com/meshtastic/Meshtastic-Android/releases diff --git a/fastlane/metadata/android/iw-IL/changelogs/default.txt b/fastlane/metadata/android/iw-IL/changelogs/default.txt new file mode 100644 index 0000000000..76874cc530 --- /dev/null +++ b/fastlane/metadata/android/iw-IL/changelogs/default.txt @@ -0,0 +1,3 @@ +Stability and reliability fixes. + +Full release notes: https://github.com/meshtastic/Meshtastic-Android/releases diff --git a/fastlane/metadata/android/ga-IE/full_description.txt b/fastlane/metadata/android/iw-IL/full_description.txt similarity index 100% rename from fastlane/metadata/android/ga-IE/full_description.txt rename to fastlane/metadata/android/iw-IL/full_description.txt diff --git a/fastlane/metadata/android/he-IL/short_description.txt b/fastlane/metadata/android/iw-IL/short_description.txt similarity index 100% rename from fastlane/metadata/android/he-IL/short_description.txt rename to fastlane/metadata/android/iw-IL/short_description.txt diff --git a/fastlane/metadata/android/he-IL/title.txt b/fastlane/metadata/android/iw-IL/title.txt similarity index 100% rename from fastlane/metadata/android/he-IL/title.txt rename to fastlane/metadata/android/iw-IL/title.txt diff --git a/fastlane/metadata/android/ja-JP/changelogs/default.txt b/fastlane/metadata/android/ja-JP/changelogs/default.txt index baa886393b..76874cc530 100644 --- a/fastlane/metadata/android/ja-JP/changelogs/default.txt +++ b/fastlane/metadata/android/ja-JP/changelogs/default.txt @@ -1 +1,3 @@ -詳細なリリースノートはこちらをご覧ください:https://github.com/meshtastic/Meshtastic-Android/releases/ \ No newline at end of file +Stability and reliability fixes. + +Full release notes: https://github.com/meshtastic/Meshtastic-Android/releases diff --git a/fastlane/metadata/android/ja-JP/full_description.txt b/fastlane/metadata/android/ja-JP/full_description.txt index 8965703c47..e14ab30b1d 100644 --- a/fastlane/metadata/android/ja-JP/full_description.txt +++ b/fastlane/metadata/android/ja-JP/full_description.txt @@ -1,11 +1,11 @@ -Meshtasticは、オープンソースのオフグリッドメッシュ無線とAndroidデバイスを連携させるためのソフトです。 このアプリは Meshtastic プロジェクトのメインクライアントであり、メッシュデバイスの管理や他のユーザーとの通信を可能にします。 +Meshtasticは、オープンソースのオフグリッドメッシュ無線とAndroidデバイスを連携させるためのソフトです。このアプリは Meshtastic プロジェクトのメインクライアントであり、メッシュデバイスの管理や他のユーザーとの通信を可能にします。 -Meshtastic プロジェクトの詳細については、ぜひ公式ウェブサイトをご覧ください:meshtastic.org。 無線デバイスのファームウェアは別のオープンソースプロジェクトであり、詳しくはこちらをご覧ください: +Meshtastic プロジェクトの詳細については、ぜひ公式ウェブサイトをご覧ください:meshtastic.org。無線デバイスのファームウェアは別のオープンソースプロジェクトであり、詳しくはこちらをご覧ください: https://github.com/meshtastic/Meshtastic-device。 コミュニティとサポート -当プロジェクトは、現時点でベータ版です。 皆様からのフィードバックをお待ちしております! ご質問・フィードバック・問題がある場合は、フレンドリーで活発なコミュニティにぜひご参加ください: +当プロジェクトは、現時点でベータ版です。皆様からのフィードバックをお待ちしております!ご質問・フィードバック・問題がある場合は、フレンドリーで活発なコミュニティにぜひご参加ください: • ディスカッションフォーラム: https://github.com/orgs/meshtastic/discussions • Discord: https://discord.gg/meshtastic diff --git a/fastlane/metadata/android/ko-KR/changelogs/default.txt b/fastlane/metadata/android/ko-KR/changelogs/default.txt index 0553de284f..76874cc530 100644 --- a/fastlane/metadata/android/ko-KR/changelogs/default.txt +++ b/fastlane/metadata/android/ko-KR/changelogs/default.txt @@ -1 +1,3 @@ -For detailed release notes, please visit: https://github.com/meshtastic/Meshtastic-Android/releases/ \ No newline at end of file +Stability and reliability fixes. + +Full release notes: https://github.com/meshtastic/Meshtastic-Android/releases diff --git a/fastlane/metadata/android/lt-LT/changelogs/default.txt b/fastlane/metadata/android/lt-LT/changelogs/default.txt deleted file mode 100644 index 0553de284f..0000000000 --- a/fastlane/metadata/android/lt-LT/changelogs/default.txt +++ /dev/null @@ -1 +0,0 @@ -For detailed release notes, please visit: https://github.com/meshtastic/Meshtastic-Android/releases/ \ No newline at end of file diff --git a/fastlane/metadata/android/lt/changelogs/default.txt b/fastlane/metadata/android/lt/changelogs/default.txt new file mode 100644 index 0000000000..76874cc530 --- /dev/null +++ b/fastlane/metadata/android/lt/changelogs/default.txt @@ -0,0 +1,3 @@ +Stability and reliability fixes. + +Full release notes: https://github.com/meshtastic/Meshtastic-Android/releases diff --git a/fastlane/metadata/android/he-IL/full_description.txt b/fastlane/metadata/android/lt/full_description.txt similarity index 100% rename from fastlane/metadata/android/he-IL/full_description.txt rename to fastlane/metadata/android/lt/full_description.txt diff --git a/fastlane/metadata/android/hr-HR/short_description.txt b/fastlane/metadata/android/lt/short_description.txt similarity index 100% rename from fastlane/metadata/android/hr-HR/short_description.txt rename to fastlane/metadata/android/lt/short_description.txt diff --git a/fastlane/metadata/android/hr-HR/title.txt b/fastlane/metadata/android/lt/title.txt similarity index 100% rename from fastlane/metadata/android/hr-HR/title.txt rename to fastlane/metadata/android/lt/title.txt diff --git a/fastlane/metadata/android/nl-NL/changelogs/default.txt b/fastlane/metadata/android/nl-NL/changelogs/default.txt index 0553de284f..76874cc530 100644 --- a/fastlane/metadata/android/nl-NL/changelogs/default.txt +++ b/fastlane/metadata/android/nl-NL/changelogs/default.txt @@ -1 +1,3 @@ -For detailed release notes, please visit: https://github.com/meshtastic/Meshtastic-Android/releases/ \ No newline at end of file +Stability and reliability fixes. + +Full release notes: https://github.com/meshtastic/Meshtastic-Android/releases diff --git a/fastlane/metadata/android/no-NO/changelogs/default.txt b/fastlane/metadata/android/no-NO/changelogs/default.txt index 0553de284f..76874cc530 100644 --- a/fastlane/metadata/android/no-NO/changelogs/default.txt +++ b/fastlane/metadata/android/no-NO/changelogs/default.txt @@ -1 +1,3 @@ -For detailed release notes, please visit: https://github.com/meshtastic/Meshtastic-Android/releases/ \ No newline at end of file +Stability and reliability fixes. + +Full release notes: https://github.com/meshtastic/Meshtastic-Android/releases diff --git a/fastlane/metadata/android/pl-PL/changelogs/default.txt b/fastlane/metadata/android/pl-PL/changelogs/default.txt index 0553de284f..76874cc530 100644 --- a/fastlane/metadata/android/pl-PL/changelogs/default.txt +++ b/fastlane/metadata/android/pl-PL/changelogs/default.txt @@ -1 +1,3 @@ -For detailed release notes, please visit: https://github.com/meshtastic/Meshtastic-Android/releases/ \ No newline at end of file +Stability and reliability fixes. + +Full release notes: https://github.com/meshtastic/Meshtastic-Android/releases diff --git a/fastlane/metadata/android/pt-BR/changelogs/default.txt b/fastlane/metadata/android/pt-BR/changelogs/default.txt index 0553de284f..76874cc530 100644 --- a/fastlane/metadata/android/pt-BR/changelogs/default.txt +++ b/fastlane/metadata/android/pt-BR/changelogs/default.txt @@ -1 +1,3 @@ -For detailed release notes, please visit: https://github.com/meshtastic/Meshtastic-Android/releases/ \ No newline at end of file +Stability and reliability fixes. + +Full release notes: https://github.com/meshtastic/Meshtastic-Android/releases diff --git a/fastlane/metadata/android/pt-PT/changelogs/default.txt b/fastlane/metadata/android/pt-PT/changelogs/default.txt index 0553de284f..76874cc530 100644 --- a/fastlane/metadata/android/pt-PT/changelogs/default.txt +++ b/fastlane/metadata/android/pt-PT/changelogs/default.txt @@ -1 +1,3 @@ -For detailed release notes, please visit: https://github.com/meshtastic/Meshtastic-Android/releases/ \ No newline at end of file +Stability and reliability fixes. + +Full release notes: https://github.com/meshtastic/Meshtastic-Android/releases diff --git a/fastlane/metadata/android/ro-RO/changelogs/default.txt b/fastlane/metadata/android/ro-RO/changelogs/default.txt deleted file mode 100644 index b254b55b83..0000000000 --- a/fastlane/metadata/android/ro-RO/changelogs/default.txt +++ /dev/null @@ -1 +0,0 @@ -Pentru note detaliate pentru versiuni, vizitați: https://github.com/meshtastic/Meshtastic-Android/releases/ \ No newline at end of file diff --git a/fastlane/metadata/android/ro/changelogs/default.txt b/fastlane/metadata/android/ro/changelogs/default.txt new file mode 100644 index 0000000000..76874cc530 --- /dev/null +++ b/fastlane/metadata/android/ro/changelogs/default.txt @@ -0,0 +1,3 @@ +Stability and reliability fixes. + +Full release notes: https://github.com/meshtastic/Meshtastic-Android/releases diff --git a/fastlane/metadata/android/ht-HT/full_description.txt b/fastlane/metadata/android/ro/full_description.txt similarity index 100% rename from fastlane/metadata/android/ht-HT/full_description.txt rename to fastlane/metadata/android/ro/full_description.txt diff --git a/fastlane/metadata/android/ro-RO/short_description.txt b/fastlane/metadata/android/ro/short_description.txt similarity index 100% rename from fastlane/metadata/android/ro-RO/short_description.txt rename to fastlane/metadata/android/ro/short_description.txt diff --git a/fastlane/metadata/android/ht-HT/title.txt b/fastlane/metadata/android/ro/title.txt similarity index 100% rename from fastlane/metadata/android/ht-HT/title.txt rename to fastlane/metadata/android/ro/title.txt diff --git a/fastlane/metadata/android/ru-RU/changelogs/default.txt b/fastlane/metadata/android/ru-RU/changelogs/default.txt index 3907315b8e..76874cc530 100644 --- a/fastlane/metadata/android/ru-RU/changelogs/default.txt +++ b/fastlane/metadata/android/ru-RU/changelogs/default.txt @@ -1 +1,3 @@ -Подробные заметки о выпуске, пожалуйста, смотрите здесь: https://github.com/meshtastic/Meshtastic-Android/releases/ \ No newline at end of file +Stability and reliability fixes. + +Full release notes: https://github.com/meshtastic/Meshtastic-Android/releases diff --git a/fastlane/metadata/android/sk-SK/changelogs/default.txt b/fastlane/metadata/android/sk-SK/changelogs/default.txt deleted file mode 100644 index 0553de284f..0000000000 --- a/fastlane/metadata/android/sk-SK/changelogs/default.txt +++ /dev/null @@ -1 +0,0 @@ -For detailed release notes, please visit: https://github.com/meshtastic/Meshtastic-Android/releases/ \ No newline at end of file diff --git a/fastlane/metadata/android/sk/changelogs/default.txt b/fastlane/metadata/android/sk/changelogs/default.txt new file mode 100644 index 0000000000..76874cc530 --- /dev/null +++ b/fastlane/metadata/android/sk/changelogs/default.txt @@ -0,0 +1,3 @@ +Stability and reliability fixes. + +Full release notes: https://github.com/meshtastic/Meshtastic-Android/releases diff --git a/fastlane/metadata/android/sk-SK/full_description.txt b/fastlane/metadata/android/sk/full_description.txt similarity index 100% rename from fastlane/metadata/android/sk-SK/full_description.txt rename to fastlane/metadata/android/sk/full_description.txt diff --git a/fastlane/metadata/android/sk-SK/short_description.txt b/fastlane/metadata/android/sk/short_description.txt similarity index 100% rename from fastlane/metadata/android/sk-SK/short_description.txt rename to fastlane/metadata/android/sk/short_description.txt diff --git a/fastlane/metadata/android/lt-LT/title.txt b/fastlane/metadata/android/sk/title.txt similarity index 100% rename from fastlane/metadata/android/lt-LT/title.txt rename to fastlane/metadata/android/sk/title.txt diff --git a/fastlane/metadata/android/sl-SI/changelogs/default.txt b/fastlane/metadata/android/sl-SI/changelogs/default.txt deleted file mode 100644 index 0553de284f..0000000000 --- a/fastlane/metadata/android/sl-SI/changelogs/default.txt +++ /dev/null @@ -1 +0,0 @@ -For detailed release notes, please visit: https://github.com/meshtastic/Meshtastic-Android/releases/ \ No newline at end of file diff --git a/fastlane/metadata/android/sl/changelogs/default.txt b/fastlane/metadata/android/sl/changelogs/default.txt new file mode 100644 index 0000000000..76874cc530 --- /dev/null +++ b/fastlane/metadata/android/sl/changelogs/default.txt @@ -0,0 +1,3 @@ +Stability and reliability fixes. + +Full release notes: https://github.com/meshtastic/Meshtastic-Android/releases diff --git a/fastlane/metadata/android/lt-LT/full_description.txt b/fastlane/metadata/android/sl/full_description.txt similarity index 100% rename from fastlane/metadata/android/lt-LT/full_description.txt rename to fastlane/metadata/android/sl/full_description.txt diff --git a/fastlane/metadata/android/ht-HT/short_description.txt b/fastlane/metadata/android/sl/short_description.txt similarity index 100% rename from fastlane/metadata/android/ht-HT/short_description.txt rename to fastlane/metadata/android/sl/short_description.txt diff --git a/fastlane/metadata/android/ro-RO/title.txt b/fastlane/metadata/android/sl/title.txt similarity index 100% rename from fastlane/metadata/android/ro-RO/title.txt rename to fastlane/metadata/android/sl/title.txt diff --git a/fastlane/metadata/android/sq-AL/changelogs/default.txt b/fastlane/metadata/android/sq-AL/changelogs/default.txt deleted file mode 100644 index 0553de284f..0000000000 --- a/fastlane/metadata/android/sq-AL/changelogs/default.txt +++ /dev/null @@ -1 +0,0 @@ -For detailed release notes, please visit: https://github.com/meshtastic/Meshtastic-Android/releases/ \ No newline at end of file diff --git a/fastlane/metadata/android/sq-AL/full_description.txt b/fastlane/metadata/android/sq-AL/full_description.txt deleted file mode 100644 index 4ade9c5360..0000000000 --- a/fastlane/metadata/android/sq-AL/full_description.txt +++ /dev/null @@ -1,21 +0,0 @@ -Meshtastic is a tool for using Android devices with open-source, off-grid mesh radios. This app is the main client for the Meshtastic project, allowing you to manage your mesh devices and communicate with other users. - -For more information about the Meshtastic project, please visit our website: meshtastic.org. The firmware that runs on the radio devices is a separate open-source project, which you can find here: https://github.com/meshtastic/Meshtastic-device. - -Community and Support - -This project is currently in beta. We would love to hear from you! If you have questions, feedback, or encounter any problems, please join our friendly and active community: - -• Discussion Forum: https://github.com/orgs/meshtastic/discussions -• Discord: https://discord.gg/meshtastic -• Report an Issue: https://github.com/meshtastic/Meshtastic-Android/issues - -Documentation - -To learn more about the features and capabilities of this app and Meshtastic, please view our official documentation: -View Documentation - -Translations - -You can help translate the app into your native language using Crowdin: -https://crowdin.meshtastic.org/android diff --git a/fastlane/metadata/android/sq-AL/short_description.txt b/fastlane/metadata/android/sq-AL/short_description.txt deleted file mode 100644 index e3f0988dbd..0000000000 --- a/fastlane/metadata/android/sq-AL/short_description.txt +++ /dev/null @@ -1 +0,0 @@ -The official app for Meshtastic, an open-source, off-grid, mesh radio. \ No newline at end of file diff --git a/fastlane/metadata/android/sq/changelogs/default.txt b/fastlane/metadata/android/sq/changelogs/default.txt new file mode 100644 index 0000000000..76874cc530 --- /dev/null +++ b/fastlane/metadata/android/sq/changelogs/default.txt @@ -0,0 +1,3 @@ +Stability and reliability fixes. + +Full release notes: https://github.com/meshtastic/Meshtastic-Android/releases diff --git a/fastlane/metadata/android/ro-RO/full_description.txt b/fastlane/metadata/android/sq/full_description.txt similarity index 100% rename from fastlane/metadata/android/ro-RO/full_description.txt rename to fastlane/metadata/android/sq/full_description.txt diff --git a/fastlane/metadata/android/lt-LT/short_description.txt b/fastlane/metadata/android/sq/short_description.txt similarity index 100% rename from fastlane/metadata/android/lt-LT/short_description.txt rename to fastlane/metadata/android/sq/short_description.txt diff --git a/fastlane/metadata/android/sk-SK/title.txt b/fastlane/metadata/android/sq/title.txt similarity index 100% rename from fastlane/metadata/android/sk-SK/title.txt rename to fastlane/metadata/android/sq/title.txt diff --git a/fastlane/metadata/android/sr-Latn/changelogs/default.txt b/fastlane/metadata/android/sr-Latn/changelogs/default.txt deleted file mode 100644 index 0553de284f..0000000000 --- a/fastlane/metadata/android/sr-Latn/changelogs/default.txt +++ /dev/null @@ -1 +0,0 @@ -For detailed release notes, please visit: https://github.com/meshtastic/Meshtastic-Android/releases/ \ No newline at end of file diff --git a/fastlane/metadata/android/sr-Latn/full_description.txt b/fastlane/metadata/android/sr-Latn/full_description.txt deleted file mode 100644 index 4ade9c5360..0000000000 --- a/fastlane/metadata/android/sr-Latn/full_description.txt +++ /dev/null @@ -1,21 +0,0 @@ -Meshtastic is a tool for using Android devices with open-source, off-grid mesh radios. This app is the main client for the Meshtastic project, allowing you to manage your mesh devices and communicate with other users. - -For more information about the Meshtastic project, please visit our website: meshtastic.org. The firmware that runs on the radio devices is a separate open-source project, which you can find here: https://github.com/meshtastic/Meshtastic-device. - -Community and Support - -This project is currently in beta. We would love to hear from you! If you have questions, feedback, or encounter any problems, please join our friendly and active community: - -• Discussion Forum: https://github.com/orgs/meshtastic/discussions -• Discord: https://discord.gg/meshtastic -• Report an Issue: https://github.com/meshtastic/Meshtastic-Android/issues - -Documentation - -To learn more about the features and capabilities of this app and Meshtastic, please view our official documentation: -View Documentation - -Translations - -You can help translate the app into your native language using Crowdin: -https://crowdin.meshtastic.org/android diff --git a/fastlane/metadata/android/sr-Latn/short_description.txt b/fastlane/metadata/android/sr-Latn/short_description.txt deleted file mode 100644 index e3f0988dbd..0000000000 --- a/fastlane/metadata/android/sr-Latn/short_description.txt +++ /dev/null @@ -1 +0,0 @@ -The official app for Meshtastic, an open-source, off-grid, mesh radio. \ No newline at end of file diff --git a/fastlane/metadata/android/sr-Latn/title.txt b/fastlane/metadata/android/sr-Latn/title.txt deleted file mode 100644 index c3ff693533..0000000000 --- a/fastlane/metadata/android/sr-Latn/title.txt +++ /dev/null @@ -1 +0,0 @@ -Meshtastic \ No newline at end of file diff --git a/fastlane/metadata/android/sr/changelogs/default.txt b/fastlane/metadata/android/sr/changelogs/default.txt new file mode 100644 index 0000000000..76874cc530 --- /dev/null +++ b/fastlane/metadata/android/sr/changelogs/default.txt @@ -0,0 +1,3 @@ +Stability and reliability fixes. + +Full release notes: https://github.com/meshtastic/Meshtastic-Android/releases diff --git a/fastlane/metadata/android/sl-SI/full_description.txt b/fastlane/metadata/android/sr/full_description.txt similarity index 100% rename from fastlane/metadata/android/sl-SI/full_description.txt rename to fastlane/metadata/android/sr/full_description.txt diff --git a/fastlane/metadata/android/sl-SI/short_description.txt b/fastlane/metadata/android/sr/short_description.txt similarity index 100% rename from fastlane/metadata/android/sl-SI/short_description.txt rename to fastlane/metadata/android/sr/short_description.txt diff --git a/fastlane/metadata/android/sl-SI/title.txt b/fastlane/metadata/android/sr/title.txt similarity index 100% rename from fastlane/metadata/android/sl-SI/title.txt rename to fastlane/metadata/android/sr/title.txt diff --git a/fastlane/metadata/android/srp/changelogs/default.txt b/fastlane/metadata/android/srp/changelogs/default.txt deleted file mode 100644 index 0553de284f..0000000000 --- a/fastlane/metadata/android/srp/changelogs/default.txt +++ /dev/null @@ -1 +0,0 @@ -For detailed release notes, please visit: https://github.com/meshtastic/Meshtastic-Android/releases/ \ No newline at end of file diff --git a/fastlane/metadata/android/srp/full_description.txt b/fastlane/metadata/android/srp/full_description.txt deleted file mode 100644 index 4ade9c5360..0000000000 --- a/fastlane/metadata/android/srp/full_description.txt +++ /dev/null @@ -1,21 +0,0 @@ -Meshtastic is a tool for using Android devices with open-source, off-grid mesh radios. This app is the main client for the Meshtastic project, allowing you to manage your mesh devices and communicate with other users. - -For more information about the Meshtastic project, please visit our website: meshtastic.org. The firmware that runs on the radio devices is a separate open-source project, which you can find here: https://github.com/meshtastic/Meshtastic-device. - -Community and Support - -This project is currently in beta. We would love to hear from you! If you have questions, feedback, or encounter any problems, please join our friendly and active community: - -• Discussion Forum: https://github.com/orgs/meshtastic/discussions -• Discord: https://discord.gg/meshtastic -• Report an Issue: https://github.com/meshtastic/Meshtastic-Android/issues - -Documentation - -To learn more about the features and capabilities of this app and Meshtastic, please view our official documentation: -View Documentation - -Translations - -You can help translate the app into your native language using Crowdin: -https://crowdin.meshtastic.org/android diff --git a/fastlane/metadata/android/srp/short_description.txt b/fastlane/metadata/android/srp/short_description.txt deleted file mode 100644 index e3f0988dbd..0000000000 --- a/fastlane/metadata/android/srp/short_description.txt +++ /dev/null @@ -1 +0,0 @@ -The official app for Meshtastic, an open-source, off-grid, mesh radio. \ No newline at end of file diff --git a/fastlane/metadata/android/srp/title.txt b/fastlane/metadata/android/srp/title.txt deleted file mode 100644 index c3ff693533..0000000000 --- a/fastlane/metadata/android/srp/title.txt +++ /dev/null @@ -1 +0,0 @@ -Meshtastic \ No newline at end of file diff --git a/fastlane/metadata/android/sv-SE/changelogs/default.txt b/fastlane/metadata/android/sv-SE/changelogs/default.txt index 739f2224ba..76874cc530 100644 --- a/fastlane/metadata/android/sv-SE/changelogs/default.txt +++ b/fastlane/metadata/android/sv-SE/changelogs/default.txt @@ -1 +1,3 @@ -För detaljerade versionsfakta, besök https://github.com/meshtastic/Meshtastic-Android/releases/ \ No newline at end of file +Stability and reliability fixes. + +Full release notes: https://github.com/meshtastic/Meshtastic-Android/releases diff --git a/fastlane/metadata/android/tr-TR/changelogs/default.txt b/fastlane/metadata/android/tr-TR/changelogs/default.txt index 0553de284f..76874cc530 100644 --- a/fastlane/metadata/android/tr-TR/changelogs/default.txt +++ b/fastlane/metadata/android/tr-TR/changelogs/default.txt @@ -1 +1,3 @@ -For detailed release notes, please visit: https://github.com/meshtastic/Meshtastic-Android/releases/ \ No newline at end of file +Stability and reliability fixes. + +Full release notes: https://github.com/meshtastic/Meshtastic-Android/releases diff --git a/fastlane/metadata/android/uk-UA/changelogs/default.txt b/fastlane/metadata/android/uk-UA/changelogs/default.txt deleted file mode 100644 index 3ba3109749..0000000000 --- a/fastlane/metadata/android/uk-UA/changelogs/default.txt +++ /dev/null @@ -1 +0,0 @@ -Для детальних нотаток до релізів, відвідайте: https://github.com/meshtastic/Meshtastic-Android/releases/ \ No newline at end of file diff --git a/fastlane/metadata/android/uk-UA/title.txt b/fastlane/metadata/android/uk-UA/title.txt deleted file mode 100644 index c3ff693533..0000000000 --- a/fastlane/metadata/android/uk-UA/title.txt +++ /dev/null @@ -1 +0,0 @@ -Meshtastic \ No newline at end of file diff --git a/fastlane/metadata/android/uk/changelogs/default.txt b/fastlane/metadata/android/uk/changelogs/default.txt new file mode 100644 index 0000000000..76874cc530 --- /dev/null +++ b/fastlane/metadata/android/uk/changelogs/default.txt @@ -0,0 +1,3 @@ +Stability and reliability fixes. + +Full release notes: https://github.com/meshtastic/Meshtastic-Android/releases diff --git a/fastlane/metadata/android/uk-UA/full_description.txt b/fastlane/metadata/android/uk/full_description.txt similarity index 100% rename from fastlane/metadata/android/uk-UA/full_description.txt rename to fastlane/metadata/android/uk/full_description.txt diff --git a/fastlane/metadata/android/uk-UA/short_description.txt b/fastlane/metadata/android/uk/short_description.txt similarity index 100% rename from fastlane/metadata/android/uk-UA/short_description.txt rename to fastlane/metadata/android/uk/short_description.txt diff --git a/fastlane/metadata/android/sq-AL/title.txt b/fastlane/metadata/android/uk/title.txt similarity index 100% rename from fastlane/metadata/android/sq-AL/title.txt rename to fastlane/metadata/android/uk/title.txt diff --git a/fastlane/metadata/android/zh-CN/changelogs/default.txt b/fastlane/metadata/android/zh-CN/changelogs/default.txt index ddae0ffeba..76874cc530 100644 --- a/fastlane/metadata/android/zh-CN/changelogs/default.txt +++ b/fastlane/metadata/android/zh-CN/changelogs/default.txt @@ -1 +1,3 @@ -欲了解详细发布说明,请访问:https://github.com/meshtastic/Meshtastic-Android/releases/ \ No newline at end of file +Stability and reliability fixes. + +Full release notes: https://github.com/meshtastic/Meshtastic-Android/releases diff --git a/fastlane/metadata/android/zh-CN/full_description.txt b/fastlane/metadata/android/zh-CN/full_description.txt index aa3c2488c0..d37f568c64 100644 --- a/fastlane/metadata/android/zh-CN/full_description.txt +++ b/fastlane/metadata/android/zh-CN/full_description.txt @@ -1,10 +1,10 @@ -Meshtastic 是一款将安卓设备与开源、无互联网、基于多跳网状形无线电配合使用的客户端程序。 此应用是Meshtastic的安卓客户端,允许您管理您的Meshtastic设备并与其他Meshtastic用户沟通。 +Meshtastic 是一款将安卓设备与开源、无互联网、基于多跳网状形无线电配合使用的客户端程序。此应用是Meshtastic的安卓客户端,允许您管理您的Meshtastic设备并与其他Meshtastic用户沟通。 有关Meshtastic项目的更多信息,请访问我们的网站: meshtastic.org. 在无线电设备上运行的固件也是一个独立的开源项目,您可以在这里找到:https://github.com/meshtastic/Meshtastic-device. 社区和支持 -此项目目前处于测试阶段。 我们非常乐意听取您的建议和意见! 如果您有任何疑问,反馈或遇到任何问题,请加入我们友好和活跃的社区: +此项目目前处于测试阶段。我们非常乐意听取您的建议和意见!如果您有任何疑问,反馈或遇到任何问题,请加入我们友好和活跃的社区: • 论坛: https://github.com/orgs/meshtastic/discussions • Discord: https://discord.gg/meshtastic diff --git a/fastlane/metadata/android/zh-TW/changelogs/default.txt b/fastlane/metadata/android/zh-TW/changelogs/default.txt index ea12fa74f0..76874cc530 100644 --- a/fastlane/metadata/android/zh-TW/changelogs/default.txt +++ b/fastlane/metadata/android/zh-TW/changelogs/default.txt @@ -1 +1,3 @@ -詳細的版本說明請參閱:https://github.com/meshtastic/Meshtastic-Android/releases/ \ No newline at end of file +Stability and reliability fixes. + +Full release notes: https://github.com/meshtastic/Meshtastic-Android/releases diff --git a/fastlane/metadata/android/zh-TW/full_description.txt b/fastlane/metadata/android/zh-TW/full_description.txt index e41405fc5d..a93eb1474d 100644 --- a/fastlane/metadata/android/zh-TW/full_description.txt +++ b/fastlane/metadata/android/zh-TW/full_description.txt @@ -1,10 +1,10 @@ -Meshtastic 是一款 Android 應用程式,可搭配開源的離網網狀網路無線電裝置使用。 此應用程式是 Meshtastic 專案的主要客戶端,讓您可以管理網狀網路裝置並與其他使用者通訊。 +Meshtastic 是一款 Android 應用程式,可搭配開源的離網網狀網路無線電裝置使用。此應用程式是 Meshtastic 專案的主要客戶端,讓您可以管理網狀網路裝置並與其他使用者通訊。 -如需更多 Meshtastic 專案相關資訊,請造訪我們的網站:meshtastic.org。 執行於無線電裝置上的韌體是一個獨立的開源專案,您可以在此找到:https://github.com/meshtastic/Meshtastic-device。 +如需更多 Meshtastic 專案相關資訊,請造訪我們的網站:meshtastic.org。執行於無線電裝置上的韌體是一個獨立的開源專案,您可以在此找到:https://github.com/meshtastic/Meshtastic-device。 社群與支援 -本專案目前為 Beta 測試階段。 我們非常期待您的回饋! 無論您有任何疑問、回饋或遭遇問題,都歡迎加入我們友善的社群: +本專案目前為 Beta 測試階段。我們非常期待您的回饋!無論您有任何疑問、回饋或遭遇問題,都歡迎加入我們友善的社群: 討論論壇:https://github.com/orgs/meshtastic/discussions Discord:https://discord.gg/meshtastic diff --git a/feature/connections/src/commonMain/kotlin/org/meshtastic/feature/connections/component/ConnectionsPreviews.kt b/feature/connections/src/commonMain/kotlin/org/meshtastic/feature/connections/component/ConnectionsPreviews.kt index 44d96319d1..d0938354fc 100644 --- a/feature/connections/src/commonMain/kotlin/org/meshtastic/feature/connections/component/ConnectionsPreviews.kt +++ b/feature/connections/src/commonMain/kotlin/org/meshtastic/feature/connections/component/ConnectionsPreviews.kt @@ -63,7 +63,17 @@ fun DeviceListItemPreview() { @PreviewLightDark @Composable private fun DeviceListItemWithLongNamePreview() { - val node = Node(num = 13444, user = User(short_name = "AB12", long_name = "James' Rooftop Solar Repeater")) + val node = + Node( + num = 13444, + user = + User.Builder() + .also { wb -> + wb.short_name = "AB12" + wb.long_name = "James' Rooftop Solar Repeater" + } + .build(), + ) val device = DeviceListEntry.Tcp(name = "Meshtastic_ab12", fullAddress = "s192.168.1.101", node = node) AppTheme { DeviceListItem(connectionState = ConnectionState.Disconnected, device = device, onSelect = {}) } } diff --git a/feature/connections/src/commonMain/kotlin/org/meshtastic/feature/connections/ui/components/ConnectingDeviceInfo.kt b/feature/connections/src/commonMain/kotlin/org/meshtastic/feature/connections/ui/components/ConnectingDeviceInfo.kt index 479cbbb019..bc83662943 100644 --- a/feature/connections/src/commonMain/kotlin/org/meshtastic/feature/connections/ui/components/ConnectingDeviceInfo.kt +++ b/feature/connections/src/commonMain/kotlin/org/meshtastic/feature/connections/ui/components/ConnectingDeviceInfo.kt @@ -14,8 +14,6 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -@file:OptIn(ExperimentalMaterial3ExpressiveApi::class) - package org.meshtastic.feature.connections.ui.components import androidx.compose.foundation.layout.Arrangement @@ -25,7 +23,6 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.material3.CircularWavyProgressIndicator -import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable diff --git a/feature/connections/src/commonMain/kotlin/org/meshtastic/feature/connections/ui/components/ConnectionActionButton.kt b/feature/connections/src/commonMain/kotlin/org/meshtastic/feature/connections/ui/components/ConnectionActionButton.kt index f964db65d4..0d644b20d0 100644 --- a/feature/connections/src/commonMain/kotlin/org/meshtastic/feature/connections/ui/components/ConnectionActionButton.kt +++ b/feature/connections/src/commonMain/kotlin/org/meshtastic/feature/connections/ui/components/ConnectionActionButton.kt @@ -14,15 +14,12 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -@file:OptIn(ExperimentalMaterial3ExpressiveApi::class) - package org.meshtastic.feature.connections.ui.components import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.size import androidx.compose.material3.Button import androidx.compose.material3.ButtonDefaults -import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi import androidx.compose.material3.FilledTonalButton import androidx.compose.material3.Icon import androidx.compose.material3.OutlinedButton diff --git a/feature/connections/src/commonMain/kotlin/org/meshtastic/feature/connections/ui/components/CurrentlyConnectedInfo.kt b/feature/connections/src/commonMain/kotlin/org/meshtastic/feature/connections/ui/components/CurrentlyConnectedInfo.kt index 35db9cb395..76a3385f3d 100644 --- a/feature/connections/src/commonMain/kotlin/org/meshtastic/feature/connections/ui/components/CurrentlyConnectedInfo.kt +++ b/feature/connections/src/commonMain/kotlin/org/meshtastic/feature/connections/ui/components/CurrentlyConnectedInfo.kt @@ -14,8 +14,6 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -@file:OptIn(ExperimentalMaterial3ExpressiveApi::class) - package org.meshtastic.feature.connections.ui.components import androidx.compose.foundation.layout.Arrangement @@ -23,7 +21,6 @@ import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding -import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable @@ -157,10 +154,28 @@ private fun CurrentlyConnectedInfoPreview() { node = Node( num = 13444, - user = User(short_name = "\uD83E\uDEE0", long_name = "John Doe"), + user = + User.Builder() + .also { wb -> + wb.short_name = "\uD83E\uDEE0" + wb.long_name = "John Doe" + } + .build(), isIgnored = false, - paxcounter = Paxcount(ble = 10, wifi = 5), - environmentMetrics = EnvironmentMetrics(temperature = 25f, relative_humidity = 60f), + paxcounter = + Paxcount.Builder() + .also { wb -> + wb.ble = 10 + wb.wifi = 5 + } + .build(), + environmentMetrics = + EnvironmentMetrics.Builder() + .also { wb -> + wb.temperature = 25f + wb.relative_humidity = 60f + } + .build(), ), text = CurrentlyConnectedText( diff --git a/feature/connections/src/commonMain/kotlin/org/meshtastic/feature/connections/ui/components/DeviceListItem.kt b/feature/connections/src/commonMain/kotlin/org/meshtastic/feature/connections/ui/components/DeviceListItem.kt index 9f4ae158ab..c2b58d59ae 100644 --- a/feature/connections/src/commonMain/kotlin/org/meshtastic/feature/connections/ui/components/DeviceListItem.kt +++ b/feature/connections/src/commonMain/kotlin/org/meshtastic/feature/connections/ui/components/DeviceListItem.kt @@ -14,8 +14,6 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -@file:OptIn(ExperimentalMaterial3ExpressiveApi::class) - package org.meshtastic.feature.connections.ui.components import androidx.compose.foundation.ExperimentalFoundationApi @@ -27,7 +25,6 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.selection.selectable import androidx.compose.material3.CircularWavyProgressIndicator -import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi import androidx.compose.material3.Icon import androidx.compose.material3.ListItem import androidx.compose.material3.ListItemDefaults diff --git a/feature/connections/src/commonMain/kotlin/org/meshtastic/feature/connections/ui/components/DeviceSectionHeader.kt b/feature/connections/src/commonMain/kotlin/org/meshtastic/feature/connections/ui/components/DeviceSectionHeader.kt index f344041c3f..8605bd7bbc 100644 --- a/feature/connections/src/commonMain/kotlin/org/meshtastic/feature/connections/ui/components/DeviceSectionHeader.kt +++ b/feature/connections/src/commonMain/kotlin/org/meshtastic/feature/connections/ui/components/DeviceSectionHeader.kt @@ -14,8 +14,6 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -@file:OptIn(ExperimentalMaterial3ExpressiveApi::class) - package org.meshtastic.feature.connections.ui.components import androidx.compose.foundation.layout.Arrangement @@ -23,7 +21,6 @@ import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding -import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi import androidx.compose.material3.LinearWavyProgressIndicator import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text diff --git a/feature/connections/src/jvmTest/kotlin/org/meshtastic/feature/connections/ui/components/CurrentlyConnectedInfoRssiLifecycleTest.kt b/feature/connections/src/jvmTest/kotlin/org/meshtastic/feature/connections/ui/components/CurrentlyConnectedInfoRssiLifecycleTest.kt index fe014dbedc..17b37cc0df 100644 --- a/feature/connections/src/jvmTest/kotlin/org/meshtastic/feature/connections/ui/components/CurrentlyConnectedInfoRssiLifecycleTest.kt +++ b/feature/connections/src/jvmTest/kotlin/org/meshtastic/feature/connections/ui/components/CurrentlyConnectedInfoRssiLifecycleTest.kt @@ -66,7 +66,17 @@ class CurrentlyConnectedInfoRssiLifecycleTest { CompositionLocalProvider(LocalLifecycleOwner provides lifecycleOwner) { MaterialTheme { CurrentlyConnectedInfo( - node = Node(num = 1, user = User(long_name = "Test Node", short_name = "TST")), + node = + Node( + num = 1, + user = + User.Builder() + .also { wb -> + wb.long_name = "Test Node" + wb.short_name = "TST" + } + .build(), + ), text = CurrentlyConnectedText( unknownLabel = "Unknown", diff --git a/feature/discovery/src/commonMain/kotlin/org/meshtastic/feature/discovery/DiscoveryHomeRestorer.kt b/feature/discovery/src/commonMain/kotlin/org/meshtastic/feature/discovery/DiscoveryHomeRestorer.kt index e250ba3695..451c655c77 100644 --- a/feature/discovery/src/commonMain/kotlin/org/meshtastic/feature/discovery/DiscoveryHomeRestorer.kt +++ b/feature/discovery/src/commonMain/kotlin/org/meshtastic/feature/discovery/DiscoveryHomeRestorer.kt @@ -385,11 +385,19 @@ internal class DiscoveryHomeRestorer( val primaryChannel = plan.primaryChannel ?.takeIf { plan.restorePrimaryChannel } - ?.let { Channel(index = 0, role = Channel.Role.PRIMARY, settings = it) } + ?.let { + Channel.Builder() + .also { wb -> + wb.index = 0 + wb.role = Channel.Role.PRIMARY + wb.settings = it + } + .build() + } val restored = radioController.restoreLocalConfiguration( expectedDeviceAddress = plan.deviceAddress, - config = Config(lora = plan.loraConfig), + config = Config.Builder().also { wb -> wb.lora = plan.loraConfig }.build(), primaryChannel = primaryChannel, ) if (!restored) return false diff --git a/feature/discovery/src/commonMain/kotlin/org/meshtastic/feature/discovery/DiscoveryScanEngine.kt b/feature/discovery/src/commonMain/kotlin/org/meshtastic/feature/discovery/DiscoveryScanEngine.kt index 94803efa58..0702830ffe 100644 --- a/feature/discovery/src/commonMain/kotlin/org/meshtastic/feature/discovery/DiscoveryScanEngine.kt +++ b/feature/discovery/src/commonMain/kotlin/org/meshtastic/feature/discovery/DiscoveryScanEngine.kt @@ -37,6 +37,9 @@ import kotlinx.coroutines.sync.withLock import kotlinx.coroutines.withTimeoutOrNull import org.koin.core.annotation.Single import org.meshtastic.core.common.di.ApplicationCoroutineScope +import org.meshtastic.core.common.state.OperationLease +import org.meshtastic.core.common.state.RadioOperation +import org.meshtastic.core.common.state.RadioOperationLock import org.meshtastic.core.common.util.latLongToMeter import org.meshtastic.core.common.util.nowMillis import org.meshtastic.core.database.dao.DiscoveryDao @@ -86,6 +89,7 @@ class DiscoveryScanEngine( private val applicationScope: ApplicationCoroutineScope, private val dispatchers: CoroutineDispatchers, private val meshPrefs: MeshPrefs, + private val radioOperationLock: RadioOperationLock, ) : DiscoveryPacketCollector { // region Public state @@ -115,7 +119,13 @@ class DiscoveryScanEngine( homeRestorer = homeRestorer, applicationScope = applicationScope, onSessionUpdated = { updated -> _currentSession.value = updated }, - onTerminalCompleted = { outcome -> _scanState.value = DiscoveryScanState.Complete(outcome) }, + onTerminalCompleted = { outcome -> + _scanState.value = DiscoveryScanState.Complete(outcome) + // Terminal cleanup has restored the home preset by now, so the radio is back where the user left it. + // Releasing any earlier — when the scan scope is cancelled — would drop the protection during the + // restore itself. Safe to run twice: the coordinator may republish a corrected outcome. + releaseScanLease() + }, cancelScan = { mutex.withLock { cancelScanInternal() } }, ) private val interruptedSessionRecovery = @@ -135,6 +145,9 @@ class DiscoveryScanEngine( }, ) private var scanScope: CoroutineScope? = null + + /** Held from scan start until terminal cleanup finishes, so it outlives [scanScope]'s cancellation. */ + private var scanLease: OperationLease? = null private var dwellJob: Job? = null private var originalLoRaConfig: Config.LoRaConfig? = null @@ -145,6 +158,9 @@ class DiscoveryScanEngine( private var tunedPrimaryChannel: Boolean = false private var sessionId: Long = 0 + /** The radio [sessionId]'s row was written against. Identifies the session across per-device databases. */ + private var sessionDeviceAddress: String? = null + /** Nodes collected for the current preset dwell. Keyed by nodeNum. */ private val collectedNodes = mutableMapOf() @@ -316,24 +332,36 @@ class DiscoveryScanEngine( if (!isScanPreparationCurrent(deviceAddress, sessionGeneration)) { discoveryDao.deleteSession(insertedSessionId) sessionId = 0L + sessionDeviceAddress = null originalLoRaConfig = null originalPrimaryChannel = null _scanState.value = DiscoveryScanState.Failed("Selected radio changed while preparing the scan") return } sessionId = insertedSessionId + sessionDeviceAddress = deviceAddress _currentSession.value = session.copy(id = sessionId) collectorRegistry.collector = this _scanState.value = DiscoveryScanState.Shifting(targets.first().label) currentPresetName = targets.first().label totalDwellSeconds = dwellDurationSeconds currentDwellPersisted = false + // Taken before the loop starts and released by the terminal callback, not by this scope. Terminal processing + // cancels scanScope before the home-preset restore runs, so a hold owned by the scope would end during the + // restore — leaving the radio on a scanned preset, which is the failure this protects against. + scanLease = radioOperationLock.acquire(RadioOperation.DiscoveryScan) CoroutineScope(dispatchers.io + SupervisorJob()).also { scope -> scanScope = scope scope.launch { runScanLoop(targets, dwellDurationSeconds) } } } + /** Drops the scan's hold on the radio. Idempotent — the terminal outcome may be published more than once. */ + private fun releaseScanLease() { + radioOperationLock.release(scanLease) + scanLease = null + } + private fun isScanPreparationCurrent(deviceAddress: String?, sessionGeneration: Long): Boolean = meshPrefs.deviceAddress.value == deviceAddress && radioController.sessionGeneration.value == sessionGeneration && @@ -382,6 +410,7 @@ class DiscoveryScanEngine( } _currentSession.value = null _scanState.value = DiscoveryScanState.Idle + releaseScanLease() } finally { mutex.unlock() } @@ -536,11 +565,22 @@ class DiscoveryScanEngine( // Start from the captured original config so unrelated fields (hop_limit, tx_power, tx_enabled, …) are carried // over instead of zeroed by a fresh LoRaConfig — a from-scratch config can e.g. break the dwell-boundary // NeighborInfo request. Only the preset (and, for custom channels, region + channel_num) is overridden. - val base = originalLoRaConfig ?: Config.LoRaConfig() + val base = originalLoRaConfig ?: Config.LoRaConfig.Builder().build() if (target.channel == null) { // Public-preset target — dwell on the preset using the radio's existing primary channel (unchanged). radioController.setLocalConfig( - Config(lora = base.copy(use_preset = true, modem_preset = target.preset.modemPreset)), + Config.Builder() + .also { wb -> + wb.lora = + base + .newBuilder() + .also { wb -> + wb.use_preset = true + wb.modem_preset = target.preset.modemPreset + } + .build() + } + .build(), ) Logger.i { "DiscoveryScanEngine: shifted to ${target.label} (use_preset=true)" } } else { @@ -548,19 +588,32 @@ class DiscoveryScanEngine( // frequency from the new name, then tune the primary channel to the offered name+PSK so nodes on that mesh // are heard. The original primary channel is restored after the scan. radioController.setLocalConfig( - Config( - lora = - base.copy( - use_preset = true, - modem_preset = target.preset.modemPreset, - region = target.region ?: base.region, - channel_num = 0, - ), - ), + Config.Builder() + .also { wb -> + wb.lora = + base + .newBuilder() + .also { wb -> + wb.use_preset = true + wb.modem_preset = target.preset.modemPreset + wb.region = target.region ?: base.region + wb.channel_num = 0 + } + .build() + } + .build(), ) currentCoroutineContext().ensureActive() mutex.withLock { tunedPrimaryChannel = true } - radioController.setLocalChannel(Channel(index = 0, role = Channel.Role.PRIMARY, settings = target.channel)) + radioController.setLocalChannel( + Channel.Builder() + .also { wb -> + wb.index = 0 + wb.role = Channel.Role.PRIMARY + wb.settings = target.channel + } + .build(), + ) Logger.i { "DiscoveryScanEngine: shifted to ${target.label} (custom channel)" } } // The firmware often restarts the radio or reboots after a LoRa config change. @@ -710,28 +763,32 @@ class DiscoveryScanEngine( if (sessionId == 0L) return mutex.withLock { if (currentDwellPersisted) return@withLock - if (collectedNodes.isEmpty()) { - persistEmptyPresetResult() - currentDwellPersisted = true - } else { - val presetResultId = persistPresetResult() - persistDiscoveredNodes(presetResultId) - currentDwellPersisted = true + val deviceAddress = sessionDeviceAddress + if (deviceAddress == null) { + Logger.w { "DiscoveryScanEngine: session $sessionId has no device address; skipping dwell persistence" } + return@withLock } + val result = if (collectedNodes.isEmpty()) emptyPresetResult() else presetResult() + // One transaction, so a device/DB switch cannot land between the parent-session check and these writes. + // A null return means this device's session row is not in the active database and the dwell is unwritable. + if (discoveryDao.insertDwellIfSessionExists(result, discoveredNodeEntities(), deviceAddress) == null) { + Logger.w { + "DiscoveryScanEngine: session $sessionId for $deviceAddress is not in the active database; " + + "skipping dwell persistence" + } + return@withLock + } + currentDwellPersisted = true } } - private suspend fun persistEmptyPresetResult() { - val emptyResult = - DiscoveryPresetResultEntity( - sessionId = sessionId, - presetName = currentPresetName, - dwellDurationSeconds = totalDwellSeconds, - ) - discoveryDao.insertPresetResult(emptyResult) - } + private fun emptyPresetResult() = DiscoveryPresetResultEntity( + sessionId = sessionId, + presetName = currentPresetName, + dwellDurationSeconds = totalDwellSeconds, + ) - private suspend fun persistPresetResult(): Long { + private fun presetResult(): DiscoveryPresetResultEntity { val (avgChannelUtil, avgAirUtil) = computeAverageMetrics() val directCount = collectedNodes.values.count { it.neighborType == "direct" } val meshCount = collectedNodes.values.count { it.neighborType == "mesh" } @@ -766,7 +823,7 @@ class DiscoveryScanEngine( numTotalNodes = lastLocalStats?.num_total_nodes ?: 0, uptimeSeconds = lastLocalStats?.uptime_seconds ?: 0, ) - return discoveryDao.insertPresetResult(presetResult) + return presetResult } /** @@ -780,13 +837,16 @@ class DiscoveryScanEngine( return successRate to failureRate } - private suspend fun persistDiscoveredNodes(presetResultId: Long) { + /** + * Builds this dwell's node rows. `presetResultId` is a placeholder - [DiscoveryDao.insertDwellIfSessionExists] + * stamps the real one inside its transaction. + */ + private suspend fun discoveredNodeEntities(): List { + if (collectedNodes.isEmpty()) return emptyList() val session = discoveryDao.getSession(sessionId) val userLat = session?.userLatitude ?: 0.0 val userLon = session?.userLongitude ?: 0.0 - - val nodeEntities = collectedNodes.values.map { data -> data.toEntity(presetResultId, userLat, userLon) } - discoveryDao.insertDiscoveredNodes(nodeEntities) + return collectedNodes.values.map { data -> data.toEntity(presetResultId = 0L, userLat, userLon) } } private fun CollectedNodeData.toEntity( diff --git a/feature/discovery/src/commonMain/kotlin/org/meshtastic/feature/discovery/DiscoveryViewModel.kt b/feature/discovery/src/commonMain/kotlin/org/meshtastic/feature/discovery/DiscoveryViewModel.kt index edb3ff2a65..a0cdfa14db 100644 --- a/feature/discovery/src/commonMain/kotlin/org/meshtastic/feature/discovery/DiscoveryViewModel.kt +++ b/feature/discovery/src/commonMain/kotlin/org/meshtastic/feature/discovery/DiscoveryViewModel.kt @@ -208,7 +208,13 @@ class DiscoveryViewModel( ScanTarget( preset = preset, label = "${preset.name} · ${bc.name}", - channel = ChannelSettings(name = bc.name, psk = bc.psk), + channel = + ChannelSettings.Builder() + .also { wb -> + wb.name = bc.name + wb.psk = bc.psk + } + .build(), region = bc.region.takeIf { it != RegionCode.UNSET }, ) } diff --git a/feature/discovery/src/commonTest/kotlin/org/meshtastic/feature/discovery/DiscoveryHomeRestorerTest.kt b/feature/discovery/src/commonTest/kotlin/org/meshtastic/feature/discovery/DiscoveryHomeRestorerTest.kt index 8da4487119..f45edafddd 100644 --- a/feature/discovery/src/commonTest/kotlin/org/meshtastic/feature/discovery/DiscoveryHomeRestorerTest.kt +++ b/feature/discovery/src/commonTest/kotlin/org/meshtastic/feature/discovery/DiscoveryHomeRestorerTest.kt @@ -66,7 +66,7 @@ class DiscoveryHomeRestorerTest { DiscoveryHomeRestorePlan( sessionId = 1L, deviceAddress = firstDevice, - loraConfig = Config.LoRaConfig(use_preset = true), + loraConfig = Config.LoRaConfig.Builder().also { wb -> wb.use_preset = true }.build(), primaryChannel = null, restorePrimaryChannel = false, finalStatus = DiscoverySessionStatus.COMPLETE, @@ -100,7 +100,7 @@ class DiscoveryHomeRestorerTest { DiscoveryHomeRestorePlan( sessionId = 1L, deviceAddress = device, - loraConfig = Config.LoRaConfig(use_preset = true), + loraConfig = Config.LoRaConfig.Builder().also { wb -> wb.use_preset = true }.build(), primaryChannel = null, restorePrimaryChannel = false, finalStatus = DiscoverySessionStatus.COMPLETE, @@ -156,7 +156,7 @@ class DiscoveryHomeRestorerTest { DiscoveryHomeRestorePlan( sessionId = 7L, deviceAddress = device, - loraConfig = Config.LoRaConfig(use_preset = true), + loraConfig = Config.LoRaConfig.Builder().also { wb -> wb.use_preset = true }.build(), primaryChannel = null, restorePrimaryChannel = false, finalStatus = DiscoverySessionStatus.COMPLETE, @@ -186,7 +186,7 @@ class DiscoveryHomeRestorerTest { DiscoveryHomeRestorePlan( sessionId = 1L, deviceAddress = device, - loraConfig = Config.LoRaConfig(use_preset = true), + loraConfig = Config.LoRaConfig.Builder().also { wb -> wb.use_preset = true }.build(), primaryChannel = null, restorePrimaryChannel = false, finalStatus = DiscoverySessionStatus.COMPLETE, @@ -217,7 +217,7 @@ class DiscoveryHomeRestorerTest { DiscoveryHomeRestorePlan( sessionId = 7L, deviceAddress = firstDevice, - loraConfig = Config.LoRaConfig(use_preset = true), + loraConfig = Config.LoRaConfig.Builder().also { wb -> wb.use_preset = true }.build(), primaryChannel = null, restorePrimaryChannel = false, finalStatus = DiscoverySessionStatus.COMPLETE, @@ -247,7 +247,7 @@ class DiscoveryHomeRestorerTest { homePreset = homePreset, completionStatus = DiscoverySessionStatus.INTERRUPTED, deviceAddress = device, - homeLoraConfig = Config.LoRaConfig(use_preset = true), + homeLoraConfig = Config.LoRaConfig.Builder().also { wb -> wb.use_preset = true }.build(), ), ) } @@ -284,8 +284,13 @@ class DiscoveryHomeRestorerTest { val restored = radioController.restoreLocalConfiguration( expectedDeviceAddress = "x:STALE", - config = Config(lora = Config.LoRaConfig(use_preset = true)), - primaryChannel = Channel(index = 0), + config = + Config.Builder() + .also { wb -> + wb.lora = Config.LoRaConfig.Builder().also { wb -> wb.use_preset = true }.build() + } + .build(), + primaryChannel = Channel.Builder().also { wb -> wb.index = 0 }.build(), ) assertFalse(restored) @@ -321,7 +326,7 @@ class DiscoveryHomeRestorerTest { DiscoveryHomeRestorePlan( sessionId = sessionId, deviceAddress = device, - loraConfig = Config.LoRaConfig(use_preset = true), + loraConfig = Config.LoRaConfig.Builder().also { wb -> wb.use_preset = true }.build(), primaryChannel = null, restorePrimaryChannel = true, finalStatus = DiscoverySessionStatus.FAILED, @@ -348,7 +353,7 @@ class DiscoveryHomeRestorerTest { failLocalConfigWritesRemaining = 2 } val discoveryDao = SharedInMemoryDiscoveryDao() - val loraConfig = Config.LoRaConfig(use_preset = true) + val loraConfig = Config.LoRaConfig.Builder().also { wb -> wb.use_preset = true }.build() val sessionId = discoveryDao.insertSession( DiscoverySessionEntity( @@ -406,7 +411,7 @@ class DiscoveryHomeRestorerTest { val serviceRepository = FakeServiceRepository().apply { setConnectionState(ConnectionState.Connected) } val radioController = DiscoveryTestRadioController().apply { selectedDeviceAddress = device } val discoveryDao = SharedInMemoryDiscoveryDao() - val loraConfig = Config.LoRaConfig(use_preset = true) + val loraConfig = Config.LoRaConfig.Builder().also { wb -> wb.use_preset = true }.build() val sessionId = discoveryDao.insertSession( DiscoverySessionEntity( @@ -463,7 +468,7 @@ class DiscoveryHomeRestorerTest { acceptConditionalRestore = false } val discoveryDao = SharedInMemoryDiscoveryDao() - val loraConfig = Config.LoRaConfig(use_preset = true) + val loraConfig = Config.LoRaConfig.Builder().also { wb -> wb.use_preset = true }.build() val sessionId = discoveryDao.insertSession( DiscoverySessionEntity( @@ -527,7 +532,7 @@ class DiscoveryHomeRestorerTest { acceptConditionalRestore = false } val discoveryDao = SharedInMemoryDiscoveryDao() - val loraConfig = Config.LoRaConfig(use_preset = true) + val loraConfig = Config.LoRaConfig.Builder().also { wb -> wb.use_preset = true }.build() val sessionId = discoveryDao.insertSession( DiscoverySessionEntity( @@ -585,7 +590,7 @@ class DiscoveryHomeRestorerTest { onLocalConfigWriteAttempt = { serviceRepository.setConnectionState(ConnectionState.Disconnected) } } val discoveryDao = SharedInMemoryDiscoveryDao() - val loraConfig = Config.LoRaConfig(use_preset = true) + val loraConfig = Config.LoRaConfig.Builder().also { wb -> wb.use_preset = true }.build() val sessionId = discoveryDao.insertSession( DiscoverySessionEntity( @@ -650,7 +655,7 @@ class DiscoveryHomeRestorerTest { failLocalConfigWritesRemaining = Int.MAX_VALUE } val discoveryDao = SharedInMemoryDiscoveryDao() - val loraConfig = Config.LoRaConfig(use_preset = true) + val loraConfig = Config.LoRaConfig.Builder().also { wb -> wb.use_preset = true }.build() val sessionId = discoveryDao.insertSession( DiscoverySessionEntity( @@ -715,7 +720,7 @@ class DiscoveryHomeRestorerTest { failLocalConfigWritesRemaining = Int.MAX_VALUE } val backingDao = SharedInMemoryDiscoveryDao() - val loraConfig = Config.LoRaConfig(use_preset = true) + val loraConfig = Config.LoRaConfig.Builder().also { wb -> wb.use_preset = true }.build() val sessionId = backingDao.insertSession( DiscoverySessionEntity( diff --git a/feature/discovery/src/commonTest/kotlin/org/meshtastic/feature/discovery/DiscoveryPacketCollectionTest.kt b/feature/discovery/src/commonTest/kotlin/org/meshtastic/feature/discovery/DiscoveryPacketCollectionTest.kt index ea9092755e..c45e6f95f3 100644 --- a/feature/discovery/src/commonTest/kotlin/org/meshtastic/feature/discovery/DiscoveryPacketCollectionTest.kt +++ b/feature/discovery/src/commonTest/kotlin/org/meshtastic/feature/discovery/DiscoveryPacketCollectionTest.kt @@ -26,6 +26,7 @@ import kotlinx.coroutines.test.runTest import okio.ByteString import okio.ByteString.Companion.toByteString import org.meshtastic.core.common.di.ApplicationCoroutineScope +import org.meshtastic.core.common.state.RadioOperationLock import org.meshtastic.core.database.entity.DiscoveryPresetResultEntity import org.meshtastic.core.database.entity.DiscoverySessionEntity import org.meshtastic.core.di.CoroutineDispatchers @@ -67,9 +68,17 @@ class DiscoveryPacketCollectionTest { private val radioConfigRepository = FakeRadioConfigRepository().apply { setLocalConfigDirect( - LocalConfig( - lora = Config.LoRaConfig(use_preset = true, modem_preset = ChannelOption.LONG_FAST.modemPreset), - ), + LocalConfig.Builder() + .also { wb -> + wb.lora = + Config.LoRaConfig.Builder() + .also { wb -> + wb.use_preset = true + wb.modem_preset = ChannelOption.LONG_FAST.modemPreset + } + .build() + } + .build(), ) } private val collectorRegistry = PacketTestCollectorRegistry() @@ -95,6 +104,7 @@ class DiscoveryPacketCollectionTest { applicationScope = appScope, dispatchers = dispatchers, meshPrefs = meshPrefs, + radioOperationLock = RadioOperationLock(), ) } @@ -281,22 +291,74 @@ class DiscoveryPacketCollectionTest { ) private fun positionPacket(from: Int, latI: Int, lonI: Int, snr: Float = 5.5f, rssi: Int = -70): MeshPacket { - val posPayload = Position.ADAPTER.encode(Position(latitude_i = latI, longitude_i = lonI)).toByteString() - val data = Data(portnum = PortNum.POSITION_APP, payload = posPayload) - return MeshPacket(from = from, decoded = data, rx_snr = snr, rx_rssi = rssi) + val posPayload = + Position.ADAPTER.encode( + Position.Builder() + .also { wb -> + wb.latitude_i = latI + wb.longitude_i = lonI + } + .build(), + ) + .toByteString() + val data = + Data.Builder() + .also { wb -> + wb.portnum = PortNum.POSITION_APP + wb.payload = posPayload + } + .build() + return MeshPacket.Builder() + .also { wb -> + wb.from = from + wb.decoded = data + wb.rx_snr = snr + wb.rx_rssi = rssi + } + .build() } private fun textMessagePacket(from: Int): MeshPacket { - val data = Data(portnum = PortNum.TEXT_MESSAGE_APP, payload = "hello".encodeToByteArray().toByteString()) - return MeshPacket(from = from, decoded = data, rx_snr = 3.0f, rx_rssi = -80) + val data = + Data.Builder() + .also { wb -> + wb.portnum = PortNum.TEXT_MESSAGE_APP + wb.payload = "hello".encodeToByteArray().toByteString() + } + .build() + return MeshPacket.Builder() + .also { wb -> + wb.from = from + wb.decoded = data + wb.rx_snr = 3.0f + wb.rx_rssi = -80 + } + .build() } private fun neighborInfoPacket(from: Int, neighborNodeIds: List): MeshPacket { - val neighbors = neighborNodeIds.map { Neighbor(node_id = it) } - val ni = NeighborInfo(node_id = from, neighbors = neighbors) + val neighbors = neighborNodeIds.map { Neighbor.Builder().also { wb -> wb.node_id = it }.build() } + val ni = + NeighborInfo.Builder() + .also { wb -> + wb.node_id = from + wb.neighbors = neighbors + } + .build() val payload = NeighborInfo.ADAPTER.encode(ni).toByteString() - val data = Data(portnum = PortNum.NEIGHBORINFO_APP, payload = payload) - return MeshPacket(from = from, decoded = data) + val data = + Data.Builder() + .also { wb -> + wb.portnum = PortNum.NEIGHBORINFO_APP + wb.payload = payload + } + .build() + return MeshPacket.Builder() + .also { wb -> + wb.from = from + wb.decoded = data + } + .build() } private fun dataPacket(from: Int) = DataPacket( diff --git a/feature/discovery/src/commonTest/kotlin/org/meshtastic/feature/discovery/DiscoveryScanEngineTest.kt b/feature/discovery/src/commonTest/kotlin/org/meshtastic/feature/discovery/DiscoveryScanEngineTest.kt index 1785f44731..2aa0dc5e03 100644 --- a/feature/discovery/src/commonTest/kotlin/org/meshtastic/feature/discovery/DiscoveryScanEngineTest.kt +++ b/feature/discovery/src/commonTest/kotlin/org/meshtastic/feature/discovery/DiscoveryScanEngineTest.kt @@ -32,6 +32,7 @@ import kotlinx.coroutines.test.runTest import okio.ByteString import okio.ByteString.Companion.toByteString import org.meshtastic.core.common.di.ApplicationCoroutineScope +import org.meshtastic.core.common.state.RadioOperationLock import org.meshtastic.core.database.dao.DiscoveryDao import org.meshtastic.core.database.entity.DiscoveredNodeEntity import org.meshtastic.core.database.entity.DiscoveryPresetResultEntity @@ -146,6 +147,24 @@ private class FakeDiscoveryDao(private val delegate: SharedInMemoryDiscoveryDao ) } + /** + * The engine writes a dwell through this one call now. Delegation would forward it straight to the backing fake and + * bypass [insertPresetResult]'s barrier hooks, so route it back through them here, and honour the parent check the + * real transaction performs - including its device-address match, not the id alone. + */ + override suspend fun insertDwellIfSessionExists( + result: DiscoveryPresetResultEntity, + nodes: List, + deviceAddress: String, + ): Long? { + if (delegate.countSessionsForDevice(result.sessionId, deviceAddress) == 0) return null + val presetResultId = insertPresetResult(result) + if (nodes.isNotEmpty()) { + delegate.insertDiscoveredNodes(nodes.map { it.copy(presetResultId = presetResultId) }) + } + return presetResultId + } + override suspend fun insertPresetResult(result: DiscoveryPresetResultEntity): Long { val id = delegate.insertPresetResult(result) nextInsertPresetResultEntered?.also { entered -> @@ -188,9 +207,17 @@ class DiscoveryScanEngineTest { private val radioConfigRepository = FakeRadioConfigRepository().apply { setLocalConfigDirect( - LocalConfig( - lora = Config.LoRaConfig(use_preset = true, modem_preset = ChannelOption.LONG_FAST.modemPreset), - ), + LocalConfig.Builder() + .also { wb -> + wb.lora = + Config.LoRaConfig.Builder() + .also { wb -> + wb.use_preset = true + wb.modem_preset = ChannelOption.LONG_FAST.modemPreset + } + .build() + } + .build(), ) } private val collectorRegistry = FakeCollectorRegistry() @@ -217,6 +244,7 @@ class DiscoveryScanEngineTest { applicationScope = appScope, dispatchers = dispatchers, meshPrefs = meshPrefs, + radioOperationLock = RadioOperationLock(), ) } @@ -266,8 +294,21 @@ class DiscoveryScanEngineTest { private fun createNodeWithPosition(num: Int, latI: Int = 0, lonI: Int = 0) = Node( num = num, - user = User(id = "!${num.toString(16)}", short_name = "T$num", long_name = "Test Node $num"), - position = Position(latitude_i = latI, longitude_i = lonI), + user = + User.Builder() + .also { wb -> + wb.id = "!${num.toString(16)}" + wb.short_name = "T$num" + wb.long_name = "Test Node $num" + } + .build(), + position = + Position.Builder() + .also { wb -> + wb.latitude_i = latI + wb.longitude_i = lonI + } + .build(), ) private fun createPositionMeshPacket( @@ -277,15 +318,50 @@ class DiscoveryScanEngineTest { snr: Float = 5.5f, rssi: Int = -70, ): MeshPacket { - val posPayload = Position.ADAPTER.encode(Position(latitude_i = latI, longitude_i = lonI)).toByteString() - val data = Data(portnum = PortNum.POSITION_APP, payload = posPayload) - return MeshPacket(from = from, decoded = data, rx_snr = snr, rx_rssi = rssi) + val posPayload = + Position.ADAPTER.encode( + Position.Builder() + .also { wb -> + wb.latitude_i = latI + wb.longitude_i = lonI + } + .build(), + ) + .toByteString() + val data = + Data.Builder() + .also { wb -> + wb.portnum = PortNum.POSITION_APP + wb.payload = posPayload + } + .build() + return MeshPacket.Builder() + .also { wb -> + wb.from = from + wb.decoded = data + wb.rx_snr = snr + wb.rx_rssi = rssi + } + .build() } private fun createTelemetryWithLocalStats(from: Int, localStats: LocalStats): MeshPacket { - val telPayload = Telemetry.ADAPTER.encode(Telemetry(local_stats = localStats)).toByteString() - val data = Data(portnum = PortNum.TELEMETRY_APP, payload = telPayload) - return MeshPacket(from = from, decoded = data) + val telPayload = + Telemetry.ADAPTER.encode(Telemetry.Builder().also { wb -> wb.local_stats = localStats }.build()) + .toByteString() + val data = + Data.Builder() + .also { wb -> + wb.portnum = PortNum.TELEMETRY_APP + wb.payload = telPayload + } + .build() + return MeshPacket.Builder() + .also { wb -> + wb.from = from + wb.decoded = data + } + .build() } private fun createDataPacket(from: Int): DataPacket = DataPacket( @@ -377,6 +453,39 @@ class DiscoveryScanEngineTest { assertTrue(radioController.configWrites.isEmpty(), "A replaced transport must not be retuned") } + /** + * Regression for Crashlytics `d79ee407` (SQLite FK 787). The engine holds `sessionId` for the whole scan while + * `SwitchingDiscoveryDao` re-resolves the active database per call, so a device/DB switch mid-scan leaves the id + * pointing at a row the active database does not have. Writing the dwell's preset result then orphans its foreign + * key. Deleting the session row reproduces what the switch does to this engine: the parent is gone by the time the + * dwell persists. + * + * The engine must skip the write, not crash. `persistCurrentDwellResults` runs on a `SupervisorJob` scope with no + * `CoroutineExceptionHandler`, so anything thrown there reaches the default handler as a crash rather than a logged + * scan abort - which is why the real fix makes the check and the writes one transaction + * ([DiscoveryDao.insertDwellIfSessionExists]) rather than a check followed by an insert. + * + * Real-database coverage of the transaction itself lives in `SwitchingDiscoveryDaoTest`, which can switch two Room + * instances deterministically; this module's tests share an in-memory fake and its Robolectric source set has no + * bundled-SQLite native library. + */ + @Test + fun dwellPersistenceIsSkippedWhenTheSessionLeavesTheActiveDatabase() = runTest { + val engine = createEngine(this) + engine.startScan(testPresets, dwellDurationSeconds = 60) + assertScanActive(engine) + val sessionId = discoveryDao.sessions.keys.single() + + // What a device/DB switch does to this engine: the parent row is no longer reachable. + discoveryDao.deleteSession(sessionId) + + engine.stopScan() + advanceUntilIdle() + + assertTrue(discoveryDao.presetResults.isEmpty(), "an unreachable session must not receive an orphan dwell row") + assertTrue(discoveryDao.sessions.isEmpty(), "and the session must not be resurrected") + } + @Test fun stopScanPersistsResultsAndTransitionsToIdle() = runTest { val engine = createEngine(this) @@ -517,14 +626,23 @@ class DiscoveryScanEngineTest { delay(100) } - val posPayload = Position.ADAPTER.encode(Position(latitude_i = 377749300)).toByteString() + val posPayload = + Position.ADAPTER.encode(Position.Builder().also { wb -> wb.latitude_i = 377749300 }.build()).toByteString() val meshPacket = - MeshPacket( - from = 12345, - decoded = Data(portnum = PortNum.POSITION_APP, payload = posPayload), - rx_snr = 5.5f, - rx_rssi = null, - ) + MeshPacket.Builder() + .also { wb -> + wb.from = 12345 + wb.decoded = + Data.Builder() + .also { wb -> + wb.portnum = PortNum.POSITION_APP + wb.payload = posPayload + } + .build() + wb.rx_snr = 5.5f + wb.rx_rssi = null + } + .build() engine.onPacketReceived(meshPacket, createDataPacket(from = 12345)) engine.stopScan() @@ -546,17 +664,19 @@ class DiscoveryScanEngineTest { // Send a telemetry packet with local_stats val localStats = - LocalStats( - num_packets_tx = 100, - num_packets_rx = 200, - num_packets_rx_bad = 5, - num_rx_dupe = 10, - num_tx_relay = 15, - num_tx_relay_canceled = 2, - num_online_nodes = 3, - num_total_nodes = 10, - uptime_seconds = 3600, - ) + LocalStats.Builder() + .also { wb -> + wb.num_packets_tx = 100 + wb.num_packets_rx = 200 + wb.num_packets_rx_bad = 5 + wb.num_rx_dupe = 10 + wb.num_tx_relay = 15 + wb.num_tx_relay_canceled = 2 + wb.num_online_nodes = 3 + wb.num_total_nodes = 10 + wb.uptime_seconds = 3600 + } + .build() val meshPacket = createTelemetryWithLocalStats(from = 12345, localStats = localStats) val dataPacket = createDataPacket(from = 12345) @@ -941,7 +1061,7 @@ class DiscoveryScanEngineTest { @Test fun scanIsRefusedWhenHomeLoraConfigCannotBeCaptured() = runTest { - radioConfigRepository.setLocalConfigDirect(LocalConfig()) + radioConfigRepository.setLocalConfigDirect(LocalConfig.Builder().build()) val engine = createEngine(this) engine.startScan(listOf(ChannelOption.SHORT_FAST), dwellDurationSeconds = 60) @@ -996,7 +1116,17 @@ class DiscoveryScanEngineTest { selectDevice(secondDevice) val secondDeviceHome = ChannelOption.LONG_SLOW radioConfigRepository.setLocalConfigDirect( - LocalConfig(lora = Config.LoRaConfig(use_preset = true, modem_preset = secondDeviceHome.modemPreset)), + LocalConfig.Builder() + .also { wb -> + wb.lora = + Config.LoRaConfig.Builder() + .also { wb -> + wb.use_preset = true + wb.modem_preset = secondDeviceHome.modemPreset + } + .build() + } + .build(), ) serviceRepository.setConnectionState(ConnectionState.Connected) engine.startScan(listOf(ChannelOption.MEDIUM_FAST), dwellDurationSeconds = 60) @@ -1048,7 +1178,12 @@ class DiscoveryScanEngineTest { homePreset: String = "LONG_FAST", completionStatus: String = DiscoverySessionStatus.IN_PROGRESS, homeLoraConfig: Config.LoRaConfig? = - Config.LoRaConfig(use_preset = true, modem_preset = ChannelOption.LONG_FAST.modemPreset), + Config.LoRaConfig.Builder() + .also { wb -> + wb.use_preset = true + wb.modem_preset = ChannelOption.LONG_FAST.modemPreset + } + .build(), ) { discoveryDao.seedSession( DiscoverySessionEntity( diff --git a/feature/discovery/src/commonTest/kotlin/org/meshtastic/feature/discovery/DiscoveryTerminalCoordinatorTest.kt b/feature/discovery/src/commonTest/kotlin/org/meshtastic/feature/discovery/DiscoveryTerminalCoordinatorTest.kt index 82cae4a963..17b2ae9bcb 100644 --- a/feature/discovery/src/commonTest/kotlin/org/meshtastic/feature/discovery/DiscoveryTerminalCoordinatorTest.kt +++ b/feature/discovery/src/commonTest/kotlin/org/meshtastic/feature/discovery/DiscoveryTerminalCoordinatorTest.kt @@ -82,7 +82,7 @@ class DiscoveryTerminalCoordinatorTest { DiscoveryHomeRestorePlan( sessionId = sessionId, deviceAddress = device, - loraConfig = Config.LoRaConfig(use_preset = true), + loraConfig = Config.LoRaConfig.Builder().also { wb -> wb.use_preset = true }.build(), primaryChannel = null, restorePrimaryChannel = false, finalStatus = DiscoverySessionStatus.STOPPED, diff --git a/feature/discovery/src/commonTest/kotlin/org/meshtastic/feature/discovery/DiscoveryTestRadioControllerTest.kt b/feature/discovery/src/commonTest/kotlin/org/meshtastic/feature/discovery/DiscoveryTestRadioControllerTest.kt index 6f02e3f8da..c30b2ce731 100644 --- a/feature/discovery/src/commonTest/kotlin/org/meshtastic/feature/discovery/DiscoveryTestRadioControllerTest.kt +++ b/feature/discovery/src/commonTest/kotlin/org/meshtastic/feature/discovery/DiscoveryTestRadioControllerTest.kt @@ -35,7 +35,12 @@ class DiscoveryTestRadioControllerTest { val restored = controller.restoreLocalConfiguration( expectedDeviceAddress = null, - config = Config(lora = Config.LoRaConfig(use_preset = true)), + config = + Config.Builder() + .also { wb -> + wb.lora = Config.LoRaConfig.Builder().also { wb -> wb.use_preset = true }.build() + } + .build(), primaryChannel = null, ) @@ -62,7 +67,10 @@ class DiscoveryTestRadioControllerTest { writeEntered.complete(Unit) releaseWrite.await() } - val config = Config(lora = Config.LoRaConfig(use_preset = true)) + val config = + Config.Builder() + .also { wb -> wb.lora = Config.LoRaConfig.Builder().also { wb -> wb.use_preset = true }.build() } + .build() val restore = async { controller.restoreLocalConfiguration( diff --git a/feature/discovery/src/commonTest/kotlin/org/meshtastic/feature/discovery/DiscoveryViewModelTest.kt b/feature/discovery/src/commonTest/kotlin/org/meshtastic/feature/discovery/DiscoveryViewModelTest.kt index 3e127ab938..67d4bce9ff 100644 --- a/feature/discovery/src/commonTest/kotlin/org/meshtastic/feature/discovery/DiscoveryViewModelTest.kt +++ b/feature/discovery/src/commonTest/kotlin/org/meshtastic/feature/discovery/DiscoveryViewModelTest.kt @@ -38,21 +38,29 @@ import kotlin.test.assertTrue */ class DiscoveryViewModelTest { - private val radioLora = LoRaConfig(use_preset = true, modem_preset = ModemPreset.LONG_FAST) + private val radioLora = + LoRaConfig.Builder() + .also { wb -> + wb.use_preset = true + wb.modem_preset = ModemPreset.LONG_FAST + } + .build() private val partyNetOffer = MeshBeaconOffer( fromNodeNum = 456, beacon = - MeshBeacon( - message = "Join us", - offer_channel = ChannelSettings(name = "PartyNet"), - offer_preset = ModemPreset.LONG_FAST, - ), + MeshBeacon.Builder() + .also { wb -> + wb.message = "Join us" + wb.offer_channel = ChannelSettings.Builder().also { wb -> wb.name = "PartyNet" }.build() + wb.offer_preset = ModemPreset.LONG_FAST + } + .build(), ) @Test fun `offer for an unconfigured channel passes through both filters`() { - val channels = listOf(ChannelSettings(name = "HomeMesh")) + val channels = listOf(ChannelSettings.Builder().also { wb -> wb.name = "HomeMesh" }.build()) val offers = filterAlreadyJoinedOffers(listOf(partyNetOffer), radioLora, channels) val beaconChannels = filterAlreadyJoinedBeaconChannels(listOf(partyNetOffer), radioLora, channels) @@ -63,7 +71,7 @@ class DiscoveryViewModelTest { @Test fun `offer matching a configured channel is dropped from both filters`() { - val channels = listOf(ChannelSettings(name = "PartyNet")) + val channels = listOf(ChannelSettings.Builder().also { wb -> wb.name = "PartyNet" }.build()) val offers = filterAlreadyJoinedOffers(listOf(partyNetOffer), radioLora, channels) val beaconChannels = filterAlreadyJoinedBeaconChannels(listOf(partyNetOffer), radioLora, channels) @@ -76,7 +84,11 @@ class DiscoveryViewModelTest { fun `filtering never mutates the offers list it is given`() { // The repository itself must never be touched by presentation-time filtering (spec: reactive, not destructive). val original = listOf(partyNetOffer) - filterAlreadyJoinedOffers(original, LoRaConfig(use_preset = true), listOf(ChannelSettings(name = "PartyNet"))) + filterAlreadyJoinedOffers( + original, + LoRaConfig.Builder().also { wb -> wb.use_preset = true }.build(), + listOf(ChannelSettings.Builder().also { wb -> wb.name = "PartyNet" }.build()), + ) assertEquals(1, original.size) } @@ -87,7 +99,7 @@ class DiscoveryViewModelTest { filterAlreadyJoinedBeaconChannels( listOf(partyNetOffer, duplicate), radioLora, - listOf(ChannelSettings(name = "HomeMesh")), + listOf(ChannelSettings.Builder().also { wb -> wb.name = "HomeMesh" }.build()), ) assertEquals(1, beaconChannels.size) } @@ -98,7 +110,8 @@ class DiscoveryViewModelTest { // reactive at presentation time, not a one-shot decision baked in when the offer arrived. val offersFlow = MutableStateFlow(listOf(partyNetOffer)) val loraFlow = MutableStateFlow(radioLora) - val channelsFlow = MutableStateFlow(listOf(ChannelSettings(name = "PartyNet"))) + val channelsFlow = + MutableStateFlow(listOf(ChannelSettings.Builder().also { wb -> wb.name = "PartyNet" }.build())) val results = mutableListOf>() val job = launch { @@ -115,7 +128,7 @@ class DiscoveryViewModelTest { assertEquals(listOf(partyNetOffer), results.last()) // The user re-adds an unrelated channel; still not a match, offer stays visible. - channelsFlow.value = listOf(ChannelSettings(name = "HomeMesh")) + channelsFlow.value = listOf(ChannelSettings.Builder().also { wb -> wb.name = "HomeMesh" }.build()) runCurrent() assertEquals(listOf(partyNetOffer), results.last()) diff --git a/feature/discovery/src/commonTest/kotlin/org/meshtastic/feature/discovery/SharedInMemoryDiscoveryDao.kt b/feature/discovery/src/commonTest/kotlin/org/meshtastic/feature/discovery/SharedInMemoryDiscoveryDao.kt index 913fb67f24..7e401d17b7 100644 --- a/feature/discovery/src/commonTest/kotlin/org/meshtastic/feature/discovery/SharedInMemoryDiscoveryDao.kt +++ b/feature/discovery/src/commonTest/kotlin/org/meshtastic/feature/discovery/SharedInMemoryDiscoveryDao.kt @@ -97,6 +97,9 @@ internal class SharedInMemoryDiscoveryDao : DiscoveryDao { override suspend fun getSession(sessionId: Long): DiscoverySessionEntity? = stateLock.withLock { mutableSessions[sessionId] } + override suspend fun countSessionsForDevice(sessionId: Long, deviceAddress: String): Int = + stateLock.withLock { if (mutableSessions[sessionId]?.deviceAddress == deviceAddress) 1 else 0 } + override suspend fun updateSessionCompletionStatus(sessionId: Long, status: String): Int { val updated = stateLock.withLock { diff --git a/feature/docs/src/commonMain/kotlin/org/meshtastic/feature/docs/data/DocBundleLoader.kt b/feature/docs/src/commonMain/kotlin/org/meshtastic/feature/docs/data/DocBundleLoader.kt index 59379b2484..1f9a291e90 100644 --- a/feature/docs/src/commonMain/kotlin/org/meshtastic/feature/docs/data/DocBundleLoader.kt +++ b/feature/docs/src/commonMain/kotlin/org/meshtastic/feature/docs/data/DocBundleLoader.kt @@ -601,9 +601,9 @@ class DefaultDocBundleLoader : DocBundleLoader { "developer", "en/developer/documentation-style.html", 11, - listOf("style", "writing", "docs", "prose", "voice", "admonition", "wording"), + listOf("style", "writing", "docs", "prose", "voice", "admonition", "wording", "standards"), listOf("style", "style-guide", "docs-style", "writing"), - 15400, + 9660, "documentation-style", ), ) diff --git a/feature/docs/src/commonMain/kotlin/org/meshtastic/feature/docs/ui/DocsBrowserScreen.kt b/feature/docs/src/commonMain/kotlin/org/meshtastic/feature/docs/ui/DocsBrowserScreen.kt index f3fc33f8c2..50ffb192e3 100644 --- a/feature/docs/src/commonMain/kotlin/org/meshtastic/feature/docs/ui/DocsBrowserScreen.kt +++ b/feature/docs/src/commonMain/kotlin/org/meshtastic/feature/docs/ui/DocsBrowserScreen.kt @@ -33,16 +33,23 @@ import androidx.compose.material3.IconButton import androidx.compose.material3.ListItem import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Scaffold +import androidx.compose.material3.SearchBarDefaults import androidx.compose.material3.Text import androidx.compose.material3.TopAppBar import androidx.compose.runtime.Composable import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.semantics.contentDescription import androidx.compose.ui.semantics.heading import androidx.compose.ui.semantics.semantics import androidx.compose.ui.unit.dp +import org.jetbrains.compose.resources.stringResource +import org.meshtastic.core.resources.Res +import org.meshtastic.core.resources.doc_loading +import org.meshtastic.core.resources.doc_no_documentation +import org.meshtastic.core.resources.doc_no_results import org.meshtastic.core.ui.icon.ArrowBack import org.meshtastic.core.ui.icon.MeshtasticIcons import org.meshtastic.feature.docs.model.AIDocAssistantSessionState @@ -73,6 +80,8 @@ fun DocsBrowserScreen( onChirpySubmit: () -> Unit = {}, onChirpyNavigateToPage: (String) -> Unit = {}, ) { + val searchScrollBehavior = SearchBarDefaults.enterAlwaysSearchBarScrollBehavior() + Scaffold( topBar = { TopAppBar( @@ -89,48 +98,30 @@ fun DocsBrowserScreen( ChirpyFab(modelReadiness = modelReadiness, onClick = onChirpyToggle) } }, - modifier = modifier, + modifier = modifier.nestedScroll(searchScrollBehavior.nestedScrollConnection), ) { innerPadding -> Column(modifier = Modifier.fillMaxSize().padding(innerPadding)) { DocsSearchBar( query = searchQuery, onQueryChange = onSearchQueryChange, modifier = Modifier.fillMaxWidth().padding(horizontal = 16.dp, vertical = 8.dp), + scrollBehavior = searchScrollBehavior, + expandedContent = { + DocsResultsContent( + pages = pages, + isLoading = isLoading, + searchQuery = searchQuery, + onSelectPage = onSelectPage, + ) + }, ) - when { - isLoading -> { - Column( - modifier = Modifier.fillMaxSize(), - verticalArrangement = Arrangement.Center, - horizontalAlignment = Alignment.CenterHorizontally, - ) { - CircularProgressIndicator() - Text( - text = "Loading documentation...", - style = MaterialTheme.typography.bodyMedium, - modifier = Modifier.padding(top = 16.dp), - ) - } - } - - pages.isEmpty() -> { - Column( - modifier = Modifier.fillMaxSize(), - verticalArrangement = Arrangement.Center, - horizontalAlignment = Alignment.CenterHorizontally, - ) { - Text( - text = if (searchQuery.isNotBlank()) "No results found" else "No documentation available", - style = MaterialTheme.typography.bodyLarge, - ) - } - } - - else -> { - DocsTocList(pages = pages, onSelectPage = onSelectPage) - } - } + DocsResultsContent( + pages = pages, + isLoading = isLoading, + searchQuery = searchQuery, + onSelectPage = onSelectPage, + ) } if (showChirpy) { @@ -146,6 +137,54 @@ fun DocsBrowserScreen( } } +/** The loading / empty / results states shown below the search field, shared between the inline and expanded bar. */ +@Composable +private fun DocsResultsContent( + pages: List, + isLoading: Boolean, + searchQuery: String, + onSelectPage: (String) -> Unit, +) { + when { + isLoading -> { + Column( + modifier = Modifier.fillMaxSize(), + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally, + ) { + CircularProgressIndicator() + Text( + text = stringResource(Res.string.doc_loading), + style = MaterialTheme.typography.bodyMedium, + modifier = Modifier.padding(top = 16.dp), + ) + } + } + + pages.isEmpty() -> { + Column( + modifier = Modifier.fillMaxSize(), + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Text( + text = + if (searchQuery.isNotBlank()) { + stringResource(Res.string.doc_no_results) + } else { + stringResource(Res.string.doc_no_documentation) + }, + style = MaterialTheme.typography.bodyLarge, + ) + } + } + + else -> { + DocsTocList(pages = pages, onSelectPage = onSelectPage) + } + } +} + @Composable private fun DocsTocList(pages: List, onSelectPage: (String) -> Unit, modifier: Modifier = Modifier) { val userGuidePages = diff --git a/feature/docs/src/commonMain/kotlin/org/meshtastic/feature/docs/ui/DocsPreviews.kt b/feature/docs/src/commonMain/kotlin/org/meshtastic/feature/docs/ui/DocsPreviews.kt index c08a620c6c..988dbe262a 100644 --- a/feature/docs/src/commonMain/kotlin/org/meshtastic/feature/docs/ui/DocsPreviews.kt +++ b/feature/docs/src/commonMain/kotlin/org/meshtastic/feature/docs/ui/DocsPreviews.kt @@ -31,6 +31,7 @@ import androidx.compose.foundation.layout.width import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.MaterialTheme import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.Surface @@ -492,6 +493,7 @@ private fun PreviewThinkingBubble(modifier: Modifier = Modifier) { @Suppress("PreviewPublic") @PreviewLightDark +@OptIn(ExperimentalMaterial3Api::class) @Composable fun DocsSearchBarEmptyPreview() { AppTheme { Surface { DocsSearchBar(query = "", onQueryChange = {}, modifier = Modifier.padding(16.dp)) } } @@ -499,6 +501,7 @@ fun DocsSearchBarEmptyPreview() { @Suppress("PreviewPublic") @PreviewLightDark +@OptIn(ExperimentalMaterial3Api::class) @Composable fun DocsSearchBarWithQueryPreview() { AppTheme { diff --git a/feature/docs/src/commonMain/kotlin/org/meshtastic/feature/docs/ui/DocsSearchBar.kt b/feature/docs/src/commonMain/kotlin/org/meshtastic/feature/docs/ui/DocsSearchBar.kt index 32ccd58375..f0b970c952 100644 --- a/feature/docs/src/commonMain/kotlin/org/meshtastic/feature/docs/ui/DocsSearchBar.kt +++ b/feature/docs/src/commonMain/kotlin/org/meshtastic/feature/docs/ui/DocsSearchBar.kt @@ -16,43 +16,156 @@ */ package org.meshtastic.feature.docs.ui +import androidx.compose.foundation.layout.ColumnScope import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.text.input.TextFieldState +import androidx.compose.foundation.text.input.clearText +import androidx.compose.foundation.text.input.placeCursorAtEnd +import androidx.compose.foundation.text.input.rememberTextFieldState +import androidx.compose.material3.ExpandedFullScreenSearchBar +import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon import androidx.compose.material3.IconButton -import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.SearchBar +import androidx.compose.material3.SearchBarDefaults +import androidx.compose.material3.SearchBarScrollBehavior +import androidx.compose.material3.SearchBarState +import androidx.compose.material3.SearchBarValue import androidx.compose.material3.Text +import androidx.compose.material3.rememberSearchBarState import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.rememberUpdatedState +import androidx.compose.runtime.snapshotFlow import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.testTag import androidx.compose.ui.semantics.contentDescription import androidx.compose.ui.semantics.semantics +import kotlinx.coroutines.launch import org.jetbrains.compose.resources.stringResource import org.meshtastic.core.resources.Res import org.meshtastic.core.resources.doc_clear_search import org.meshtastic.core.resources.doc_search_placeholder +import org.meshtastic.core.resources.navigate_back +import org.meshtastic.core.ui.icon.ArrowBack import org.meshtastic.core.ui.icon.Close import org.meshtastic.core.ui.icon.MeshtasticIcons import org.meshtastic.core.ui.icon.Search -/** Search bar for filtering documentation pages by keywords. */ +/** + * Tag for the collapsed field; distinct from [DOCS_SEARCH_BAR_EXPANDED_INPUT_FIELD_TAG] so tests can target it alone. + */ +const val DOCS_SEARCH_BAR_INPUT_FIELD_TAG = "DocsSearchBarInputField" + +/** Tag for the field instance the full-screen overlay shows once expanded. */ +const val DOCS_SEARCH_BAR_EXPANDED_INPUT_FIELD_TAG = "DocsSearchBarExpandedInputField" + +/** + * Search bar for filtering documentation pages by keywords. + * + * A real M3 [SearchBar] driven by [rememberSearchBarState]: collapsed it sits inline above the results, and focusing it + * expands to a full-screen overlay showing [expandedContent] under the same field. + */ +@OptIn(ExperimentalMaterial3Api::class) @Composable -fun DocsSearchBar(query: String, onQueryChange: (String) -> Unit, modifier: Modifier = Modifier) { +fun DocsSearchBar( + query: String, + onQueryChange: (String) -> Unit, + modifier: Modifier = Modifier, + scrollBehavior: SearchBarScrollBehavior? = null, + expandedContent: @Composable ColumnScope.() -> Unit = {}, +) { val searchPlaceholder = stringResource(Res.string.doc_search_placeholder) - OutlinedTextField( - value = query, - onValueChange = onQueryChange, - placeholder = { Text(searchPlaceholder) }, - leadingIcon = { Icon(imageVector = MeshtasticIcons.Search, contentDescription = null) }, + val clearSearchDescription = stringResource(Res.string.doc_clear_search) + val backDescription = stringResource(Res.string.navigate_back) + val textFieldState = rememberTextFieldState(query) + val searchBarState = rememberSearchBarState() + val latestOnQueryChange by rememberUpdatedState(onQueryChange) + + // A caller-driven query change (e.g. an external reset) must be mirrored into the field. + LaunchedEffect(query, textFieldState) { + if (textFieldState.text.toString() != query) { + textFieldState.edit { + replace(0, length, query) + placeCursorAtEnd() + } + } + } + // Edits made through the field are reported back to the caller. + LaunchedEffect(textFieldState) { + snapshotFlow { textFieldState.text.toString() }.collect { latestOnQueryChange(it) } + } + + val barModifier = + modifier.fillMaxWidth().let { base -> + scrollBehavior?.let { behavior -> with(behavior) { base.searchBarScrollBehavior() } } ?: base + } + SearchBar( + state = searchBarState, + inputField = { + DocsSearchInputField( + textFieldState = textFieldState, + searchBarState = searchBarState, + placeholder = searchPlaceholder, + clearDescription = clearSearchDescription, + backDescription = backDescription, + testTag = DOCS_SEARCH_BAR_INPUT_FIELD_TAG, + ) + }, + modifier = barModifier, + ) + ExpandedFullScreenSearchBar( + state = searchBarState, + inputField = { + DocsSearchInputField( + textFieldState = textFieldState, + searchBarState = searchBarState, + placeholder = searchPlaceholder, + clearDescription = clearSearchDescription, + backDescription = backDescription, + testTag = DOCS_SEARCH_BAR_EXPANDED_INPUT_FIELD_TAG, + ) + }, + content = expandedContent, + ) +} + +/** The [SearchBarDefaults.InputField] content shared by the collapsed bar and the expanded overlay. */ +@OptIn(ExperimentalMaterial3Api::class) +@Composable +private fun DocsSearchInputField( + textFieldState: TextFieldState, + searchBarState: SearchBarState, + placeholder: String, + clearDescription: String, + backDescription: String, + testTag: String, +) { + val scope = rememberCoroutineScope() + val isExpanded = searchBarState.currentValue == SearchBarValue.Expanded + SearchBarDefaults.InputField( + textFieldState = textFieldState, + searchBarState = searchBarState, + onSearch = { scope.launch { searchBarState.animateToCollapsed() } }, + placeholder = { Text(placeholder) }, + leadingIcon = { + if (isExpanded) { + IconButton(onClick = { scope.launch { searchBarState.animateToCollapsed() } }) { + Icon(imageVector = MeshtasticIcons.ArrowBack, contentDescription = backDescription) + } + } else { + Icon(imageVector = MeshtasticIcons.Search, contentDescription = null) + } + }, trailingIcon = { - if (query.isNotEmpty()) { - IconButton(onClick = { onQueryChange("") }) { - Icon( - imageVector = MeshtasticIcons.Close, - contentDescription = stringResource(Res.string.doc_clear_search), - ) + if (textFieldState.text.isNotEmpty()) { + IconButton(onClick = { textFieldState.clearText() }) { + Icon(imageVector = MeshtasticIcons.Close, contentDescription = clearDescription) } } }, - singleLine = true, - modifier = modifier.fillMaxWidth().semantics { contentDescription = searchPlaceholder }, + modifier = Modifier.testTag(testTag).semantics { contentDescription = placeholder }, ) } diff --git a/feature/docs/src/commonTest/kotlin/org/meshtastic/feature/docs/ui/DocsBrowserScreenTest.kt b/feature/docs/src/commonTest/kotlin/org/meshtastic/feature/docs/ui/DocsBrowserScreenTest.kt index 716f568e68..649eaba61d 100644 --- a/feature/docs/src/commonTest/kotlin/org/meshtastic/feature/docs/ui/DocsBrowserScreenTest.kt +++ b/feature/docs/src/commonTest/kotlin/org/meshtastic/feature/docs/ui/DocsBrowserScreenTest.kt @@ -53,7 +53,7 @@ class DocsBrowserScreenTest { onBack = {}, ) } - onNodeWithText("Loading documentation...").assertIsDisplayed() + onNodeWithText("Loading documentation…").assertIsDisplayed() } @Test diff --git a/feature/docs/src/commonTest/kotlin/org/meshtastic/feature/docs/ui/DocsSearchBarTest.kt b/feature/docs/src/commonTest/kotlin/org/meshtastic/feature/docs/ui/DocsSearchBarTest.kt index dcf4ee7050..6513bb4e7b 100644 --- a/feature/docs/src/commonTest/kotlin/org/meshtastic/feature/docs/ui/DocsSearchBarTest.kt +++ b/feature/docs/src/commonTest/kotlin/org/meshtastic/feature/docs/ui/DocsSearchBarTest.kt @@ -16,17 +16,17 @@ */ package org.meshtastic.feature.docs.ui +import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.ui.test.ExperimentalTestApi import androidx.compose.ui.test.assertIsDisplayed -import androidx.compose.ui.test.hasSetTextAction import androidx.compose.ui.test.onNodeWithContentDescription +import androidx.compose.ui.test.onNodeWithTag import androidx.compose.ui.test.performClick import androidx.compose.ui.test.performTextInput import androidx.compose.ui.test.v2.runComposeUiTest import kotlin.test.Test -import kotlin.test.assertEquals -@OptIn(ExperimentalTestApi::class) +@OptIn(ExperimentalTestApi::class, ExperimentalMaterial3Api::class) class DocsSearchBarTest { @Test @@ -46,15 +46,17 @@ class DocsSearchBarTest { var received: String? = null setContent { DocsSearchBar(query = "bluetooth", onQueryChange = { received = it }) } onNodeWithContentDescription("Clear search").performClick() - runOnIdle { assertEquals("", received) } + // The clear edit is reported back through a snapshotFlow hop, not synchronously. + waitUntil(timeoutMillis = 5_000) { received == "" } } @Test fun textInput_callsOnQueryChange() = runComposeUiTest { val queries = mutableListOf() setContent { DocsSearchBar(query = "", onQueryChange = { queries += it }) } - // OutlinedTextField placeholder is not findable by text; use semantics matcher - onNode(hasSetTextAction()).performTextInput("mesh") - runOnIdle { assertEquals("mesh", queries.last()) } + // Focusing this field also drives SearchBarState toward Expanded, which mounts a second, + // shared inputField instance in the (unrendered-here) overlay - target the collapsed one by tag. + onNodeWithTag(DOCS_SEARCH_BAR_INPUT_FIELD_TAG).performTextInput("mesh") + waitUntil(timeoutMillis = 5_000) { queries.lastOrNull() == "mesh" } } } diff --git a/feature/firmware/src/commonMain/kotlin/org/meshtastic/feature/firmware/DefaultFirmwareUpdateManager.kt b/feature/firmware/src/commonMain/kotlin/org/meshtastic/feature/firmware/DefaultFirmwareUpdateManager.kt index 8380d0ecb5..cdde4a6ece 100644 --- a/feature/firmware/src/commonMain/kotlin/org/meshtastic/feature/firmware/DefaultFirmwareUpdateManager.kt +++ b/feature/firmware/src/commonMain/kotlin/org/meshtastic/feature/firmware/DefaultFirmwareUpdateManager.kt @@ -17,6 +17,8 @@ package org.meshtastic.feature.firmware import org.koin.core.annotation.Single +import org.meshtastic.core.common.state.RadioOperation +import org.meshtastic.core.common.state.RadioOperationLock import org.meshtastic.core.common.util.CommonUri import org.meshtastic.core.database.entity.FirmwareRelease import org.meshtastic.core.model.DeviceHardware @@ -37,6 +39,7 @@ class DefaultFirmwareUpdateManager( private val secureDfuHandler: SecureDfuHandler, private val usbUpdateHandler: UsbUpdateHandler, private val esp32OtaUpdateHandler: Esp32OtaUpdateHandler, + private val radioOperationLock: RadioOperationLock, ) : FirmwareUpdateManager { override suspend fun startUpdate( @@ -45,11 +48,11 @@ class DefaultFirmwareUpdateManager( address: String, updateState: (FirmwareUpdateState) -> Unit, firmwareUri: CommonUri?, - ): FirmwareArtifact? { + ): FirmwareArtifact? = radioOperationLock.withOperation(RadioOperation.FirmwareUpdate) { val handler = getHandler(hardware) val target = getTarget(address) - return handler.startUpdate( + handler.startUpdate( release = release, hardware = hardware, target = target, @@ -63,7 +66,7 @@ class DefaultFirmwareUpdateManager( hardware: DeviceHardware, address: String, updateState: (FirmwareUpdateState) -> Unit, - ): FirmwareArtifact? = + ): FirmwareArtifact? = radioOperationLock.withOperation(RadioOperation.FirmwareUpdate) { // Recovery is inherently a BLE DFU operation — route straight to the DFU handler rather than the // connection-type dispatch in getHandler(), which would fail with no live connection. The handler derives // MAC+1 from address and its buttonless trigger already no-ops when the device is already in DFU mode. @@ -74,6 +77,7 @@ class DefaultFirmwareUpdateManager( updateState = updateState, firmwareUri = null, ) + } internal fun getHandler(hardware: DeviceHardware): FirmwareUpdateHandler = when { radioPrefs.isSerial() -> { diff --git a/feature/firmware/src/commonMain/kotlin/org/meshtastic/feature/firmware/FirmwareUpdateScreen.kt b/feature/firmware/src/commonMain/kotlin/org/meshtastic/feature/firmware/FirmwareUpdateScreen.kt index 88a1742e39..05ca499c7f 100644 --- a/feature/firmware/src/commonMain/kotlin/org/meshtastic/feature/firmware/FirmwareUpdateScreen.kt +++ b/feature/firmware/src/commonMain/kotlin/org/meshtastic/feature/firmware/FirmwareUpdateScreen.kt @@ -14,7 +14,6 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -@file:OptIn(ExperimentalMaterial3ExpressiveApi::class) @file:Suppress("TooManyFunctions") package org.meshtastic.feature.firmware @@ -45,7 +44,6 @@ import androidx.compose.material3.CenterAlignedTopAppBar import androidx.compose.material3.CircularWavyProgressIndicator import androidx.compose.material3.ElevatedCard import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.LinearWavyProgressIndicator @@ -1018,7 +1016,6 @@ private fun ProgressContent( tint = MaterialTheme.colorScheme.primary, ) } else { - @OptIn(ExperimentalMaterial3ExpressiveApi::class) CircularWavyProgressIndicator( progress = { if (isUpdating) progressState.progress else 1f }, modifier = Modifier.size(64.dp), @@ -1058,7 +1055,6 @@ private fun ProgressContent( Spacer(Modifier.height(12.dp)) if (isDownloading || isUpdating) { - @OptIn(ExperimentalMaterial3ExpressiveApi::class) LinearWavyProgressIndicator( progress = { progressState.progress }, modifier = Modifier.fillMaxWidth().padding(horizontal = 8.dp), @@ -1252,9 +1248,7 @@ internal fun SuccessState(wasLowSpeedTransfer: Boolean = false, deviceWasWiped: TextButton(onClick = { openUrl(OTAFIX_BOOTLOADER_URL) }) { Text(stringResource(Res.string.learn_more)) } } Spacer(Modifier.height(32.dp)) - @OptIn(ExperimentalMaterial3ExpressiveApi::class) val largeHeight = ButtonDefaults.LargeContainerHeight - @OptIn(ExperimentalMaterial3ExpressiveApi::class) Button( onClick = onDone, shapes = ButtonDefaults.shapesFor(largeHeight), diff --git a/feature/firmware/src/commonMain/kotlin/org/meshtastic/feature/firmware/FirmwareUpdateViewModel.kt b/feature/firmware/src/commonMain/kotlin/org/meshtastic/feature/firmware/FirmwareUpdateViewModel.kt index 6aa24bc14e..db49f8121c 100644 --- a/feature/firmware/src/commonMain/kotlin/org/meshtastic/feature/firmware/FirmwareUpdateViewModel.kt +++ b/feature/firmware/src/commonMain/kotlin/org/meshtastic/feature/firmware/FirmwareUpdateViewModel.kt @@ -39,8 +39,10 @@ import kotlinx.coroutines.withTimeoutOrNull import org.jetbrains.compose.resources.StringResource import org.koin.core.annotation.KoinViewModel import org.meshtastic.core.common.di.ApplicationCoroutineScope -import org.meshtastic.core.common.state.FirmwareMaintenanceLock import org.meshtastic.core.common.state.HiddenFeaturesUnlock +import org.meshtastic.core.common.state.OperationLease +import org.meshtastic.core.common.state.RadioOperation +import org.meshtastic.core.common.state.RadioOperationLock import org.meshtastic.core.common.util.CommonUri import org.meshtastic.core.common.util.safeCatching import org.meshtastic.core.database.entity.FirmwareRelease @@ -125,13 +127,16 @@ class FirmwareUpdateViewModel( private val usbManager: FirmwareUsbManager, private val fileHandler: FirmwareFileHandler, private val firmwareRetriever: FirmwareRetriever, - private val firmwareMaintenanceLock: FirmwareMaintenanceLock, + private val radioOperationLock: RadioOperationLock, private val applicationScope: ApplicationCoroutineScope, private val hiddenFeaturesUnlock: HiddenFeaturesUnlock, private val analytics: PlatformAnalytics, private val nodeRestartTracker: NodeRestartTracker, ) : ViewModel() { + /** The USB maintenance sequence's hold on the radio. Spans several passes, so it cannot use `withOperation`. */ + private var maintenanceLease: OperationLease? = null + private val _state = MutableStateFlow(FirmwareUpdateState.Idle) val state: StateFlow = _state.asStateFlow() @@ -170,6 +175,14 @@ class FirmwareUpdateViewModel( /** Hardware the running maintenance sequence targets; needed to choose images once a volume is read. */ private var maintenanceHardware: DeviceHardware? = null + /** + * The in-flight USB maintenance pass, which runs on its own job rather than [updateJob]. + * + * Tracked so cancellation can stop the write before the sequence's hold on the radio is handed back — releasing + * while the write is still running would re-enable transport recovery mid-write. + */ + private var maintenanceWriteJob: Job? = null + /** * True once an erase or bootloader image has been written, which is the point the device stops having a working * application. From then on failures re-offer the pass instead of surfacing a dead end. @@ -200,7 +213,7 @@ class FirmwareUpdateViewModel( // passes (e.g. navigating away after the erase leg but before picking the firmware save location), and a // leaked lock would permanently suppress the radio transport's auto-reconnect for the rest of the app // session — see startUsbMaintenance/advancePastPass. A no-op if no sequence was in flight. - firmwareMaintenanceLock.release() + releaseMaintenanceLease() // viewModelScope is already cancelled when onCleared() runs, so launch cleanup on the // application-wide scope (SupervisorJob + ioDispatcher). ATOMIC start + NonCancellable // context keeps cleanup running even if something tries to cancel it mid-flight. @@ -225,11 +238,31 @@ class FirmwareUpdateViewModel( fun cancelUpdate() { updateJob?.cancel() + val write = maintenanceWriteJob + if (write?.isActive == true) { + // A pass is mid-write on its own job, which updateJob?.cancel() does not reach. Cancelling it is what + // stops the write; its own cleanup releases the hold. Releasing here would re-enable transport recovery + // while the USB write is still going. + write.cancel() + } else { + // Nothing is writing — typically parked at AwaitingFileSave, where the job has already completed, so + // cancelling it releases nothing and the hold would otherwise survive until onCleared(). + endMaintenanceSequence() + } clearPendingLocalFirmwareFile() _state.value = FirmwareUpdateState.Idle checkForUpdates() } + /** Drops a USB maintenance sequence and the hold it owns. Idempotent. */ + private fun endMaintenanceSequence() { + maintenanceWriteJob = null + pendingUsbPasses = emptyList() + destructiveWriteDone = false + maintenanceHardware = null + releaseMaintenanceLease() + } + @Suppress("LongMethod") fun checkForUpdates() { updateJob?.cancel() @@ -599,7 +632,7 @@ class FirmwareUpdateViewModel( if (!checkBatteryLevel()) return@launch // Held until the sequence finishes or fails. Without it the environmental-recovery listeners restart // the radio transport mid-sequence and bind it to the erase firmware's bare CDC port. - firmwareMaintenanceLock.acquire() + maintenanceLease = radioOperationLock.acquire(RadioOperation.FirmwareMaintenance) updateJob?.cancel() updateJob = viewModelScope.launch { @@ -624,7 +657,7 @@ class FirmwareUpdateViewModel( _state.value = FirmwareUpdateState.Error(UiText.Resource(Res.string.firmware_update_failed)) } finally { // Preparation that produced no passes never reached the device; hand the transport back. - if (pendingUsbPasses.isEmpty()) firmwareMaintenanceLock.release() + if (pendingUsbPasses.isEmpty()) releaseMaintenanceLease() } } } @@ -644,20 +677,25 @@ class FirmwareUpdateViewModel( if (pass.step != currentState.step) return val hardware = maintenanceHardware ?: return - viewModelScope.launch { - try { - // Capture the ports present before the write so the erase image's port can be told from pre-existing - // ones. - val portsBefore = usbManager.serialPortKeys() - val result = usbPassWriter(portsBefore).write(pass, treeUri, hardware) { _state.value = it } - handlePassResult(pass, result) - } catch (e: CancellationException) { - throw e - } catch (@Suppress("TooGenericExceptionCaught") e: Exception) { - Logger.e(e) { "Writing ${pass.step} failed" } - reofferOrFail(pass, UiText.Resource(Res.string.firmware_update_failed)) + maintenanceWriteJob = + viewModelScope.launch { + try { + // Capture the ports present before the write so the erase image's port can be told from + // pre-existing + // ones. + val portsBefore = usbManager.serialPortKeys() + val result = usbPassWriter(portsBefore).write(pass, treeUri, hardware) { _state.value = it } + handlePassResult(pass, result) + } catch (e: CancellationException) { + // The write has stopped, so the device can be handed back. Doing this here rather than in + // cancelUpdate() is the point: the sequence keeps the radio until the write actually ends. + endMaintenanceSequence() + throw e + } catch (@Suppress("TooGenericExceptionCaught") e: Exception) { + Logger.e(e) { "Writing ${pass.step} failed" } + reofferOrFail(pass, UiText.Resource(Res.string.firmware_update_failed)) + } } - } } private suspend fun handlePassResult(pass: UsbFileSavePass, result: UsbPassResult) = when (result) { @@ -685,7 +723,7 @@ class FirmwareUpdateViewModel( val next = pendingUsbPasses.firstOrNull() if (next == null) { // Sequence complete: hand the device back before verifying, so the normal reconnect can run. - firmwareMaintenanceLock.release() + releaseMaintenanceLease() verifyUpdateResult(originalDeviceAddress) } else { _state.value = next.toAwaitingFileSave() @@ -704,7 +742,7 @@ class FirmwareUpdateViewModel( // Still mid-sequence — hold the lock, the user is being asked to retry this pass. _state.value = pass.toAwaitingFileSave(retryMessage = message) } else { - firmwareMaintenanceLock.release() + releaseMaintenanceLease() _state.value = FirmwareUpdateState.Error(message) } } @@ -727,35 +765,45 @@ class FirmwareUpdateViewModel( val firmwareArtifact = currentState.uf2Artifact ?: return viewModelScope.launch { - try { - _state.value = - FirmwareUpdateState.Processing(ProgressState(UiText.Resource(Res.string.firmware_update_copying))) - fileHandler.copyToUri(firmwareArtifact, uri) + // The plain single-pass USB write happens here, not in FirmwareUpdateManager.startUpdate — that returned + // at AwaitingFileSave so the UI could open the file picker, releasing its lease. Without a hold of its own + // the copy, the flash and the detach wait run with no foreground service and no wake lock. + radioOperationLock.withOperation(RadioOperation.FirmwareUpdate) { + try { + _state.value = + FirmwareUpdateState.Processing( + ProgressState(UiText.Resource(Res.string.firmware_update_copying)), + ) + fileHandler.copyToUri(firmwareArtifact, uri) - _state.value = - FirmwareUpdateState.Processing(ProgressState(UiText.Resource(Res.string.firmware_update_flashing))) - withTimeoutOrNull(DEVICE_DETACH_TIMEOUT) { usbManager.deviceDetachFlow().first() } - ?: Logger.w { "Timed out waiting for device to detach, assuming success" } + _state.value = + FirmwareUpdateState.Processing( + ProgressState(UiText.Resource(Res.string.firmware_update_flashing)), + ) + withTimeoutOrNull(DEVICE_DETACH_TIMEOUT) { usbManager.deviceDetachFlow().first() } + ?: Logger.w { "Timed out waiting for device to detach, assuming success" } - // All writes are done once the device detached. Release before verifying: for serial, - // verification relies on SharedRadioInterfaceService's USB auto-recovery to reconnect the - // radio — which is exactly what the lock suppresses. (The release in finally is a no-op then.) - firmwareMaintenanceLock.release() - verifyUpdateResult(originalDeviceAddress) - } catch (e: CancellationException) { - throw e - } catch (e: Exception) { - Logger.e(e) { "Error saving DFU file" } - _state.value = FirmwareUpdateState.Error(UiText.Resource(Res.string.firmware_update_failed)) - } finally { - cleanupTemporaryFiles(fileHandler, tempFirmwareFile) - // This is also the terminal pass of a USB maintenance sequence when the FromVolume leg has already - // completed: the Prepared firmware artifact is saved through this pre-existing single-pass path - // rather than writeMaintenancePass/advancePastPass, so releasing here is the only place that - // sequence's lock gets freed. A no-op for a plain single-pass update, which never acquires the lock. - firmwareMaintenanceLock.release() - pendingUsbPasses = emptyList() - maintenanceHardware = null + // All writes are done once the device detached. Release before verifying: for serial, + // verification relies on SharedRadioInterfaceService's USB auto-recovery to reconnect the + // radio — which is exactly what the lock suppresses. (The release in finally is a no-op then.) + releaseMaintenanceLease() + verifyUpdateResult(originalDeviceAddress) + } catch (e: CancellationException) { + throw e + } catch (e: Exception) { + Logger.e(e) { "Error saving DFU file" } + _state.value = FirmwareUpdateState.Error(UiText.Resource(Res.string.firmware_update_failed)) + } finally { + cleanupTemporaryFiles(fileHandler, tempFirmwareFile) + // This is also the terminal pass of a USB maintenance sequence when the FromVolume leg has already + // completed: the Prepared firmware artifact is saved through this pre-existing single-pass path + // rather than writeMaintenancePass/advancePastPass, so releasing here is the only place that + // sequence's lock gets freed. A no-op for a plain single-pass update, which never acquires the + // lock. + releaseMaintenanceLease() + pendingUsbPasses = emptyList() + maintenanceHardware = null + } } } } @@ -1081,7 +1129,25 @@ class FirmwareUpdateViewModel( } } - private suspend fun verifyUpdateResult(address: String?, wasLowSpeedTransfer: Boolean = false) { + /** Ends the USB maintenance hold. Idempotent — several failure paths call it. */ + private fun releaseMaintenanceLease() { + radioOperationLock.release(maintenanceLease) + maintenanceLease = null + } + + /** + * Verifies the update, holding the radio operation for the whole wait. + * + * The device reboots into the new firmware here and the app waits for it to come back — over USB that is a ~20s + * re-enumeration. It is the most fragile stretch of an update and it runs after the flash returns, so without its + * own hold the process would lose the foreground service and wake lock exactly when the device is mid-reboot. + */ + private suspend fun verifyUpdateResult(address: String?, wasLowSpeedTransfer: Boolean = false) = + radioOperationLock.withOperation(RadioOperation.FirmwareUpdate) { + performUpdateVerification(address, wasLowSpeedTransfer) + } + + private suspend fun performUpdateVerification(address: String?, wasLowSpeedTransfer: Boolean = false) { // Consume the wipe opt-in up front: whatever this verification concludes, it must never leak into a // later update flow that did not opt in (e.g. a retry via a local file). val factoryResetAfterVerify = pendingFactoryResetAfterUpdate diff --git a/feature/firmware/src/commonTest/kotlin/org/meshtastic/feature/firmware/DefaultFirmwareUpdateManagerTest.kt b/feature/firmware/src/commonTest/kotlin/org/meshtastic/feature/firmware/DefaultFirmwareUpdateManagerTest.kt index e224c3fff1..3bc877cc25 100644 --- a/feature/firmware/src/commonTest/kotlin/org/meshtastic/feature/firmware/DefaultFirmwareUpdateManagerTest.kt +++ b/feature/firmware/src/commonTest/kotlin/org/meshtastic/feature/firmware/DefaultFirmwareUpdateManagerTest.kt @@ -24,6 +24,7 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.MutableStateFlow import org.meshtastic.core.ble.BleConnectionFactory import org.meshtastic.core.ble.BleScanner +import org.meshtastic.core.common.state.RadioOperationLock import org.meshtastic.core.di.CoroutineDispatchers import org.meshtastic.core.model.DeviceHardware import org.meshtastic.core.repository.FirmwareUpdateStatusRepository @@ -107,6 +108,7 @@ class DefaultFirmwareUpdateManagerTest { secureDfuHandler = secureDfuHandler, usbUpdateHandler = usbUpdateHandler, esp32OtaUpdateHandler = esp32OtaHandler, + radioOperationLock = RadioOperationLock(), ) } diff --git a/feature/firmware/src/commonTest/kotlin/org/meshtastic/feature/firmware/FirmwareUpdateIntegrationTest.kt b/feature/firmware/src/commonTest/kotlin/org/meshtastic/feature/firmware/FirmwareUpdateIntegrationTest.kt index 29fd5dbade..f9c010fce4 100644 --- a/feature/firmware/src/commonTest/kotlin/org/meshtastic/feature/firmware/FirmwareUpdateIntegrationTest.kt +++ b/feature/firmware/src/commonTest/kotlin/org/meshtastic/feature/firmware/FirmwareUpdateIntegrationTest.kt @@ -32,8 +32,8 @@ import kotlinx.coroutines.test.advanceUntilIdle import kotlinx.coroutines.test.resetMain import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.setMain -import org.meshtastic.core.common.state.FirmwareMaintenanceLock import org.meshtastic.core.common.state.HiddenFeaturesUnlock +import org.meshtastic.core.common.state.RadioOperationLock import org.meshtastic.core.database.entity.FirmwareRelease import org.meshtastic.core.datastore.BootloaderWarningDataSource import org.meshtastic.core.datastore.FirmwareRecoveryDataSource @@ -125,7 +125,7 @@ class FirmwareUpdateIntegrationTest { usbManager, fileHandler, firmwareRetriever, - FirmwareMaintenanceLock(), + RadioOperationLock(), TestApplicationCoroutineScope(testDispatcher), HiddenFeaturesUnlock(), analytics, diff --git a/feature/firmware/src/commonTest/kotlin/org/meshtastic/feature/firmware/FirmwareUpdateViewModelTest.kt b/feature/firmware/src/commonTest/kotlin/org/meshtastic/feature/firmware/FirmwareUpdateViewModelTest.kt index d9c4007d5d..cefa45375f 100644 --- a/feature/firmware/src/commonTest/kotlin/org/meshtastic/feature/firmware/FirmwareUpdateViewModelTest.kt +++ b/feature/firmware/src/commonTest/kotlin/org/meshtastic/feature/firmware/FirmwareUpdateViewModelTest.kt @@ -37,8 +37,8 @@ import kotlinx.coroutines.test.resetMain import kotlinx.coroutines.test.runCurrent import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.setMain -import org.meshtastic.core.common.state.FirmwareMaintenanceLock import org.meshtastic.core.common.state.HiddenFeaturesUnlock +import org.meshtastic.core.common.state.RadioOperationLock import org.meshtastic.core.database.entity.FirmwareRelease import org.meshtastic.core.database.entity.FirmwareReleaseType import org.meshtastic.core.datastore.BootloaderWarningDataSource @@ -153,7 +153,7 @@ class FirmwareUpdateViewModelTest { usbManager, fileHandler, firmwareRetriever, - FirmwareMaintenanceLock(), + RadioOperationLock(), TestApplicationCoroutineScope(testDispatcher), hiddenFeaturesUnlock, analytics, diff --git a/feature/firmware/src/commonTest/kotlin/org/meshtastic/feature/firmware/ota/Esp32OtaUpdateHandlerTest.kt b/feature/firmware/src/commonTest/kotlin/org/meshtastic/feature/firmware/ota/Esp32OtaUpdateHandlerTest.kt index 4c8d2f437e..670427e7e0 100644 --- a/feature/firmware/src/commonTest/kotlin/org/meshtastic/feature/firmware/ota/Esp32OtaUpdateHandlerTest.kt +++ b/feature/firmware/src/commonTest/kotlin/org/meshtastic/feature/firmware/ota/Esp32OtaUpdateHandlerTest.kt @@ -139,7 +139,9 @@ class Esp32OtaUpdateHandlerTest { // that can fire too early (or too late) under slower host timing. confirmationMessage?.let { msg -> radioController.onRequestRebootOta = { _, _, _, _ -> - radioController.setClientNotification(ClientNotification(message = msg)) + radioController.setClientNotification( + ClientNotification.Builder().also { wb -> wb.message = msg }.build(), + ) } } diff --git a/feature/firmware/src/jvmTest/kotlin/org/meshtastic/feature/firmware/FirmwareUpdateViewModelFileTest.kt b/feature/firmware/src/jvmTest/kotlin/org/meshtastic/feature/firmware/FirmwareUpdateViewModelFileTest.kt index 1cd4d55c25..410e5c537e 100644 --- a/feature/firmware/src/jvmTest/kotlin/org/meshtastic/feature/firmware/FirmwareUpdateViewModelFileTest.kt +++ b/feature/firmware/src/jvmTest/kotlin/org/meshtastic/feature/firmware/FirmwareUpdateViewModelFileTest.kt @@ -40,8 +40,9 @@ import kotlinx.coroutines.test.resetMain import kotlinx.coroutines.test.runCurrent import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.setMain -import org.meshtastic.core.common.state.FirmwareMaintenanceLock import org.meshtastic.core.common.state.HiddenFeaturesUnlock +import org.meshtastic.core.common.state.RadioOperation +import org.meshtastic.core.common.state.RadioOperationLock import org.meshtastic.core.common.util.CommonUri import org.meshtastic.core.database.entity.FirmwareRelease import org.meshtastic.core.datastore.BootloaderWarningDataSource @@ -95,7 +96,7 @@ class FirmwareUpdateViewModelFileTest { private val usbManager: FirmwareUsbManager = mock(MockMode.autofill) private val fileHandler: FirmwareFileHandler = mock(MockMode.autofill) private val firmwareRetriever: FirmwareRetriever = mock(MockMode.autofill) - private val firmwareMaintenanceLock = FirmwareMaintenanceLock() + private val radioOperationLock = RadioOperationLock() private val analytics: PlatformAnalytics = mock(MockMode.autofill) private lateinit var viewModel: FirmwareUpdateViewModel @@ -174,7 +175,7 @@ class FirmwareUpdateViewModelFileTest { usbManager, fileHandler, firmwareRetriever, - firmwareMaintenanceLock, + radioOperationLock, TestApplicationCoroutineScope(testDispatcher), HiddenFeaturesUnlock(), analytics, @@ -895,7 +896,10 @@ class FirmwareUpdateViewModelFileTest { assertIs(viewModel.state.value) verifySuspend(mode = VerifyMode.not) { firmwareRetriever.retrieveUsbFirmware(any(), any(), any()) } - assertFalse(firmwareMaintenanceLock.isActive, "a refused request must never take the maintenance lock") + assertFalse( + radioOperationLock.activeOperations.contains(RadioOperation.FirmwareMaintenance), + "a refused request must never take the maintenance lock", + ) } @Test @@ -933,7 +937,7 @@ class FirmwareUpdateViewModelFileTest { advanceUntilIdle() assertFalse( - firmwareMaintenanceLock.isActive, + radioOperationLock.activeOperations.contains(RadioOperation.FirmwareMaintenance), "refusing before the sequence starts must leave the radio transport unblocked", ) } @@ -954,7 +958,10 @@ class FirmwareUpdateViewModelFileTest { runUntilSettled { viewModel.state.value is FirmwareUpdateState.Error } assertIs(viewModel.state.value) - assertFalse(firmwareMaintenanceLock.isActive, "a failed preparation must not leak the lock") + assertFalse( + radioOperationLock.activeOperations.contains(RadioOperation.FirmwareMaintenance), + "a failed preparation must not leak the lock", + ) } @Test @@ -998,17 +1005,20 @@ class FirmwareUpdateViewModelFileTest { (viewModel.state.value as? FirmwareUpdateState.AwaitingFileSave)?.step == UsbFileSaveStep.Firmware } - assertTrue(firmwareMaintenanceLock.isActive, "the lock must still be held between passes") + assertTrue( + radioOperationLock.activeOperations.contains(RadioOperation.FirmwareMaintenance), + "the lock must still be held between passes", + ) val awaitingFirmware = assertIs(viewModel.state.value) assertEquals(UsbFileSaveStep.Firmware, awaitingFirmware.step) assertNotNull(awaitingFirmware.uf2Artifact, "the terminal pass must carry its artifact") // Second, terminal pass (firmware image) — writes through the pre-existing saveDfuFile. viewModel.saveDfuFile(CommonUri.parse("file:///output/firmware.uf2")) - runUntilSettled { !firmwareMaintenanceLock.isActive } + runUntilSettled { !radioOperationLock.activeOperations.contains(RadioOperation.FirmwareMaintenance) } assertFalse( - firmwareMaintenanceLock.isActive, + radioOperationLock.activeOperations.contains(RadioOperation.FirmwareMaintenance), "completing the sequence's terminal pass must release the lock, or auto-reconnect stays suppressed forever", ) diff --git a/feature/map-maplibre/src/commonMain/kotlin/org/meshtastic/feature/map/maplibre/CameraPersistence.kt b/feature/map-maplibre/src/commonMain/kotlin/org/meshtastic/feature/map/maplibre/CameraPersistence.kt index ee6991135b..b964d4c7ef 100644 --- a/feature/map-maplibre/src/commonMain/kotlin/org/meshtastic/feature/map/maplibre/CameraPersistence.kt +++ b/feature/map-maplibre/src/commonMain/kotlin/org/meshtastic/feature/map/maplibre/CameraPersistence.kt @@ -17,6 +17,7 @@ package org.meshtastic.feature.map.maplibre import androidx.compose.runtime.Composable +import androidx.compose.runtime.Immutable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf @@ -26,42 +27,54 @@ import androidx.compose.runtime.snapshotFlow import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.filterNotNull import org.koin.compose.koinInject -import org.maplibre.compose.camera.CameraState +import org.maplibre.compose.camera.CameraPosition +import org.maplibre.compose.map.MapState import org.maplibre.spatialk.geojson.Position import org.meshtastic.core.repository.MapCameraPosition import org.meshtastic.core.repository.MapPrefs /** - * Restores the map to wherever the user left it, and keeps that saved. + * Reads back the camera the user left the map on. * * The OSMdroid map did this and the Google flavor still does; the MapLibre map lost it in the cutover even though - * `MapPrefs.setCameraPosition` and its stored value survived untouched. Returns null until the stored position has been - * read, then whether there was one — the caller needs that to decide between the remembered view and framing the mesh, - * and must not do either while the answer is unknown. + * `MapPrefs.setCameraPosition` and its stored value survived untouched. + * + * The stored position is handed to `rememberMapState` as its initial camera rather than written to the map afterwards, + * which is what maplibre-compose 0.16.0 made possible — a map created at the remembered position opens there instead of + * opening on a default and then moving. + * + * Pair with [SaveCameraPosition], which is what keeps the value up to date. */ @Composable -internal fun rememberRestoredCamera(cameraState: CameraState): Boolean? { +internal fun rememberRestoredCamera(): RestoredCamera? { val mapPrefs: MapPrefs = koinInject() - var restored by remember { mutableStateOf(null) } + var restored by remember { mutableStateOf(null) } LaunchedEffect(Unit) { val saved = mapPrefs.awaitCameraPosition() - if (saved != null) { - cameraState.position = - cameraState.position.copy( - target = Position(longitude = saved.longitude, latitude = saved.latitude), - zoom = saved.zoom, - ) - } - restored = saved != null + restored = + RestoredCamera( + saved?.let { + CameraPosition(target = Position(longitude = it.longitude, latitude = it.latitude), zoom = it.zoom) + }, + ) } - LaunchedEffect(restored) { - // Saving only starts once the restore has been attempted, and only while the camera is settled. Writing - // before that would overwrite the remembered view with wherever the map happened to open. - if (restored == null) return@LaunchedEffect + return restored +} - snapshotFlow { cameraState.position.takeUnless { cameraState.isCameraMoving } } +/** + * Keeps the stored camera position in step with [mapState]. + * + * Only writes while the camera is settled: saving mid-gesture would store every frame of a pan, and the value that + * matters is the one the user stopped on. + */ +@Composable +internal fun SaveCameraPosition(mapState: MapState) { + val mapPrefs: MapPrefs = koinInject() + + LaunchedEffect(mapState) { + snapshotFlow { mapState.cameraPosition.takeUnless { mapState.isCameraMoving } } .filterNotNull() .distinctUntilChanged() .collect { position -> @@ -74,6 +87,12 @@ internal fun rememberRestoredCamera(cameraState: CameraState): Boolean? { ) } } - - return restored } + +/** + * Where the user left the map, once that is known. + * + * [position] is null when there was nothing stored, which is the caller's cue to frame the mesh instead. The whole + * value is null while the answer is still being read, and the caller must do neither until it arrives. + */ +@Immutable internal class RestoredCamera(val position: CameraPosition?) diff --git a/feature/map-maplibre/src/commonMain/kotlin/org/meshtastic/feature/map/maplibre/MapCamera.kt b/feature/map-maplibre/src/commonMain/kotlin/org/meshtastic/feature/map/maplibre/MapCamera.kt index 4936e04309..f06fefda9e 100644 --- a/feature/map-maplibre/src/commonMain/kotlin/org/meshtastic/feature/map/maplibre/MapCamera.kt +++ b/feature/map-maplibre/src/commonMain/kotlin/org/meshtastic/feature/map/maplibre/MapCamera.kt @@ -16,7 +16,12 @@ */ package org.meshtastic.feature.map.maplibre -import org.maplibre.compose.camera.CameraState +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.ui.unit.dp +import org.maplibre.compose.camera.CameraAnimation +import org.maplibre.compose.camera.CameraPosition +import org.maplibre.compose.map.MapState +import org.maplibre.spatialk.geojson.BoundingBox /** * Steps the camera zoom by [delta], clamped to [range]. @@ -24,11 +29,31 @@ import org.maplibre.compose.camera.CameraState * Shared by the main map's toolbar and the node-track map's. MapLibre publishes no zoom ornament, so every map that * wants buttons has to do this itself, and the clamp matters: pushing past a source's maximum zoom leaves the renderer * with no tiles to draw. + * + * Every camera step here eases; a button press is a nudge, not a flight. */ -internal suspend fun CameraState.zoomBy(delta: Double, range: ClosedFloatingPointRange) { - val target = (position.zoom + delta).coerceIn(range.start.toDouble(), range.endInclusive.toDouble()) - if (target != position.zoom) animateTo(position.copy(zoom = target)) +internal suspend fun MapState.zoomBy(delta: Double, range: ClosedFloatingPointRange) { + val target = (cameraPosition.zoom + delta).coerceIn(range.start.toDouble(), range.endInclusive.toDouble()) + if (target != cameraPosition.zoom) { + animateCameraPosition(cameraPosition.copy(zoom = target), animation = CameraAnimation.Ease()) + } } /** One zoom level per button press, which is what both predecessors' zoom controls did. */ internal const val ZOOM_STEP = 1.0 + +/** + * Frames [bounds] without zooming past [FRAME_MAX_ZOOM]: the fit is computed first and capped before the camera moves, + * so nodes standing metres apart open on their surroundings rather than on empty tiles. Eased, not the default flight. + */ +internal suspend fun MapState.frameBounds(bounds: BoundingBox, padding: PaddingValues = PaddingValues(0.dp)) { + val fitted = cameraForBounds(bounds, padding = padding) + animateCameraPosition(fitted.cappedTo(FRAME_MAX_ZOOM), CameraAnimation.Ease()) +} + +/** This position, zoomed out to [maxZoom] if it is tighter than that. */ +internal fun CameraPosition.cappedTo(maxZoom: Double): CameraPosition = + if (zoom > maxZoom) copy(zoom = maxZoom) else this + +/** The zoom a map opens at on a single point of interest; framing a mesh never goes tighter than that. */ +internal const val FRAME_MAX_ZOOM = DETAIL_ZOOM diff --git a/feature/map-maplibre/src/commonMain/kotlin/org/meshtastic/feature/map/maplibre/MapLibreMapViewProvider.kt b/feature/map-maplibre/src/commonMain/kotlin/org/meshtastic/feature/map/maplibre/MapLibreMapViewProvider.kt index 59c7181607..86475ac01c 100644 --- a/feature/map-maplibre/src/commonMain/kotlin/org/meshtastic/feature/map/maplibre/MapLibreMapViewProvider.kt +++ b/feature/map-maplibre/src/commonMain/kotlin/org/meshtastic/feature/map/maplibre/MapLibreMapViewProvider.kt @@ -36,15 +36,16 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle import kotlinx.coroutines.launch import org.koin.compose.koinInject import org.koin.compose.viewmodel.koinViewModel -import org.maplibre.compose.camera.CameraState -import org.maplibre.compose.camera.rememberCameraState +import org.maplibre.compose.camera.CameraAnimation +import org.maplibre.compose.camera.CameraPosition import org.maplibre.compose.location.BearingUpdate import org.maplibre.compose.location.LocationPermission import org.maplibre.compose.location.LocationState +import org.maplibre.compose.location.rememberDefaultHeadingProvider import org.maplibre.compose.location.rememberDefaultLocationProvider -import org.maplibre.compose.location.rememberDefaultOrientationProvider import org.maplibre.compose.location.rememberLocationState import org.maplibre.compose.location.rememberSystemSettingsLauncher +import org.maplibre.compose.map.MapState import org.meshtastic.core.ui.util.KeepScreenOn import org.meshtastic.core.ui.util.MapViewProvider import org.meshtastic.feature.map.SharedMapViewModel @@ -136,48 +137,51 @@ class MapLibreMapViewProvider( if (!LocalMapLibreRuntimeProbe.current()) return MapEngineUnavailable(modifier) val viewModel: SharedMapViewModel = koinViewModel() - // Null for the one frame before the basemap preference has loaded from disk; see rememberBasemapSelection. - val basemaps = rememberBasemapSelection(customBasemaps()) ?: return - - val cameraState = rememberCameraState() - val location = rememberLocationControls(cameraState) + // Both are null for the one frame before their stored value has been read from disk: the basemap preference + // (see rememberBasemapSelection) and the remembered camera (see rememberRestoredCamera). The map must not + // open on a default and then jump, nor frame the mesh over a view the user chose. + val basemaps = rememberBasemapSelection(customBasemaps()) + val restored = rememberRestoredCamera() + if (basemaps == null || restored == null) return + val location = rememberLocationControls() val waypoints = rememberWaypointEditing() val screen = rememberMapScreenState(waypointId = waypointId, sitePlannerNodeNum = sitePlannerNodeNum) + val mapState = + rememberMapScreenMapState( + basemap = basemaps.current, + restored = restored, + screen = screen, + waypoints = waypoints, + location = location, + customLayers = customLayers(), + navigateToNodeDetails = navigateToNodeDetails, + ) + + SaveCameraPosition(mapState) + // Following the user means the screen is the thing being watched — the Google flavor holds it awake for the // same reason, and a map that sleeps mid-walk is the one complaint a location-follow feature always draws. KeepScreenOn(location.following) Box(modifier = modifier.fillMaxSize()) { MeshMap( - viewModel = viewModel, - navigateToNodeDetails = navigateToNodeDetails, + mapState = mapState, modifier = Modifier.fillMaxSize(), basemap = basemaps.current, - overlays = screen.overlays, - layerOpacity = koinInject().opacity.collectAsState().value, - customLayers = customLayers(), - cameraState = cameraState, - locationState = location.state, - followLocation = location.following, - bearingUpdate = location.bearingUpdate, - frameOnNodes = rememberRestoredCamera(cameraState) == false, - onWaypointClick = { screen.infoWaypointId = it }, - onClusterMembers = { screen.clusterMembers = it }, onMapLongClick = waypoints.onLongPress, onMapClick = waypoints.onMapTap, - boxCorner = waypoints.firstCorner, ) - MapZoom(cameraState = cameraState, basemap = basemaps.current) + MapZoom(mapState = mapState, basemap = basemaps.current) OfflineIndicator(viewModel) MapToolbar( basemaps = basemaps, location = location, - cameraState = cameraState, + mapState = mapState, overlays = screen.overlays, onOverlaysChange = { screen.overlays = it }, basemapMenuExtra = basemapMenuExtra, @@ -191,12 +195,12 @@ class MapLibreMapViewProvider( SitePlannerSlot( open = screen.plannerOpen, nodeNum = sitePlannerNodeNum, - cameraState = cameraState, + mapState = mapState, planner = sitePlanner, onDismiss = { screen.plannerOpen = false }, ) - BoxAuthoringSlot(editing = waypoints, cameraState = cameraState) + BoxAuthoringSlot(editing = waypoints, mapState = mapState) WaypointDialogs( viewModel = viewModel, @@ -209,6 +213,37 @@ class MapLibreMapViewProvider( } } +/** The mesh map's state, wired to everything this screen feeds into it. */ +@Composable +private fun rememberMapScreenMapState( + basemap: Basemap, + restored: RestoredCamera, + screen: MapScreenState, + waypoints: WaypointEditing, + location: LocationControls, + customLayers: List, + navigateToNodeDetails: (Int) -> Unit, +): MapState { + val viewModel: SharedMapViewModel = koinViewModel() + return rememberMeshMapState( + viewModel = viewModel, + navigateToNodeDetails = navigateToNodeDetails, + basemap = basemap, + initialCameraPosition = restored.position ?: CameraPosition(), + overlays = screen.overlays, + layerOpacity = koinInject().opacity.collectAsState().value, + customLayers = customLayers, + onClusterMembers = { screen.clusterMembers = it }, + onWaypointClick = { screen.infoWaypointId = it }, + boxCorner = waypoints.firstCorner, + locationState = location.state, + followLocation = location.following, + bearingUpdate = location.bearingUpdate, + // Only when there was nothing stored: a remembered view is the user's own and is not yanked away. + frameOnNodes = restored.position == null, + ) +} + /** The "you're offline" pill, top-start so it never collides with the toolbar's top-center controls. */ @Composable private fun BoxScope.OfflineIndicator(viewModel: SharedMapViewModel) { @@ -248,12 +283,12 @@ private fun rememberMapScreenState(waypointId: Int?, sitePlannerNodeNum: Int?): * dialogs — clear of the zoom pair and the attribution row that share that edge. */ @Composable -private fun BoxScope.BoxAuthoringSlot(editing: WaypointEditing, cameraState: CameraState) { +private fun BoxScope.BoxAuthoringSlot(editing: WaypointEditing, mapState: MapState) { if (editing.boxDraft == null) return BoxAuthoringBar( onCancel = editing.onCancelBox, - onUseVisibleRegion = { cameraState.viewport?.visibleBoundingBox?.let(editing.onUseVisibleRegion) }, + onUseVisibleRegion = { mapState.viewport?.visibleBounds?.toBoundingBox()?.let(editing.onUseVisibleRegion) }, modifier = Modifier.align(Alignment.BottomCenter) .padding(start = AUTHORING_BAR_SIDE.dp, end = AUTHORING_BAR_SIDE.dp, bottom = AUTHORING_BAR_BOTTOM.dp), @@ -295,7 +330,7 @@ private fun ClusterMembersSlot(members: List, onPick: (Int) -> Un private fun SitePlannerSlot( open: Boolean, nodeNum: Int?, - cameraState: CameraState, + mapState: MapState, planner: (@Composable (SitePlannerSession) -> Unit)?, onDismiss: () -> Unit, ) { @@ -306,8 +341,15 @@ private fun SitePlannerSlot( planner( SitePlannerSession( nodeNum = nodeNum, - mapCenter = { cameraState.position.target }, - moveTo = { target -> scope.launch { cameraState.animateTo(cameraState.position.copy(target = target)) } }, + mapCenter = { mapState.cameraPosition.target }, + moveTo = { target -> + scope.launch { + mapState.animateCameraPosition( + mapState.cameraPosition.copy(target = target), + animation = CameraAnimation.Ease(), + ) + } + }, onDismiss = onDismiss, ), ) @@ -326,7 +368,7 @@ private fun SitePlannerSlot( private fun BoxScope.MapToolbar( basemaps: BasemapSelection, location: LocationControls, - cameraState: CameraState, + mapState: MapState, overlays: List, onOverlaysChange: (List) -> Unit, basemapMenuExtra: @Composable () -> Unit, @@ -344,9 +386,23 @@ private fun BoxScope.MapToolbar( modifier = Modifier.align(Alignment.TopCenter).padding(top = TOOLBAR_INSET.dp), onToggleFilterMenu = { filterMenuExpanded = !filterMenuExpanded }, filtersActive = filterState.isNarrowing, - bearing = cameraState.position.bearing.toFloat(), + bearing = mapState.cameraPosition.bearing.toFloat(), followPhoneBearing = location.followingBearing, - onCompassClick = location.onCompassClick, + // Matches the Google flavor: while following, the compass toggles heading-lock; otherwise it straightens + // the map back to north. Assembled here because it needs both the controls and the map state, and the + // controls are built before the state exists. + onCompassClick = { + if (location.following) { + location.onToggleBearingLock() + } else { + scope.launch { + mapState.animateCameraPosition( + mapState.cameraPosition.copy(bearing = 0.0), + animation = CameraAnimation.Ease(), + ) + } + } + }, filterDropdownContent = { if (filterMenuExpanded) { MapFilterSheet( @@ -364,9 +420,9 @@ private fun BoxScope.MapToolbar( offlineTarget = OfflineMapTarget( styleUrl = (basemaps.current as? Basemap.Vector)?.styleUri, - bounds = { cameraState.viewport?.visibleBoundingBox }, - zoom = { cameraState.position.zoom }, - showRegion = { box -> scope.launch { cameraState.animateTo(boundingBox = box) } }, + bounds = { mapState.viewport?.visibleBounds?.toBoundingBox() }, + zoom = { mapState.cameraPosition.zoom }, + showRegion = { box -> scope.launch { mapState.animateCameraToBounds(box) } }, ), offlineMapsSupported = offlineMapsSupported, extra = layersSheetExtra, @@ -385,10 +441,10 @@ private class LocationControls( val following: Boolean, val followingBearing: Boolean, val onToggleFollow: () -> Unit, - val onCompassClick: () -> Unit, + val onToggleBearingLock: () -> Unit, ) { val bearingUpdate: BearingUpdate - get() = if (followingBearing) BearingUpdate.TRACK_ORIENTATION else BearingUpdate.IGNORE + get() = if (followingBearing) BearingUpdate.TRACK_HEADING else BearingUpdate.IGNORE } /** @@ -399,9 +455,7 @@ private class LocationControls( * fused location lives behind the separate `location-runtime-gms` artifact and must never enter an F-Droid build. */ @Composable -private fun rememberLocationControls(cameraState: CameraState): LocationControls { - val scope = rememberCoroutineScope() - +private fun rememberLocationControls(): LocationControls { var following by remember { mutableStateOf(false) } var followingBearing by remember { mutableStateOf(false) } var enableAfterPermission by remember { mutableStateOf(false) } @@ -410,7 +464,7 @@ private fun rememberLocationControls(cameraState: CameraState): LocationControls rememberLocationState( enabled = following, provider = rememberDefaultLocationProvider(), - orientationProvider = rememberDefaultOrientationProvider(), + headingProvider = rememberDefaultHeadingProvider(), ) val settingsLauncher = rememberSystemSettingsLauncher() @@ -446,15 +500,9 @@ private fun rememberLocationControls(cameraState: CameraState): LocationControls } } }, - // Matches the Google flavor: while following, the compass toggles heading-lock; otherwise it straightens the - // map back to north. - onCompassClick = { - if (following) { - followingBearing = !followingBearing - } else { - scope.launch { cameraState.animateTo(cameraState.position.copy(bearing = 0.0)) } - } - }, + // Only the heading-lock half lives here; straightening the map to north needs the map state, which does + // not exist yet at this point — see MapToolbar. + onToggleBearingLock = { followingBearing = !followingBearing }, ) } diff --git a/feature/map-maplibre/src/commonMain/kotlin/org/meshtastic/feature/map/maplibre/MapLibreRuntime.kt b/feature/map-maplibre/src/commonMain/kotlin/org/meshtastic/feature/map/maplibre/MapLibreRuntime.kt index eba0c6030b..abda4f0ba6 100644 --- a/feature/map-maplibre/src/commonMain/kotlin/org/meshtastic/feature/map/maplibre/MapLibreRuntime.kt +++ b/feature/map-maplibre/src/commonMain/kotlin/org/meshtastic/feature/map/maplibre/MapLibreRuntime.kt @@ -22,9 +22,13 @@ import androidx.compose.runtime.staticCompositionLocalOf * Whether MapLibre's native rendering engine can actually load on this device. * * MapLibre Compose reaches its C++ engine through a JNI shim, and an app can be built for an architecture that shim was - * never published for. maplibre-native-ffi 0.202608.3 ships arm64-v8a and x86_64 only, so an armeabi-v7a build carries - * no engine and the first frame of a map dies with `UnsatisfiedLinkError` — see #7001. A missing renderer is not worth - * a crash when every other screen still works, so the map screens ask first and say so instead. + * never published for. maplibre-native-ffi 0.202608.3 shipped arm64-v8a and x86_64 only, so an armeabi-v7a build + * carried no engine and the first frame of a map died with `UnsatisfiedLinkError` — see #7001 and #7005. A missing + * renderer is not worth a crash when every other screen still works, so the map screens ask first and say so instead. + * + * maplibre-compose 0.16.0 closed that gap: its FFI ships armeabi-v7a too, so every ABI the app builds now carries an + * engine and this answers `true` everywhere. The probe stays as the cheap defence it always was — it is one lazy + * `System.loadLibrary` and it fails soft — but nothing is expected to reach the unavailable path any more. * * Off Android there is no such gap, and the actuals there answer `true` unconditionally. */ diff --git a/feature/map-maplibre/src/commonMain/kotlin/org/meshtastic/feature/map/maplibre/MeshMap.kt b/feature/map-maplibre/src/commonMain/kotlin/org/meshtastic/feature/map/maplibre/MeshMap.kt index adf22ebd3c..5267205a96 100644 --- a/feature/map-maplibre/src/commonMain/kotlin/org/meshtastic/feature/map/maplibre/MeshMap.kt +++ b/feature/map-maplibre/src/commonMain/kotlin/org/meshtastic/feature/map/maplibre/MeshMap.kt @@ -22,28 +22,39 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.rememberUpdatedState import androidx.compose.runtime.setValue +import androidx.compose.runtime.snapshotFlow import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.filterNotNull +import kotlinx.coroutines.flow.first import kotlinx.coroutines.launch import kotlinx.serialization.json.JsonObject -import org.maplibre.compose.camera.CameraState -import org.maplibre.compose.camera.rememberCameraState +import org.maplibre.compose.camera.CameraAnimation +import org.maplibre.compose.camera.CameraPosition import org.maplibre.compose.expressions.dsl.const +import org.maplibre.compose.interaction.ClickResult +import org.maplibre.compose.interaction.MapInteractions import org.maplibre.compose.layers.CircleLayer import org.maplibre.compose.location.BearingUpdate import org.maplibre.compose.location.LocationPuck import org.maplibre.compose.location.LocationState import org.maplibre.compose.location.LocationTrackingEffect -import org.maplibre.compose.location.mostAccurateBearing import org.maplibre.compose.location.updateCamera +import org.maplibre.compose.map.CameraConstraints +import org.maplibre.compose.map.LocalMapState +import org.maplibre.compose.map.LocalViewport +import org.maplibre.compose.map.MapState import org.maplibre.compose.map.MaplibreMap +import org.maplibre.compose.map.rememberMapState import org.maplibre.compose.material3.LocationPuckDefaults -import org.maplibre.compose.util.ClickResult +import org.maplibre.compose.overlay.include import org.maplibre.compose.util.MaplibreComposable +import org.maplibre.spatialk.geojson.BoundingBox import org.maplibre.spatialk.geojson.Feature import org.maplibre.spatialk.geojson.FeatureCollection import org.maplibre.spatialk.geojson.Point @@ -72,35 +83,37 @@ import org.meshtastic.feature.map.maplibre.style.zoomRange import kotlin.math.floor /** - * The mesh map, rendered by MapLibre. + * Everything the mesh map draws, as one [MapState]. + * + * Since maplibre-compose 0.16.0 the base style and the sources and layers over it belong to the state rather than to a + * trailing block on `MaplibreMap`, so this is where the mesh data is read and turned into layers. [MeshMap] presents + * the result. The state is returned because the caller also needs it: the toolbar reads the bearing off it, the zoom + * buttons move it, and the Site Planner asks it where the map is pointed. + * + * The camera effects stay out of the style block, even though that is where upstream puts `LocationTrackingEffect`. The + * library hosts style content in a subcomposition keyed on the loaded style and disposes it on every base-style switch, + * so a `remember` in there does not survive the user changing basemap. * * Shared by the F-Droid Android flavor and the desktop app — the two differ only in what they hand in, not in what gets * drawn. */ @Composable -fun MeshMap( +@Suppress("LongParameterList") +fun rememberMeshMapState( viewModel: BaseMapViewModel, navigateToNodeDetails: (Int) -> Unit, - modifier: Modifier = Modifier, basemap: Basemap = Basemaps.default, + /** Where the map opens. [rememberRestoredCamera] supplies the position the user left it on. */ + initialCameraPosition: CameraPosition = CameraPosition(), overlays: List = emptyList(), layerOpacity: Map = emptyMap(), customLayers: List = emptyList(), - onWaypointClick: (Int) -> Unit = {}, /** Called with the nodes of a tapped cluster that cannot be zoomed apart any further. */ onClusterMembers: (List) -> Unit = {}, - /** Called with the pressed position on a long press, which is how a new waypoint gets placed. */ - onMapLongClick: (Position) -> Unit = {}, - /** Called with the tapped position. Used to collect the two corners of a waypoint's geofence bounding box. */ - onMapClick: (Position) -> Unit = {}, + onWaypointClick: (Int) -> Unit = {}, /** The first corner of a box being authored, drawn so the tap reads as registered. */ boxCorner: Position? = null, - /** - * Hoisted so the host can read the bearing for a compass and steer the camera itself. Defaults to a map-owned state - * for callers that only want the map to frame itself. - */ - cameraState: CameraState = rememberCameraState(), - /** Location and orientation state to draw a puck for and optionally follow. Null disables both. */ + /** Location and heading state to draw a puck for and optionally follow. Null disables both. */ locationState: LocationState? = null, /** * Whether the user has asked to be followed. Gates both the camera and the puck, so switching tracking off leaves @@ -114,10 +127,7 @@ fun MeshMap( * user's own view is not yanked away from them. */ frameOnNodes: Boolean = true, -) { - // No engine on this device means composing a map is the UnsatisfiedLinkError crash in #7001. - if (!LocalMapLibreRuntimeProbe.current()) return MapEngineUnavailable(modifier) - +): MapState { val nodes by viewModel.nodesWithPosition.collectAsStateWithLifecycle() val waypoints by viewModel.waypoints.collectAsStateWithLifecycle() val filterState by viewModel.mapFilterStateFlow.collectAsStateWithLifecycle() @@ -125,70 +135,118 @@ fun MeshMap( val displayUnits by viewModel.displayUnits.collectAsStateWithLifecycle() val scope = rememberCoroutineScope() - // This body recomposes on every camera frame (it reads the viewport below); the mesh-wide filter should not. + // The style block recomposes on every camera frame (it reads the viewport below); the mesh-wide filter should not. // `nowSeconds` is deliberately not a key: a packet arriving already changes the node list. val visibleNodes = remember(nodes, filterState, myNodeInfo) { MapNodePolicy.visibleNodes(nodes, filterState, nowSeconds, myNodeInfo?.myNodeNum) } - FrameOnce(enabled = frameOnNodes, nodes = visibleNodes, cameraState = cameraState) + val mapState = + rememberMapState(baseStyle = basemap.toBaseStyle(), initialCameraPosition = initialCameraPosition) { + val state = checkNotNull(LocalMapState.current) + // The viewport local, rather than the state's own property: it is what the library scopes viewport + // recomposition to, and these reads are the whole reason this block runs per frame. + val viewportBounds = LocalViewport.current?.visibleBounds?.toBoundingBox() + + if (basemap is Basemap.Raster) { + RasterBasemapLayer(basemap) + } + MapOverlayLayers(overlays, layerOpacity) + TerrainLayers( + viewportBounds = viewportBounds, + zoom = state.cameraPosition.zoom, + displayUnits = displayUnits, + ) + CustomLayers(customLayers, layerOpacity) + + if (filterState.showWaypoints) { + WaypointLayers(waypoints = waypoints.values, onWaypointClick = onWaypointClick) + } + + MeshMapNodeLayers( + visibleNodes = visibleNodes, + mapState = state, + viewportBounds = viewportBounds, + filterState = filterState, + myNodeNum = myNodeInfo?.myNodeNum, + navigateToNodeDetails = navigateToNodeDetails, + onClusterMembers = onClusterMembers, + scope = scope, + ) + + // Declared last so the user's own position and an in-progress box corner draw above the mesh. + UserLocationPuck(locationState = locationState, visible = followLocation) + BoxCornerMarker(boxCorner) + } + + // Both effects stay out here, in this composition rather than the style block's. The library hosts the style + // content in a subcomposition keyed on the loaded style and disposes it on every base-style switch, so a + // `remember` in there is lost whenever the user changes basemap — which would reset FrameOnce's latch and + // re-frame the mesh over wherever they had panned to. + FrameOnce(enabled = frameOnNodes, nodes = visibleNodes, mapState = mapState) // Follows position whenever tracking is on; touches bearing only when the caller asks for it, so a user who // has rotated the map is not straightened out behind their back. FollowUserLocation( locationState = locationState, - cameraState = cameraState, + mapState = mapState, followLocation = followLocation, bearingUpdate = bearingUpdate, ) + return mapState +} + +/** + * The mesh map, rendered by MapLibre. + * + * Presents [mapState] and nothing else; what gets drawn is declared by [rememberMeshMapState]. + * + * [basemap] must be the same one that state was built with: it supplies the zoom range the camera is held to, and a + * different value here would clamp the camera to a range the loaded style cannot serve. + */ +@Composable +fun MeshMap( + mapState: MapState, + modifier: Modifier = Modifier, + basemap: Basemap = Basemaps.default, + /** Called with the pressed position on a long press, which is how a new waypoint gets placed. */ + onMapLongClick: (Position) -> Unit = {}, + /** Called with the tapped position. Used to collect the two corners of a waypoint's geofence bounding box. */ + onMapClick: (Position) -> Unit = {}, +) { + // No engine on this device means presenting a map is the UnsatisfiedLinkError crash in #7001. Guarded here and + // not at the state, which is pure Kotlin: the native library is loaded by the map *view*. + if (!LocalMapLibreRuntimeProbe.current()) return MapEngineUnavailable(modifier) + + val zoomRange = basemap.zoomRange() MaplibreMap( - baseStyle = basemap.toBaseStyle(), - cameraState = cameraState, modifier = modifier, + state = mapState, // Honour what the source can actually serve, as the OSMdroid map did. - zoomRange = basemap.zoomRange(), - onMapLongClick = { position, _ -> - onMapLongClick(position) - ClickResult.Consume + cameraConstraints = + CameraConstraints(minZoom = zoomRange.start.toDouble(), maxZoom = zoomRange.endInclusive.toDouble()), + interactions = + MapInteractions { + callbacks { + longClick { + onEvent { event -> + event.position?.let(onMapLongClick) + ClickResult.Consume + } + } + // Pass, not Consume: a tap has to keep reaching the layers underneath or nothing is selectable. + click { + onEvent { event -> + event.position?.let(onMapClick) + ClickResult.Pass + } + } + } }, - // Pass, not Consume: a tap has to keep reaching the layers underneath or nothing on the map is selectable. - onMapClick = { position, _ -> - onMapClick(position) - ClickResult.Pass - }, - overlay = MeshMapOrnaments, - ) { - if (basemap is Basemap.Raster) { - RasterBasemapLayer(basemap) - } - MapOverlayLayers(overlays, layerOpacity) - TerrainLayers( - viewportBounds = cameraState.viewport?.visibleBoundingBox, - zoom = cameraState.position.zoom, - displayUnits = displayUnits, - ) - CustomLayers(customLayers, layerOpacity) - - if (filterState.showWaypoints) { - WaypointLayers(waypoints = waypoints.values, onWaypointClick = onWaypointClick) - } - - MeshMapNodeLayers( - visibleNodes = visibleNodes, - cameraState = cameraState, - filterState = filterState, - myNodeNum = myNodeInfo?.myNodeNum, - navigateToNodeDetails = navigateToNodeDetails, - onClusterMembers = onClusterMembers, - scope = scope, - ) - - // Declared last so the user's own position and an in-progress box corner draw above the mesh. - UserLocationPuck(locationState = locationState, cameraState = cameraState, visible = followLocation) - BoxCornerMarker(boxCorner) - } + overlay = { include(MeshMapOrnaments) }, + ) } /** The node chips, clusters and precision circles — split out of [MeshMap] itself only to keep that function short. */ @@ -196,7 +254,8 @@ fun MeshMap( @MaplibreComposable private fun MeshMapNodeLayers( visibleNodes: List, - cameraState: CameraState, + mapState: MapState, + viewportBounds: BoundingBox?, filterState: BaseMapViewModel.MapFilterState, myNodeNum: Int?, navigateToNodeDetails: (Int) -> Unit, @@ -207,20 +266,23 @@ private fun MeshMapNodeLayers( nodes = visibleNodes, // Padded so a modest pan keeps the same nodes in view, and the chips are not redrawn for every frame of a // drag. Without a viewport at all — the first composition — every node is a candidate, as before. - visibleBounds = cameraState.viewport?.visibleBoundingBox?.padded(CHIP_VIEW_PADDING), + visibleBounds = viewportBounds?.padded(CHIP_VIEW_PADDING), // Floored: clustering indexes per whole zoom level, so the set only changes when the level does — and a // pinch does not recompute it for every fractional step in between. - zoom = floor(cameraState.position.zoom).toInt(), + zoom = floor(mapState.cameraPosition.zoom).toInt(), myNodeNum = myNodeNum, showPrecisionCircles = filterState.showPrecisionCircle, onNodeClick = navigateToNodeDetails, onClusterMembers = onClusterMembers, onClusterZoom = { centre, expansionZoom -> scope.launch { - val current = cameraState.position + val current = mapState.cameraPosition // A cluster that cannot report an expansion zoom answers with a sentinel (0 on // Android and desktop, -1 on iOS), so clamp — never zoom out on a tap. - cameraState.animateTo(current.copy(target = centre, zoom = maxOf(expansionZoom, current.zoom))) + mapState.animateCameraPosition( + current.copy(target = centre, zoom = maxOf(expansionZoom, current.zoom)), + animation = CameraAnimation.Ease(), + ) } }, ) @@ -233,20 +295,26 @@ private fun MeshMapNodeLayers( * mesh that is still filling in that would be continuous. */ @Composable -private fun FrameOnce(enabled: Boolean, nodes: List, cameraState: CameraState) { +private fun FrameOnce(enabled: Boolean, nodes: List, mapState: MapState) { if (!enabled) return var hasFramed by remember { mutableStateOf(false) } - val hasViewport = cameraState.viewport != null + val hasViewport = mapState.viewport != null + // The node list is read through a snapshot rather than keyed on, so an arriving packet cannot cancel this. + // Keying on it meant the effect restarted mid-fit: `frameBounds` suspends, and whether the latch was set + // before the call (fit lost, latch kept, mesh never framed) or after it (latch lost to a user pan, so a later + // packet re-frames over them) one of the two failure modes was always reachable. Nothing here restarts on + // node changes now, so the latch and the fit cannot come apart. + val currentNodes by rememberUpdatedState(nodes) // An effect, not composition-body work: a launch from composition fires even if the composition is - // abandoned, while its state write is rolled back — a camera jump with no framing recorded. The viewport - // gate is FitBoundsOnceVisible's: fitting before the map reports a viewport lands on a default. - LaunchedEffect(nodes, hasViewport) { + // abandoned, while its state write is rolled back — a camera jump with no framing recorded. Fitting before + // the map reports a viewport silently lands on a default, hence the gate. + LaunchedEffect(hasViewport) { if (hasFramed || !hasViewport) return@LaunchedEffect - nodesBoundingBox(nodes)?.let { box -> - hasFramed = true - cameraState.jumpTo(boundingBox = box) - } + // Waits for the first node set that has anything to frame; a mesh still filling in reports none. + val box = snapshotFlow { nodesBoundingBox(currentNodes) }.filterNotNull().first() + hasFramed = true + mapState.frameBounds(box) } } @@ -254,7 +322,7 @@ private fun FrameOnce(enabled: Boolean, nodes: List, cameraState: CameraSt @Composable private fun FollowUserLocation( locationState: LocationState?, - cameraState: CameraState, + mapState: MapState, followLocation: Boolean, bearingUpdate: BearingUpdate, ) { @@ -265,23 +333,18 @@ private fun FollowUserLocation( enabled = followLocation, trackBearing = bearingUpdate != BearingUpdate.IGNORE, ) { - updateCamera(camera = cameraState, updateBearing = bearingUpdate) + updateCamera(mapState = mapState, updateBearing = bearingUpdate) } } /** The user's position, accuracy and heading. Drawn only while tracking, so switching it off leaves no stale dot. */ @Composable @MaplibreComposable -private fun UserLocationPuck(locationState: LocationState?, cameraState: CameraState, visible: Boolean) { +private fun UserLocationPuck(locationState: LocationState?, visible: Boolean) { if (locationState == null || !visible) return - LocationPuck( - idPrefix = "user-location", - location = locationState.location, - cameraState = cameraState, - bearing = locationState.mostAccurateBearing(), - colors = LocationPuckDefaults.colors(), - ) + // The state overload, which resolves the latest measurement and its most accurate bearing itself. + LocationPuck(idPrefix = "user-location", locationState = locationState, colors = LocationPuckDefaults.colors()) } /** diff --git a/feature/map-maplibre/src/commonMain/kotlin/org/meshtastic/feature/map/maplibre/NodeTrackMap.kt b/feature/map-maplibre/src/commonMain/kotlin/org/meshtastic/feature/map/maplibre/NodeTrackMap.kt index d8c2070137..3791849329 100644 --- a/feature/map-maplibre/src/commonMain/kotlin/org/meshtastic/feature/map/maplibre/NodeTrackMap.kt +++ b/feature/map-maplibre/src/commonMain/kotlin/org/meshtastic/feature/map/maplibre/NodeTrackMap.kt @@ -32,7 +32,6 @@ import kotlinx.serialization.json.int import kotlinx.serialization.json.jsonPrimitive import kotlinx.serialization.json.put import org.koin.compose.viewmodel.koinViewModel -import org.maplibre.compose.camera.rememberCameraState import org.maplibre.compose.expressions.dsl.asNumber import org.maplibre.compose.expressions.dsl.const import org.maplibre.compose.expressions.dsl.feature @@ -40,10 +39,10 @@ import org.maplibre.compose.expressions.dsl.interpolate import org.maplibre.compose.expressions.dsl.linear import org.maplibre.compose.expressions.value.LineCap import org.maplibre.compose.expressions.value.LineJoin +import org.maplibre.compose.interaction.ClickResult import org.maplibre.compose.layers.CircleLayer import org.maplibre.compose.layers.LineLayer import org.maplibre.compose.sources.GeoJsonOptions -import org.maplibre.compose.util.ClickResult import org.maplibre.spatialk.geojson.Feature import org.maplibre.spatialk.geojson.FeatureCollection import org.maplibre.spatialk.geojson.LineString @@ -99,7 +98,6 @@ fun MapLibreNodeTrackMap( // runs from the start of the line, the fade runs from index 0, and the chip goes on the last point. The view model // hands these over newest-first for its list, so an unsorted track drew the whole thing backwards. val allPoints = remember(positions) { positions.mapNotNull { it.toTrackPoint() }.sortedBy { it.second } } - val cameraState = rememberCameraState() // The basemap is a shared preference, so a track opens on whatever the main map is set to. The OSMdroid track map // resolved the same preference; hardcoding the default meant picking Dark on the main map and getting Liberty here. @@ -115,22 +113,14 @@ fun MapLibreNodeTrackMap( val trackFilter = filterState.lastHeardTrackFilter val points = remember(allPoints, trackFilter) { allPoints.olderThanCutoffRemoved(trackFilter) } - // Frame the whole track, not just its midpoint: a fixed zoom on the centre cropped long tracks at both ends. Keyed - // on the *unfiltered* track, so tightening the age filter does not yank the camera around under the user. - FitBoundsOnceVisible(cameraState = cameraState, key = allPoints) { - positionsBoundingBox(allPoints.map { it.first }) - } - if (allPoints.isEmpty()) return // The colour the rest of the app draws this node in. The track used to be a flat blue, which said nothing about // whose track it was — the Google flavor fades the node's own colour along it, and so does this now. val trackColor = node?.let { Color(it.colors.second) } ?: MapColors.Slate - // The map and its toolbar stay up even when the filter empties the track — otherwise the control that emptied it - // disappears along with the points, leaving no way back. - Box(modifier = modifier) { - SecondaryMapSurface(basemaps = basemaps, cameraState = cameraState) { + val mapState = + rememberSecondaryMapState(basemaps) { // A LineString needs two coordinates; a filter tight enough to leave one point would otherwise throw. if (points.size > 1) TrackLineLayer(points = points, color = trackColor) if (points.isNotEmpty()) { @@ -143,8 +133,17 @@ fun MapLibreNodeTrackMap( if (node != null) NewestPositionChip(node = node, newest = points.last()) } } + + // Frame the whole track, not just its midpoint: a fixed zoom on the centre cropped long tracks at both ends. Keyed + // on the *unfiltered* track, so tightening the age filter does not yank the camera around under the user. + FitBoundsOnceVisible(mapState = mapState, key = allPoints) { positionsBoundingBox(allPoints.map { it.first }) } + + // The map and its toolbar stay up even when the filter empties the track — otherwise the control that emptied it + // disappears along with the points, leaving no way back. + Box(modifier = modifier) { + SecondaryMapSurface(mapState = mapState, basemaps = basemaps) SecondaryMapChrome( - cameraState = cameraState, + mapState = mapState, basemaps = basemaps, filterMenu = { expanded, onDismissRequest -> NodeTrackFilterMenu( diff --git a/feature/map-maplibre/src/commonMain/kotlin/org/meshtastic/feature/map/maplibre/SecondaryMapScaffold.kt b/feature/map-maplibre/src/commonMain/kotlin/org/meshtastic/feature/map/maplibre/SecondaryMapScaffold.kt index a67877b28f..3527c4ba65 100644 --- a/feature/map-maplibre/src/commonMain/kotlin/org/meshtastic/feature/map/maplibre/SecondaryMapScaffold.kt +++ b/feature/map-maplibre/src/commonMain/kotlin/org/meshtastic/feature/map/maplibre/SecondaryMapScaffold.kt @@ -27,9 +27,13 @@ import androidx.compose.runtime.rememberUpdatedState import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp -import org.maplibre.compose.camera.CameraState -import org.maplibre.compose.map.MapOptions +import org.maplibre.compose.camera.CameraPosition +import org.maplibre.compose.interaction.MapInteractions +import org.maplibre.compose.map.CameraConstraints +import org.maplibre.compose.map.MapState +import org.maplibre.compose.map.MapUiOptions import org.maplibre.compose.map.MaplibreMap +import org.maplibre.compose.map.rememberMapState import org.maplibre.spatialk.geojson.BoundingBox import org.meshtastic.feature.map.component.MapEngineUnavailable import org.meshtastic.feature.map.maplibre.component.BasemapSelection @@ -41,41 +45,66 @@ import org.meshtastic.feature.map.maplibre.style.toBaseStyle import org.meshtastic.feature.map.maplibre.style.zoomRange /** - * The map surface the maps outside the main one all draw on. + * The map state the maps outside the main one all share. * * They differ in what they draw and which controls they carry, but not in how the map itself is set up: the chosen * basemap supplies both the style and the zoom range it can actually serve, and a raster basemap needs a layer of its * own over the empty style. Repeating that in four places is how three of them ended up with slightly different * versions of it. * + * Since maplibre-compose 0.16.0 the style content is declared here, on the state, rather than in a trailing block on + * `MaplibreMap` — the state owns the base style and the sources and layers over it, and the composable only presents + * it. The state is returned so the caller can drive the camera and hang its chrome off the same one. + * * [content] is a plain `@Composable`, matching the layer composables it will hold — `NodeLayers` and - * `RasterBasemapLayer` are declared that way and compose inside `MaplibreMap` without the applier annotation. Spelling - * `@MaplibreComposable` here is what spotless and detekt disagree about formatting. + * `RasterBasemapLayer` are declared that way and compose inside the style block without the applier annotation. + * Spelling `@MaplibreComposable` here is what spotless and detekt disagree about formatting. * * The raster underlay is composed **before** [content] and must stay that way: layers stack in composition order, so a * basemap added afterwards paints over the mesh data instead of sitting under it. */ @Composable -internal fun SecondaryMapSurface( +internal fun rememberSecondaryMapState( basemaps: BasemapSelection, - cameraState: CameraState, - modifier: Modifier = Modifier.fillMaxSize(), - options: MapOptions = SecondaryMapOptions, + initialCameraPosition: CameraPosition = CameraPosition(), content: @Composable () -> Unit, -) { - // Same guard as MeshMap: the secondary maps compose MaplibreMap directly, so they crash the same way. - if (!LocalMapLibreRuntimeProbe.current()) return MapEngineUnavailable(modifier) - - MaplibreMap( - baseStyle = basemaps.current.toBaseStyle(), - cameraState = cameraState, - modifier = modifier, - options = options, - zoomRange = basemaps.current.zoomRange(), - ) { +): MapState = + rememberMapState(baseStyle = basemaps.current.toBaseStyle(), initialCameraPosition = initialCameraPosition) { (basemaps.current as? Basemap.Raster)?.let { RasterBasemapLayer(it) } content() } + +/** + * The map surface the maps outside the main one all draw on. + * + * Presents [mapState] and nothing else: what gets drawn is declared by [rememberSecondaryMapState], which the caller + * holds so it can also frame the camera and place the chrome. + * + * [basemaps] must be the same selection that state was built with: it supplies the zoom range the camera is held to, + * and a different value here would clamp the camera to a range the loaded style cannot serve. + */ +@Composable +internal fun SecondaryMapSurface( + mapState: MapState, + basemaps: BasemapSelection, + modifier: Modifier = Modifier.fillMaxSize(), + interactions: MapInteractions = SecondaryMapInteractions, + uiOptions: MapUiOptions = MapUiOptions.Standard, +) { + // Same guard as MeshMap, and in the same place: the state is pure Kotlin, the map view is what loads the + // native library. The style content is never composed without a presentation, so it stops here too. + if (!LocalMapLibreRuntimeProbe.current()) return MapEngineUnavailable(modifier) + + val zoomRange = basemaps.current.zoomRange() + MaplibreMap( + modifier = modifier, + state = mapState, + // Honour what the source can actually serve, as the OSMdroid map did. + cameraConstraints = + CameraConstraints(minZoom = zoomRange.start.toDouble(), maxZoom = zoomRange.endInclusive.toDouble()), + interactions = interactions, + uiOptions = uiOptions, + ) } /** @@ -87,13 +116,13 @@ internal fun SecondaryMapSurface( */ @Composable internal fun BoxScope.SecondaryMapChrome( - cameraState: CameraState, + mapState: MapState, basemaps: BasemapSelection, filterMenu: (@Composable (expanded: Boolean, onDismissRequest: () -> Unit) -> Unit)? = null, ) { - MapZoom(cameraState = cameraState, basemap = basemaps.current) + MapZoom(mapState = mapState, basemap = basemaps.current) SecondaryMapControls( - cameraState = cameraState, + mapState = mapState, basemaps = basemaps, modifier = Modifier.align(Alignment.TopCenter).padding(top = TOOLBAR_INSET.dp), filterMenu = filterMenu, @@ -103,27 +132,29 @@ internal fun BoxScope.SecondaryMapChrome( /** * Frames [bounds] once the map can report a viewport. * - * Fitting a bounding box needs a viewport size, and on first composition there is none — the fit silently lands on a - * default instead, which is what made these maps open zoomed past the very thing they exist to show. Null bounds is a - * no-op, which covers having nothing to frame yet. + * Fitting before the map reports a viewport silently lands on a default instead, and that is what made these maps open + * zoomed past the very thing they exist to show. Null bounds is a no-op, which covers having nothing to frame yet. * * [key] is what re-frames the camera, and it is deliberately the caller's choice rather than the bounds themselves: * each map has its own answer for when the user would want the camera moved under them. */ @Composable internal fun FitBoundsOnceVisible( - cameraState: CameraState, + mapState: MapState, key: Any?, padding: PaddingValues = SecondaryMapFitPadding, bounds: () -> BoundingBox?, ) { - val hasViewport = cameraState.viewport != null // The effect restarts on [key], not on `bounds`, so it must read the current lambda rather than the one captured // when it last restarted — otherwise a recomposition that passes new bounds keeps framing the old ones. val currentBounds by rememberUpdatedState(bounds) + // Gated on the viewport rather than letting `frameBounds` wait for one inside the effect. Both reach the same + // camera, but waiting inside leaves the call cancellable for the whole time the map has yet to render, and it + // is cancelled by user input as well as by [key]: a fit lost that way is not retried until [key] changes + // again, which for these maps may be never. + val hasViewport = mapState.viewport != null LaunchedEffect(key, hasViewport) { - if (hasViewport) { - currentBounds()?.let { cameraState.jumpTo(boundingBox = it, padding = padding) } - } + if (!hasViewport) return@LaunchedEffect + currentBounds()?.let { mapState.frameBounds(it, padding = padding) } } } diff --git a/feature/map-maplibre/src/commonMain/kotlin/org/meshtastic/feature/map/maplibre/SecondaryMaps.kt b/feature/map-maplibre/src/commonMain/kotlin/org/meshtastic/feature/map/maplibre/SecondaryMaps.kt index b9856b26c2..8c80fcffcd 100644 --- a/feature/map-maplibre/src/commonMain/kotlin/org/meshtastic/feature/map/maplibre/SecondaryMaps.kt +++ b/feature/map-maplibre/src/commonMain/kotlin/org/meshtastic/feature/map/maplibre/SecondaryMaps.kt @@ -40,17 +40,17 @@ import kotlinx.serialization.json.int import kotlinx.serialization.json.jsonPrimitive import kotlinx.serialization.json.put import org.jetbrains.compose.resources.stringResource +import org.maplibre.compose.camera.CameraAnimation import org.maplibre.compose.camera.CameraPosition -import org.maplibre.compose.camera.rememberCameraState import org.maplibre.compose.expressions.dsl.asBoolean import org.maplibre.compose.expressions.dsl.const import org.maplibre.compose.expressions.dsl.feature import org.maplibre.compose.expressions.dsl.not import org.maplibre.compose.expressions.value.LineCap +import org.maplibre.compose.interaction.ClickResult +import org.maplibre.compose.interaction.MapInteractions import org.maplibre.compose.layers.LineLayer -import org.maplibre.compose.map.GestureOptions -import org.maplibre.compose.map.MapOptions -import org.maplibre.compose.util.ClickResult +import org.maplibre.compose.map.MapUiOptions import org.maplibre.spatialk.geojson.BoundingBox import org.maplibre.spatialk.geojson.Feature import org.maplibre.spatialk.geojson.FeatureCollection @@ -103,13 +103,24 @@ fun MapLibreInlineMap( // Null for the one frame before the basemap preference has loaded from disk; see rememberBasemapSelection. val basemaps = rememberBasemapSelection(customBasemaps()) ?: return val target = GeoPosition(longitude = node.longitude, latitude = node.latitude) - val cameraState = rememberCameraState(CameraPosition(target = target, zoom = INLINE_ZOOM)) + val mapState = + rememberSecondaryMapState(basemaps, CameraPosition(target = target, zoom = INLINE_ZOOM)) { + val source = rememberFeatureSource(node) { nodesToFeatureCollection(listOf(node)) } + + // The node-detail sheet is where a degraded position matters most — it is the screen a user opens to ask + // how precisely this node is placed. The Google mini-map draws the circle; this one left it out. + NodePrecisionLayer(id = "inline-precision", nodes = listOf(node)) + NodeChipLayer(id = "inline-node", source = source, nodes = listOf(node)) + } // Follows the node as fresh positions arrive, as the Google mini-map does. Guarded on the current target so the - // first composition — which rememberCameraState has already centred — does not animate to where the camera is. + // first composition does not animate to where the camera already is. LaunchedEffect(target) { - if (cameraState.position.target != target) { - cameraState.animateTo(cameraState.position.copy(target = target)) + if (mapState.cameraPosition.target != target) { + mapState.animateCameraPosition( + mapState.cameraPosition.copy(target = target), + animation = CameraAnimation.Ease(), + ) } } @@ -121,16 +132,9 @@ fun MapLibreInlineMap( // The same control every other map uses, at the same size. A shrunken variant was tried and looked out of place // against the rest of the map chrome for the ~25% of height it saved. Box(modifier = modifier) { - SecondaryMapSurface(basemaps = basemaps, cameraState = cameraState, options = InlineMapOptions) { - val source = rememberFeatureSource(node) { nodesToFeatureCollection(listOf(node)) } + SecondaryMapSurface(mapState = mapState, basemaps = basemaps, uiOptions = InlineMapUiOptions) - // The node-detail sheet is where a degraded position matters most — it is the screen a user opens to ask - // how precisely this node is placed. The Google mini-map draws the circle; this one left it out. - NodePrecisionLayer(id = "inline-precision", nodes = listOf(node)) - NodeChipLayer(id = "inline-node", source = source, nodes = listOf(node)) - } - - MapZoom(cameraState = cameraState, basemap = basemaps.current) + MapZoom(mapState = mapState, basemap = basemaps.current) } } @@ -149,21 +153,23 @@ fun MapLibreTracerouteMap( modifier: Modifier = Modifier, customBasemaps: @Composable () -> List = { customRasterBasemaps() }, ) { - val cameraState = rememberCameraState() // Null for the one frame before the basemap preference has loaded from disk; see rememberBasemapSelection. val basemaps = rememberBasemapSelection(customBasemaps()) ?: return val hops = (forwardRoute + returnRoute).distinct().mapNotNull { nodeLookup[it] } - // Keyed on the hops: the route is what this map exists to frame, so a different route should re-frame it. The - // padding is what keeps the outermost hops clear of the toolbar and the legend. - FitBoundsOnceVisible(cameraState = cameraState, key = hops) { nodesBoundingBox(hops) } - - Box(modifier = modifier) { - SecondaryMapSurface(basemaps = basemaps, cameraState = cameraState) { + val mapState = + rememberSecondaryMapState(basemaps) { TracerouteLayers(forwardRoute = forwardRoute, returnRoute = returnRoute, nodeLookup = nodeLookup) TracerouteHopLayers(hops) } - SecondaryMapChrome(cameraState = cameraState, basemaps = basemaps) + + // Keyed on the hops: the route is what this map exists to frame, so a different route should re-frame it. The + // padding is what keeps the outermost hops clear of the toolbar and the legend. + FitBoundsOnceVisible(mapState = mapState, key = hops) { nodesBoundingBox(hops) } + + Box(modifier = modifier) { + SecondaryMapSurface(mapState = mapState, basemaps = basemaps) + SecondaryMapChrome(mapState = mapState, basemaps = basemaps) } } @@ -189,7 +195,6 @@ fun MapLibreDiscoveryMap( customBasemaps: @Composable () -> List = { customRasterBasemaps() }, ) { val scanner = GeoPosition(longitude = userLongitude, latitude = userLatitude) - val cameraState = rememberCameraState(CameraPosition(target = scanner, zoom = DETAIL_ZOOM)) // Null for the one frame before the basemap preference has loaded from disk; see rememberBasemapSelection. val basemaps = rememberBasemapSelection(customBasemaps()) ?: return // The node itself, not its index. The feature carries an index into `located`, which is only meaningful for @@ -203,18 +208,21 @@ fun MapLibreDiscoveryMap( // list to agree on. val located = remember(nodes) { nodes.filter { it.latitude != 0.0 || it.longitude != 0.0 } } - // Frame the scanner together with everything it heard, which is the OSMdroid map's behaviour and the only view - // that answers the question this map is opened to answer. Opening at a fixed zoom on the scanner put every - // discovered node off screen. Keyed on the located nodes, so a scan that hears another one re-frames to include it. - FitBoundsOnceVisible(cameraState = cameraState, key = located) { discoveryBoundingBox(scanner, located) } - - Box(modifier = modifier) { - SecondaryMapSurface(basemaps = basemaps, cameraState = cameraState) { + val mapState = + rememberSecondaryMapState(basemaps, CameraPosition(target = scanner, zoom = DETAIL_ZOOM)) { DiscoveryTopologyLayer(scanner = scanner, nodes = located) DiscoveredNodeLayers(nodes = located, onNodeClick = { selectedNode = located.getOrNull(it) }) ScannerLayer(scanner = scanner, label = stringResource(Res.string.you)) } - SecondaryMapChrome(cameraState = cameraState, basemaps = basemaps) + + // Frame the scanner together with everything it heard, which is the OSMdroid map's behaviour and the only view + // that answers the question this map is opened to answer. Opening at a fixed zoom on the scanner put every + // discovered node off screen. Keyed on the located nodes, so a scan that hears another one re-frames to include it. + FitBoundsOnceVisible(mapState = mapState, key = located) { discoveryBoundingBox(scanner, located) } + + Box(modifier = modifier) { + SecondaryMapSurface(mapState = mapState, basemaps = basemaps) + SecondaryMapChrome(mapState = mapState, basemaps = basemaps) // The signal figures the Google map shows in a marker snippet. A MapLibre marker is a layer feature and has no // snippet, so the tapped node's numbers go at the foot of the map. @@ -385,9 +393,12 @@ private fun ScannerLayer(scanner: GeoPosition, label: String) { * two-finger drag pitches the map with nothing on screen to put it back. The Google flavor makes the same call with * `tiltGesturesEnabled = isMainMode`, and the OSMdroid map never had tilt at all. Rotation stays, which is what Google * does too. + * + * Says that directly now: 0.16.0 replaced the per-gesture flags with camera capabilities, and the old + * `isDragRotateTiltEnabled = false` had bundled rotation in with tilt, so a mouse drag could not rotate these maps + * either despite the intent above. It can now. */ -internal val SecondaryMapOptions = - MapOptions(gestureOptions = GestureOptions(isDragRotateTiltEnabled = false, isTwoFingerTiltEnabled = false)) +internal val SecondaryMapInteractions = MapInteractions { camera { tilt { enabled = false } } } /** * Gestures off entirely, for the mini-map embedded in the node detail sheet. @@ -396,25 +407,26 @@ internal val SecondaryMapOptions = * one the column is also trying to read. The Google flavor resolves it the same way, turning off scroll, zoom, rotate * and tilt in its own `InlineMap` and leaving the zoom buttons as the only way in. Keyboard pan and zoom stay: they * cost the column nothing and are the desktop's route to the same thing. + * + * The gesture families are switched off one by one rather than with `MapUiOptions.None`, which would also take the keys + * and the feature taps this map still wants. Momentum needs no separate mention: the gestures that could carry it are + * gone. */ -internal val InlineMapOptions = - MapOptions( - gestureOptions = - GestureOptions( - isDragPanEnabled = false, - isDragRotateTiltEnabled = false, - isPinchZoomEnabled = false, - isTwoFingerRotateEnabled = false, - isTwoFingerTiltEnabled = false, - isTwoFingerTapZoomEnabled = false, - isScrollZoomEnabled = false, - isDoubleClickZoomEnabled = false, - isQuickZoomEnabled = false, - isFlingEnabled = false, - isPinchZoomVelocityEnabled = false, - isRotateVelocityEnabled = false, - ), - ) +internal val InlineMapUiOptions = MapUiOptions { + bindings { + drag { enabled = false } + transform { + pan { enabled = false } + zoom { enabled = false } + rotate { enabled = false } + tilt { enabled = false } + } + scroll { enabled = false } + doubleTap { enabled = false } + twoFingerTap { enabled = false } + tapDrag { enabled = false } + } +} /** Keeps a floating toolbar clear of the top edge, matching the main map's own inset. */ internal const val TOOLBAR_INSET = 8 diff --git a/feature/map-maplibre/src/commonMain/kotlin/org/meshtastic/feature/map/maplibre/component/CacheUsage.kt b/feature/map-maplibre/src/commonMain/kotlin/org/meshtastic/feature/map/maplibre/component/CacheUsage.kt new file mode 100644 index 0000000000..2535322733 --- /dev/null +++ b/feature/map-maplibre/src/commonMain/kotlin/org/meshtastic/feature/map/maplibre/component/CacheUsage.kt @@ -0,0 +1,84 @@ +/* + * 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 . + */ +package org.meshtastic.feature.map.maplibre.component + +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.flowOf +import org.jetbrains.compose.resources.stringResource +import org.maplibre.compose.offline.DownloadProgress +import org.maplibre.compose.offline.OfflinePack +import org.meshtastic.core.resources.Res +import org.meshtastic.core.resources.map_cache_megabytes +import org.meshtastic.core.resources.map_cache_size +import org.meshtastic.core.resources.map_cache_tiles + +/** Disk occupied by every downloaded pack. */ +internal data class CacheUsage(val tiles: Long = 0L, val bytes: Long = 0L) + +/** + * Totals across the packs, each of which reports its progress as its own flow. + * + * `combine` over an empty list never emits, so no pack means no value at all rather than a zero. + */ +@Composable +internal fun rememberCacheUsage(packs: Set): CacheUsage { + val usage by + remember(packs) { + if (packs.isEmpty()) { + flowOf(CacheUsage()) + } else { + combine(packs.map { pack -> pack.downloadProgress }) { progress -> + val healthy = progress.filterIsInstance() + // Resource bytes, not tile bytes: a pack also holds the style, its glyphs and its sprites, and + // all of it occupies the same disk the user is being asked about. + CacheUsage( + tiles = healthy.sumOf { it.completedTileCount }, + bytes = healthy.sumOf { it.completedResourceBytes }, + ) + } + } + } + .collectAsState(CacheUsage()) + return usage +} + +/** + * How much disk the downloaded packs occupy. + * + * The OSMdroid map reported this in MB, which is the number that answers "is this filling my phone". No capacity beside + * it: OSMdroid had one bounded SQLite cache, whereas MapLibre has explicitly downloaded packs the user deletes by hand + * plus a separate ambient cache, and quoting a ceiling that governs neither would be a lie. + */ +@Composable +internal fun CacheUsageLine(storedBytes: Long, storedTiles: Long) { + Text( + text = + stringResource(Res.string.map_cache_size) + + ": " + + stringResource(Res.string.map_cache_megabytes, storedBytes.megabytes()) + + " · " + + stringResource(Res.string.map_cache_tiles, storedTiles.toInt()), + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) +} diff --git a/feature/map-maplibre/src/commonMain/kotlin/org/meshtastic/feature/map/maplibre/component/MapOrnaments.kt b/feature/map-maplibre/src/commonMain/kotlin/org/meshtastic/feature/map/maplibre/component/MapOrnaments.kt index b37e7f5492..b1b2462830 100644 --- a/feature/map-maplibre/src/commonMain/kotlin/org/meshtastic/feature/map/maplibre/component/MapOrnaments.kt +++ b/feature/map-maplibre/src/commonMain/kotlin/org/meshtastic/feature/map/maplibre/component/MapOrnaments.kt @@ -16,14 +16,19 @@ */ package org.meshtastic.feature.map.maplibre.component -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.consumeWindowInsets +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.safeDrawing +import androidx.compose.foundation.layout.windowInsetsPadding import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import org.maplibre.compose.map.LocalMapState import org.maplibre.compose.overlay.DisappearingScaleBar -import org.maplibre.compose.overlay.ExpandingAttributionButton -import org.maplibre.compose.overlay.MaplibreLogo +import org.maplibre.compose.overlay.LocalCameraPadding +import org.maplibre.compose.overlay.include import org.maplibre.compose.overlay.MapOverlay as MaplibreOverlay /** @@ -40,21 +45,26 @@ import org.maplibre.compose.overlay.MapOverlay as MaplibreOverlay * app takes through `localeUnitsProvider`. */ internal val MeshMapOrnaments: MaplibreOverlay = MaplibreOverlay { - DisappearingScaleBar( - metersPerDp = cameraState.viewport?.metersPerDpAtTarget ?: 0.0, - zoom = cameraState.position.zoom, - modifier = Modifier.align(Alignment.TopStart), - ) + val mapState = checkNotNull(LocalMapState.current) + val cameraPadding = LocalCameraPadding.current - // Read before entering the Row, whose scope shadows this one. - val camera = cameraState - val style = styleState - Row( - modifier = Modifier.align(Alignment.BottomStart).fillMaxWidth(), - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically, + // A custom overlay fills the map and the library keeps its own inset helper internal, so the scale bar has to + // carry the camera padding, safe-area insets and edge margin that the built-in controls apply for themselves. + Box( + modifier = + Modifier.fillMaxSize() + .padding(cameraPadding) + .consumeWindowInsets(cameraPadding) + .windowInsetsPadding(WindowInsets.safeDrawing) + .padding(MaplibreOverlay.Spacing), ) { - MaplibreLogo() - ExpandingAttributionButton(cameraState = camera, styleState = style) + DisappearingScaleBar( + metersPerDp = mapState.viewport?.metersPerDpAtTarget ?: 0.0, + zoom = mapState.cameraPosition.zoom, + modifier = Modifier.align(Alignment.TopStart), + ) } + + // The logo and the attribution button, in the places `MapOverlay.Default` puts them. + include(MaplibreOverlay.AttributionOnly) } diff --git a/feature/map-maplibre/src/commonMain/kotlin/org/meshtastic/feature/map/maplibre/component/OfflineMapTarget.kt b/feature/map-maplibre/src/commonMain/kotlin/org/meshtastic/feature/map/maplibre/component/OfflineMapTarget.kt index 8db87e44fb..b0524a5328 100644 --- a/feature/map-maplibre/src/commonMain/kotlin/org/meshtastic/feature/map/maplibre/component/OfflineMapTarget.kt +++ b/feature/map-maplibre/src/commonMain/kotlin/org/meshtastic/feature/map/maplibre/component/OfflineMapTarget.kt @@ -29,21 +29,23 @@ import androidx.compose.material3.LinearProgressIndicator import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.unit.dp import co.touchlab.kermit.Logger import kotlinx.coroutines.launch import org.jetbrains.compose.resources.stringResource +import org.maplibre.compose.map.DefaultMapRuntime import org.maplibre.compose.offline.DownloadProgress import org.maplibre.compose.offline.DownloadStatus import org.maplibre.compose.offline.OfflineManager import org.maplibre.compose.offline.OfflinePack import org.maplibre.compose.offline.OfflinePackDefinition -import org.maplibre.compose.offline.rememberOfflineManager import org.maplibre.spatialk.geojson.BoundingBox import org.meshtastic.core.common.util.NumberFormatter import org.meshtastic.core.common.util.ioDispatcher @@ -52,7 +54,6 @@ import org.meshtastic.core.resources.Res import org.meshtastic.core.resources.delete import org.meshtastic.core.resources.map_cache_manager import org.meshtastic.core.resources.map_cache_megabytes -import org.meshtastic.core.resources.map_cache_size import org.meshtastic.core.resources.map_cache_tiles import org.meshtastic.core.resources.map_download_status_complete import org.meshtastic.core.resources.map_download_status_downloading @@ -86,27 +87,26 @@ internal class OfflineMapTarget( */ @Composable internal fun OfflineMapsSection(target: OfflineMapTarget, onShowRegion: (BoundingBox) -> Unit) { - val manager = rememberOfflineManager() + // 0.16.0 moved the manager onto the runtime; `rememberOfflineManager()` is gone. The default runtime is + // the one every map here uses, so its packs are the ones the user sees on the map. + val manager = DefaultMapRuntime.instance.offlineManager val scope = rememberCoroutineScope() - val packs = manager.packs + val packs by manager.packs.collectAsState() + // A pack definition now carries the pixel ratio it was downloaded at, so the tiles match this display. + val pixelRatio = LocalDensity.current.density val range = target.zoomRange() val estimate = target.bounds()?.tileCount(range.first, range.last) ?: 0L - val storedTiles = - packs.sumOf { pack -> (pack.downloadProgress as? DownloadProgress.Healthy)?.completedTileCount ?: 0L } - // Resource bytes, not tile bytes: a pack also holds the style, its glyphs and its sprites, and all of it occupies - // the same disk the user is being asked about. - val storedBytes = - packs.sumOf { pack -> (pack.downloadProgress as? DownloadProgress.Healthy)?.completedResourceBytes ?: 0L } + val usage = rememberCacheUsage(packs) Column(modifier = Modifier.fillMaxWidth().padding(horizontal = 16.dp, vertical = 8.dp)) { Text(text = stringResource(Res.string.map_cache_manager), style = MaterialTheme.typography.titleMedium) - CacheUsageLine(storedBytes = storedBytes, storedTiles = storedTiles) + CacheUsageLine(storedBytes = usage.bytes, storedTiles = usage.tiles) DownloadEstimateLines(estimate = estimate, range = range) Button( - onClick = { scope.launch { manager.downloadVisibleArea(target) } }, + onClick = { scope.launch { manager.downloadVisibleArea(target, pixelRatio) } }, enabled = target.styleUrl != null && estimate > 0L, modifier = Modifier.padding(vertical = 8.dp), ) { @@ -135,27 +135,6 @@ internal fun OfflineMapsSection(target: OfflineMapTarget, onShowRegion: (Boundin } } -/** - * How much disk the downloaded packs occupy. - * - * The OSMdroid map reported this in MB, which is the number that answers "is this filling my phone". No capacity beside - * it: OSMdroid had one bounded SQLite cache, whereas MapLibre has explicitly downloaded packs the user deletes by hand - * plus a separate ambient cache, and quoting a ceiling that governs neither would be a lie. - */ -@Composable -private fun CacheUsageLine(storedBytes: Long, storedTiles: Long) { - Text( - text = - stringResource(Res.string.map_cache_size) + - ": " + - stringResource(Res.string.map_cache_megabytes, storedBytes.megabytes()) + - " · " + - stringResource(Res.string.map_cache_tiles, storedTiles.toInt()), - style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.onSurfaceVariant, - ) -} - /** * What the next download will cost, before committing to it. * @@ -186,7 +165,7 @@ private fun OfflinePackRow( onToggle: () -> Unit, onDelete: () -> Unit, ) { - val progress = pack.downloadProgress + val progress = pack.downloadProgress.collectAsState().value val bounds = (pack.definition as? OfflinePackDefinition.TilePyramid)?.bounds Row( @@ -244,7 +223,7 @@ private fun OfflinePackRow( * Creates the pack only. A created pack is paused, and stays that way until the user presses play on its row — * [OfflineManager.resume] is deliberately not called here, so a download never starts itself. */ -private suspend fun OfflineManager.downloadVisibleArea(target: OfflineMapTarget): Boolean { +private suspend fun OfflineManager.downloadVisibleArea(target: OfflineMapTarget, pixelRatio: Float): Boolean { val styleUrl = target.styleUrl val bounds = target.bounds() if (styleUrl == null || bounds == null) return false @@ -256,6 +235,7 @@ private suspend fun OfflineManager.downloadVisibleArea(target: OfflineMapTarget) OfflinePackDefinition.TilePyramid( styleUrl = styleUrl, bounds = bounds, + pixelRatio = pixelRatio, minZoom = range.first, maxZoom = range.last, ), diff --git a/feature/map-maplibre/src/commonMain/kotlin/org/meshtastic/feature/map/maplibre/component/SecondaryMapControls.kt b/feature/map-maplibre/src/commonMain/kotlin/org/meshtastic/feature/map/maplibre/component/SecondaryMapControls.kt index c50020e174..c92ec965a5 100644 --- a/feature/map-maplibre/src/commonMain/kotlin/org/meshtastic/feature/map/maplibre/component/SecondaryMapControls.kt +++ b/feature/map-maplibre/src/commonMain/kotlin/org/meshtastic/feature/map/maplibre/component/SecondaryMapControls.kt @@ -24,7 +24,8 @@ import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import kotlinx.coroutines.launch -import org.maplibre.compose.camera.CameraState +import org.maplibre.compose.camera.CameraAnimation +import org.maplibre.compose.map.MapState import org.meshtastic.feature.map.component.MapControlsOverlay /** @@ -39,7 +40,7 @@ import org.meshtastic.feature.map.component.MapControlsOverlay */ @Composable internal fun SecondaryMapControls( - cameraState: CameraState, + mapState: MapState, basemaps: BasemapSelection, modifier: Modifier = Modifier, filterMenu: (@Composable (expanded: Boolean, onDismissRequest: () -> Unit) -> Unit)? = null, @@ -51,8 +52,15 @@ internal fun SecondaryMapControls( modifier = modifier, onToggleFilterMenu = filterMenu?.let { { filterMenuExpanded = !filterMenuExpanded } }, filterDropdownContent = { filterMenu?.invoke(filterMenuExpanded) { filterMenuExpanded = false } }, - bearing = cameraState.position.bearing.toFloat(), - onCompassClick = { scope.launch { cameraState.animateTo(cameraState.position.copy(bearing = 0.0)) } }, + bearing = mapState.cameraPosition.bearing.toFloat(), + onCompassClick = { + scope.launch { + mapState.animateCameraPosition( + mapState.cameraPosition.copy(bearing = 0.0), + animation = CameraAnimation.Ease(), + ) + } + }, mapTypeContent = { BasemapButton(selection = basemaps) }, onToggleLocationTracking = null, ) diff --git a/feature/map-maplibre/src/commonMain/kotlin/org/meshtastic/feature/map/maplibre/component/WaypointDialogs.kt b/feature/map-maplibre/src/commonMain/kotlin/org/meshtastic/feature/map/maplibre/component/WaypointDialogs.kt index 4cd65ebdb0..8507323fee 100644 --- a/feature/map-maplibre/src/commonMain/kotlin/org/meshtastic/feature/map/maplibre/component/WaypointDialogs.kt +++ b/feature/map-maplibre/src/commonMain/kotlin/org/meshtastic/feature/map/maplibre/component/WaypointDialogs.kt @@ -98,7 +98,7 @@ internal fun WaypointDialogs( canDeleteForEveryone = waypoint.removableForEveryone(viewModel.myNodeNum, isConnected), onDeleteForMe = { viewModel.deleteWaypoint(waypoint.id) }, onDeleteForEveryone = { - viewModel.sendWaypoint(waypoint.copy(expire = 1)) + viewModel.sendWaypoint(waypoint.newBuilder().also { wb -> wb.expire = 1 }.build()) viewModel.deleteWaypoint(waypoint.id) }, onDone = { deletingId = null }, @@ -203,10 +203,12 @@ internal fun rememberWaypointEditing(): WaypointEditing { onLongPress = { position -> if (isConnected && box.draft == null) { pending = - Waypoint( - latitude_i = (position.latitude / DEG_SCALE).toInt(), - longitude_i = (position.longitude / DEG_SCALE).toInt(), - ) + Waypoint.Builder() + .also { wb -> + wb.latitude_i = (position.latitude / DEG_SCALE).toInt() + wb.longitude_i = (position.longitude / DEG_SCALE).toInt() + } + .build() } }, pending = pending, @@ -220,7 +222,7 @@ internal fun rememberWaypointEditing(): WaypointEditing { // left the waypoint on everyone else's map — while deleting the same waypoint from its info dialog, two // taps away, removed it properly. The Google flavor broadcasts from both. if (toDelete.removableForEveryone(viewModel.myNodeNum, isConnected)) { - viewModel.sendWaypoint(toDelete.copy(expire = 1)) + viewModel.sendWaypoint(toDelete.newBuilder().also { wb -> wb.expire = 1 }.build()) } viewModel.deleteWaypoint(toDelete.id) pending = null @@ -262,7 +264,7 @@ private fun rememberBoxAuthoring(onApplyBox: (Waypoint) -> Unit, onReopenEditor: var firstCorner by remember { mutableStateOf(null) } fun apply(corners: ProtoBoundingBox) { - draft?.let { onApplyBox(it.copy(bounding_box = corners)) } + draft?.let { onApplyBox(it.newBuilder().also { wb -> wb.bounding_box = corners }.build()) } draft = null firstCorner = null } @@ -311,18 +313,24 @@ private fun Position.isDistinctFrom(other: Position): Boolean = abs(latitude - other.latitude) >= MIN_CORNER_DELTA_DEG && abs(longitude - other.longitude) >= MIN_CORNER_DELTA_DEG /** A proto bounding box (degrees x 1e7) from two opposite corners, in either order. */ -internal fun boundingBoxFromCorners(a: Position, b: Position): ProtoBoundingBox = ProtoBoundingBox( - longitude_west_i = (minOf(a.longitude, b.longitude) / DEG_SCALE).toInt(), - latitude_south_i = (minOf(a.latitude, b.latitude) / DEG_SCALE).toInt(), - longitude_east_i = (maxOf(a.longitude, b.longitude) / DEG_SCALE).toInt(), - latitude_north_i = (maxOf(a.latitude, b.latitude) / DEG_SCALE).toInt(), -) +internal fun boundingBoxFromCorners(a: Position, b: Position): ProtoBoundingBox = ProtoBoundingBox.Builder() + .also { wb -> + wb.longitude_west_i = (minOf(a.longitude, b.longitude) / DEG_SCALE).toInt() + wb.latitude_south_i = (minOf(a.latitude, b.latitude) / DEG_SCALE).toInt() + wb.longitude_east_i = (maxOf(a.longitude, b.longitude) / DEG_SCALE).toInt() + wb.latitude_north_i = (maxOf(a.latitude, b.latitude) / DEG_SCALE).toInt() + } + .build() private const val MIN_CORNER_DELTA_DEG = 1e-4 /** A new waypoint arrives with id 0 and no icon; it needs both before it goes on air. */ -private fun Waypoint.readyToSend(nextPacketId: () -> Int): Waypoint = - copy(id = if (id == 0) nextPacketId() else id, icon = icon.waypointIconOrDefault()) +private fun Waypoint.readyToSend(nextPacketId: () -> Int): Waypoint = this.newBuilder() + .also { wb -> + wb.id = if (id == 0) nextPacketId() else id + wb.icon = icon.waypointIconOrDefault() + } + .build() /** Waypoint coordinates travel as degrees scaled by 1e7. */ private const val DEG_SCALE = 1e-7 diff --git a/feature/map-maplibre/src/commonMain/kotlin/org/meshtastic/feature/map/maplibre/component/ZoomControls.kt b/feature/map-maplibre/src/commonMain/kotlin/org/meshtastic/feature/map/maplibre/component/ZoomControls.kt index 1108082e58..6e86c9f088 100644 --- a/feature/map-maplibre/src/commonMain/kotlin/org/meshtastic/feature/map/maplibre/component/ZoomControls.kt +++ b/feature/map-maplibre/src/commonMain/kotlin/org/meshtastic/feature/map/maplibre/component/ZoomControls.kt @@ -24,7 +24,7 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import kotlinx.coroutines.launch -import org.maplibre.compose.camera.CameraState +import org.maplibre.compose.map.MapState import org.meshtastic.feature.map.component.MapZoomControls import org.meshtastic.feature.map.maplibre.ZOOM_STEP import org.meshtastic.feature.map.maplibre.style.Basemap @@ -51,13 +51,13 @@ private const val ZOOM_BOTTOM_INSET = 56 * showing — see [MeshMapOrnaments]. */ @Composable -internal fun BoxScope.MapZoom(cameraState: CameraState, basemap: Basemap) { +internal fun BoxScope.MapZoom(mapState: MapState, basemap: Basemap) { val scope = rememberCoroutineScope() val zoomRange = basemap.zoomRange() MapZoomControls( - onZoomIn = { scope.launch { cameraState.zoomBy(ZOOM_STEP, zoomRange) } }, - onZoomOut = { scope.launch { cameraState.zoomBy(-ZOOM_STEP, zoomRange) } }, + onZoomIn = { scope.launch { mapState.zoomBy(ZOOM_STEP, zoomRange) } }, + onZoomOut = { scope.launch { mapState.zoomBy(-ZOOM_STEP, zoomRange) } }, modifier = Modifier.align(Alignment.BottomEnd).padding(end = ZOOM_INSET.dp, bottom = ZOOM_BOTTOM_INSET.dp), ) } diff --git a/feature/map-maplibre/src/commonMain/kotlin/org/meshtastic/feature/map/maplibre/layers/BasemapLayers.kt b/feature/map-maplibre/src/commonMain/kotlin/org/meshtastic/feature/map/maplibre/layers/BasemapLayers.kt index 314499bbad..92f7e0119b 100644 --- a/feature/map-maplibre/src/commonMain/kotlin/org/meshtastic/feature/map/maplibre/layers/BasemapLayers.kt +++ b/feature/map-maplibre/src/commonMain/kotlin/org/meshtastic/feature/map/maplibre/layers/BasemapLayers.kt @@ -23,8 +23,8 @@ import org.maplibre.compose.layers.HillshadeLayer import org.maplibre.compose.layers.RasterLayer import org.maplibre.compose.sources.RasterDemEncoding import org.maplibre.compose.sources.TileSetOptions -import org.maplibre.compose.sources.rememberRasterDemSource -import org.maplibre.compose.sources.rememberRasterSource +import org.maplibre.compose.sources.rememberRasterDemTileSource +import org.maplibre.compose.sources.rememberRasterTileSource import org.meshtastic.feature.map.layers.opacityOf import org.meshtastic.feature.map.maplibre.style.Basemap import org.meshtastic.feature.map.maplibre.style.MapOverlay @@ -42,7 +42,7 @@ internal fun RasterTileSpec.toTileSetOptions(): TileSetOptions = */ @Composable internal fun RasterBasemapLayer(basemap: Basemap.Raster) { - val source = rememberRasterSource(tiles = basemap.spec.tiles, options = basemap.spec.toTileSetOptions()) + val source = rememberRasterTileSource(tiles = basemap.spec.tiles, options = basemap.spec.toTileSetOptions()) RasterLayer(id = "basemap-${basemap.id}", source = source) } @@ -60,7 +60,8 @@ internal fun MapOverlayLayers(overlays: List, opacity: Map HillshadeOverlayLayer(overlay, layerOpacity) is MapOverlay.Raster -> { - val source = rememberRasterSource(tiles = overlay.spec.tiles, options = overlay.spec.toTileSetOptions()) + val source = + rememberRasterTileSource(tiles = overlay.spec.tiles, options = overlay.spec.toTileSetOptions()) RasterLayer(id = "overlay-${overlay.id}", source = source, opacity = const(layerOpacity)) } } @@ -77,7 +78,7 @@ internal fun MapOverlayLayers(overlays: List, opacity: Map GroundOverlayLayer(layer.id, index, overlay, opacity) } - val fill = coalesce(feature["fill"].asString(), feature["color"].asString()) - val stroke = coalesce(feature["stroke"].asString(), feature["color"].asString()) + // The untyped property goes into coalesce and the conversion happens on the result: since 0.16.0 an + // assertion *inside* coalesce aborts on a null input rather than falling through to the next value, so + // asserting first would make a feature with no "fill" skip "color" too. + val fill = coalesce(feature["fill"], feature["color"]) + val stroke = coalesce(feature["stroke"], feature["color"]) val strokeColor = stroke.convertToColor(const(CustomLayerBlue)) - val strokeWidth = coalesce(feature["stroke-width"].asNumber(), const(DEFAULT_STROKE_WIDTH)).dp + // One key, so the assertion's own fallback does the job coalesce used to. + val strokeWidth = feature["stroke-width"].asNumber(const(DEFAULT_STROKE_WIDTH)).dp // One source, three layers, each filtered to the geometry it can actually draw. Unfiltered, the fill layer // painted a LineString's vertices as a solid wedge and the circle layer put a dot on every polygon corner — @@ -109,7 +113,7 @@ private fun ImportedLayer(layer: CustomLayer, opacity: Float) { color = fill.convertToColor(const(CustomLayerBlue)), // The layer's own opacity scales whatever the feature asked for, rather than replacing it: an import that // styles some features translucent stays relatively translucent as the whole layer fades. - opacity = coalesce(feature["fill-opacity"].asNumber(), const(DEFAULT_FILL_OPACITY)) * const(opacity), + opacity = feature["fill-opacity"].asNumber(const(DEFAULT_FILL_OPACITY)) * const(opacity), ) LineLayer( id = "custom-${layer.id}-line", @@ -124,7 +128,7 @@ private fun ImportedLayer(layer: CustomLayer, opacity: Float) { GeometryType.MultiPolygon, ), color = strokeColor, - opacity = coalesce(feature["stroke-opacity"].asNumber(), const(1f)) * const(opacity), + opacity = feature["stroke-opacity"].asNumber(const(1f)) * const(opacity), width = strokeWidth, ) CircleLayer( @@ -150,7 +154,7 @@ private fun ImportedLayer(layer: CustomLayer, opacity: Float) { filter = iconIsOneOf(icons.keys), iconImage = switch( - input = coalesce(feature[ICON_URL_PROPERTY].asString(), const("")), + input = feature[ICON_URL_PROPERTY].asString(const("")), *icons.map { (url, painter) -> case(url, image(painter, ICON_SIZE)) }.toTypedArray(), // Unreachable given the filter, and required: `switch` has no way to say "draw nothing". fallback = image(icons.values.first(), ICON_SIZE), @@ -178,11 +182,8 @@ private fun rememberLayerIcons(urls: Set): Map { val loaded = mutableMapOf() ordered.forEach { url -> key(url) { - // See `decodeForSoftwareCanvas` — without it the app dies the moment an icon finishes loading. val painter = - rememberAsyncImagePainter( - ImageRequest.Builder(LocalPlatformContext.current).data(url).decodeForSoftwareCanvas().build(), - ) + rememberAsyncImagePainter(ImageRequest.Builder(LocalPlatformContext.current).data(url).build()) val state by painter.state.collectAsState() // The loaded painter, not the async wrapper around it: MapLibre rasterizes a painter outside the // composition driving it, where an AsyncImagePainter draws nothing. diff --git a/feature/map-maplibre/src/commonMain/kotlin/org/meshtastic/feature/map/maplibre/layers/NodeChipLayer.kt b/feature/map-maplibre/src/commonMain/kotlin/org/meshtastic/feature/map/maplibre/layers/NodeChipLayer.kt index 672a6a92eb..5c60df41a4 100644 --- a/feature/map-maplibre/src/commonMain/kotlin/org/meshtastic/feature/map/maplibre/layers/NodeChipLayer.kt +++ b/feature/map-maplibre/src/commonMain/kotlin/org/meshtastic/feature/map/maplibre/layers/NodeChipLayer.kt @@ -52,14 +52,13 @@ import org.maplibre.compose.expressions.dsl.condition import org.maplibre.compose.expressions.dsl.const import org.maplibre.compose.expressions.dsl.feature import org.maplibre.compose.expressions.dsl.image -import org.maplibre.compose.expressions.dsl.nil import org.maplibre.compose.expressions.dsl.switch import org.maplibre.compose.expressions.value.BooleanValue import org.maplibre.compose.expressions.value.FloatValue +import org.maplibre.compose.interaction.ClickResult +import org.maplibre.compose.layers.FeaturesClickHandler import org.maplibre.compose.layers.SymbolLayer -import org.maplibre.compose.sources.Source -import org.maplibre.compose.util.ClickResult -import org.maplibre.compose.util.FeaturesClickHandler +import org.maplibre.compose.sources.VectorSource import org.meshtastic.core.model.Node import org.meshtastic.core.ui.icon.MeshtasticIcons import org.meshtastic.core.ui.icon.Person @@ -95,10 +94,10 @@ import org.meshtastic.feature.map.maplibre.geojson.toNodeChip @Composable internal fun NodeChipLayer( id: String, - source: Source, + source: VectorSource, nodes: List, onNodeClick: ((Int) -> Unit)? = null, - chipFilter: Expression = nil(), + chipFilter: Expression? = null, ) = MapChipLayer( id = id, source = source, @@ -156,9 +155,9 @@ private fun chipSortKey(): Expression = switch( @Composable internal fun MapChipLayer( id: String, - source: Source, + source: VectorSource, chips: List, - filter: Expression = nil(), + filter: Expression? = null, onClick: FeaturesClickHandler? = null, ) { val distinct = remember(chips) { chips.distinct().take(MAX_CHIP_IMAGES) } diff --git a/feature/map-maplibre/src/commonMain/kotlin/org/meshtastic/feature/map/maplibre/layers/NodeLayers.kt b/feature/map-maplibre/src/commonMain/kotlin/org/meshtastic/feature/map/maplibre/layers/NodeLayers.kt index 228493bb6d..19cac37feb 100644 --- a/feature/map-maplibre/src/commonMain/kotlin/org/meshtastic/feature/map/maplibre/layers/NodeLayers.kt +++ b/feature/map-maplibre/src/commonMain/kotlin/org/meshtastic/feature/map/maplibre/layers/NodeLayers.kt @@ -31,11 +31,12 @@ import org.maplibre.compose.expressions.dsl.convertToColor import org.maplibre.compose.expressions.dsl.feature import org.maplibre.compose.expressions.dsl.not import org.maplibre.compose.expressions.dsl.step +import org.maplibre.compose.interaction.ClickResult import org.maplibre.compose.layers.CircleLayer import org.maplibre.compose.layers.FillLayer import org.maplibre.compose.layers.SymbolLayer +import org.maplibre.compose.map.LocalMapState import org.maplibre.compose.sources.GeoJsonOptions -import org.maplibre.compose.util.ClickResult import org.maplibre.spatialk.geojson.BoundingBox import org.maplibre.spatialk.geojson.Point import org.maplibre.spatialk.geojson.Position @@ -87,7 +88,7 @@ private const val CLUSTER_MIN_POINTS = 10 */ @Composable @Suppress("LongMethod") -internal fun NodeLayers( +fun NodeLayers( nodes: List, myNodeNum: Int?, showPrecisionCircles: Boolean, @@ -135,6 +136,10 @@ internal fun NodeLayers( // no longer block the caller), and onClick is not a suspending callback. val clusterScope = rememberCoroutineScope() + // 0.16.0 moved the cluster queries off the source and onto its style handle. Read the state here, in + // composition, and resolve the handle at click time — see the comment at the query itself. + val mapState = checkNotNull(LocalMapState.current) + val nodeSource = rememberFeatureSource( nodes, @@ -173,12 +178,16 @@ internal fun NodeLayers( val centre = (clusterFeature.geometry as? Point)?.coordinates if (centre != null) { clusterScope.launch { + // A handle belongs to the style generation it was read from, so it is resolved per + // click rather than remembered: switching basemap replaces the style and any handle + // held across that switch is stale. Null while no style is loaded. + val handle = mapState.style.sources[nodeSource] ?: return@launch // A cluster that can still be broken apart is worth zooming into. One that cannot is // nodes sitting on the same spot, so zooming would do nothing — list them instead. - val expansionZoom = nodeSource.getClusterExpansionZoom(clusterFeature) + val expansionZoom = handle.getClusterExpansionZoom(clusterFeature) if (expansionZoom <= NO_EXPANSION_ZOOM) { onClusterMembers( - nodeSource + handle .getClusterLeaves(clusterFeature, CLUSTER_LEAF_LIMIT, CLUSTER_LEAF_OFFSET) .toClusterMembers(), ) diff --git a/feature/map-maplibre/src/commonMain/kotlin/org/meshtastic/feature/map/maplibre/layers/NodePulseLayer.kt b/feature/map-maplibre/src/commonMain/kotlin/org/meshtastic/feature/map/maplibre/layers/NodePulseLayer.kt index 83b83271e9..23a0ffd48f 100644 --- a/feature/map-maplibre/src/commonMain/kotlin/org/meshtastic/feature/map/maplibre/layers/NodePulseLayer.kt +++ b/feature/map-maplibre/src/commonMain/kotlin/org/meshtastic/feature/map/maplibre/layers/NodePulseLayer.kt @@ -32,7 +32,7 @@ import org.maplibre.compose.expressions.dsl.feature import org.maplibre.compose.expressions.dsl.gt import org.maplibre.compose.expressions.dsl.not import org.maplibre.compose.layers.CircleLayer -import org.maplibre.compose.sources.Source +import org.maplibre.compose.sources.VectorSource import org.meshtastic.core.common.util.nowSeconds import org.meshtastic.core.model.Node import org.meshtastic.feature.map.RECENTLY_HEARD_SECONDS @@ -56,7 +56,7 @@ import org.meshtastic.feature.map.maplibre.style.MapColors * between packets is invisible. */ @Composable -internal fun NodePulseLayer(nodes: List, source: Source) { +internal fun NodePulseLayer(nodes: List, source: VectorSource) { val heardJustNow = remember(nodes) { nodes.heardJustNow(nowSeconds) } // Frozen with the membership so the filter cannot drift out from under a pulse already running. val cutoff = remember(heardJustNow) { (nowSeconds - RECENTLY_HEARD_SECONDS).toInt() } diff --git a/feature/map-maplibre/src/commonMain/kotlin/org/meshtastic/feature/map/maplibre/layers/TerrainLayers.kt b/feature/map-maplibre/src/commonMain/kotlin/org/meshtastic/feature/map/maplibre/layers/TerrainLayers.kt index bae08b6e97..14b2acd0a7 100644 --- a/feature/map-maplibre/src/commonMain/kotlin/org/meshtastic/feature/map/maplibre/layers/TerrainLayers.kt +++ b/feature/map-maplibre/src/commonMain/kotlin/org/meshtastic/feature/map/maplibre/layers/TerrainLayers.kt @@ -37,7 +37,7 @@ import org.maplibre.compose.sources.GeoJsonData import org.maplibre.compose.sources.RasterDemEncoding import org.maplibre.compose.sources.TileSetOptions import org.maplibre.compose.sources.rememberGeoJsonSource -import org.maplibre.compose.sources.rememberRasterDemSource +import org.maplibre.compose.sources.rememberRasterDemTileSource import org.maplibre.spatialk.geojson.BoundingBox import org.maplibre.spatialk.geojson.FeatureCollection import org.maplibre.spatialk.geojson.LineString @@ -104,9 +104,9 @@ private const val MAPTERHORN_ATTRIBUTION_HTML = /** * The two hillshade tiers, as two [HillshadeLayer]s over two `file://` raster-dem sources (see - * [org.maplibre.compose.sources.rememberRasterDemSource]) — see this feature's PR description for why MapLibre's own - * internal Horn's-method shading means no decoded elevation grid or [org.meshtastic.feature.map.terrain.Hillshade] call - * is needed here at all, unlike the contours below. + * [org.maplibre.compose.sources.rememberRasterDemTileSource]) — see this feature's PR description for why MapLibre's + * own internal Horn's-method shading means no decoded elevation grid or [org.meshtastic.feature.map.terrain.Hillshade] + * call is needed here at all, unlike the contours below. * * A style layer's own `minZoom`/`maxZoom` (exclusive on the max, per the style spec) is how the two tiers hand off: the * global layer stops at [MapterhornEndpoints.REGIONAL_MIN_ZOOM] once regional detail exists, and the regional layer @@ -118,7 +118,7 @@ private const val MAPTERHORN_ATTRIBUTION_HTML = private fun HillshadeTiers(repository: OfflineTerrainRepository, region: OfflineTerrainRegion) { val globalMaxZoom = minOf(region.maxZoom, MapterhornEndpoints.GLOBAL_MAX_ZOOM) val globalSource = - rememberRasterDemSource( + rememberRasterDemTileSource( tiles = listOf(repository.tileUrlTemplate(TerrainSource.GLOBAL)), options = TileSetOptions(minZoom = 0, maxZoom = globalMaxZoom, attributionHtml = MAPTERHORN_ATTRIBUTION_HTML), @@ -137,7 +137,7 @@ private fun HillshadeTiers(repository: OfflineTerrainRepository, region: Offline val regionalMaxZoom = minOf(region.maxZoom, MapterhornEndpoints.REGIONAL_MAX_ZOOM) val regionalSource = - rememberRasterDemSource( + rememberRasterDemTileSource( tiles = listOf(repository.tileUrlTemplate(TerrainSource.REGIONAL)), options = TileSetOptions( diff --git a/feature/map-maplibre/src/commonMain/kotlin/org/meshtastic/feature/map/maplibre/layers/WaypointLayers.kt b/feature/map-maplibre/src/commonMain/kotlin/org/meshtastic/feature/map/maplibre/layers/WaypointLayers.kt index 87c0beee5d..baf4332983 100644 --- a/feature/map-maplibre/src/commonMain/kotlin/org/meshtastic/feature/map/maplibre/layers/WaypointLayers.kt +++ b/feature/map-maplibre/src/commonMain/kotlin/org/meshtastic/feature/map/maplibre/layers/WaypointLayers.kt @@ -37,10 +37,11 @@ import org.maplibre.compose.expressions.dsl.const import org.maplibre.compose.expressions.dsl.eq import org.maplibre.compose.expressions.dsl.feature import org.maplibre.compose.expressions.dsl.image +import org.maplibre.compose.expressions.dsl.textOffset +import org.maplibre.compose.interaction.ClickResult import org.maplibre.compose.layers.FillLayer import org.maplibre.compose.layers.LineLayer import org.maplibre.compose.layers.SymbolLayer -import org.maplibre.compose.util.ClickResult import org.meshtastic.core.model.DataPacket import org.meshtastic.feature.map.maplibre.geojson.WaypointFeatureKeys import org.meshtastic.feature.map.maplibre.geojson.geofencesToFeatureCollection @@ -100,7 +101,7 @@ internal fun WaypointLayers(waypoints: Collection, onWaypointClick: textColor = const(Color.White), textHaloColor = const(Color.Black), textHaloWidth = const(1.dp), - textOffset = org.maplibre.compose.expressions.dsl.offset(0f.em, WAYPOINT_LABEL_OFFSET_EM.em), + textOffset = textOffset(0f.em, WAYPOINT_LABEL_OFFSET_EM.em), ) } diff --git a/feature/map-maplibre/src/commonMain/kotlin/org/meshtastic/feature/map/maplibre/terrain/OfflineTerrainRepository.kt b/feature/map-maplibre/src/commonMain/kotlin/org/meshtastic/feature/map/maplibre/terrain/OfflineTerrainRepository.kt index 56fa3c2ad9..cdd2bcffba 100644 --- a/feature/map-maplibre/src/commonMain/kotlin/org/meshtastic/feature/map/maplibre/terrain/OfflineTerrainRepository.kt +++ b/feature/map-maplibre/src/commonMain/kotlin/org/meshtastic/feature/map/maplibre/terrain/OfflineTerrainRepository.kt @@ -177,8 +177,9 @@ class OfflineTerrainRepository(private val fileSystem: FileSystem, private val b } /** - * The `file://` URL template [rememberRasterDemSource][org.maplibre.compose.sources.rememberRasterDemSource] - * (hillshade) can use to reach [source]'s downloaded tiles directly, matching [TerrainTileStore]'s own + * The `file://` URL template + * [rememberRasterDemTileSource][org.maplibre.compose.sources.rememberRasterDemTileSource] (hillshade) can use to + * reach [source]'s downloaded tiles directly, matching [TerrainTileStore]'s own * `////.webp` layout. * * `baseDir` is always an absolute path (see [terrainStorageDirectory]'s platform actuals), so this yields a @@ -188,7 +189,7 @@ class OfflineTerrainRepository(private val fileSystem: FileSystem, private val b * The directory portion is percent-encoded before interpolation — a Desktop/JVM user-data path can legitimately * contain characters reserved in a URL (a space in a Windows/macOS username is the common case, `#`/`%` less so but * just as real) — while the trailing `{z}/{x}/{y}` placeholders are appended afterwards, literally, so - * [rememberRasterDemSource]'s own substitution still sees them unescaped. + * [rememberRasterDemTileSource]'s own substitution still sees them unescaped. */ fun tileUrlTemplate(source: TerrainSource): String { val encodedDir = (baseDir / TILES_DIR_NAME / source.dirName).toString().percentEncodeUrlReserved() diff --git a/feature/map-maplibre/src/commonTest/kotlin/org/meshtastic/feature/map/maplibre/MapCameraTest.kt b/feature/map-maplibre/src/commonTest/kotlin/org/meshtastic/feature/map/maplibre/MapCameraTest.kt new file mode 100644 index 0000000000..a17f075700 --- /dev/null +++ b/feature/map-maplibre/src/commonTest/kotlin/org/meshtastic/feature/map/maplibre/MapCameraTest.kt @@ -0,0 +1,42 @@ +/* + * 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 . + */ +package org.meshtastic.feature.map.maplibre + +import org.maplibre.compose.camera.CameraPosition +import org.maplibre.spatialk.geojson.Position +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertSame + +class MapCameraTest { + private val fit = CameraPosition(target = Position(longitude = -122.0, latitude = 45.0), zoom = FRAME_MAX_ZOOM) + + @Test + fun `a fit tighter than the framing ceiling is zoomed out to it`() { + val tight = fit.copy(zoom = FRAME_MAX_ZOOM + 4) + val capped = tight.cappedTo(FRAME_MAX_ZOOM) + assertEquals(FRAME_MAX_ZOOM, capped.zoom) + assertEquals(tight.target, capped.target) + } + + @Test + fun `a fit at or wider than the ceiling is left as it is`() { + val wide = fit.copy(zoom = FRAME_MAX_ZOOM - 4) + assertSame(wide, wide.cappedTo(FRAME_MAX_ZOOM)) + assertSame(fit, fit.cappedTo(FRAME_MAX_ZOOM)) + } +} diff --git a/feature/map-maplibre/src/commonTest/kotlin/org/meshtastic/feature/map/maplibre/MapLibreMapTest.kt b/feature/map-maplibre/src/commonTest/kotlin/org/meshtastic/feature/map/maplibre/MapLibreMapTest.kt index f8594a8d5d..b0384aea27 100644 --- a/feature/map-maplibre/src/commonTest/kotlin/org/meshtastic/feature/map/maplibre/MapLibreMapTest.kt +++ b/feature/map-maplibre/src/commonTest/kotlin/org/meshtastic/feature/map/maplibre/MapLibreMapTest.kt @@ -46,11 +46,13 @@ private fun node( ) = Node( num = num, position = - Position( - latitude_i = (latitude * 1e7).toInt(), - longitude_i = (longitude * 1e7).toInt(), - precision_bits = precisionBits, - ), + Position.Builder() + .also { wb -> + wb.latitude_i = (latitude * 1e7).toInt() + wb.longitude_i = (longitude * 1e7).toInt() + wb.precision_bits = precisionBits + } + .build(), lastHeard = lastHeard, isFavorite = isFavorite, ) diff --git a/feature/map-maplibre/src/commonTest/kotlin/org/meshtastic/feature/map/maplibre/NodeChipTest.kt b/feature/map-maplibre/src/commonTest/kotlin/org/meshtastic/feature/map/maplibre/NodeChipTest.kt index 85e5a9306a..b34c952bbe 100644 --- a/feature/map-maplibre/src/commonTest/kotlin/org/meshtastic/feature/map/maplibre/NodeChipTest.kt +++ b/feature/map-maplibre/src/commonTest/kotlin/org/meshtastic/feature/map/maplibre/NodeChipTest.kt @@ -38,7 +38,7 @@ import kotlin.test.assertTrue class NodeChipKeyTest { private fun chipNode(num: Int, shortName: String, isIgnored: Boolean = false) = - Node(num = num, user = User(short_name = shortName), isIgnored = isIgnored) + Node(num = num, user = User.Builder().also { wb -> wb.short_name = shortName }.build(), isIgnored = isIgnored) @Test fun `two nodes with the same name and different colours get different keys`() { @@ -177,8 +177,12 @@ class NodesInViewTest { private fun at(latitude: Double, longitude: Double) = Node(num = (latitude * 1000).toInt(), position = protoPosition(latitude, longitude)) - private fun protoPosition(latitude: Double, longitude: Double) = - org.meshtastic.proto.Position(latitude_i = (latitude * 1e7).toInt(), longitude_i = (longitude * 1e7).toInt()) + private fun protoPosition(latitude: Double, longitude: Double) = org.meshtastic.proto.Position.Builder() + .also { wb -> + wb.latitude_i = (latitude * 1e7).toInt() + wb.longitude_i = (longitude * 1e7).toInt() + } + .build() private val box = BoundingBox(west = -108.0, south = 34.0, east = -107.0, north = 35.0) diff --git a/feature/map-maplibre/src/commonTest/kotlin/org/meshtastic/feature/map/maplibre/UnclusteredNodesTest.kt b/feature/map-maplibre/src/commonTest/kotlin/org/meshtastic/feature/map/maplibre/UnclusteredNodesTest.kt index fa31dbf7cf..5a43eba9c6 100644 --- a/feature/map-maplibre/src/commonTest/kotlin/org/meshtastic/feature/map/maplibre/UnclusteredNodesTest.kt +++ b/feature/map-maplibre/src/commonTest/kotlin/org/meshtastic/feature/map/maplibre/UnclusteredNodesTest.kt @@ -38,7 +38,13 @@ class UnclusteredNodesTest { private fun node(num: Int, latitude: Double, longitude: Double) = Node( num = num, - position = Position(latitude_i = (latitude * 1e7).toInt(), longitude_i = (longitude * 1e7).toInt()), + position = + Position.Builder() + .also { wb -> + wb.latitude_i = (latitude * 1e7).toInt() + wb.longitude_i = (longitude * 1e7).toInt() + } + .build(), ) /** [count] nodes on the same spot, which is what a crowded venue looks like. */ diff --git a/feature/map-maplibre/src/commonTest/kotlin/org/meshtastic/feature/map/maplibre/geojson/WaypointFeaturesTest.kt b/feature/map-maplibre/src/commonTest/kotlin/org/meshtastic/feature/map/maplibre/geojson/WaypointFeaturesTest.kt index 3efad56591..bf6a29ae27 100644 --- a/feature/map-maplibre/src/commonTest/kotlin/org/meshtastic/feature/map/maplibre/geojson/WaypointFeaturesTest.kt +++ b/feature/map-maplibre/src/commonTest/kotlin/org/meshtastic/feature/map/maplibre/geojson/WaypointFeaturesTest.kt @@ -31,7 +31,17 @@ class WaypointFeaturesTest { fun `a waypoint with both coordinates becomes a feature`() { val features = waypointsToFeatureCollection( - listOf(packet(Waypoint(id = 1, latitude_i = 416_000_000, longitude_i = -936_000_000))), + listOf( + packet( + Waypoint.Builder() + .also { wb -> + wb.id = 1 + wb.latitude_i = 416_000_000 + wb.longitude_i = -936_000_000 + } + .build(), + ), + ), ) assertEquals(1, features.features.size) @@ -40,8 +50,24 @@ class WaypointFeaturesTest { @Test fun `a waypoint missing one coordinate is dropped rather than pinned to the equator`() { // Substituting 0 for the absent ordinate put a confident marker at the wrong place, which is worse than none. - val latitudeOnly = packet(Waypoint(id = 2, latitude_i = 416_000_000)) - val longitudeOnly = packet(Waypoint(id = 3, longitude_i = -936_000_000)) + val latitudeOnly = + packet( + Waypoint.Builder() + .also { wb -> + wb.id = 2 + wb.latitude_i = 416_000_000 + } + .build(), + ) + val longitudeOnly = + packet( + Waypoint.Builder() + .also { wb -> + wb.id = 3 + wb.longitude_i = -936_000_000 + } + .build(), + ) assertTrue(waypointsToFeatureCollection(listOf(latitudeOnly, longitudeOnly)).features.isEmpty()) } @@ -49,7 +75,19 @@ class WaypointFeaturesTest { @Test fun `null island is still dropped`() { assertTrue( - waypointsToFeatureCollection(listOf(packet(Waypoint(id = 4, latitude_i = 0, longitude_i = 0)))) + waypointsToFeatureCollection( + listOf( + packet( + Waypoint.Builder() + .also { wb -> + wb.id = 4 + wb.latitude_i = 0 + wb.longitude_i = 0 + } + .build(), + ), + ), + ) .features .isEmpty(), ) diff --git a/feature/map-maplibre/src/jvmTest/kotlin/org/meshtastic/feature/map/maplibre/MapLibreRuntimeTest.kt b/feature/map-maplibre/src/jvmTest/kotlin/org/meshtastic/feature/map/maplibre/MapLibreRuntimeTest.kt index 6387988e79..38ee3e575f 100644 --- a/feature/map-maplibre/src/jvmTest/kotlin/org/meshtastic/feature/map/maplibre/MapLibreRuntimeTest.kt +++ b/feature/map-maplibre/src/jvmTest/kotlin/org/meshtastic/feature/map/maplibre/MapLibreRuntimeTest.kt @@ -17,25 +17,26 @@ package org.meshtastic.feature.map.maplibre import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.ui.Modifier import androidx.compose.ui.test.ExperimentalTestApi import androidx.compose.ui.test.assertIsDisplayed import androidx.compose.ui.test.onNodeWithText import androidx.compose.ui.test.v2.runComposeUiTest -import org.maplibre.compose.camera.rememberCameraState import org.meshtastic.core.resources.Res import org.meshtastic.core.resources.getString import org.meshtastic.core.resources.map_engine_unavailable -import org.meshtastic.feature.map.maplibre.component.BasemapSelection -import org.meshtastic.feature.map.maplibre.style.Basemaps import kotlin.test.Test -import kotlin.test.assertEquals import kotlin.test.assertFalse import kotlin.test.assertTrue /** * The crash in #7001 is a missing native library, and a missing library is an `UnsatisfiedLinkError` — an `Error`, not - * an `Exception`. The probe has to survive exactly that, and the map surfaces have to stop before composing MapLibre - * when it says no. + * an `Exception`. The probe has to survive exactly that, and the map screen has to stop before reaching MapLibre when + * it says no. + * + * The guard is the first statement of [MapLibreMapViewProvider.MapView], and this test leans on that: nothing else in + * this module's Koin graph is available here, so anything composed past the guard would fail with a Koin error rather + * than the notice. A guard that drifts later in the function fails this test rather than passing it quietly. */ @OptIn(ExperimentalTestApi::class) class MapLibreRuntimeTest { @@ -51,19 +52,18 @@ class MapLibreRuntimeTest { } @Test - fun `the secondary map surface composes the fallback and never its content`() = runComposeUiTest { - var contentCompositions = 0 + fun `the map screen composes the fallback and nothing else`() = runComposeUiTest { setContent { CompositionLocalProvider(LocalMapLibreRuntimeProbe provides { false }) { - SecondaryMapSurface( - basemaps = BasemapSelection(Basemaps.default, emptyList(), emptyList()) {}, - cameraState = rememberCameraState(), - ) { - contentCompositions++ - } + MapLibreMapViewProvider() + .MapView( + modifier = Modifier, + navigateToNodeDetails = {}, + waypointId = null, + sitePlannerNodeNum = null, + ) } } onNodeWithText(getString(Res.string.map_engine_unavailable)).assertIsDisplayed() - assertEquals(0, contentCompositions, "map content must not be composed without an engine to draw it") } } diff --git a/feature/map-maplibre/src/jvmTest/kotlin/org/meshtastic/feature/map/maplibre/component/WaypointInfoSlotTest.kt b/feature/map-maplibre/src/jvmTest/kotlin/org/meshtastic/feature/map/maplibre/component/WaypointInfoSlotTest.kt index bd5b7ee355..798da7a5a3 100644 --- a/feature/map-maplibre/src/jvmTest/kotlin/org/meshtastic/feature/map/maplibre/component/WaypointInfoSlotTest.kt +++ b/feature/map-maplibre/src/jvmTest/kotlin/org/meshtastic/feature/map/maplibre/component/WaypointInfoSlotTest.kt @@ -54,28 +54,72 @@ class WaypointInfoSlotTest { @Test fun `a waypoint locked to our own node is still editable`() = runComposeUiTest { - setContent { InfoSlot(waypoint = Waypoint(id = 1, locked_to = myNodeNum), isConnected = true) } + setContent { + InfoSlot( + waypoint = + Waypoint.Builder() + .also { wb -> + wb.id = 1 + wb.locked_to = myNodeNum + } + .build(), + isConnected = true, + ) + } onNodeWithText(getString(Res.string.edit)).assertIsDisplayed() } @Test fun `an unlocked waypoint is editable while connected`() = runComposeUiTest { - setContent { InfoSlot(waypoint = Waypoint(id = 1, locked_to = 0), isConnected = true) } + setContent { + InfoSlot( + waypoint = + Waypoint.Builder() + .also { wb -> + wb.id = 1 + wb.locked_to = 0 + } + .build(), + isConnected = true, + ) + } onNodeWithText(getString(Res.string.edit)).assertIsDisplayed() } @Test fun `editing is withheld while disconnected because saving re-broadcasts`() = runComposeUiTest { - setContent { InfoSlot(waypoint = Waypoint(id = 1, locked_to = 0), isConnected = false) } + setContent { + InfoSlot( + waypoint = + Waypoint.Builder() + .also { wb -> + wb.id = 1 + wb.locked_to = 0 + } + .build(), + isConnected = false, + ) + } onNodeWithText(getString(Res.string.edit)).assertDoesNotExist() } @Test fun `a waypoint locked to another node offers no editor`() = runComposeUiTest { - setContent { InfoSlot(waypoint = Waypoint(id = 1, locked_to = otherNodeNum), isConnected = true) } + setContent { + InfoSlot( + waypoint = + Waypoint.Builder() + .also { wb -> + wb.id = 1 + wb.locked_to = otherNodeNum + } + .build(), + isConnected = true, + ) + } onNodeWithText(getString(Res.string.edit)).assertDoesNotExist() } @@ -83,7 +127,18 @@ class WaypointInfoSlotTest { @Test fun `a waypoint locked to another node can still be dropped locally`() = runComposeUiTest { // Dropping our own copy is not a mesh operation, so a foreign lock does not withhold it. - setContent { InfoSlot(waypoint = Waypoint(id = 1, locked_to = otherNodeNum), isConnected = false) } + setContent { + InfoSlot( + waypoint = + Waypoint.Builder() + .also { wb -> + wb.id = 1 + wb.locked_to = otherNodeNum + } + .build(), + isConnected = false, + ) + } onNodeWithText(getString(Res.string.delete_for_me)).assertIsDisplayed() } diff --git a/feature/map/src/androidMain/kotlin/org/meshtastic/feature/map/component/WaypointInfoDialogPreviews.kt b/feature/map/src/androidMain/kotlin/org/meshtastic/feature/map/component/WaypointInfoDialogPreviews.kt index 1a07a3d67f..4110b5dffa 100644 --- a/feature/map/src/androidMain/kotlin/org/meshtastic/feature/map/component/WaypointInfoDialogPreviews.kt +++ b/feature/map/src/androidMain/kotlin/org/meshtastic/feature/map/component/WaypointInfoDialogPreviews.kt @@ -22,16 +22,22 @@ import org.meshtastic.core.common.util.MeasurementSystem import org.meshtastic.core.ui.theme.AppTheme import org.meshtastic.proto.Waypoint -private fun sampleGeofence(lockedTo: Int) = Waypoint( - id = 42, - name = "Trailhead", - description = "North gate of the reserve", - latitude_i = 377_749_000, - longitude_i = -1_224_194_000, - geofence_radius = 500, - notify_on_enter = true, - locked_to = lockedTo, -) +// Sample coordinates for the previews below. Exempt as named arguments until the +// buildersOnly rewrite turned them into assignments; the previews themselves are +// covered by MagicNumber's ignoreAnnotated. +@Suppress("MagicNumber") +private fun sampleGeofence(lockedTo: Int) = Waypoint.Builder() + .also { wb -> + wb.id = 42 + wb.name = "Trailhead" + wb.description = "North gate of the reserve" + wb.latitude_i = 377_749_000 + wb.longitude_i = -1_224_194_000 + wb.geofence_radius = 500 + wb.notify_on_enter = true + wb.locked_to = lockedTo + } + .build() /** Locked foreign geofence: opt-in off, no Edit affordance — but the local delete is still offered. */ @PreviewLightDark diff --git a/feature/map/src/commonMain/kotlin/org/meshtastic/feature/map/BaseMapViewModel.kt b/feature/map/src/commonMain/kotlin/org/meshtastic/feature/map/BaseMapViewModel.kt index 8dea23aa5c..38ae55a33f 100644 --- a/feature/map/src/commonMain/kotlin/org/meshtastic/feature/map/BaseMapViewModel.kt +++ b/feature/map/src/commonMain/kotlin/org/meshtastic/feature/map/BaseMapViewModel.kt @@ -17,7 +17,6 @@ package org.meshtastic.feature.map import androidx.lifecycle.ViewModel -import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.map @@ -35,6 +34,7 @@ import org.meshtastic.core.model.TracerouteOverlay import org.meshtastic.core.model.geofence.activeWaypointPackets import org.meshtastic.core.model.isFromLocal import org.meshtastic.core.network.repository.NetworkRepository +import org.meshtastic.core.repository.MapFilterPrefs import org.meshtastic.core.repository.MapPrefs import org.meshtastic.core.repository.NodeRepository import org.meshtastic.core.repository.NotificationPrefs @@ -129,40 +129,31 @@ open class BaseMapViewModel( /** True if the waypoint with [id] was created by this device (vs. received from another node over the mesh). */ fun isMyWaypoint(id: Int): Boolean = waypoints.value[id]?.isFromLocal(myNodeNum) == true - // Every filter reads its persisted flow directly rather than snapshotting `.value` into a mirror at - // construction. MapPrefsImpl's flows start eagerly but load from DataStore asynchronously, so a view model built + // Every filter reads the persisted flow directly rather than snapshotting `.value` into a mirror at + // construction. MapPrefsImpl's flow starts eagerly but loads from DataStore asynchronously, so a view model built // before that first read kept the defaults forever — nothing wrote the persisted values back into a mirror. - val showOnlyFavoritesOnMap: StateFlow = mapPrefs.showOnlyFavorites + private val storedFilters: StateFlow = mapPrefs.mapFilters - fun toggleOnlyFavorites() = mapPrefs.setShowOnlyFavorites(!showOnlyFavoritesOnMap.value) + fun toggleOnlyFavorites() = mapPrefs.updateMapFilters { it.copy(onlyFavorites = !it.onlyFavorites) } - val showWaypointsOnMap: StateFlow = mapPrefs.showWaypointsOnMap + fun toggleShowWaypointsOnMap() = mapPrefs.updateMapFilters { it.copy(showWaypoints = !it.showWaypoints) } - fun toggleShowWaypointsOnMap() = mapPrefs.setShowWaypointsOnMap(!showWaypointsOnMap.value) + fun toggleShowPrecisionCircleOnMap() = + mapPrefs.updateMapFilters { it.copy(showPrecisionCircle = !it.showPrecisionCircle) } - val showPrecisionCircleOnMap: StateFlow = mapPrefs.showPrecisionCircleOnMap + fun toggleOnlyOnline() = mapPrefs.updateMapFilters { it.copy(onlyOnline = !it.onlyOnline) } - fun toggleShowPrecisionCircleOnMap() = mapPrefs.setShowPrecisionCircleOnMap(!showPrecisionCircleOnMap.value) + fun toggleOnlyDirect() = mapPrefs.updateMapFilters { it.copy(onlyDirect = !it.onlyDirect) } - val onlyOnlineOnMap: StateFlow = mapPrefs.onlyOnlineOnMap + fun toggleOnlySigned() = mapPrefs.updateMapFilters { it.copy(onlySigned = !it.onlySigned) } - fun toggleOnlyOnline() = mapPrefs.setOnlyOnlineOnMap(!onlyOnlineOnMap.value) + fun toggleOnlyEncrypted() = mapPrefs.updateMapFilters { it.copy(onlyEncrypted = !it.onlyEncrypted) } - val onlyDirectOnMap: StateFlow = mapPrefs.onlyDirectOnMap + fun toggleExcludeMqtt() = mapPrefs.updateMapFilters { it.copy(excludeMqtt = !it.excludeMqtt) } - fun toggleOnlyDirect() = mapPrefs.setOnlyDirectOnMap(!onlyDirectOnMap.value) + fun toggleShowIgnored() = mapPrefs.updateMapFilters { it.copy(showIgnored = !it.showIgnored) } - val excludeMqttOnMap: StateFlow = mapPrefs.excludeMqttOnMap - - fun toggleExcludeMqtt() = mapPrefs.setExcludeMqttOnMap(!excludeMqttOnMap.value) - - val showIgnoredOnMap: StateFlow = mapPrefs.showIgnoredOnMap - - fun toggleShowIgnored() = mapPrefs.setShowIgnoredOnMap(!showIgnoredOnMap.value) - - val includeUnknownOnMap: StateFlow = mapPrefs.includeUnknownOnMap - - fun toggleIncludeUnknown() = mapPrefs.setIncludeUnknownOnMap(!includeUnknownOnMap.value) + fun toggleIncludeUnknown() = mapPrefs.updateMapFilters { it.copy(includeUnknown = !it.includeUnknown) } /** * The nodes the map draws from. @@ -171,40 +162,32 @@ open class BaseMapViewModel( * right default for the pickers that read it, but it left the map's own show-ignored filter with nothing to add * back. [MapNodePolicy] still decides; this only stops the discard happening before it is asked. * - * Declared here rather than beside [nodes] because it reads [showIgnoredOnMap], and a property initialiser cannot - * see one declared below it. + * Declared here rather than beside [nodes] because it reads [storedFilters], and a property initialiser cannot see + * one declared below it. */ val nodesWithPosition: StateFlow> = - combine(nodeRepository.getNodes(), showIgnoredOnMap) { all, showIgnored -> - all.filter { node -> node.validPosition != null && (showIgnored || !node.isIgnored) } + combine(nodeRepository.getNodes(), storedFilters) { all, filters -> + all.filter { node -> node.validPosition != null && (filters.showIgnored || !node.isIgnored) } } .stateInWhileSubscribed(initialValue = emptyList()) - val excludedMapRoles: StateFlow> = - mapPrefs.excludedMapRoles - .map(::decodeExcludedRoles) - .stateInWhileSubscribed(decodeExcludedRoles(mapPrefs.excludedMapRoles.value)) - - fun toggleRoleExcluded(role: Config.DeviceConfig.Role) { - val newValue = excludedMapRoles.value.let { if (role in it) it - role else it + role } - mapPrefs.setExcludedMapRoles(newValue.mapTo(mutableSetOf()) { it.name }) + /** + * Toggles by name rather than through [decodeExcludedRoles], which drops names the current protobufs do not define. + * Decoding and re-encoding here would discard a role excluded by a newer build on every toggle. + */ + fun toggleRoleExcluded(role: Config.DeviceConfig.Role) = mapPrefs.updateMapFilters { prefs -> + val excluded = prefs.excludedRoles + val next = if (role.name in excluded) excluded - role.name else excluded + role.name + prefs.copy(excludedRoles = next) } - fun clearExcludedRoles() = mapPrefs.setExcludedMapRoles(emptySet()) + fun clearExcludedRoles() = mapPrefs.updateMapFilters { it.copy(excludedRoles = emptySet()) } - val lastHeardFilter: StateFlow = - mapPrefs.lastHeardFilter - .map(LastHeardFilter::fromSeconds) - .stateInWhileSubscribed(LastHeardFilter.fromSeconds(mapPrefs.lastHeardFilter.value)) + fun setLastHeardFilter(filter: LastHeardFilter) = + mapPrefs.updateMapFilters { it.copy(lastHeardSeconds = filter.seconds) } - fun setLastHeardFilter(filter: LastHeardFilter) = mapPrefs.setLastHeardFilter(filter.seconds) - - val lastHeardTrackFilter: StateFlow = - mapPrefs.lastHeardTrackFilter - .map(LastHeardFilter::fromSeconds) - .stateInWhileSubscribed(LastHeardFilter.fromSeconds(mapPrefs.lastHeardTrackFilter.value)) - - fun setLastHeardTrackFilter(filter: LastHeardFilter) = mapPrefs.setLastHeardTrackFilter(filter.seconds) + fun setLastHeardTrackFilter(filter: LastHeardFilter) = + mapPrefs.updateMapFilters { it.copy(lastHeardTrackSeconds = filter.seconds) } open fun getUser(userId: String?) = nodeRepository.getUser(userId ?: org.meshtastic.core.model.NodeAddress.ID_BROADCAST) @@ -243,6 +226,10 @@ open class BaseMapViewModel( val excludedRoles: Set = emptySet(), val onlyOnline: Boolean = false, val onlyDirect: Boolean = false, + /** Show only nodes whose signed broadcasts the radio has verified (design#149). */ + val onlySigned: Boolean = false, + /** Show only nodes a public key is on file for. */ + val onlyEncrypted: Boolean = false, val excludeMqtt: Boolean = false, val showIgnored: Boolean = false, val includeUnknown: Boolean = true, @@ -254,67 +241,31 @@ open class BaseMapViewModel( excludedRoles.isNotEmpty() || onlyOnline || onlyDirect || + onlySigned || + onlyEncrypted || excludeMqtt || !includeUnknown || lastHeardFilter != LastHeardFilter.Any } - // Two intermediate combines rather than one: `combine` tops out at five flows, and there are eleven. - private val displayFilters: Flow = - combine( - showOnlyFavoritesOnMap, - showWaypointsOnMap, - showPrecisionCircleOnMap, - lastHeardFilter, - lastHeardTrackFilter, - ) { favoritesOnly, showWaypoints, showPrecisionCircle, lastHeardFilter, lastHeardTrackFilter -> - MapFilterState(favoritesOnly, showWaypoints, showPrecisionCircle, lastHeardFilter, lastHeardTrackFilter) - } - - private val nodeFilters: Flow = - combine(excludedMapRoles, onlyOnlineOnMap, onlyDirectOnMap, excludeMqttOnMap, showIgnoredOnMap, ::NodeFilters) - + // One flow, one map: the store hands every filter back as a single value, so there is nothing to recombine. val mapFilterStateFlow: StateFlow = - combine(displayFilters, nodeFilters, includeUnknownOnMap) { display, nodes, includeUnknown -> - display.with(nodes, includeUnknown) - } - .stateInWhileSubscribed( - initialValue = - MapFilterState( - showOnlyFavoritesOnMap.value, - showWaypointsOnMap.value, - showPrecisionCircleOnMap.value, - lastHeardFilter.value, - lastHeardTrackFilter.value, - ) - .with( - NodeFilters( - excludedMapRoles.value, - onlyOnlineOnMap.value, - onlyDirectOnMap.value, - excludeMqttOnMap.value, - showIgnoredOnMap.value, - ), - includeUnknownOnMap.value, - ), - ) + storedFilters.map(::toFilterState).stateInWhileSubscribed(toFilterState(storedFilters.value)) - /** The five node-level toggles, boxed so they fit one `combine`. */ - private data class NodeFilters( - val excludedRoles: Set, - val onlyOnline: Boolean, - val onlyDirect: Boolean, - val excludeMqtt: Boolean, - val showIgnored: Boolean, - ) - - private fun MapFilterState.with(nodes: NodeFilters, includeUnknown: Boolean) = copy( - excludedRoles = nodes.excludedRoles, - onlyOnline = nodes.onlyOnline, - onlyDirect = nodes.onlyDirect, - excludeMqtt = nodes.excludeMqtt, - showIgnored = nodes.showIgnored, - includeUnknown = includeUnknown, + private fun toFilterState(prefs: MapFilterPrefs) = MapFilterState( + onlyFavorites = prefs.onlyFavorites, + showWaypoints = prefs.showWaypoints, + showPrecisionCircle = prefs.showPrecisionCircle, + lastHeardFilter = LastHeardFilter.fromSeconds(prefs.lastHeardSeconds), + lastHeardTrackFilter = LastHeardFilter.fromSeconds(prefs.lastHeardTrackSeconds), + excludedRoles = decodeExcludedRoles(prefs.excludedRoles), + onlyOnline = prefs.onlyOnline, + onlyDirect = prefs.onlyDirect, + onlySigned = prefs.onlySigned, + onlyEncrypted = prefs.onlyEncrypted, + excludeMqtt = prefs.excludeMqtt, + showIgnored = prefs.showIgnored, + includeUnknown = prefs.includeUnknown, ) } diff --git a/feature/map/src/commonMain/kotlin/org/meshtastic/feature/map/MapNodePolicy.kt b/feature/map/src/commonMain/kotlin/org/meshtastic/feature/map/MapNodePolicy.kt index 4f96fdc354..59689b0527 100644 --- a/feature/map/src/commonMain/kotlin/org/meshtastic/feature/map/MapNodePolicy.kt +++ b/feature/map/src/commonMain/kotlin/org/meshtastic/feature/map/MapNodePolicy.kt @@ -79,16 +79,39 @@ object MapNodePolicy { */ private fun Node.passesFilters(filterState: BaseMapViewModel.MapFilterState, nowSeconds: Long): Boolean { val secondsSinceHeard = nowSeconds - lastHeard - val cutoff = filterState.lastHeardFilter.seconds - return (!filterState.onlyFavorites || isFavorite) && + return passesIdentityFilters(filterState) && + passesSecurityFilters(filterState) && + passesReachabilityFilters(filterState, secondsSinceHeard) + } + + /** Who the node is: favourited, ignored, its role, and whether its name has arrived yet. */ + private fun Node.passesIdentityFilters(filterState: BaseMapViewModel.MapFilterState): Boolean = + (!filterState.onlyFavorites || isFavorite) && (!isIgnored || filterState.showIgnored) && user.role !in filterState.excludedRoles && - (!filterState.onlyOnline || secondsSinceHeard <= ONLINE_WINDOW_SECONDS) && - (!filterState.onlyDirect || hopsAway == 0) && + (filterState.includeUnknown || user.short_name.isNotEmpty()) + + /** How the node is reachable: online, hop count, bearer, and how recently it was heard. */ + private fun Node.passesReachabilityFilters( + filterState: BaseMapViewModel.MapFilterState, + secondsSinceHeard: Long, + ): Boolean { + val cutoff = filterState.lastHeardFilter.seconds + return (!filterState.onlyOnline || secondsSinceHeard <= ONLINE_WINDOW_SECONDS) && + (!filterState.onlyDirect || (hopsAway == 0 && !viaMqtt)) && (!filterState.excludeMqtt || !viaMqtt) && - (filterState.includeUnknown || user.short_name.isNotEmpty()) && (cutoff == LastHeardFilter.Any.seconds || secondsSinceHeard <= cutoff) } + + /** + * The two security filters (design#149). + * + * Signing reads the raw flag rather than the version-gated indicator: the filter answers "has the radio verified + * this node's signature", which is what the badge beside it claims. Encrypted excludes a stored key that stopped + * matching — a mismatch is not a key you can safely encrypt to. + */ + private fun Node.passesSecurityFilters(filterState: BaseMapViewModel.MapFilterState): Boolean = + (!filterState.onlySigned || signsPackets) && (!filterState.onlyEncrypted || (hasPKC && !mismatchKey)) } /** diff --git a/feature/map/src/commonMain/kotlin/org/meshtastic/feature/map/component/BasemapMenu.kt b/feature/map/src/commonMain/kotlin/org/meshtastic/feature/map/component/BasemapMenu.kt index f44f95b9a6..b90cc19a5a 100644 --- a/feature/map/src/commonMain/kotlin/org/meshtastic/feature/map/component/BasemapMenu.kt +++ b/feature/map/src/commonMain/kotlin/org/meshtastic/feature/map/component/BasemapMenu.kt @@ -14,14 +14,11 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -@file:OptIn(ExperimentalMaterial3ExpressiveApi::class) - package org.meshtastic.feature.map.component import androidx.compose.material3.DropdownMenu import androidx.compose.material3.DropdownMenuGroup import androidx.compose.material3.DropdownMenuItem -import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi import androidx.compose.material3.Icon import androidx.compose.material3.MenuDefaults import androidx.compose.material3.Text diff --git a/feature/map/src/commonMain/kotlin/org/meshtastic/feature/map/component/EditWaypointDialog.kt b/feature/map/src/commonMain/kotlin/org/meshtastic/feature/map/component/EditWaypointDialog.kt index 03bee71db3..b3bbbe401b 100644 --- a/feature/map/src/commonMain/kotlin/org/meshtastic/feature/map/component/EditWaypointDialog.kt +++ b/feature/map/src/commonMain/kotlin/org/meshtastic/feature/map/component/EditWaypointDialog.kt @@ -155,7 +155,8 @@ fun EditWaypointDialog( selectedDateString = DateFormatter.formatDate(instant.toEpochMilliseconds()) selectedTimeString = DateFormatter.formatTime(instant.toEpochMilliseconds()) if (!expireValue.isExpirySet()) { - waypointInput = waypointInput.copy(expire = instant.epochSeconds.toInt()) + waypointInput = + waypointInput.newBuilder().also { wb -> wb.expire = instant.epochSeconds.toInt() }.build() } } else { selectedDateString = "" @@ -178,7 +179,9 @@ fun EditWaypointDialog( Column(modifier = Modifier.fillMaxWidth().verticalScroll(rememberScrollState())) { OutlinedTextField( value = waypointInput.name, - onValueChange = { waypointInput = waypointInput.copy(name = it.take(29)) }, + onValueChange = { + waypointInput = waypointInput.newBuilder().also { wb -> wb.name = it.take(29) }.build() + }, label = { Text(stringResource(Res.string.name)) }, singleLine = true, keyboardOptions = @@ -199,7 +202,10 @@ fun EditWaypointDialog( Spacer(modifier = Modifier.size(8.dp)) OutlinedTextField( value = waypointInput.description, - onValueChange = { waypointInput = waypointInput.copy(description = it.take(99)) }, + onValueChange = { + waypointInput = + waypointInput.newBuilder().also { wb -> wb.description = it.take(99) }.build() + }, label = { Text(stringResource(Res.string.description)) }, keyboardOptions = KeyboardOptions.Default.copy(keyboardType = KeyboardType.Text, imeAction = ImeAction.Done), @@ -231,7 +237,11 @@ fun EditWaypointDialog( // stay unlocked). enabled = myNodeNum != null, onCheckedChange = { locked -> - waypointInput = waypointInput.copy(locked_to = if (locked) myNodeNum ?: 0 else 0) + waypointInput = + waypointInput + .newBuilder() + .also { wb -> wb.locked_to = if (locked) myNodeNum ?: 0 else 0 } + .build() }, ) } @@ -256,10 +266,15 @@ fun EditWaypointDialog( if (checked) { if (!waypointInput.expire.isExpirySet()) { val default = waypointInput.expire.expiryInstantOrDefault() - waypointInput = waypointInput.copy(expire = default.epochSeconds.toInt()) + waypointInput = + waypointInput + .newBuilder() + .also { wb -> wb.expire = default.epochSeconds.toInt() } + .build() } } else { - waypointInput = waypointInput.copy(expire = Int.MAX_VALUE) + waypointInput = + waypointInput.newBuilder().also { wb -> wb.expire = Int.MAX_VALUE }.build() } }, ) @@ -284,7 +299,10 @@ fun EditWaypointDialog( nanosecond = ldt.nanosecond, ) waypointInput = - waypointInput.copy(expire = newLdt.toInstant(tz).epochSeconds.toInt()) + waypointInput + .newBuilder() + .also { wb -> wb.expire = newLdt.toInstant(tz).epochSeconds.toInt() } + .build() showDatePicker = false }, ) @@ -304,7 +322,10 @@ fun EditWaypointDialog( nanosecond = ldt.nanosecond, ) waypointInput = - waypointInput.copy(expire = newLdt.toInstant(tz).epochSeconds.toInt()) + waypointInput + .newBuilder() + .also { wb -> wb.expire = newLdt.toInstant(tz).epochSeconds.toInt() } + .build() showTimePicker = false }, ) @@ -371,7 +392,9 @@ fun EditWaypointDialog( } else { EmojiPickerDialog(onDismiss = { showEmojiPickerView = false }) { selectedEmoji -> showEmojiPickerView = false - selectedEmoji.firstCodePointOrNull()?.let { waypointInput = waypointInput.copy(icon = it) } + selectedEmoji.firstCodePointOrNull()?.let { + waypointInput = waypointInput.newBuilder().also { wb -> wb.icon = it }.build() + } } } } @@ -413,7 +436,13 @@ private fun GeofenceSection( FilterChip( selected = meters == selectedRadius, onClick = { - onWaypointChange(waypoint.copy(geofence_radius = meters).normalizeGeofenceNotifications()) + onWaypointChange( + waypoint + .newBuilder() + .also { wb -> wb.geofence_radius = meters } + .build() + .normalizeGeofenceNotifications(), + ) }, label = { Text(label) }, ) @@ -431,7 +460,15 @@ private fun GeofenceSection( } if (hasBox) { TextButton( - onClick = { onWaypointChange(waypoint.copy(bounding_box = null).normalizeGeofenceNotifications()) }, + onClick = { + onWaypointChange( + waypoint + .newBuilder() + .also { wb -> wb.bounding_box = null } + .build() + .normalizeGeofenceNotifications(), + ) + }, ) { Text(stringResource(Res.string.geofence_remove_area)) } @@ -453,19 +490,30 @@ private fun GeofenceNotificationControls(waypoint: Waypoint, onWaypointChange: ( label = stringResource(Res.string.geofence_notify_on_enter), checked = waypoint.notify_on_enter, onCheckedChange = { - onWaypointChange(waypoint.copy(notify_on_enter = it).normalizeGeofenceNotifications()) + onWaypointChange( + waypoint + .newBuilder() + .also { wb -> wb.notify_on_enter = it } + .build() + .normalizeGeofenceNotifications(), + ) }, ) GeofenceToggleRow( label = stringResource(Res.string.geofence_notify_on_exit), checked = waypoint.notify_on_exit, - onCheckedChange = { onWaypointChange(waypoint.copy(notify_on_exit = it).normalizeGeofenceNotifications()) }, + onCheckedChange = { + val updated = waypoint.newBuilder().also { wb -> wb.notify_on_exit = it }.build() + onWaypointChange(updated.normalizeGeofenceNotifications()) + }, ) if (waypoint.notify_on_enter || waypoint.notify_on_exit) { GeofenceToggleRow( label = stringResource(Res.string.geofence_favorites_only), checked = waypoint.notify_favorites_only, - onCheckedChange = { onWaypointChange(waypoint.copy(notify_favorites_only = it)) }, + onCheckedChange = { + onWaypointChange(waypoint.newBuilder().also { wb -> wb.notify_favorites_only = it }.build()) + }, ) } } @@ -492,9 +540,15 @@ private fun GeofenceToggleRow(label: String, checked: Boolean, onCheckedChange: */ private fun Waypoint.normalizeGeofenceNotifications(): Waypoint = when { geofence_radius <= 0 && bounding_box == null -> - copy(notify_on_enter = false, notify_on_exit = false, notify_favorites_only = false) + this.newBuilder() + .also { wb -> + wb.notify_on_enter = false + wb.notify_on_exit = false + wb.notify_favorites_only = false + } + .build() - !notify_on_enter && !notify_on_exit -> copy(notify_favorites_only = false) + !notify_on_enter && !notify_on_exit -> this.newBuilder().also { wb -> wb.notify_favorites_only = false }.build() else -> this } diff --git a/feature/map/src/commonMain/kotlin/org/meshtastic/feature/map/component/MapControlsOverlay.kt b/feature/map/src/commonMain/kotlin/org/meshtastic/feature/map/component/MapControlsOverlay.kt index 3e3207701d..e77412a8d1 100644 --- a/feature/map/src/commonMain/kotlin/org/meshtastic/feature/map/component/MapControlsOverlay.kt +++ b/feature/map/src/commonMain/kotlin/org/meshtastic/feature/map/component/MapControlsOverlay.kt @@ -14,8 +14,6 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -@file:OptIn(ExperimentalMaterial3ExpressiveApi::class) - package org.meshtastic.feature.map.component import androidx.compose.foundation.layout.Box @@ -24,7 +22,6 @@ import androidx.compose.foundation.layout.size import androidx.compose.material3.Badge import androidx.compose.material3.BadgedBox import androidx.compose.material3.CircularWavyProgressIndicator -import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi import androidx.compose.material3.FloatingToolbarDefaults import androidx.compose.material3.HorizontalFloatingToolbar import androidx.compose.material3.MaterialTheme @@ -68,7 +65,6 @@ import org.meshtastic.core.ui.theme.StatusColors.StatusRed * @param isRefreshing Whether a refresh is currently in progress. * @param onRefresh Callback when the refresh button is clicked. */ -@OptIn(ExperimentalMaterial3ExpressiveApi::class) @Suppress("LongParameterList") @Composable fun MapControlsOverlay( diff --git a/feature/map/src/commonMain/kotlin/org/meshtastic/feature/map/component/MapFilterActions.kt b/feature/map/src/commonMain/kotlin/org/meshtastic/feature/map/component/MapFilterActions.kt index 49de12b093..49ca482fc2 100644 --- a/feature/map/src/commonMain/kotlin/org/meshtastic/feature/map/component/MapFilterActions.kt +++ b/feature/map/src/commonMain/kotlin/org/meshtastic/feature/map/component/MapFilterActions.kt @@ -38,6 +38,8 @@ class MapFilterActions( val onClearExcludedRoles: () -> Unit, val onToggleOnlyOnline: () -> Unit, val onToggleOnlyDirect: () -> Unit, + val onToggleOnlySigned: () -> Unit, + val onToggleOnlyEncrypted: () -> Unit, val onToggleExcludeMqtt: () -> Unit, val onToggleShowIgnored: () -> Unit, val onToggleIncludeUnknown: () -> Unit, @@ -46,9 +48,9 @@ class MapFilterActions( /** * The standard wiring from a view model to [MapFilterSheet]. * - * Both flavours build the identical bag of method references, and eleven of them written out twice is eleven chances - * for the two maps to drift apart again — which is exactly what [org.meshtastic.feature.map.MapNodePolicy] exists to - * prevent on the rules side. + * Both flavours build the identical bag of method references, and writing them out twice is that many chances for the + * two maps to drift apart again — which is exactly what [org.meshtastic.feature.map.MapNodePolicy] exists to prevent on + * the rules side. */ fun BaseMapViewModel.mapFilterActions(): MapFilterActions = MapFilterActions( onToggleOnlyFavorites = ::toggleOnlyFavorites, @@ -59,6 +61,8 @@ fun BaseMapViewModel.mapFilterActions(): MapFilterActions = MapFilterActions( onClearExcludedRoles = ::clearExcludedRoles, onToggleOnlyOnline = ::toggleOnlyOnline, onToggleOnlyDirect = ::toggleOnlyDirect, + onToggleOnlySigned = ::toggleOnlySigned, + onToggleOnlyEncrypted = ::toggleOnlyEncrypted, onToggleExcludeMqtt = ::toggleExcludeMqtt, onToggleShowIgnored = ::toggleShowIgnored, onToggleIncludeUnknown = ::toggleIncludeUnknown, diff --git a/feature/map/src/commonMain/kotlin/org/meshtastic/feature/map/component/MapFilterSheet.kt b/feature/map/src/commonMain/kotlin/org/meshtastic/feature/map/component/MapFilterSheet.kt index b5bd6a5d78..96eae3bf0b 100644 --- a/feature/map/src/commonMain/kotlin/org/meshtastic/feature/map/component/MapFilterSheet.kt +++ b/feature/map/src/commonMain/kotlin/org/meshtastic/feature/map/component/MapFilterSheet.kt @@ -51,7 +51,9 @@ import org.meshtastic.core.resources.map_filter_title import org.meshtastic.core.resources.node_filter_exclude_mqtt import org.meshtastic.core.resources.node_filter_include_unknown import org.meshtastic.core.resources.node_filter_only_direct +import org.meshtastic.core.resources.node_filter_only_encrypted import org.meshtastic.core.resources.node_filter_only_online +import org.meshtastic.core.resources.node_filter_only_signed import org.meshtastic.core.resources.only_favorites import org.meshtastic.core.resources.show_precision_circle import org.meshtastic.core.resources.show_waypoints @@ -135,8 +137,8 @@ internal fun MapFilterSheetContent(filterState: BaseMapViewModel.MapFilterState, } /** - * The node-level filters, in the node list's own words — four of these five labels are its string resources, so a user - * who has met them there does not have to learn them twice. + * The node-level filters, in the node list's own words — all but one of these labels are its string resources, so a + * user who has met them there does not have to learn them twice. */ @Composable private fun NodeFilterToggles(filterState: BaseMapViewModel.MapFilterState, actions: MapFilterActions) = Column { @@ -150,13 +152,23 @@ private fun NodeFilterToggles(filterState: BaseMapViewModel.MapFilterState, acti checked = filterState.onlyDirect, onToggle = actions.onToggleOnlyDirect, ) + FilterToggle( + label = stringResource(Res.string.node_filter_only_signed), + checked = filterState.onlySigned, + onToggle = actions.onToggleOnlySigned, + ) + FilterToggle( + label = stringResource(Res.string.node_filter_only_encrypted), + checked = filterState.onlyEncrypted, + onToggle = actions.onToggleOnlyEncrypted, + ) FilterToggle( label = stringResource(Res.string.node_filter_exclude_mqtt), checked = filterState.excludeMqtt, onToggle = actions.onToggleExcludeMqtt, ) - // The fifth is ours: the list's `node_filter_show_ignored` reads "Only show ignored Nodes", which is what the - // list does and the opposite of what this does. + // The one exception is ours: the list's `node_filter_show_ignored` reads "Only show ignored Nodes", which is what + // the list does and the opposite of what this does. FilterToggle( label = stringResource(Res.string.map_filter_show_ignored), checked = filterState.showIgnored, diff --git a/feature/map/src/commonMain/kotlin/org/meshtastic/feature/map/component/MapZoomControls.kt b/feature/map/src/commonMain/kotlin/org/meshtastic/feature/map/component/MapZoomControls.kt index 9d6603e468..ae96981cbb 100644 --- a/feature/map/src/commonMain/kotlin/org/meshtastic/feature/map/component/MapZoomControls.kt +++ b/feature/map/src/commonMain/kotlin/org/meshtastic/feature/map/component/MapZoomControls.kt @@ -14,11 +14,8 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -@file:OptIn(ExperimentalMaterial3ExpressiveApi::class) - package org.meshtastic.feature.map.component -import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi import androidx.compose.material3.FloatingToolbarDefaults import androidx.compose.material3.VerticalFloatingToolbar import androidx.compose.runtime.Composable diff --git a/feature/map/src/commonMain/kotlin/org/meshtastic/feature/map/component/NodeTrackFilterMenu.kt b/feature/map/src/commonMain/kotlin/org/meshtastic/feature/map/component/NodeTrackFilterMenu.kt index 0dae691996..0e4a5b60e8 100644 --- a/feature/map/src/commonMain/kotlin/org/meshtastic/feature/map/component/NodeTrackFilterMenu.kt +++ b/feature/map/src/commonMain/kotlin/org/meshtastic/feature/map/component/NodeTrackFilterMenu.kt @@ -14,14 +14,11 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -@file:OptIn(ExperimentalMaterial3ExpressiveApi::class) - package org.meshtastic.feature.map.component import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.padding import androidx.compose.material3.DropdownMenu -import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Slider import androidx.compose.material3.Text diff --git a/feature/map/src/commonMain/kotlin/org/meshtastic/feature/map/component/SitePlannerLaunch.kt b/feature/map/src/commonMain/kotlin/org/meshtastic/feature/map/component/SitePlannerLaunch.kt index 92afbfac00..d5b86f92c1 100644 --- a/feature/map/src/commonMain/kotlin/org/meshtastic/feature/map/component/SitePlannerLaunch.kt +++ b/feature/map/src/commonMain/kotlin/org/meshtastic/feature/map/component/SitePlannerLaunch.kt @@ -62,13 +62,26 @@ private fun dbmToWatts(dbm: Int): Double? = if (dbm <= 0) null else 10.0.pow((db @Suppress("MagicNumber") private fun sensitivityDbmFor(preset: ModemPreset?): Double = when (preset) { ModemPreset.SHORT_TURBO -> -126.0 + ModemPreset.SHORT_FAST -> -129.0 + ModemPreset.SHORT_SLOW -> -131.5 + ModemPreset.MEDIUM_FAST -> -134.0 + ModemPreset.MEDIUM_SLOW -> -136.5 + + // 500 kHz at SF11: the same spreading factor as LONG_FAST, but 3 dB more thermal noise in the wider passband — + // the 150 vs 153 dB link budgets in the radio-settings docs table. + ModemPreset.LONG_TURBO -> -136.0 + ModemPreset.LONG_FAST -> -139.0 + ModemPreset.LONG_MODERATE -> -142.0 + ModemPreset.LONG_SLOW -> -144.5 + ModemPreset.VERY_LONG_SLOW -> -147.5 + else -> SitePlannerParams.DEFAULT_RX_SENSITIVITY_DBM } diff --git a/feature/map/src/commonTest/kotlin/org/meshtastic/feature/map/BaseMapViewModelTest.kt b/feature/map/src/commonTest/kotlin/org/meshtastic/feature/map/BaseMapViewModelTest.kt index 208c5b465c..e663cffeb9 100644 --- a/feature/map/src/commonTest/kotlin/org/meshtastic/feature/map/BaseMapViewModelTest.kt +++ b/feature/map/src/commonTest/kotlin/org/meshtastic/feature/map/BaseMapViewModelTest.kt @@ -17,8 +17,10 @@ package org.meshtastic.feature.map import app.cash.turbine.test +import dev.mokkery.answering.calls import dev.mokkery.answering.returns import dev.mokkery.every +import dev.mokkery.matcher.any import dev.mokkery.mock import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi @@ -34,6 +36,7 @@ import org.meshtastic.core.model.DataPacket import org.meshtastic.core.model.Node import org.meshtastic.core.model.NodeAddress import org.meshtastic.core.network.repository.NetworkRepository +import org.meshtastic.core.repository.MapFilterPrefs import org.meshtastic.core.repository.MapPrefs import org.meshtastic.core.repository.PacketRepository import org.meshtastic.core.testing.FakeLocaleUnitsProvider @@ -42,6 +45,7 @@ import org.meshtastic.core.testing.FakeNotificationPrefs import org.meshtastic.core.testing.FakeRadioConfigRepository import org.meshtastic.core.testing.FakeRadioController import org.meshtastic.core.testing.TestDataFactory +import org.meshtastic.proto.Config import org.meshtastic.proto.Position import org.meshtastic.proto.Waypoint import kotlin.test.AfterTest @@ -60,7 +64,7 @@ class BaseMapViewModelTest { private lateinit var radioConfigRepository: FakeRadioConfigRepository private lateinit var waypointPacketsFlow: MutableStateFlow> private val mapPrefs: MapPrefs = mock() - private val showIgnored = MutableStateFlow(false) + private val storedFilters = MutableStateFlow(MapFilterPrefs()) private val packetRepository: PacketRepository = mock() private val localeUnitsProvider = FakeLocaleUnitsProvider() private val networkRepository: NetworkRepository = mock() @@ -73,17 +77,12 @@ class BaseMapViewModelTest { radioConfigRepository = FakeRadioConfigRepository() radioController.setConnectionState(ConnectionState.Disconnected) - every { mapPrefs.showOnlyFavorites } returns MutableStateFlow(false) - every { mapPrefs.showWaypointsOnMap } returns MutableStateFlow(false) - every { mapPrefs.showPrecisionCircleOnMap } returns MutableStateFlow(false) - every { mapPrefs.lastHeardFilter } returns MutableStateFlow(0L) - every { mapPrefs.lastHeardTrackFilter } returns MutableStateFlow(0L) - every { mapPrefs.onlyOnlineOnMap } returns MutableStateFlow(false) - every { mapPrefs.onlyDirectOnMap } returns MutableStateFlow(false) - every { mapPrefs.excludeMqttOnMap } returns MutableStateFlow(false) - every { mapPrefs.showIgnoredOnMap } returns showIgnored - every { mapPrefs.includeUnknownOnMap } returns MutableStateFlow(true) - every { mapPrefs.excludedMapRoles } returns MutableStateFlow(emptySet()) + storedFilters.value = MapFilterPrefs(showWaypoints = false, showPrecisionCircle = false) + every { mapPrefs.mapFilters } returns storedFilters + every { mapPrefs.updateMapFilters(any()) } calls + { (transform: (MapFilterPrefs) -> MapFilterPrefs) -> + storedFilters.value = transform(storedFilters.value) + } waypointPacketsFlow = MutableStateFlow(emptyList()) every { packetRepository.getWaypoints() } returns waypointPacketsFlow @@ -158,7 +157,7 @@ class BaseMapViewModelTest { // Set on the prefs flow, after the view model was built: that is the DataStore-arrives-late case, which // used to be lost because the view model snapshotted `.value` into a mirror nothing updated. nodeRepository.setNodes(listOf(positioned(1), positioned(2, isIgnored = true))) - showIgnored.value = true + storedFilters.value = storedFilters.value.copy(showIgnored = true) viewModel.nodesWithPosition.test { assertEquals(listOf(1, 2), awaitItem().map { it.num }.sorted()) @@ -168,7 +167,13 @@ class BaseMapViewModelTest { private fun positioned(num: Int, isIgnored: Boolean = false) = Node( num = num, - position = Position(latitude_i = 450_000_000, longitude_i = -1_220_000_000), + position = + Position.Builder() + .also { wb -> + wb.latitude_i = 450_000_000 + wb.longitude_i = -1_220_000_000 + } + .build(), isIgnored = isIgnored, ) @@ -263,9 +268,31 @@ class BaseMapViewModelTest { } } + @Test + fun `toggling a role keeps role names this build does not know`() { + // decodeExcludedRoles drops names the current protobufs do not define, so decoding and re-encoding on every + // toggle would silently discard a role a newer build excluded. + storedFilters.value = storedFilters.value.copy(excludedRoles = setOf("ROLE_FROM_A_NEWER_BUILD")) + + viewModel.toggleRoleExcluded(Config.DeviceConfig.Role.ROUTER) + + assertEquals(setOf("ROLE_FROM_A_NEWER_BUILD", "ROUTER"), storedFilters.value.excludedRoles) + + viewModel.toggleRoleExcluded(Config.DeviceConfig.Role.ROUTER) + + assertEquals(setOf("ROLE_FROM_A_NEWER_BUILD"), storedFilters.value.excludedRoles) + } + private fun waypointPacket(id: Int, expire: Int): DataPacket = DataPacket( to = NodeAddress.ID_BROADCAST, channel = 0, - waypoint = Waypoint(id = id, name = "Waypoint $id", expire = expire), + waypoint = + Waypoint.Builder() + .also { wb -> + wb.id = id + wb.name = "Waypoint $id" + wb.expire = expire + } + .build(), ) } diff --git a/feature/map/src/commonTest/kotlin/org/meshtastic/feature/map/MapBoundsTest.kt b/feature/map/src/commonTest/kotlin/org/meshtastic/feature/map/MapBoundsTest.kt index a3ca919998..da669fb099 100644 --- a/feature/map/src/commonTest/kotlin/org/meshtastic/feature/map/MapBoundsTest.kt +++ b/feature/map/src/commonTest/kotlin/org/meshtastic/feature/map/MapBoundsTest.kt @@ -28,7 +28,13 @@ class MapBoundsTest { private fun node(num: Int, latitude: Double, longitude: Double) = Node( num = num, - position = Position(latitude_i = (latitude * 1e7).toInt(), longitude_i = (longitude * 1e7).toInt()), + position = + Position.Builder() + .also { wb -> + wb.latitude_i = (latitude * 1e7).toInt() + wb.longitude_i = (longitude * 1e7).toInt() + } + .build(), ) @Test diff --git a/feature/map/src/commonTest/kotlin/org/meshtastic/feature/map/MapNodePolicyTest.kt b/feature/map/src/commonTest/kotlin/org/meshtastic/feature/map/MapNodePolicyTest.kt index 9ac476ed7f..f27548cf38 100644 --- a/feature/map/src/commonTest/kotlin/org/meshtastic/feature/map/MapNodePolicyTest.kt +++ b/feature/map/src/commonTest/kotlin/org/meshtastic/feature/map/MapNodePolicyTest.kt @@ -16,7 +16,10 @@ */ package org.meshtastic.feature.map +import okio.ByteString +import okio.ByteString.Companion.toByteString import org.meshtastic.core.model.Node +import org.meshtastic.core.model.Node.Companion.PUBLIC_KEY_SIZE import org.meshtastic.proto.Config import org.meshtastic.proto.Position import org.meshtastic.proto.User @@ -39,15 +42,31 @@ class MapNodePolicyTest { viaMqtt: Boolean = false, isIgnored: Boolean = false, shortName: String = "ABCD", + signsPackets: Boolean = false, + publicKey: ByteString? = null, ) = Node( num = num, - position = Position(latitude_i = (latitude * 1e7).toInt(), longitude_i = (longitude * 1e7).toInt()), + position = + Position.Builder() + .also { wb -> + wb.latitude_i = (latitude * 1e7).toInt() + wb.longitude_i = (longitude * 1e7).toInt() + } + .build(), lastHeard = lastHeard, isFavorite = isFavorite, - user = User(short_name = shortName, role = role), + user = + User.Builder() + .also { wb -> + wb.short_name = shortName + wb.role = role + } + .build(), hopsAway = hopsAway, viaMqtt = viaMqtt, isIgnored = isIgnored, + signsPackets = signsPackets, + publicKey = publicKey, ) @Suppress("LongParameterList") @@ -57,6 +76,8 @@ class MapNodePolicyTest { excludedRoles: Set = emptySet(), onlyOnline: Boolean = false, onlyDirect: Boolean = false, + onlySigned: Boolean = false, + onlyEncrypted: Boolean = false, excludeMqtt: Boolean = false, showIgnored: Boolean = false, includeUnknown: Boolean = true, @@ -69,6 +90,8 @@ class MapNodePolicyTest { excludedRoles = excludedRoles, onlyOnline = onlyOnline, onlyDirect = onlyDirect, + onlySigned = onlySigned, + onlyEncrypted = onlyEncrypted, excludeMqtt = excludeMqtt, showIgnored = showIgnored, includeUnknown = includeUnknown, @@ -152,12 +175,20 @@ class MapNodePolicyTest { @Test fun `the direct filter drops both relayed nodes and nodes of unknown distance`() { - // The node list's query is `hops_away <= 0 AND hops_away >= 0`, so -1 — never measured — is not direct. + // A non-local direct node needs `hops_away = 0` and `via_mqtt = 0`, so -1 — never measured — is not direct. val nodes = listOf(node(1, 45.0, -122.0, hopsAway = 2), node(2, 45.1, -122.1), node(3, 45.2, -122.2, hopsAway = -1)) assertEquals(listOf(2), visible(nodes, filters(onlyDirect = true))) } + @Test + fun `the direct filter drops an mqtt node reporting zero hops`() { + // An MQTT-bridged node carries the hop count its uplink gateway heard, not ours, so zero hops there is no + // claim about our radio. Direct is the same `hopsAway == 0 && !viaMqtt` the row renderers read. + val nodes = listOf(node(1, 45.0, -122.0, viaMqtt = true), node(2, 45.1, -122.1)) + assertEquals(listOf(2), visible(nodes, filters(onlyDirect = true))) + } + @Test fun `the mqtt filter drops nodes heard over mqtt`() { val nodes = listOf(node(1, 45.0, -122.0, viaMqtt = true), node(2, 45.1, -122.1)) @@ -209,4 +240,27 @@ class MapNodePolicyTest { ) assertEquals(listOf(1), visible(listOf(mine), state, now = 100_000, mine = 1)) } + + @Test + fun `the signed filter keeps only nodes whose signature the radio verified`() { + val nodes = listOf(node(1, 1.0, 1.0), node(2, 1.0, 1.0, signsPackets = true)) + + assertEquals(listOf(1, 2), visible(nodes, filters())) + assertEquals(listOf(2), visible(nodes, filters(onlySigned = true))) + } + + @Test + fun `the encrypted filter keeps a key on file and drops one that stopped matching`() { + val key = ByteArray(PUBLIC_KEY_SIZE) { 1 }.toByteString() + val nodes = + listOf( + node(1, 1.0, 1.0), + node(2, 1.0, 1.0, publicKey = key), + // A mismatch is not a key you can safely encrypt to, so it is excluded rather than counted. + node(3, 1.0, 1.0, publicKey = Node.ERROR_BYTE_STRING), + ) + + assertEquals(listOf(1, 2, 3), visible(nodes, filters())) + assertEquals(listOf(2), visible(nodes, filters(onlyEncrypted = true))) + } } diff --git a/feature/map/src/commonTest/kotlin/org/meshtastic/feature/map/TracerouteNodeSelectionTest.kt b/feature/map/src/commonTest/kotlin/org/meshtastic/feature/map/TracerouteNodeSelectionTest.kt index 76ae250663..a9464632d8 100644 --- a/feature/map/src/commonTest/kotlin/org/meshtastic/feature/map/TracerouteNodeSelectionTest.kt +++ b/feature/map/src/commonTest/kotlin/org/meshtastic/feature/map/TracerouteNodeSelectionTest.kt @@ -25,10 +25,18 @@ import kotlin.test.assertTrue class TracerouteNodeSelectionTest { - private fun nodeWithPosition(num: Int, latI: Int = num * 100000, lonI: Int = num * 200000): Node = - Node(num = num, position = Position(latitude_i = latI, longitude_i = lonI)) + private fun nodeWithPosition(num: Int, latI: Int = num * 100000, lonI: Int = num * 200000): Node = Node( + num = num, + position = + Position.Builder() + .also { wb -> + wb.latitude_i = latI + wb.longitude_i = lonI + } + .build(), + ) - private fun nodeWithoutPosition(num: Int): Node = Node(num = num, position = Position()) + private fun nodeWithoutPosition(num: Int): Node = Node(num = num, position = Position.Builder().build()) private val defaultGetNodeOrFallback: (Int) -> Node = { num -> Node(num = num) } @@ -72,8 +80,20 @@ class TracerouteNodeSelectionTest { val overlay = TracerouteOverlay(requestId = 1, forwardRoute = listOf(10, 20), returnRoute = listOf(20, 10)) val snapshotPositions = mapOf( - 10 to Position(latitude_i = 400000000, longitude_i = -700000000), - 20 to Position(latitude_i = 410000000, longitude_i = -710000000), + 10 to + Position.Builder() + .also { wb -> + wb.latitude_i = 400000000 + wb.longitude_i = -700000000 + } + .build(), + 20 to + Position.Builder() + .also { wb -> + wb.latitude_i = 410000000 + wb.longitude_i = -710000000 + } + .build(), ) val liveNodes = listOf( @@ -101,8 +121,20 @@ class TracerouteNodeSelectionTest { val overlay = TracerouteOverlay(requestId = 1, forwardRoute = listOf(10, 20)) val snapshotPositions = mapOf( - 10 to Position(latitude_i = 400000000, longitude_i = -700000000), - 20 to Position(latitude_i = 410000000, longitude_i = -710000000), + 10 to + Position.Builder() + .also { wb -> + wb.latitude_i = 400000000 + wb.longitude_i = -700000000 + } + .build(), + 20 to + Position.Builder() + .also { wb -> + wb.latitude_i = 410000000 + wb.longitude_i = -710000000 + } + .build(), ) val result = tracerouteNodeSelection( @@ -122,9 +154,27 @@ class TracerouteNodeSelectionTest { val overlay = TracerouteOverlay(requestId = 1, forwardRoute = listOf(10, 20)) val snapshotPositions = mapOf( - 10 to Position(latitude_i = 400000000, longitude_i = -700000000), - 20 to Position(latitude_i = 410000000, longitude_i = -710000000), - 30 to Position(latitude_i = 420000000, longitude_i = -720000000), + 10 to + Position.Builder() + .also { wb -> + wb.latitude_i = 400000000 + wb.longitude_i = -700000000 + } + .build(), + 20 to + Position.Builder() + .also { wb -> + wb.latitude_i = 410000000 + wb.longitude_i = -710000000 + } + .build(), + 30 to + Position.Builder() + .also { wb -> + wb.latitude_i = 420000000 + wb.longitude_i = -720000000 + } + .build(), ) val result = tracerouteNodeSelection( @@ -195,7 +245,16 @@ class TracerouteNodeSelectionTest { @Test fun getNodeOrFallback_usedForSnapshotNodeLookup() { val overlay = TracerouteOverlay(requestId = 1, forwardRoute = listOf(10)) - val snapshotPositions = mapOf(10 to Position(latitude_i = 400000000, longitude_i = -700000000)) + val snapshotPositions = + mapOf( + 10 to + Position.Builder() + .also { wb -> + wb.latitude_i = 400000000 + wb.longitude_i = -700000000 + } + .build(), + ) var lookupCalledWith: Int? = null val result = tracerouteNodeSelection( diff --git a/feature/map/src/commonTest/kotlin/org/meshtastic/feature/map/component/SitePlannerLaunchTest.kt b/feature/map/src/commonTest/kotlin/org/meshtastic/feature/map/component/SitePlannerLaunchTest.kt index 529cc00da8..0c9a999bd7 100644 --- a/feature/map/src/commonTest/kotlin/org/meshtastic/feature/map/component/SitePlannerLaunchTest.kt +++ b/feature/map/src/commonTest/kotlin/org/meshtastic/feature/map/component/SitePlannerLaunchTest.kt @@ -43,6 +43,14 @@ class SitePlannerLaunchTest { ) } - private fun positionedNode(num: Int, latitudeI: Int, longitudeI: Int): Node = - Node(num = num, position = Position(latitude_i = latitudeI, longitude_i = longitudeI)) + private fun positionedNode(num: Int, latitudeI: Int, longitudeI: Int): Node = Node( + num = num, + position = + Position.Builder() + .also { wb -> + wb.latitude_i = latitudeI + wb.longitude_i = longitudeI + } + .build(), + ) } diff --git a/feature/map/src/jvmTest/kotlin/org/meshtastic/feature/map/component/MapFilterSheetTest.kt b/feature/map/src/jvmTest/kotlin/org/meshtastic/feature/map/component/MapFilterSheetTest.kt index 242ba621fb..c680c67a02 100644 --- a/feature/map/src/jvmTest/kotlin/org/meshtastic/feature/map/component/MapFilterSheetTest.kt +++ b/feature/map/src/jvmTest/kotlin/org/meshtastic/feature/map/component/MapFilterSheetTest.kt @@ -51,6 +51,8 @@ class MapFilterSheetTest { onClearExcludedRoles = {}, onToggleOnlyOnline = {}, onToggleOnlyDirect = {}, + onToggleOnlySigned = {}, + onToggleOnlyEncrypted = {}, onToggleExcludeMqtt = {}, onToggleShowIgnored = {}, onToggleIncludeUnknown = {}, diff --git a/feature/messaging/detekt-baseline.xml b/feature/messaging/detekt-baseline.xml index b4f5ccab37..520ad131b5 100644 --- a/feature/messaging/detekt-baseline.xml +++ b/feature/messaging/detekt-baseline.xml @@ -15,7 +15,7 @@ LambdaParameterEventTrailing:QuickChat.kt:onNavigateUp: () -> Unit ModifierMissing:Contacts.kt:@Suppress("LongMethod", "CyclomaticComplexMethod", "LongParameterList") @Composable fun ContactsScreen ModifierMissing:DeliveryInfoDialog.kt:@Composable fun DeliveryInfo - ModifierMissing:Message.kt:@Suppress("LongMethod", "CyclomaticComplexMethod") @Composable fun MessageScreen + ModifierMissing:Message.kt:@Suppress("LongMethod", "CyclomaticComplexMethod") @OptIn(ExperimentalFoundationApi::class) @Composable fun MessageScreen ModifierMissing:MessageActionsBottomSheet.kt:@Suppress("LongMethod") @Composable fun MessageActionsContent ModifierMissing:MessageScreenComponents.kt:@Composable fun ReplySnippet ModifierMissing:MessageScreenComponents.kt:@OptIn(ExperimentalMaterial3Api::class) @Composable fun ActionModeTopBar @@ -29,7 +29,9 @@ ParameterNaming:MessageScreenComponents.kt:onToggleFilteringDisabled: () -> Unit = {} ParameterNaming:MessageScreenComponents.kt:onToggleShowFiltered: () -> Unit ParameterNaming:MessageScreenComponents.kt:onToggleShowFiltered: () -> Unit = {} + PreviewPublic:ContactItem.kt:@PreviewLightDark @Composable fun ContactItemArchivedPreview PreviewPublic:Message.kt:@PreviewLightDark @Composable fun MessageInputPreview + PreviewPublic:Message.kt:@PreviewLightDark @Composable fun RetiredChannelNoticePreview PreviewPublic:QuickChatPreviews.kt:@PreviewLightDark @Composable fun EditQuickChatDialogPreview PreviewPublic:QuickChatPreviews.kt:@PreviewLightDark @Composable fun QuickChatItemPreview PreviewPublic:ReactionPreviews.kt:@PreviewLightDark @Composable fun ReactionItemPreview diff --git a/feature/messaging/src/commonMain/kotlin/org/meshtastic/feature/messaging/Message.kt b/feature/messaging/src/commonMain/kotlin/org/meshtastic/feature/messaging/Message.kt index 2aa24130e2..e79486f851 100644 --- a/feature/messaging/src/commonMain/kotlin/org/meshtastic/feature/messaging/Message.kt +++ b/feature/messaging/src/commonMain/kotlin/org/meshtastic/feature/messaging/Message.kt @@ -66,7 +66,7 @@ import androidx.compose.ui.input.key.Key import androidx.compose.ui.input.key.KeyEventType import androidx.compose.ui.input.key.isShiftPressed import androidx.compose.ui.input.key.key -import androidx.compose.ui.input.key.onKeyEvent +import androidx.compose.ui.input.key.onPreviewKeyEvent import androidx.compose.ui.input.key.type import androidx.compose.ui.platform.LocalClipboard import androidx.compose.ui.platform.LocalFocusManager @@ -99,16 +99,19 @@ import org.meshtastic.core.model.Node import org.meshtastic.core.model.NodeAddress import org.meshtastic.core.model.util.getChannel import org.meshtastic.core.resources.Res +import org.meshtastic.core.resources.archived_channel_read_only import org.meshtastic.core.resources.send import org.meshtastic.core.resources.type_a_message import org.meshtastic.core.resources.unknown_channel import org.meshtastic.core.ui.component.InlineStyle import org.meshtastic.core.ui.component.SharedContactDialog import org.meshtastic.core.ui.component.smartScrollToIndex +import org.meshtastic.core.ui.icon.History import org.meshtastic.core.ui.icon.MeshtasticIcons import org.meshtastic.core.ui.icon.Send import org.meshtastic.core.ui.theme.AppTheme import org.meshtastic.core.ui.util.createClipEntry +import org.meshtastic.core.ui.util.isFromSoftKeyboard import org.meshtastic.feature.messaging.component.ActionModeTopBar import org.meshtastic.feature.messaging.component.DeleteMessageDialog import org.meshtastic.feature.messaging.component.FormattingToolbar @@ -142,6 +145,7 @@ private const val COUNTER_VISIBLE_WITHIN_BYTES = 20 * @param onNavigateBack Callback to navigate back from this screen. */ @Suppress("LongMethod", "CyclomaticComplexMethod") +@OptIn(ExperimentalFoundationApi::class) @Composable fun MessageScreen( contactKey: String, @@ -219,6 +223,7 @@ fun MessageScreen( } // Derived state, memoized for performance + val isRetiredChannel = remember(contactKey) { ContactKey(contactKey).isRetired } val channelInfo = remember(contactKey, channels) { val parsedKey = ContactKey(contactKey) @@ -229,7 +234,9 @@ fun MessageScreen( } val (channelIndex, nodeId, rawChannelName) = channelInfo val unknownChannelText = stringResource(Res.string.unknown_channel) - val channelName = rawChannelName ?: unknownChannelText + // A retired channel has no slot to look a name up in — its own stored label is all that is left of it. + val retiredName = contactSettings[contactKey]?.displayName.orEmpty() + val channelName = rawChannelName ?: retiredName.ifEmpty { unknownChannelText } val title = remember(nodeId, channelName, viewModel) { @@ -246,6 +253,8 @@ fun MessageScreen( val inSelectionMode by remember { derivedStateOf { selectedMessageIds.value.isNotEmpty() } } + // No cache window here: this pane renders inside ThreePaneScaffold's LookaheadScope, where a + // prefetched item can reach the main placement pass before the lookahead pass has measured it. val listState = rememberLazyListState() // Track unread messages using lightweight metadata queries @@ -466,37 +475,43 @@ fun MessageScreen( } }, bottomBar = { - Column { - AnimatedVisibility(visible = showQuickChat) { - QuickChatRow( - enabled = connectionState is ConnectionState.Connected, - actions = quickChatActions, - onClick = { action -> - handleQuickChatAction( - action = action, - messageInputState = messageInputState, - onSendMessage = { text -> onEvent(MessageScreenEvent.SendMessage(text)) }, - ) + // A retired channel is read-only: there is no slot left to send on, so the composer and the quick-chat + // row are replaced outright rather than merely disabled. + if (isRetiredChannel) { + RetiredChannelNotice() + } else { + Column { + AnimatedVisibility(visible = showQuickChat) { + QuickChatRow( + enabled = connectionState is ConnectionState.Connected, + actions = quickChatActions, + onClick = { action -> + handleQuickChatAction( + action = action, + messageInputState = messageInputState, + onSendMessage = { text -> onEvent(MessageScreenEvent.SendMessage(text)) }, + ) + }, + ) + } + ReplySnippet( + originalMessage = originalMessage, + onClearReply = { replyingToPacketId = null }, + ourNode = ourNode, + ) + MessageInput( + isEnabled = connectionState is ConnectionState.Connected, + isHomoglyphEncodingEnabled = homoglyphEncodingEnabled, + textFieldState = messageInputState, + mentionCandidates = mentionCandidates, + onSendMessage = { + val messageText = messageInputState.text.toString().trim { it.isWhitespace() } + if (messageText.isNotEmpty()) { + onEvent(MessageScreenEvent.SendMessage(messageText, replyingToPacketId)) + } }, ) } - ReplySnippet( - originalMessage = originalMessage, - onClearReply = { replyingToPacketId = null }, - ourNode = ourNode, - ) - MessageInput( - isEnabled = connectionState is ConnectionState.Connected, - isHomoglyphEncodingEnabled = homoglyphEncodingEnabled, - textFieldState = messageInputState, - mentionCandidates = mentionCandidates, - onSendMessage = { - val messageText = messageInputState.text.toString().trim { it.isWhitespace() } - if (messageText.isNotEmpty()) { - onEvent(MessageScreenEvent.SendMessage(messageText, replyingToPacketId)) - } - }, - ) } }, ) { paddingValues -> @@ -519,17 +534,26 @@ fun MessageScreen( searchQuery = if (isSearchActive) searchQuery else "", translationAvailable = translationAvailable, showFullMessageTimestamps = showFullMessageTimestamps, + canReact = !isRetiredChannel, + canSend = !isRetiredChannel, ), handlers = MessageListHandlers( onUnreadChanged = { messageUuid, timestamp -> onEvent(MessageScreenEvent.ClearUnreadCount(messageUuid, timestamp)) }, - onSendReaction = { emoji, id -> onEvent(MessageScreenEvent.SendReaction(emoji, id)) }, + // A retired conversation is read-only; the send path refuses these anyway, so do not offer + // them. + onSendReaction = + if (isRetiredChannel) { + { _, _ -> } + } else { + { emoji, id -> onEvent(MessageScreenEvent.SendReaction(emoji, id)) } + }, onClickChip = { onEvent(MessageScreenEvent.NodeDetails(it)) }, onDeleteMessages = { viewModel.deleteMessages(it) }, - onSendMessage = { text, key -> viewModel.sendMessage(text, key) }, - onReply = { message -> replyingToPacketId = message?.packetId }, + onSendMessage = { text, key -> if (!isRetiredChannel) viewModel.sendMessage(text, key) }, + onReply = { message -> if (!isRetiredChannel) replyingToPacketId = message?.packetId }, onTranslate = { onEvent(MessageScreenEvent.TranslateMessage(it)) }, onToggleTranslation = { onEvent(MessageScreenEvent.ToggleShowTranslated(it)) }, ), @@ -748,6 +772,34 @@ private fun mentionOutputTransformation(candidatesById: () -> Map - val isEnterNoShift = keyEvent.key == Key.Enter && !keyEvent.isShiftPressed + // Tunnel phase, not bubble: a multi-line field consumes Enter to insert its newline, so an + // Enter-to-send shortcut has to claim the event before the field ever sees it. + .onPreviewKeyEvent { keyEvent -> + // Enter-to-send is a physical-keyboard shortcut, reached past with Shift. An on-screen keyboard + // has no Shift, so its Enter is left alone to insert the newline it is labelled with. + val isEnterNoShift = + keyEvent.key == Key.Enter && !keyEvent.isShiftPressed && !keyEvent.isFromSoftKeyboard() if (isEnterNoShift) { if (keyEvent.type == KeyEventType.KeyUp) onSendAction() true // consume both KeyDown and KeyUp to prevent newline insertion @@ -848,9 +905,10 @@ internal fun MessageInput( shape = RoundedCornerShape(ROUNDED_CORNER_PERCENT.toFloat()), isError = isOverLimit, placeholder = { Text(stringResource(Res.string.type_a_message)) }, + // A multi-line field must keep its Enter key: Compose only sets IME_FLAG_NO_ENTER_ACTION for + // ImeAction.Default, and without it an IME may swap Enter for the action, leaving no way to type a newline. keyboardOptions = - KeyboardOptions(capitalization = KeyboardCapitalization.Sentences, imeAction = ImeAction.Send), - onKeyboardAction = { onSendAction() }, + KeyboardOptions(capitalization = KeyboardCapitalization.Sentences, imeAction = ImeAction.Default), supportingText = { // The counter is only useful as the limit approaches. Showing 0/200 before a character is typed is // chrome that every chat client has learned to hide. @@ -932,6 +990,30 @@ private fun MentionSuggestions(suggestions: List, onPick: (Men } } +/** + * The composer a live channel gets, above the notice that replaces it once that channel leaves the radio. Shown + * together because the point is the swap: the affordance is removed, not greyed out. + */ +@PreviewLightDark +@Composable +fun RetiredChannelNoticePreview() { + AppTheme { + Surface { + Column(modifier = Modifier.padding(8.dp)) { + MessageInput( + isEnabled = true, + isHomoglyphEncodingEnabled = false, + mentionCandidates = persistentMapOf(), + textFieldState = rememberTextFieldState("Still on the air"), + onSendMessage = {}, + ) + Spacer(Modifier.size(16.dp)) + RetiredChannelNotice() + } + } + } +} + @PreviewLightDark @Composable fun MessageInputPreview() { diff --git a/feature/messaging/src/commonMain/kotlin/org/meshtastic/feature/messaging/MessageListPaged.kt b/feature/messaging/src/commonMain/kotlin/org/meshtastic/feature/messaging/MessageListPaged.kt index 6968b14938..18d5bc2d40 100644 --- a/feature/messaging/src/commonMain/kotlin/org/meshtastic/feature/messaging/MessageListPaged.kt +++ b/feature/messaging/src/commonMain/kotlin/org/meshtastic/feature/messaging/MessageListPaged.kt @@ -110,6 +110,10 @@ internal data class MessageListPagedState( val searchQuery: String = "", val translationAvailable: Boolean = false, val showFullMessageTimestamps: Boolean = false, + /** False for an archived conversation: existing reactions still render, but none can be added. */ + val canReact: Boolean = true, + /** False for an archived conversation: its channel is gone, so nothing can be sent or resent into it. */ + val canSend: Boolean = true, ) private fun MutableState>.toggle(uuid: Long) { @@ -142,10 +146,14 @@ internal fun MessageListPaged( MessageStatusDialog( message = message, isDirectMessage = isDirectMessageConversation, - resendOption = message.isStatusRetryable(isDirectMessageConversation), + resendOption = message.isStatusRetryable(isDirectMessageConversation) && state.canSend, onResend = { - handlers.onDeleteMessages(listOf(message.uuid)) - handlers.onSendMessage(message.text, state.contactKey) + // Resend deletes the old row and sends a fresh one. Never take the first half without the second: + // on an archived conversation the send is refused, which would leave the message simply gone. + if (state.canSend) { + handlers.onDeleteMessages(listOf(message.uuid)) + handlers.onSendMessage(message.text, state.contactKey) + } showStatusDialog = null }, onDismiss = { showStatusDialog = null }, @@ -157,6 +165,7 @@ internal fun MessageListPaged( ReactionDialog( reactions = reactions, myId = state.ourNode?.user?.id, + canReact = state.canReact, onDismiss = { showReactionDialog = null }, onResend = { reaction -> handlers.onSendReaction(reaction.emoji, reaction.replyId) @@ -442,6 +451,8 @@ private fun RenderPagedChatMessageRow( hasSamePrev = hasSamePrev, hasSameNext = hasSameNext, quickEmojis = quickEmojis, + canReact = state.canReact, + canReply = state.canSend, searchQuery = state.searchQuery, translationAvailable = state.translationAvailable, isDirectMessage = isDirectMessageConversation, diff --git a/feature/messaging/src/commonMain/kotlin/org/meshtastic/feature/messaging/MessageViewModel.kt b/feature/messaging/src/commonMain/kotlin/org/meshtastic/feature/messaging/MessageViewModel.kt index 5bac3d3633..d1f030f616 100644 --- a/feature/messaging/src/commonMain/kotlin/org/meshtastic/feature/messaging/MessageViewModel.kt +++ b/feature/messaging/src/commonMain/kotlin/org/meshtastic/feature/messaging/MessageViewModel.kt @@ -187,7 +187,7 @@ class MessageViewModel( val nodeList: StateFlow> = nodeRepository.getNodes().stateInWhileSubscribed(initialValue = emptyList()) - val channels = radioConfigRepository.channelSetFlow.stateInWhileSubscribed(ChannelSet()) + val channels = radioConfigRepository.channelSetFlow.stateInWhileSubscribed(ChannelSet.Builder().build()) val showQuickChat = uiPrefs.showQuickChat diff --git a/feature/messaging/src/commonMain/kotlin/org/meshtastic/feature/messaging/component/MessageActionsBottomSheet.kt b/feature/messaging/src/commonMain/kotlin/org/meshtastic/feature/messaging/component/MessageActionsBottomSheet.kt index 09b3a868f7..f7a53bdaf1 100644 --- a/feature/messaging/src/commonMain/kotlin/org/meshtastic/feature/messaging/component/MessageActionsBottomSheet.kt +++ b/feature/messaging/src/commonMain/kotlin/org/meshtastic/feature/messaging/component/MessageActionsBottomSheet.kt @@ -89,6 +89,8 @@ fun MessageActionsContent( onSelect: () -> Unit, onDelete: () -> Unit, onStatus: () -> Unit, + /** False for an archived conversation: there is no channel left to reply into. */ + canReply: Boolean = true, statusString: Pair? = null, status: MessageStatus? = null, timestamp: String? = null, @@ -98,9 +100,13 @@ fun MessageActionsContent( onToggleTranslation: () -> Unit = {}, ) { Column { - QuickEmojiRow(quickEmojis = quickEmojis, onReact = onReact, onMoreReactions = onMoreReactions) + // An archived conversation shows no reaction affordances: the send path refuses them, so offering the row + // would be a control that silently does nothing. + if (quickEmojis.isNotEmpty()) { + QuickEmojiRow(quickEmojis = quickEmojis, onReact = onReact, onMoreReactions = onMoreReactions) - HorizontalDivider(modifier = Modifier.padding(vertical = 8.dp)) + HorizontalDivider(modifier = Modifier.padding(vertical = 8.dp)) + } if (xeddsaSigned) { ListItem( @@ -151,16 +157,7 @@ fun MessageActionsContent( ) } - ListItem( - headlineContent = { Text(stringResource(Res.string.reply)) }, - leadingContent = { Icon(MeshtasticIcons.Reply, contentDescription = stringResource(Res.string.reply)) }, - modifier = - Modifier.clickable( - onClickLabel = stringResource(Res.string.action_send_reply), - role = Role.Button, - onClick = onReply, - ), - ) + ReplyAction(canReply = canReply, onReply = onReply) ListItem( headlineContent = { Text(stringResource(Res.string.copy)) }, @@ -295,3 +292,19 @@ internal fun QuickEmojiRow( } } } + +/** The sheet's Reply row, absent on an archived conversation because there is nothing to reply into. */ +@Composable +private fun ReplyAction(canReply: Boolean, onReply: () -> Unit) { + if (!canReply) return + ListItem( + headlineContent = { Text(stringResource(Res.string.reply)) }, + leadingContent = { Icon(MeshtasticIcons.Reply, contentDescription = stringResource(Res.string.reply)) }, + modifier = + Modifier.clickable( + onClickLabel = stringResource(Res.string.action_send_reply), + role = Role.Button, + onClick = onReply, + ), + ) +} diff --git a/feature/messaging/src/commonMain/kotlin/org/meshtastic/feature/messaging/component/MessageItem.kt b/feature/messaging/src/commonMain/kotlin/org/meshtastic/feature/messaging/component/MessageItem.kt index 4a55f3f47f..aa238d1593 100644 --- a/feature/messaging/src/commonMain/kotlin/org/meshtastic/feature/messaging/component/MessageItem.kt +++ b/feature/messaging/src/commonMain/kotlin/org/meshtastic/feature/messaging/component/MessageItem.kt @@ -138,6 +138,10 @@ fun MessageItem( showFullMessageTimestamp: Boolean = false, emojis: List = emptyList(), quickEmojis: List = listOf("👍", "👎", "😂", "🔥", "❤️", "😮"), + /** False for an archived conversation: existing reactions still render, but none can be added. */ + canReact: Boolean = true, + /** False for an archived conversation: its channel is gone, so there is nothing to reply into. */ + canReply: Boolean = true, /** * Hoisted so the list can keep at most one bar open and close it on a tap anywhere else; owning it here would leave * every row's bar independent, and none of them able to see a tap outside their own bubble. @@ -195,7 +199,8 @@ fun MessageItem( when (activeSheet) { ActiveSheet.Actions -> { MessageActionsContent( - quickEmojis = quickEmojis, + quickEmojis = if (canReact) quickEmojis else emptyList(), + canReply = canReply, onReply = { activeSheet = null onReply() @@ -326,6 +331,8 @@ fun MessageItem( // on a // duty-cycled radio. The overflow button keeps the full actions sheet one tap away. AnimatedVisibility( + // Stays available on an archived conversation even though its emoji are gone: this bar carries the only + // route to the actions sheet, and copy, select, delete and status all still apply to archived messages. visible = quickReactionsOpen && !inSelectionMode, modifier = Modifier.align(if (message.fromLocal) Alignment.End else Alignment.Start), ) { @@ -361,16 +368,20 @@ fun MessageItem( var swipeArmed by remember(message.uuid) { mutableStateOf(false) } Box(modifier = Modifier.fillMaxWidth()) { - ReplySwipeAffordance( - progress = (swipeOffset.value * swipeSign / swipeThresholdPx).coerceIn(0f, 1f), - modifier = Modifier.align(if (message.fromLocal) Alignment.CenterEnd else Alignment.CenterStart), - ) + // An archived conversation cannot send, so the swipe neither renders nor arms. Left in place it would buzz + // at the threshold and then drop the reply silently. + if (canReply) { + ReplySwipeAffordance( + progress = (swipeOffset.value * swipeSign / swipeThresholdPx).coerceIn(0f, 1f), + modifier = Modifier.align(if (message.fromLocal) Alignment.CenterEnd else Alignment.CenterStart), + ) + } Surface( modifier = Modifier.align(if (message.fromLocal) Alignment.CenterEnd else Alignment.CenterStart) .graphicsLayer { translationX = swipeOffset.value } - .pointerInput(message.uuid, inSelectionMode) { - if (inSelectionMode) return@pointerInput + .pointerInput(message.uuid, inSelectionMode, canReply) { + if (inSelectionMode || !canReply) return@pointerInput detectHorizontalDragGestures( onDragEnd = { val reached = swipeOffset.value * swipeSign >= swipeThresholdPx @@ -548,6 +559,7 @@ fun MessageItem( ), reactions = if (message.fromLocal) emojis.reversed() else emojis, myId = ourNode.user.id, + canReact = canReact, onSendReaction = sendReaction, onShowReactions = onShowReactions, ) diff --git a/feature/messaging/src/commonMain/kotlin/org/meshtastic/feature/messaging/component/MessageScreenComponents.kt b/feature/messaging/src/commonMain/kotlin/org/meshtastic/feature/messaging/component/MessageScreenComponents.kt index 893793482e..296996cb11 100644 --- a/feature/messaging/src/commonMain/kotlin/org/meshtastic/feature/messaging/component/MessageScreenComponents.kt +++ b/feature/messaging/src/commonMain/kotlin/org/meshtastic/feature/messaging/component/MessageScreenComponents.kt @@ -15,7 +15,6 @@ * along with this program. If not, see . */ @file:Suppress("TooManyFunctions") -@file:OptIn(ExperimentalMaterial3ExpressiveApi::class) package org.meshtastic.feature.messaging.component @@ -42,7 +41,6 @@ import androidx.compose.material3.DropdownMenu import androidx.compose.material3.DropdownMenuGroup import androidx.compose.material3.DropdownMenuItem import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi import androidx.compose.material3.ExtendedFloatingActionButton import androidx.compose.material3.FloatingActionButton import androidx.compose.material3.HorizontalDivider diff --git a/feature/messaging/src/commonMain/kotlin/org/meshtastic/feature/messaging/component/Reaction.kt b/feature/messaging/src/commonMain/kotlin/org/meshtastic/feature/messaging/component/Reaction.kt index a26ef1c5e8..e2295721f1 100644 --- a/feature/messaging/src/commonMain/kotlin/org/meshtastic/feature/messaging/component/Reaction.kt +++ b/feature/messaging/src/commonMain/kotlin/org/meshtastic/feature/messaging/component/Reaction.kt @@ -135,6 +135,8 @@ internal fun ReactionRow( modifier: Modifier = Modifier, reactions: List = emptyList(), myId: String? = null, + /** False for an archived conversation: existing reactions still show, but nothing new can be added. */ + canReact: Boolean = true, onSendReaction: (String) -> Unit = {}, onShowReactions: () -> Unit = {}, ) { @@ -150,11 +152,13 @@ internal fun ReactionRow( emoji = emoji, emojiCount = reactions.size, status = localReaction?.status ?: MessageStatus.RECEIVED, - onClick = { onSendReaction(emoji) }, + onClick = { if (canReact) onSendReaction(emoji) }, onLongClick = onShowReactions, ) } - item { AddReactionButton(onSendReaction = onSendReaction) } + if (canReact) { + item { AddReactionButton(onSendReaction = onSendReaction) } + } } } } @@ -194,6 +198,8 @@ internal fun ReactionDialog( reactions: List, onDismiss: () -> Unit = {}, myId: String? = null, + /** False for an archived conversation: a failed reaction cannot be retried there. */ + canReact: Boolean = true, onResend: (Reaction) -> Unit = {}, ) = ModalBottomSheet( onDismissRequest = onDismiss, @@ -216,7 +222,8 @@ internal fun ReactionDialog( title = title, text = text, detail = getMessageStatusDetailRes(reaction.status, reaction.routingError), - resendOption = isMessageStatusRetryable(reaction.status, reaction.routingError, isDirectMessage), + resendOption = + canReact && isMessageStatusRetryable(reaction.status, reaction.routingError, isDirectMessage), onConfirm = { onResend(reaction) showStatusDialog = null diff --git a/feature/messaging/src/commonMain/kotlin/org/meshtastic/feature/messaging/component/ReactionPreviews.kt b/feature/messaging/src/commonMain/kotlin/org/meshtastic/feature/messaging/component/ReactionPreviews.kt index aca71e2932..9740bf2686 100644 --- a/feature/messaging/src/commonMain/kotlin/org/meshtastic/feature/messaging/component/ReactionPreviews.kt +++ b/feature/messaging/src/commonMain/kotlin/org/meshtastic/feature/messaging/component/ReactionPreviews.kt @@ -48,7 +48,7 @@ private fun ReactionRowPreview() { listOf( Reaction( replyId = 1, - user = User(), + user = User.Builder().build(), emoji = "\uD83D\uDE42", timestamp = 1L, snr = -1.0f, @@ -57,7 +57,7 @@ private fun ReactionRowPreview() { ), Reaction( replyId = 1, - user = User(), + user = User.Builder().build(), emoji = "\uD83D\uDE42", timestamp = 1L, snr = -1.0f, diff --git a/feature/messaging/src/commonMain/kotlin/org/meshtastic/feature/messaging/ui/contact/ContactItem.kt b/feature/messaging/src/commonMain/kotlin/org/meshtastic/feature/messaging/ui/contact/ContactItem.kt index e423ee00fa..74ebfd5001 100644 --- a/feature/messaging/src/commonMain/kotlin/org/meshtastic/feature/messaging/ui/contact/ContactItem.kt +++ b/feature/messaging/src/commonMain/kotlin/org/meshtastic/feature/messaging/ui/contact/ContactItem.kt @@ -14,8 +14,6 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -@file:OptIn(ExperimentalMaterial3ExpressiveApi::class) - package org.meshtastic.feature.messaging.ui.contact import androidx.compose.animation.AnimatedVisibility @@ -37,9 +35,9 @@ import androidx.compose.material3.AssistChip import androidx.compose.material3.AssistChipDefaults import androidx.compose.material3.Card import androidx.compose.material3.CardDefaults -import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment @@ -51,18 +49,22 @@ import androidx.compose.ui.semantics.semantics import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.tooling.preview.PreviewLightDark import androidx.compose.ui.unit.dp import org.jetbrains.compose.resources.stringResource import org.meshtastic.core.common.util.DateFormatter import org.meshtastic.core.model.Contact import org.meshtastic.core.model.ContactKey import org.meshtastic.core.resources.Res +import org.meshtastic.core.resources.archived_channel import org.meshtastic.core.resources.contact_draft_prefix import org.meshtastic.core.resources.contact_pinned import org.meshtastic.core.ui.component.SecurityIcon +import org.meshtastic.core.ui.icon.History import org.meshtastic.core.ui.icon.Keep import org.meshtastic.core.ui.icon.MeshtasticIcons import org.meshtastic.core.ui.icon.VolumeOff +import org.meshtastic.core.ui.theme.AppTheme import org.meshtastic.proto.ChannelSet @Suppress("LongMethod") @@ -146,7 +148,15 @@ private fun ContactHeader( // Show unlock icon for broadcast with default PSK val isBroadcast = with(contact.contactKey) { getOrNull(1) == '^' || endsWith("^all") || endsWith("^broadcast") } - if (isBroadcast && channels != null) { + if (contact.isRetired) { + // The channel is gone, so there is no key state left to report; say the conversation is archived instead. + Icon( + imageVector = MeshtasticIcons.History, + contentDescription = stringResource(Res.string.archived_channel), + tint = MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier.size(16.dp), + ) + } else if (isBroadcast && channels != null) { ContactKey(contact.contactKey).channelOrNull?.let { index -> SecurityIcon(channels, index) } } @@ -228,3 +238,51 @@ private fun ChatMetadata(contact: Contact, modifier: Modifier = Modifier) { } } } + +/** + * A live channel row above the same conversation once its channel has left the radio. + * + * The archived row is the point: it keeps the name the channel had (its slot now belongs to someone else), swaps the + * key-state lock for a history icon, and is flagged unmessageable. + */ +@PreviewLightDark +@Composable +fun ContactItemArchivedPreview() { + AppTheme { + Surface { + Column { + ContactItem( + contact = + Contact( + contactKey = ContactKey.broadcast(0).value, + shortName = "0", + longName = "LongFast", + lastMessageTime = null, + lastMessageText = "On the air", + unreadCount = 0, + messageCount = 1, + isMuted = false, + isUnmessageable = false, + ), + selected = false, + ) + ContactItem( + contact = + Contact( + contactKey = ContactKey.retiredBroadcast("a1b2c3d4").value, + shortName = "VCFM", + longName = "VCFMW", + lastMessageTime = null, + lastMessageText = "See you next year", + unreadCount = 0, + messageCount = 42, + isMuted = false, + isUnmessageable = true, + isRetired = true, + ), + selected = false, + ) + } + } + } +} diff --git a/feature/messaging/src/commonMain/kotlin/org/meshtastic/feature/messaging/ui/contact/Contacts.kt b/feature/messaging/src/commonMain/kotlin/org/meshtastic/feature/messaging/ui/contact/Contacts.kt index 2cde583f3f..d39130cc16 100644 --- a/feature/messaging/src/commonMain/kotlin/org/meshtastic/feature/messaging/ui/contact/Contacts.kt +++ b/feature/messaging/src/commonMain/kotlin/org/meshtastic/feature/messaging/ui/contact/Contacts.kt @@ -14,8 +14,6 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -@file:OptIn(ExperimentalMaterial3ExpressiveApi::class) - package org.meshtastic.feature.messaging.ui.contact import androidx.compose.foundation.background @@ -32,7 +30,6 @@ import androidx.compose.foundation.lazy.LazyListState import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.selection.selectable import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.LocalContentColor @@ -571,8 +568,10 @@ private fun ContactListView( // rise to the top of their own section — Kotlin's sort is stable, so the order below is preserved within // each group. Pinning matters most for DMs, where an active conversation otherwise pushes others down; // channels already have a deterministic order, so pinning one just reorders that. - channelPart.sortedBy { ContactKey(it.contactKey).channel }.sortedByDescending { it.isPinned } to - dmPart.sortedByDescending { it.isPinned } + // A retired channel has no slot, so it sorts after every live one rather than ahead of the primary. + channelPart + .sortedBy { ContactKey(it.contactKey).channelOrNull ?: Int.MAX_VALUE } + .sortedByDescending { it.isPinned } to dmPart.sortedByDescending { it.isPinned } } val channelsTitle = stringResource(Res.string.channels) val dmTitle = stringResource(Res.string.direct_messages) diff --git a/feature/messaging/src/commonMain/kotlin/org/meshtastic/feature/messaging/ui/contact/ContactsViewModel.kt b/feature/messaging/src/commonMain/kotlin/org/meshtastic/feature/messaging/ui/contact/ContactsViewModel.kt index df19b6cc82..6c4dad5c94 100644 --- a/feature/messaging/src/commonMain/kotlin/org/meshtastic/feature/messaging/ui/contact/ContactsViewModel.kt +++ b/feature/messaging/src/commonMain/kotlin/org/meshtastic/feature/messaging/ui/contact/ContactsViewModel.kt @@ -71,7 +71,8 @@ class ContactsViewModel( val unreadCountTotal = packetRepository.getUnreadCountTotal().stateInWhileSubscribed(0) - val channels = radioConfigRepository.channelSetFlow.stateInWhileSubscribed(initialValue = ChannelSet()) + val channels = + radioConfigRepository.channelSetFlow.stateInWhileSubscribed(initialValue = ChannelSet.Builder().build()) // Combine node info and myId to reduce argument count in subsequent combines private val identityFlow: Flow> = @@ -115,6 +116,9 @@ class ContactsViewModel( // Determine if this is my message (originated on this device) val fromLocal = packetData.isFromLocal(myNodeNum) val toBroadcast = packetData.isBroadcast + // A retired conversation's channel has left the radio. Its rows keep the index they were retired + // from, so labelling it by slot would name whichever channel took the slot over. + val isRetired = ContactKey(contactKey).isRetired // grab usernames from NodeInfo val userId = if (fromLocal) packetData.to else packetData.from @@ -123,15 +127,23 @@ class ContactsViewModel( val shortName = user.short_name val longName = - if (toBroadcast) { - channelSet.getChannel(packetData.channel)?.name ?: "Channel ${packetData.channel}" - } else { - user.long_name + when { + isRetired -> settings[contactKey]?.displayName.orEmpty() + + toBroadcast -> + channelSet.getChannel(packetData.channel)?.name ?: "Channel ${packetData.channel}" + + else -> user.long_name } Contact( contactKey = contactKey, - shortName = if (toBroadcast) packetData.channel.toString() else shortName, + shortName = + when { + isRetired -> longName.take(RETIRED_SHORT_NAME_LENGTH) + toBroadcast -> packetData.channel.toString() + else -> shortName + }, longName = longName, lastMessageTime = if (packetData.time != 0L) packetData.time else null, lastMessageText = if (fromLocal) packetData.text else "$shortName: ${packetData.text}", @@ -141,8 +153,11 @@ class ContactsViewModel( draft = settings[contactKey]?.draft.orEmpty(), isPinned = settings[contactKey]?.pinned == true, // Keyless DM threads stay in the list (never hide a conversation) but are flagged: without a - // public key the radio refuses to send, the same as an unmessagable node. - isUnmessageable = (user.is_unmessagable ?: false) || (!toBroadcast && node.num !in keyedNodes), + // public key the radio refuses to send, the same as an unmessagable node. A retired channel has + // no slot at all, so nothing can be sent there either. + isUnmessageable = + isRetired || (user.is_unmessagable ?: false) || (!toBroadcast && node.num !in keyedNodes), + isRetired = isRetired, nodeColors = if (!toBroadcast) { node.colors @@ -195,6 +210,9 @@ class ContactsViewModel( companion object { private const val KEY_COLLAPSED_SECTIONS = "collapsed_contact_sections" + + /** A retired channel has no slot number to show as an avatar, so its name is abbreviated instead. */ + private const val RETIRED_SHORT_NAME_LENGTH = 4 } } @@ -206,7 +224,10 @@ enum class ContactSection(val key: String) { DIRECT_MESSAGES("direct_messages"), } -/** Channel/broadcast contact keys carry a leading channel-digit prefix (e.g. `"0^all"`); DM keys don't. */ +/** + * Channel/broadcast contact keys carry a leading channel-digit prefix (e.g. `"0^all"`); DM keys don't. A retired + * channel still ends `^all`, so it stays with the channels it used to be one of. + */ fun Contact.section(): ContactSection = if (contactKey.getOrNull(1) == '^' || contactKey.endsWith("^all") || contactKey.endsWith("^broadcast")) { ContactSection.CHANNELS diff --git a/feature/messaging/src/commonMain/kotlin/org/meshtastic/feature/messaging/ui/sharing/Share.kt b/feature/messaging/src/commonMain/kotlin/org/meshtastic/feature/messaging/ui/sharing/Share.kt index adeb4b28df..1b07e39d8f 100644 --- a/feature/messaging/src/commonMain/kotlin/org/meshtastic/feature/messaging/ui/sharing/Share.kt +++ b/feature/messaging/src/commonMain/kotlin/org/meshtastic/feature/messaging/ui/sharing/Share.kt @@ -26,6 +26,7 @@ import androidx.compose.material3.Button import androidx.compose.material3.Icon import androidx.compose.material3.Scaffold import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember @@ -48,13 +49,21 @@ import org.meshtastic.feature.messaging.ui.contact.ContactsViewModel @Composable fun ShareScreen(viewModel: ContactsViewModel, onConfirm: (String) -> Unit, onNavigateUp: () -> Unit) { val contactList by viewModel.contactList.collectAsStateWithLifecycle() + // A retired channel has left the radio, so it can receive nothing; offering it here would fail silently. + val shareable = remember(contactList) { contactList.filterNot { it.isRetired } } - ShareScreen(contacts = contactList, onConfirm = onConfirm, onNavigateUp = onNavigateUp) + ShareScreen(contacts = shareable, onConfirm = onConfirm, onNavigateUp = onNavigateUp) } @Composable fun ShareScreen(contacts: List, onConfirm: (String) -> Unit, onNavigateUp: () -> Unit) { var selectedContact by remember { mutableStateOf("") } + // A conversation can leave the list while the sheet is open, most obviously when its channel is archived mid + // share. Drop the selection with it rather than leaving the button armed on a key that can no longer receive. + val selectionIsSendable = contacts.any { it.contactKey == selectedContact } + LaunchedEffect(contacts) { + if (selectedContact.isNotEmpty() && contacts.none { it.contactKey == selectedContact }) selectedContact = "" + } Scaffold( topBar = { @@ -86,9 +95,9 @@ fun ShareScreen(contacts: List, onConfirm: (String) -> Unit, onNavigate } Button( - onClick = { onConfirm(selectedContact) }, + onClick = { if (selectionIsSendable) onConfirm(selectedContact) }, modifier = Modifier.fillMaxWidth().padding(24.dp), - enabled = selectedContact.isNotEmpty(), + enabled = selectionIsSendable, ) { Icon(imageVector = MeshtasticIcons.Send, contentDescription = stringResource(Res.string.share)) } diff --git a/feature/messaging/src/commonTest/kotlin/org/meshtastic/feature/messaging/MessageInputEnterKeyTest.kt b/feature/messaging/src/commonTest/kotlin/org/meshtastic/feature/messaging/MessageInputEnterKeyTest.kt new file mode 100644 index 0000000000..b80be64269 --- /dev/null +++ b/feature/messaging/src/commonTest/kotlin/org/meshtastic/feature/messaging/MessageInputEnterKeyTest.kt @@ -0,0 +1,105 @@ +/* + * 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 . + */ +package org.meshtastic.feature.messaging + +import androidx.compose.foundation.text.input.TextFieldState +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.ui.input.key.Key +import androidx.compose.ui.test.ExperimentalTestApi +import androidx.compose.ui.test.hasSetTextAction +import androidx.compose.ui.test.performKeyInput +import androidx.compose.ui.test.pressKey +import androidx.compose.ui.test.requestFocus +import androidx.compose.ui.test.v2.runComposeUiTest +import androidx.compose.ui.test.withKeyDown +import kotlinx.collections.immutable.persistentMapOf +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertFalse +import kotlin.test.assertTrue + +/** + * Enter-to-send is a physical-keyboard shortcut (#5246). These run on the desktop target, where every key event is + * physical, so they pin that half of the contract; the on-screen keyboard is exempted by isFromSoftKeyboard and cannot + * be driven from here. + */ +@OptIn(ExperimentalTestApi::class) +class MessageInputEnterKeyTest { + + @Test + fun enterSendsAndLeavesTheTextAlone() = runComposeUiTest { + var sent = 0 + val textFieldState = TextFieldState(DRAFT) + setContent { Composer(textFieldState) { sent++ } } + + val field = onNode(hasSetTextAction()) + field.requestFocus() + field.performKeyInput { pressKey(Key.Enter) } + waitForIdle() + + assertEquals(1, sent, "Enter on a physical keyboard should send") + assertEquals(DRAFT, textFieldState.text.toString(), "sending must not leave a trailing newline behind") + } + + @Test + fun shiftEnterInsertsANewlineAndDoesNotSend() = runComposeUiTest { + var sent = 0 + val textFieldState = TextFieldState(DRAFT) + setContent { Composer(textFieldState) { sent++ } } + + val field = onNode(hasSetTextAction()) + field.requestFocus() + field.performKeyInput { withKeyDown(Key.ShiftLeft) { pressKey(Key.Enter) } } + waitForIdle() + + assertEquals(0, sent, "Shift+Enter composes, it does not send") + assertTrue(textFieldState.text.contains('\n'), "Shift+Enter should insert a newline") + } + + @Test + fun enterOnAnEmptyComposerSendsNothing() = runComposeUiTest { + var sent = 0 + val textFieldState = TextFieldState("") + setContent { Composer(textFieldState) { sent++ } } + + val field = onNode(hasSetTextAction()) + field.requestFocus() + field.performKeyInput { pressKey(Key.Enter) } + waitForIdle() + + assertEquals(0, sent, "an empty composer has nothing to send") + assertFalse(textFieldState.text.contains('\n'), "the consumed Enter must not fall through to the field") + } + + @Composable + private fun Composer(textFieldState: TextFieldState, onSendMessage: () -> Unit) { + MaterialTheme { + MessageInput( + isEnabled = true, + isHomoglyphEncodingEnabled = false, + textFieldState = textFieldState, + mentionCandidates = persistentMapOf(), + onSendMessage = onSendMessage, + ) + } + } + + private companion object { + const val DRAFT = "hello mesh" + } +} diff --git a/feature/messaging/src/commonTest/kotlin/org/meshtastic/feature/messaging/MessageViewModelTest.kt b/feature/messaging/src/commonTest/kotlin/org/meshtastic/feature/messaging/MessageViewModelTest.kt index 6e6846e37a..cd45f9e619 100644 --- a/feature/messaging/src/commonTest/kotlin/org/meshtastic/feature/messaging/MessageViewModelTest.kt +++ b/feature/messaging/src/commonTest/kotlin/org/meshtastic/feature/messaging/MessageViewModelTest.kt @@ -108,10 +108,10 @@ class MessageViewModelTest { contactSettingsFlow.value = emptyMap() // Core flows - MUST be separate every blocks - every { radioConfigRepository.channelSetFlow } returns MutableStateFlow(ChannelSet()) - every { radioConfigRepository.localConfigFlow } returns MutableStateFlow(LocalConfig()) - every { radioConfigRepository.moduleConfigFlow } returns MutableStateFlow(LocalModuleConfig()) - every { radioConfigRepository.deviceProfileFlow } returns MutableStateFlow(DeviceProfile()) + every { radioConfigRepository.channelSetFlow } returns MutableStateFlow(ChannelSet.Builder().build()) + every { radioConfigRepository.localConfigFlow } returns MutableStateFlow(LocalConfig.Builder().build()) + every { radioConfigRepository.moduleConfigFlow } returns MutableStateFlow(LocalModuleConfig.Builder().build()) + every { radioConfigRepository.deviceProfileFlow } returns MutableStateFlow(DeviceProfile.Builder().build()) every { connectionStateProvider.connectionState } returns connectionStateFlow diff --git a/feature/messaging/src/commonTest/kotlin/org/meshtastic/feature/messaging/MessageViewModelTranslationTest.kt b/feature/messaging/src/commonTest/kotlin/org/meshtastic/feature/messaging/MessageViewModelTranslationTest.kt index 5c1b7990e3..9c6bf3df8e 100644 --- a/feature/messaging/src/commonTest/kotlin/org/meshtastic/feature/messaging/MessageViewModelTranslationTest.kt +++ b/feature/messaging/src/commonTest/kotlin/org/meshtastic/feature/messaging/MessageViewModelTranslationTest.kt @@ -165,7 +165,7 @@ class MessageViewModelTranslationTest { } } - every { radioConfigRepository.channelSetFlow } returns MutableStateFlow(ChannelSet()) + every { radioConfigRepository.channelSetFlow } returns MutableStateFlow(ChannelSet.Builder().build()) every { connectionStateProvider.connectionState } returns MutableStateFlow(ConnectionState.Disconnected) every { customEmojiPrefs.customEmojiFrequency } returns MutableStateFlow(null) diff --git a/feature/messaging/src/commonTest/kotlin/org/meshtastic/feature/messaging/ui/contact/ContactsViewModelTest.kt b/feature/messaging/src/commonTest/kotlin/org/meshtastic/feature/messaging/ui/contact/ContactsViewModelTest.kt index 8ba31c5f2d..56ccd63888 100644 --- a/feature/messaging/src/commonTest/kotlin/org/meshtastic/feature/messaging/ui/contact/ContactsViewModelTest.kt +++ b/feature/messaging/src/commonTest/kotlin/org/meshtastic/feature/messaging/ui/contact/ContactsViewModelTest.kt @@ -60,7 +60,7 @@ class ContactsViewModelTest { every { connectionStateProvider.connectionState } returns MutableStateFlow(ConnectionState.Disconnected) every { packetRepository.getUnreadCountTotal() } returns MutableStateFlow(0) - every { radioConfigRepository.channelSetFlow } returns MutableStateFlow(ChannelSet()) + every { radioConfigRepository.channelSetFlow } returns MutableStateFlow(ChannelSet.Builder().build()) viewModel = ContactsViewModel( diff --git a/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/component/AdministrationSection.kt b/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/component/AdministrationSection.kt index bdf5c2c5ff..d19b5136dc 100644 --- a/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/component/AdministrationSection.kt +++ b/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/component/AdministrationSection.kt @@ -14,8 +14,6 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -@file:OptIn(ExperimentalMaterial3ExpressiveApi::class) - package org.meshtastic.feature.node.component import androidx.compose.animation.AnimatedVisibility @@ -25,7 +23,6 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.material3.AssistChip import androidx.compose.material3.AssistChipDefaults -import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi import androidx.compose.material3.LinearWavyProgressIndicator import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable diff --git a/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/component/AirQualityMetrics.kt b/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/component/AirQualityMetrics.kt index b7498faa57..43bb16959d 100644 --- a/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/component/AirQualityMetrics.kt +++ b/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/component/AirQualityMetrics.kt @@ -16,11 +16,11 @@ */ package org.meshtastic.feature.node.component +import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.runtime.remember import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.vector.ImageVector -import org.jetbrains.compose.resources.StringResource import org.jetbrains.compose.resources.stringResource import org.meshtastic.core.common.util.NumberFormatter import org.meshtastic.core.common.util.nowSeconds @@ -37,6 +37,8 @@ import org.meshtastic.core.resources.micrograms_per_cubic_meter import org.meshtastic.core.resources.pm10 import org.meshtastic.core.resources.pm1_0 import org.meshtastic.core.resources.pm2_5 +import org.meshtastic.core.resources.pm_sensor_status +import org.meshtastic.core.resources.pm_status_unknown_flags import org.meshtastic.core.resources.ppm import org.meshtastic.core.ui.component.Co2Severity import org.meshtastic.core.ui.component.PmAqiSeverity @@ -44,6 +46,8 @@ import org.meshtastic.core.ui.icon.AirQuality import org.meshtastic.core.ui.icon.Humidity import org.meshtastic.core.ui.icon.MeshtasticIcons import org.meshtastic.core.ui.icon.Temperature +import org.meshtastic.feature.node.metrics.PmStatusFault +import org.meshtastic.feature.node.model.MetricInfo import org.meshtastic.feature.node.model.VectorMetricInfo import org.meshtastic.proto.AirQualityMetrics import org.meshtastic.proto.Telemetry @@ -62,6 +66,7 @@ private fun nowCastAqi(pm25History: List): Pair? private fun buildAirQualityCards( metrics: AirQualityMetrics, aqiText: String?, + statusValues: List, ugm3: String, ppmUnit: String, icon: ImageVector, @@ -97,15 +102,38 @@ private fun buildAirQualityCards( }, ), ) + // One card per fault: the info card has no wrapping, so a joined list would make one very wide card. + statusValues.forEach { add(VectorMetricInfo(Res.string.pm_sensor_status, it, icon).asGroup()) } +} + +/** + * The PM sensor's status register decoded to one display string per set bit, each mapped to whether it is a fault + * (true) or a warning. Empty when the register is absent or 0 (a healthy sensor); the export carries it raw either way. + */ +@Composable +private fun pmStatusValues(flags: Int?): Map { + val nonZero = flags?.takeIf { it != 0 } ?: return emptyMap() + val faults = PmStatusFault.decode(nonZero).associate { stringResource(it.labelRes) to !it.isWarning } + val unknown = + PmStatusFault.unknownBitsHex(nonZero)?.let { stringResource(Res.string.pm_status_unknown_flags, it) to false } + return if (unknown == null) faults else faults + unknown } /** * Severity color for a metric's value text, or null to keep the default card color. The AQI tone is resolved by the - * caller because [PmAqiSeverity.color] is `@Composable`. + * caller because [PmAqiSeverity.color] is `@Composable`. Status cards share one label, so faults are told from warnings + * by value. */ -private fun metricValueColor(label: StringResource, co2Color: Color?, aqiColor: Color?): Color? = when (label) { +private fun metricValueColor( + metric: MetricInfo, + co2Color: Color?, + aqiColor: Color?, + faultColor: Color, + faultValues: Set, +): Color? = when (metric.label) { Res.string.co2 -> co2Color Res.string.aqi -> aqiColor + Res.string.pm_sensor_status -> faultColor.takeIf { metric.value in faultValues } else -> null } @@ -134,18 +162,35 @@ internal fun AirQualityInfoCards( aqi?.let { (value, severity) -> stringResource(Res.string.aqi_value_with_severity, value, stringResource(severity.labelRes)) } + val status = pmStatusValues(metrics.pm_status_flags) + val statusValues = status.keys.toList() val icon = MeshtasticIcons.AirQuality val tempIcon = MeshtasticIcons.Temperature val humidityIcon = MeshtasticIcons.Humidity val cards = - remember(metrics, aqiText, ugm3, ppmUnit, icon, tempIcon, humidityIcon, isFahrenheit) { - buildAirQualityCards(metrics, aqiText, ugm3, ppmUnit, icon, tempIcon, humidityIcon, isFahrenheit) + remember(metrics, aqiText, statusValues, ugm3, ppmUnit, icon, tempIcon, humidityIcon, isFahrenheit) { + buildAirQualityCards( + metrics, + aqiText, + statusValues, + ugm3, + ppmUnit, + icon, + tempIcon, + humidityIcon, + isFahrenheit, + ) } if (cards.none { it.isNotEmpty() }) return val co2Color = Co2Severity.fromPpm(metrics.co2 ?: 0)?.color val aqiColor = aqi?.second?.color() + val faultColor = MaterialTheme.colorScheme.error + val faultValues = status.filterValues { it }.keys - MetricCardFlow(groups = cards, valueColor = { metric -> metricValueColor(metric.label, co2Color, aqiColor) }) + MetricCardFlow( + groups = cards, + valueColor = { metric -> metricValueColor(metric, co2Color, aqiColor, faultColor, faultValues) }, + ) } diff --git a/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/component/CompassBottomSheet.kt b/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/component/CompassBottomSheet.kt index 01e7a0672b..d84cf9455e 100644 --- a/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/component/CompassBottomSheet.kt +++ b/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/component/CompassBottomSheet.kt @@ -14,8 +14,6 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -@file:OptIn(ExperimentalMaterial3ExpressiveApi::class) - package org.meshtastic.feature.node.component import androidx.compose.foundation.Canvas @@ -28,7 +26,6 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.width import androidx.compose.material3.Button -import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Surface diff --git a/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/component/EnvironmentMetrics.kt b/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/component/EnvironmentMetrics.kt index 920895c00f..8965833bcd 100644 --- a/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/component/EnvironmentMetrics.kt +++ b/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/component/EnvironmentMetrics.kt @@ -27,6 +27,7 @@ import org.meshtastic.core.model.util.UnitConversions.toTempString import org.meshtastic.core.model.util.adcVoltage import org.meshtastic.core.model.util.oneWireTemperature import org.meshtastic.core.model.util.toSmallDistanceString +import org.meshtastic.core.model.util.toStormDistanceString import org.meshtastic.core.resources.Res import org.meshtastic.core.resources.adc_voltage import org.meshtastic.core.resources.current @@ -35,9 +36,12 @@ import org.meshtastic.core.resources.distance import org.meshtastic.core.resources.gas_resistance import org.meshtastic.core.resources.humidity import org.meshtastic.core.resources.iaq +import org.meshtastic.core.resources.ic_bolt import org.meshtastic.core.resources.ic_dew_point import org.meshtastic.core.resources.ic_electric_bolt import org.meshtastic.core.resources.ic_radioactive +import org.meshtastic.core.resources.lightning_distance +import org.meshtastic.core.resources.lightning_strikes_1h import org.meshtastic.core.resources.lux import org.meshtastic.core.resources.one_wire_temperature import org.meshtastic.core.resources.pressure @@ -239,6 +243,27 @@ internal fun EnvironmentMetrics(node: Node, displayUnits: MeasurementSystem, isF .asGroup(), ) } + // Strike count and storm distance come from the same AS3935 detector, so they share a column. + add( + listOfNotNull( + lightning_strike_count_1h?.let { + DrawableMetricInfo( + label = Res.string.lightning_strikes_1h, + value = it.toString(), + icon = Res.drawable.ic_bolt, + ) + }, + lightning_distance_km + ?.takeUnless { it.isNaN() } + ?.let { + DrawableMetricInfo( + label = Res.string.lightning_distance, + value = it.toStormDistanceString(displayUnits), + icon = Res.drawable.ic_bolt, + ) + }, + ), + ) // 1-Wire probes and ADC inputs are independent channels, so one card each. Absent channels are null; a // reported 0°C or 0 V is a real reading and stays visible. for (channel in 0 until TELEMETRY_CHANNEL_COUNT) { diff --git a/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/component/NodeContextMenu.kt b/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/component/NodeContextMenu.kt index cc8d309629..c9b2a0d45d 100644 --- a/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/component/NodeContextMenu.kt +++ b/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/component/NodeContextMenu.kt @@ -14,14 +14,11 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -@file:OptIn(ExperimentalMaterial3ExpressiveApi::class) - package org.meshtastic.feature.node.component import androidx.compose.material3.DropdownMenu import androidx.compose.material3.DropdownMenuGroup import androidx.compose.material3.DropdownMenuItem -import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi import androidx.compose.material3.Icon import androidx.compose.material3.LocalContentColor import androidx.compose.material3.MaterialTheme diff --git a/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/component/NodeDetailComponentPreviews.kt b/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/component/NodeDetailComponentPreviews.kt index f775bf166c..ffb9e4ccde 100644 --- a/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/component/NodeDetailComponentPreviews.kt +++ b/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/component/NodeDetailComponentPreviews.kt @@ -170,14 +170,16 @@ fun PowerMetricsPreview() { val node = previewData.mickeyMouse.copy( powerMetrics = - org.meshtastic.proto.PowerMetrics( - ch1_voltage = 4.19f, - ch1_current = 128.4f, - ch2_voltage = 3.72f, - ch2_current = 12.5f, - ch3_voltage = 5.02f, - ch3_current = 431.7f, - ), + org.meshtastic.proto.PowerMetrics.Builder() + .also { wb -> + wb.ch1_voltage = 4.19f + wb.ch1_current = 128.4f + wb.ch2_voltage = 3.72f + wb.ch2_current = 12.5f + wb.ch3_voltage = 5.02f + wb.ch3_current = 431.7f + } + .build(), ) AppTheme { Surface { PowerMetrics(node = node) } } } @@ -189,7 +191,13 @@ fun PowerMetricsPartialPreview() { // Only channel 1 reports a voltage — a single column, matching the partial layout in issue #4507. val node = previewData.mickeyMouse.copy( - powerMetrics = org.meshtastic.proto.PowerMetrics(ch1_voltage = 4.19f, ch1_current = 128.4f), + powerMetrics = + org.meshtastic.proto.PowerMetrics.Builder() + .also { wb -> + wb.ch1_voltage = 4.19f + wb.ch1_current = 128.4f + } + .build(), ) AppTheme { Surface { PowerMetrics(node = node) } } } @@ -201,7 +209,13 @@ fun PowerMetricsNoCurrentPreview() { // Channels report voltage but no current at all — voltage-only columns, no fabricated 0.0mA cards. val node = previewData.mickeyMouse.copy( - powerMetrics = org.meshtastic.proto.PowerMetrics(ch1_voltage = 4.19f, ch2_voltage = 3.72f), + powerMetrics = + org.meshtastic.proto.PowerMetrics.Builder() + .also { wb -> + wb.ch1_voltage = 4.19f + wb.ch2_voltage = 3.72f + } + .build(), ) AppTheme { Surface { PowerMetrics(node = node) } } } @@ -217,20 +231,22 @@ fun EnvironmentMetricsPreview() { val node = previewData.mickeyMouse.copy( environmentMetrics = - org.meshtastic.proto.EnvironmentMetrics( - temperature = 21.5f, - relative_humidity = 47f, - barometric_pressure = 1013f, - gas_resistance = 1200f, - voltage = 4.19f, - current = 128.4f, - iaq = 62, - lux = 480f, - uv_lux = 12f, - soil_temperature = 18.2f, - soil_moisture = 33, - radiation = 0.15f, - ), + org.meshtastic.proto.EnvironmentMetrics.Builder() + .also { wb -> + wb.temperature = 21.5f + wb.relative_humidity = 47f + wb.barometric_pressure = 1013f + wb.gas_resistance = 1200f + wb.voltage = 4.19f + wb.current = 128.4f + wb.iaq = 62 + wb.lux = 480f + wb.uv_lux = 12f + wb.soil_temperature = 18.2f + wb.soil_moisture = 33 + wb.radiation = 0.15f + } + .build(), ) AppTheme { Surface { EnvironmentMetrics(node = node, displayUnits = MeasurementSystem.METRIC) } } } @@ -242,14 +258,97 @@ fun AirQualityInfoCardsPreview() { val node = previewData.mickeyMouse.copy( airQualityMetrics = - org.meshtastic.proto.AirQualityMetrics( - pm10_standard = 8, - pm25_standard = 12, - pm100_standard = 18, - co2 = 640, - co2_temperature = 22.1f, - co2_humidity = 44f, - ), + org.meshtastic.proto.AirQualityMetrics.Builder() + .also { wb -> + wb.pm10_standard = 8 + wb.pm25_standard = 12 + wb.pm100_standard = 18 + wb.co2 = 640 + wb.co2_temperature = 22.1f + wb.co2_humidity = 44f + } + .build(), + ) + AppTheme { Surface { AirQualityInfoCards(node = node) } } +} + +// --------------------------------------------------------------------------- +// SoilWaterMetrics / lightning / PM status previews +// --------------------------------------------------------------------------- + +/** + * Every soil and water field populated, so each probe group renders. Built in a function rather than a top-level `val` + * so the proto construction stays out of this file's class initializer. + */ +private fun sampleSoilWaterMetrics(): org.meshtastic.proto.SoilWaterMetrics = + org.meshtastic.proto.SoilWaterMetrics.Builder() + .also { wb -> + wb.soil_ph = 6.8f + wb.nitrogen = 42f + wb.phosphorus = 18f + wb.potassium = 96f + wb.ph = 7.4f + wb.electrical_conductivity = 1.24f + wb.salinity = 620f + wb.dissolved_oxygen = 8.3f + wb.orp = 215f + wb.chemical_oxygen_demand = 48f + wb.biochemical_oxygen_demand = 12f + wb.turbidity = 3.6f + wb.nitrate = 5.2f + wb.ammonium = 0.4f + wb.solar_irradiance = 780f + } + .build() + +@PreviewLightDark +@Suppress("PreviewPublic") +@Composable +fun SoilWaterMetricsPreview() { + val node = previewData.mickeyMouse.copy(soilWaterMetrics = sampleSoilWaterMetrics()) + AppTheme { Surface { SoilWaterMetrics(node = node) } } +} + +/** Temperature and humidity for context, plus the AS3935 pair that shares a column. */ +@PreviewLightDark +@Suppress("PreviewPublic") +@Composable +fun EnvironmentMetricsLightningPreview() { + val node = + previewData.mickeyMouse.copy( + environmentMetrics = + org.meshtastic.proto.EnvironmentMetrics.Builder() + .also { wb -> + wb.temperature = 19.4f + wb.relative_humidity = 71f + wb.lightning_strike_count_1h = 3 + wb.lightning_distance_km = 12f + } + .build(), + ) + AppTheme { Surface { EnvironmentMetrics(node = node, displayUnits = MeasurementSystem.METRIC) } } +} + +/** + * A status register setting a fault (fan, bit 4), a warning (fan speed, bit 21) and a bit this build does not name (bit + * 30) — the three value tones the status cards can take. + */ +@PreviewLightDark +@Suppress("PreviewPublic") +@Composable +fun AirQualityInfoCardsStatusPreview() { + val node = + previewData.mickeyMouse.copy( + airQualityMetrics = + org.meshtastic.proto.AirQualityMetrics.Builder() + .also { wb -> + wb.pm10_standard = 8 + wb.pm25_standard = 12 + wb.pm100_standard = 18 + wb.co2 = 640 + wb.pm_status_flags = (1 shl 4) or (1 shl 21) or (1 shl 30) + } + .build(), ) AppTheme { Surface { AirQualityInfoCards(node = node) } } } diff --git a/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/component/NodeDetailComponents.kt b/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/component/NodeDetailComponents.kt index d76933fbc7..3d2548eb1f 100644 --- a/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/component/NodeDetailComponents.kt +++ b/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/component/NodeDetailComponents.kt @@ -85,7 +85,6 @@ internal fun SectionCard( } } -@OptIn(ExperimentalFoundationApi::class) @Composable internal fun InfoItem( label: String, @@ -97,6 +96,28 @@ internal fun InfoItem( iconTint: Color = MaterialTheme.colorScheme.primary.copy(alpha = 0.8f), iconSize: Dp = 14.dp, onClick: (() -> Unit)? = null, +) = InfoItem( + label = label, + value = value, + modifier = modifier, + valueStyle = valueStyle, + valueColor = valueColor, + onClick = onClick, +) { + Icon(imageVector = icon, contentDescription = null, modifier = Modifier.size(iconSize), tint = iconTint) +} + +/** [InfoItem] with the leading glyph as a slot, for a composite icon that is not a single [ImageVector]. */ +@OptIn(ExperimentalFoundationApi::class) +@Composable +internal fun InfoItem( + label: String, + value: String, + modifier: Modifier = Modifier, + valueStyle: TextStyle = MaterialTheme.typography.titleMedium.copy(fontWeight = FontWeight.SemiBold), + valueColor: Color = MaterialTheme.colorScheme.onSurface, + onClick: (() -> Unit)? = null, + icon: @Composable () -> Unit, ) { val clipboard: Clipboard = LocalClipboard.current val coroutineScope = rememberCoroutineScope() @@ -121,7 +142,7 @@ internal fun InfoItem( }, ) { Row(verticalAlignment = Alignment.CenterVertically) { - Icon(imageVector = icon, contentDescription = null, modifier = Modifier.size(iconSize), tint = iconTint) + icon() Spacer(Modifier.width(6.dp)) Text( text = label, diff --git a/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/component/NodeDetailsSection.kt b/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/component/NodeDetailsSection.kt index f7c05510a4..3405c8ed55 100644 --- a/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/component/NodeDetailsSection.kt +++ b/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/component/NodeDetailsSection.kt @@ -60,6 +60,7 @@ import org.meshtastic.core.common.util.MetricFormatter import org.meshtastic.core.model.DeviceHardware import org.meshtastic.core.model.Node import org.meshtastic.core.model.NodeAddress +import org.meshtastic.core.model.NodeSecurityIndicator import org.meshtastic.core.model.util.formatUptime import org.meshtastic.core.resources.Res import org.meshtastic.core.resources.a11y_label_value @@ -78,18 +79,18 @@ import org.meshtastic.core.resources.public_key import org.meshtastic.core.resources.request_user_info import org.meshtastic.core.resources.role import org.meshtastic.core.resources.rssi -import org.meshtastic.core.resources.security_signed_node -import org.meshtastic.core.resources.security_signed_node_desc +import org.meshtastic.core.resources.security import org.meshtastic.core.resources.short_name import org.meshtastic.core.resources.snr import org.meshtastic.core.resources.status_message -import org.meshtastic.core.resources.supported import org.meshtastic.core.resources.transport import org.meshtastic.core.resources.uptime import org.meshtastic.core.resources.user_id -import org.meshtastic.core.ui.component.SignedNodeDialog +import org.meshtastic.core.ui.component.Glyph +import org.meshtastic.core.ui.component.NodeSecurityDialog import org.meshtastic.core.ui.component.determineSignalQuality import org.meshtastic.core.ui.component.label +import org.meshtastic.core.ui.component.title import org.meshtastic.core.ui.component.transportInfo import org.meshtastic.core.ui.icon.ArrowCircleUp import org.meshtastic.core.ui.icon.DeviceNumbers @@ -102,11 +103,8 @@ import org.meshtastic.core.ui.icon.MeshtasticIcons import org.meshtastic.core.ui.icon.Notes import org.meshtastic.core.ui.icon.Person import org.meshtastic.core.ui.icon.Rssi -import org.meshtastic.core.ui.icon.ShieldCheck import org.meshtastic.core.ui.icon.Snr -import org.meshtastic.core.ui.icon.Verified import org.meshtastic.core.ui.icon.role -import org.meshtastic.core.ui.theme.StatusColors.StatusGreen import org.meshtastic.core.ui.theme.StatusColors.StatusYellow import org.meshtastic.core.ui.util.LocalModemPreset import org.meshtastic.core.ui.util.LocalNoiseFloor @@ -198,10 +196,8 @@ private fun MainNodeDetails(node: Node, isLocal: Boolean, onRequestUserInfo: (() SectionDivider() TransportRow(node) } - if (node.manuallyVerified || node.signsPackets) { - SectionDivider() - VerificationRow(node) - } + SectionDivider() + SecurityRow(node, isLocal) val publicKey = node.publicKey ?: node.user.public_key if (publicKey.size > 0) { SectionDivider() @@ -352,42 +348,32 @@ private fun TransportRow(node: Node) { } } -/** Trust signals: automatic XEdDSA signing (left, tappable) and user-asserted key verification (right). */ +/** + * The node's security state, read through the same [NodeSecurityIndicator] the list row's glyph uses so the two can + * never contradict each other (design#149, point 9). Tapping it opens the explanation and the full legend. + */ @Composable -private fun VerificationRow(node: Node) { - Row(modifier = Modifier.fillMaxWidth()) { - if (node.signsPackets) { - SignedNodeItem(Modifier.weight(1f)) - } else { - Spacer(Modifier.weight(1f)) - } - if (node.manuallyVerified) { - InfoItem( - label = stringResource(Res.string.supported), - value = "Verified", - icon = MeshtasticIcons.Verified, - modifier = Modifier.weight(1f), - ) - } else { - Spacer(Modifier.weight(1f)) - } - } -} - -/** "Signed node" trust cell — tap opens the plain-language explanation ([SignedNodeDialog]). */ -@Composable -private fun SignedNodeItem(modifier: Modifier = Modifier) { +private fun SecurityRow(node: Node, isLocal: Boolean) { + val indicator = NodeSecurityIndicator.of(node, isOwnNode = isLocal) var showDialog by remember { mutableStateOf(false) } - if (showDialog) SignedNodeDialog(onDismiss = { showDialog = false }) - InfoItem( - label = stringResource(Res.string.security_signed_node), - value = stringResource(Res.string.security_signed_node_desc), - icon = MeshtasticIcons.ShieldCheck, - modifier = modifier, - iconTint = MaterialTheme.colorScheme.StatusGreen, - iconSize = 20.dp, - onClick = { showDialog = true }, - ) + if (showDialog) { + NodeSecurityDialog( + indicator = indicator, + key = node.publicKey ?: node.user.public_key, + onDismiss = { showDialog = false }, + ) + } + Row(modifier = Modifier.fillMaxWidth()) { + InfoItem( + label = stringResource(Res.string.security), + value = stringResource(indicator.title), + modifier = Modifier.weight(1f), + onClick = { showDialog = true }, + ) { + indicator.Glyph(Modifier.size(20.dp)) + } + Spacer(Modifier.weight(1f)) + } } @OptIn(ExperimentalFoundationApi::class, ExperimentalEncodingApi::class) diff --git a/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/component/NodeFilterTextField.kt b/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/component/NodeFilterTextField.kt index 83e27c819d..8a3ed94cff 100644 --- a/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/component/NodeFilterTextField.kt +++ b/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/component/NodeFilterTextField.kt @@ -14,8 +14,6 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -@file:OptIn(ExperimentalMaterial3ExpressiveApi::class) - package org.meshtastic.feature.node.component import androidx.compose.foundation.background @@ -28,6 +26,7 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.heightIn import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.wrapContentHeight import androidx.compose.foundation.text.KeyboardActions import androidx.compose.foundation.text.KeyboardOptions @@ -35,7 +34,6 @@ import androidx.compose.material3.Checkbox import androidx.compose.material3.DropdownMenu import androidx.compose.material3.DropdownMenuGroup import androidx.compose.material3.DropdownMenuItem -import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme @@ -68,40 +66,30 @@ import org.meshtastic.core.resources.node_filter_exclude_unheard import org.meshtastic.core.resources.node_filter_ignored import org.meshtastic.core.resources.node_filter_include_unknown import org.meshtastic.core.resources.node_filter_only_direct +import org.meshtastic.core.resources.node_filter_only_encrypted import org.meshtastic.core.resources.node_filter_only_online +import org.meshtastic.core.resources.node_filter_only_signed import org.meshtastic.core.resources.node_filter_placeholder import org.meshtastic.core.resources.node_filter_show_ignored import org.meshtastic.core.resources.node_filter_title import org.meshtastic.core.resources.node_sort_button import org.meshtastic.core.resources.node_sort_title +import org.meshtastic.core.ui.component.SignedNodeIcon import org.meshtastic.core.ui.icon.Close +import org.meshtastic.core.ui.icon.Lock import org.meshtastic.core.ui.icon.MeshtasticIcons import org.meshtastic.core.ui.icon.Search import org.meshtastic.core.ui.icon.Sort +import org.meshtastic.core.ui.theme.StatusColors.StatusGreen -@Suppress("LongParameterList") @Composable fun NodeFilterTextField( - modifier: Modifier = Modifier, filterText: String, onTextChange: (String) -> Unit, currentSortOption: NodeSortOption, onSortSelect: (NodeSortOption) -> Unit, - includeUnknown: Boolean, - onToggleIncludeUnknown: () -> Unit, - excludeInfrastructure: Boolean, - onToggleExcludeInfrastructure: () -> Unit, - onlyOnline: Boolean, - onToggleOnlyOnline: () -> Unit, - onlyDirect: Boolean, - onToggleOnlyDirect: () -> Unit, - showIgnored: Boolean, - onToggleShowIgnored: () -> Unit, - ignoredNodeCount: Int, - excludeUnheard: Boolean, - onToggleExcludeUnheard: () -> Unit, - excludeMqtt: Boolean, - onToggleExcludeMqtt: () -> Unit, + toggles: NodeFilterToggles, + modifier: Modifier = Modifier, ) { Column(modifier = modifier.background(MaterialTheme.colorScheme.background)) { Row { @@ -111,32 +99,15 @@ fun NodeFilterTextField( modifier = Modifier.align(Alignment.CenterVertically), currentSortOption = currentSortOption, onSortSelect = onSortSelect, - toggles = - NodeFilterToggles( - includeUnknown = includeUnknown, - onToggleIncludeUnknown = onToggleIncludeUnknown, - excludeInfrastructure = excludeInfrastructure, - onToggleExcludeInfrastructure = onToggleExcludeInfrastructure, - onlyOnline = onlyOnline, - onToggleOnlyOnline = onToggleOnlyOnline, - onlyDirect = onlyDirect, - onToggleOnlyDirect = onToggleOnlyDirect, - showIgnored = showIgnored, - onToggleShowIgnored = onToggleShowIgnored, - ignoredNodeCount = ignoredNodeCount, - excludeMqtt = excludeMqtt, - onToggleExcludeMqtt = onToggleExcludeMqtt, - excludeUnheard = excludeUnheard, - onToggleExcludeUnheard = onToggleExcludeUnheard, - ), + toggles = toggles, ) } - if (showIgnored) { + if (toggles.showIgnored) { Box( modifier = Modifier.fillMaxWidth() .background(MaterialTheme.colorScheme.surfaceDim) - .clickable { onToggleShowIgnored() } + .clickable { toggles.onToggleShowIgnored() } .padding(vertical = 16.dp, horizontal = 24.dp), ) { Text( @@ -167,6 +138,10 @@ data class NodeFilterToggles( val onToggleExcludeUnheard: () -> Unit, val excludeMqtt: Boolean, val onToggleExcludeMqtt: () -> Unit, + val onlySigned: Boolean, + val onToggleOnlySigned: () -> Unit, + val onlyEncrypted: Boolean, + val onToggleOnlyEncrypted: () -> Unit, ) @Composable @@ -294,6 +269,27 @@ private fun NodeSortButton( }, ) + DropdownMenuCheck( + text = stringResource(Res.string.node_filter_only_signed), + checked = toggles.onlySigned, + onClick = toggles.onToggleOnlySigned, + trailing = { SignedNodeIcon(Modifier.size(20.dp)) }, + ) + + DropdownMenuCheck( + text = stringResource(Res.string.node_filter_only_encrypted), + checked = toggles.onlyEncrypted, + onClick = toggles.onToggleOnlyEncrypted, + trailing = { + Icon( + imageVector = MeshtasticIcons.Lock, + contentDescription = null, + tint = MaterialTheme.colorScheme.StatusGreen, + modifier = Modifier.size(20.dp), + ) + }, + ) + DropdownMenuCheck( text = stringResource(Res.string.node_filter_exclude_mqtt), checked = toggles.excludeMqtt, diff --git a/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/component/NodeListItemPreviews.kt b/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/component/NodeListItemPreviews.kt index 1932f34fc7..ca6f9fe88f 100644 --- a/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/component/NodeListItemPreviews.kt +++ b/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/component/NodeListItemPreviews.kt @@ -84,8 +84,8 @@ fun NodeItemCompleteActivePreview() { @PreviewLightDark @Composable fun NodeItemUnheardPreview() { - // Heard recently enough to count as online, but not since the radio's LoRa config changed. The unheard marker - // has to win over the online tint here: green would claim it is reachable. + // Heard recently enough to count as online, but not on the LoRa config the radio is using now. The unheard + // marker has to win over the online tint here: green would claim it is reachable. val unheardNode = previewNodes.minnieMouse.copy( lastHeard = (org.meshtastic.core.common.util.nowSeconds - 300).toInt(), diff --git a/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/component/NotesSection.kt b/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/component/NotesSection.kt index 6347901da7..779a284c42 100644 --- a/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/component/NotesSection.kt +++ b/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/component/NotesSection.kt @@ -14,8 +14,6 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -@file:OptIn(ExperimentalMaterial3ExpressiveApi::class) - package org.meshtastic.feature.node.component import androidx.compose.foundation.layout.Column @@ -27,7 +25,6 @@ import androidx.compose.foundation.text.KeyboardActions import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.material3.CardDefaults import androidx.compose.material3.ElevatedCard -import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme diff --git a/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/component/SoilWaterMetrics.kt b/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/component/SoilWaterMetrics.kt new file mode 100644 index 0000000000..aaeaf197d7 --- /dev/null +++ b/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/component/SoilWaterMetrics.kt @@ -0,0 +1,103 @@ +/* + * 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 . + */ +package org.meshtastic.feature.node.component + +import androidx.compose.runtime.Composable +import androidx.compose.ui.graphics.vector.ImageVector +import org.jetbrains.compose.resources.StringResource +import org.meshtastic.core.common.util.NumberFormatter +import org.meshtastic.core.model.Node +import org.meshtastic.core.resources.Res +import org.meshtastic.core.resources.ammonium +import org.meshtastic.core.resources.biochemical_oxygen_demand +import org.meshtastic.core.resources.chemical_oxygen_demand +import org.meshtastic.core.resources.dissolved_oxygen +import org.meshtastic.core.resources.electrical_conductivity +import org.meshtastic.core.resources.nitrate +import org.meshtastic.core.resources.nitrogen +import org.meshtastic.core.resources.orp +import org.meshtastic.core.resources.phosphorus +import org.meshtastic.core.resources.potassium +import org.meshtastic.core.resources.salinity +import org.meshtastic.core.resources.soil_ph +import org.meshtastic.core.resources.solar_irradiance +import org.meshtastic.core.resources.turbidity +import org.meshtastic.core.resources.water_ph +import org.meshtastic.core.ui.icon.Humidity +import org.meshtastic.core.ui.icon.LightMode +import org.meshtastic.core.ui.icon.MeshtasticIcons +import org.meshtastic.core.ui.icon.SoilMoisture +import org.meshtastic.core.ui.icon.Voltage +import org.meshtastic.feature.node.model.VectorMetricInfo + +/** One reading as a card, or null when the probe did not report it. A reported 0 is a measurement and stays visible. */ +private fun reading( + label: StringResource, + value: Float?, + unit: String, + icon: ImageVector, + fractionDigits: Int, +): VectorMetricInfo? = value + ?.takeUnless { it.isNaN() } + ?.let { + val number = NumberFormatter.format(it, fractionDigits) + VectorMetricInfo(label, if (unit.isEmpty()) number else "$number $unit", icon) + } + +/** + * Displays soil-probe and water-sonde chemistry for a node. Readings one probe reports together share a column: the NPK + * probe's nutrients, conductivity with the salinity derived from it, the two oxygen demands, and the two nitrogen + * species. Units are the proto's, so no locale conversion applies. + */ +@Suppress("LongMethod") +@Composable +internal fun SoilWaterMetrics(node: Node) { + val soil = MeshtasticIcons.SoilMoisture + val water = MeshtasticIcons.Humidity + val groups: List = + with(node.soilWaterMetrics) { + listOf( + listOfNotNull(reading(Res.string.soil_ph, soil_ph, "", soil, 1)), + listOfNotNull( + reading(Res.string.nitrogen, nitrogen, "mg/kg", soil, 0), + reading(Res.string.phosphorus, phosphorus, "mg/kg", soil, 0), + reading(Res.string.potassium, potassium, "mg/kg", soil, 0), + ), + listOfNotNull(reading(Res.string.water_ph, ph, "", water, 1)), + listOfNotNull( + reading(Res.string.electrical_conductivity, electrical_conductivity, "mS/cm", water, 2), + reading(Res.string.salinity, salinity, "mg/l", water, 0), + ), + listOfNotNull(reading(Res.string.dissolved_oxygen, dissolved_oxygen, "mg/l", water, 1)), + listOfNotNull(reading(Res.string.orp, orp, "mV", MeshtasticIcons.Voltage, 0)), + listOfNotNull( + reading(Res.string.chemical_oxygen_demand, chemical_oxygen_demand, "mg/l", water, 0), + reading(Res.string.biochemical_oxygen_demand, biochemical_oxygen_demand, "mg/l", water, 0), + ), + listOfNotNull(reading(Res.string.turbidity, turbidity, "NTU", water, 1)), + listOfNotNull( + reading(Res.string.nitrate, nitrate, "ppm", water, 1), + reading(Res.string.ammonium, ammonium, "ppm", water, 1), + ), + listOfNotNull( + reading(Res.string.solar_irradiance, solar_irradiance, "W/m²", MeshtasticIcons.LightMode, 0), + ), + ) + } + + MetricCardFlow(groups = groups) +} diff --git a/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/component/TelemetricActionsSection.kt b/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/component/TelemetricActionsSection.kt index 42629c844c..fd1143422a 100644 --- a/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/component/TelemetricActionsSection.kt +++ b/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/component/TelemetricActionsSection.kt @@ -54,9 +54,11 @@ import org.meshtastic.core.resources.Res import org.meshtastic.core.resources.ic_air import org.meshtastic.core.resources.ic_person import org.meshtastic.core.resources.ic_thermostat +import org.meshtastic.core.resources.ic_water_drop import org.meshtastic.core.resources.logs import org.meshtastic.core.resources.request_air_quality_metrics import org.meshtastic.core.resources.request_telemetry +import org.meshtastic.core.resources.soil_water_metrics import org.meshtastic.core.resources.telemetry import org.meshtastic.core.resources.userinfo import org.meshtastic.core.ui.icon.MeshtasticIcons @@ -194,6 +196,16 @@ private fun rememberTelemetricFeatures( content = { node, _ -> AirQualityInfoCards(node, airQualityHistory, isFahrenheit) }, hasContent = { it.hasAirQualityMetrics }, ), + // Soil-water telemetry has no log screen and no on-demand request yet, so the row is cards only and appears + // only once the node has reported some. + TelemetricFeature( + titleRes = Res.string.soil_water_metrics, + icon = Res.drawable.ic_water_drop, + requestAction = null, + isVisible = { it.hasSoilWaterMetrics }, + content = { node, _ -> SoilWaterMetrics(node) }, + hasContent = { it.hasSoilWaterMetrics }, + ), TelemetricFeature( titleRes = LogsType.POWER.titleRes, icon = LogsType.POWER.icon, diff --git a/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/detail/NodeDetailContent.kt b/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/detail/NodeDetailContent.kt index cc1983108b..877eaa7816 100644 --- a/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/detail/NodeDetailContent.kt +++ b/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/detail/NodeDetailContent.kt @@ -14,8 +14,6 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -@file:OptIn(ExperimentalMaterial3ExpressiveApi::class) - package org.meshtastic.feature.node.detail import androidx.compose.animation.Crossfade @@ -27,7 +25,6 @@ import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyListState import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.material3.CircularWavyProgressIndicator -import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier diff --git a/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/detail/NodeDetailScreens.kt b/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/detail/NodeDetailScreens.kt index b79d792fe0..dccbae94f8 100644 --- a/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/detail/NodeDetailScreens.kt +++ b/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/detail/NodeDetailScreens.kt @@ -144,7 +144,15 @@ private fun NodeDetailScaffold( ) } - NodeDetailOverlays(activeOverlay, node, compassUiState, actualCompassViewModel, { activeOverlay = null }) { + val isLocalNode = node != null && node.num == uiState.ourNode?.num + NodeDetailOverlays( + activeOverlay, + node, + isLocalNode, + compassUiState, + actualCompassViewModel, + { activeOverlay = null }, + ) { viewModel.handleNodeMenuAction(NodeMenuAction.RequestPosition(it)) } } @@ -154,6 +162,7 @@ private fun NodeDetailScaffold( private fun NodeDetailOverlays( overlay: NodeDetailOverlay?, node: Node?, + isLocal: Boolean, compassUiState: CompassUiState, compassViewModel: CompassViewModel?, onDismiss: () -> Unit, @@ -181,7 +190,7 @@ private fun NodeDetailOverlays( } when (overlay) { - is NodeDetailOverlay.SharedContact -> node?.let { SharedContactDialog(it, onDismiss) } + is NodeDetailOverlay.SharedContact -> node?.let { SharedContactDialog(it, onDismiss, isOwnContact = isLocal) } is NodeDetailOverlay.FirmwareReleaseInfo -> NodeDetailBottomSheet(onDismiss) { FirmwareReleaseSheetContent(firmwareRelease = overlay.release) } diff --git a/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/domain/usecase/CommonGetNodeDetailsUseCase.kt b/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/domain/usecase/CommonGetNodeDetailsUseCase.kt index 5569cc67a2..c1b2c6330c 100644 --- a/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/domain/usecase/CommonGetNodeDetailsUseCase.kt +++ b/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/domain/usecase/CommonGetNodeDetailsUseCase.kt @@ -116,7 +116,7 @@ constructor( combine( nodeRepository.ourNodeInfo, nodeRepository.myNodeInfo, - radioConfigRepository.deviceProfileFlow.onStart { emit(DeviceProfile()) }, + radioConfigRepository.deviceProfileFlow.onStart { emit(DeviceProfile.Builder().build()) }, ) { ourNode, myInfo, profile -> IdentityGroup(ourNode, myInfo, profile) } diff --git a/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/domain/usecase/GetFilteredNodesUseCase.kt b/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/domain/usecase/GetFilteredNodesUseCase.kt index d84990e392..fcc2f4bde3 100644 --- a/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/domain/usecase/GetFilteredNodesUseCase.kt +++ b/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/domain/usecase/GetFilteredNodesUseCase.kt @@ -59,10 +59,14 @@ open class GetFilteredNodesUseCase constructor(private val nodeRepository: NodeR true } } + // The raw signing flag, not the version-gated indicator: the filter answers "has the radio verified + // this node's signature", which is what the badge beside it claims. + .filter { node -> if (filter.onlySigned) node.signsPackets else true } + .filter { node -> if (filter.onlyEncrypted) node.hasPKC && !node.mismatchKey else true } .filter { node -> if (filter.excludeMqtt) !node.viaMqtt else true } // The connected node is never unreachable from itself, and both row renderers already exempt it. .filter { node -> - if (filter.excludeUnheard) node.heardOnCurrentLora || node.num == ourNum else true + if (filter.excludeUnheard) !node.isUnheardOnCurrentLora || node.num == ourNum else true } } } diff --git a/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/list/NodeFilterPreferences.kt b/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/list/NodeFilterPreferences.kt index 57ccca82bb..5f1d759a69 100644 --- a/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/list/NodeFilterPreferences.kt +++ b/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/list/NodeFilterPreferences.kt @@ -24,13 +24,8 @@ import org.meshtastic.core.repository.UiPrefs @Single @Suppress("TooManyFunctions") open class NodeFilterPreferences constructor(private val uiPrefs: UiPrefs) { - open val includeUnknown = uiPrefs.includeUnknown - open val excludeInfrastructure = uiPrefs.excludeInfrastructure - open val onlyOnline = uiPrefs.onlyOnline - open val onlyDirect = uiPrefs.onlyDirect - open val showIgnored = uiPrefs.showIgnored - open val excludeMqtt = uiPrefs.excludeMqtt - open val excludeUnheard = uiPrefs.excludeUnheard + /** Every node-list filter as one value; see [org.meshtastic.core.repository.NodeFilterPrefs]. */ + open val filters = uiPrefs.nodeFilters // Node list layout preferences open val nodeListDensity = uiPrefs.nodeListDensity @@ -92,30 +87,38 @@ open class NodeFilterPreferences constructor(private val uiPrefs: UiPrefs) { } open fun toggleIncludeUnknown() { - uiPrefs.setIncludeUnknown(!includeUnknown.value) + uiPrefs.updateNodeFilters { it.copy(includeUnknown = !it.includeUnknown) } } open fun toggleExcludeInfrastructure() { - uiPrefs.setExcludeInfrastructure(!excludeInfrastructure.value) + uiPrefs.updateNodeFilters { it.copy(excludeInfrastructure = !it.excludeInfrastructure) } } open fun toggleOnlyOnline() { - uiPrefs.setOnlyOnline(!onlyOnline.value) + uiPrefs.updateNodeFilters { it.copy(onlyOnline = !it.onlyOnline) } } open fun toggleOnlyDirect() { - uiPrefs.setOnlyDirect(!onlyDirect.value) + uiPrefs.updateNodeFilters { it.copy(onlyDirect = !it.onlyDirect) } } open fun toggleShowIgnored() { - uiPrefs.setShowIgnored(!showIgnored.value) + uiPrefs.updateNodeFilters { it.copy(showIgnored = !it.showIgnored) } + } + + open fun toggleOnlySigned() { + uiPrefs.updateNodeFilters { it.copy(onlySigned = !it.onlySigned) } + } + + open fun toggleOnlyEncrypted() { + uiPrefs.updateNodeFilters { it.copy(onlyEncrypted = !it.onlyEncrypted) } } open fun toggleExcludeMqtt() { - uiPrefs.setExcludeMqtt(!excludeMqtt.value) + uiPrefs.updateNodeFilters { it.copy(excludeMqtt = !it.excludeMqtt) } } open fun toggleExcludeUnheard() { - uiPrefs.setExcludeUnheard(!excludeUnheard.value) + uiPrefs.updateNodeFilters { it.copy(excludeUnheard = !it.excludeUnheard) } } } diff --git a/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/list/NodeListScreen.kt b/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/list/NodeListScreen.kt index 08568c6924..0f37b3c935 100644 --- a/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/list/NodeListScreen.kt +++ b/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/list/NodeListScreen.kt @@ -17,6 +17,7 @@ package org.meshtastic.feature.node.list import androidx.compose.animation.core.animateFloatAsState +import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.background import androidx.compose.foundation.focusable import androidx.compose.foundation.layout.Arrangement @@ -35,7 +36,6 @@ import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.material3.Button import androidx.compose.material3.Card import androidx.compose.material3.CardDefaults -import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme @@ -99,6 +99,7 @@ import org.meshtastic.feature.node.component.LocalNodeContextMenu import org.meshtastic.feature.node.component.NodeContextMenu import org.meshtastic.feature.node.component.NodeCountSummary import org.meshtastic.feature.node.component.NodeFilterTextField +import org.meshtastic.feature.node.component.NodeFilterToggles import org.meshtastic.feature.node.component.NodeHopHistogramSheet import org.meshtastic.feature.node.component.NodeListHelp @@ -110,7 +111,7 @@ internal fun canEditStatusMessage(node: Node, ourNode: Node?, connectionState: C node.num == ourNode?.num && connectionState == ConnectionState.Connected && node.capabilities.supportsStatusMessage @Suppress("LongMethod", "CyclomaticComplexMethod") -@OptIn(ExperimentalMaterial3ExpressiveApi::class) +@OptIn(ExperimentalFoundationApi::class) @Composable fun NodeListScreen( navigateToNodeDetails: (Int) -> Unit, @@ -141,18 +142,12 @@ fun NodeListScreen( // radio is not something the node list may delete. ourNode and unfilteredNodes come from independent flows, so // the list can already contain the local node while ourNode is still null. Offer nothing until it is known, // rather than risk removing the user's own node from the radio. - val unheardNodes = - remember(unfilteredNodes, ourNode) { - val ourNum = ourNode?.num - if (ourNum == null) { - emptyList() - } else { - unfilteredNodes.filter { !it.heardOnCurrentLora && !it.isFavorite && it.num != ourNum } - } - } + val unheardNodes = remember(unfilteredNodes, ourNode) { selectRemovableUnheardNodes(unfilteredNodes, ourNode?.num) } val deviceImageUrls by viewModel.deviceImageUrls.collectAsStateWithLifecycle() val ignoredNodeCount = unfilteredNodes.count { it.isIgnored } + // No cache window here: this pane renders inside ThreePaneScaffold's LookaheadScope, where a + // prefetched item can reach the main placement pass before the lookahead pass has measured it. val listState = rememberLazyListState() val coroutineScope = rememberCoroutineScope() @@ -194,7 +189,7 @@ fun NodeListScreen( var showShareContact by remember { mutableStateOf(false) } if (showShareContact) { - SharedContactDialog(contact = ourNode, onDismiss = { showShareContact = false }) + SharedContactDialog(contact = ourNode, onDismiss = { showShareContact = false }, isOwnContact = true) } Scaffold( @@ -267,29 +262,35 @@ fun NodeListScreen( onRemoveAll = { unheardNodes.forEach(viewModel::removeNode) }, modifier = Modifier.fillMaxWidth().padding(horizontal = 4.dp), ) + val filterPrefs = viewModel.nodeFilterPreferences NodeFilterTextField( - modifier = Modifier.fillMaxWidth(), filterText = state.filter.filterText, onTextChange = { viewModel.nodeFilterText = it }, currentSortOption = state.sort, onSortSelect = viewModel::setSortOption, - includeUnknown = state.filter.includeUnknown, - onToggleIncludeUnknown = { viewModel.nodeFilterPreferences.toggleIncludeUnknown() }, - excludeInfrastructure = state.filter.excludeInfrastructure, - onToggleExcludeInfrastructure = { - viewModel.nodeFilterPreferences.toggleExcludeInfrastructure() - }, - onlyOnline = state.filter.onlyOnline, - onToggleOnlyOnline = { viewModel.nodeFilterPreferences.toggleOnlyOnline() }, - onlyDirect = state.filter.onlyDirect, - onToggleOnlyDirect = { viewModel.nodeFilterPreferences.toggleOnlyDirect() }, - showIgnored = state.filter.showIgnored, - onToggleShowIgnored = { viewModel.nodeFilterPreferences.toggleShowIgnored() }, - ignoredNodeCount = ignoredNodeCount, - excludeMqtt = state.filter.excludeMqtt, - onToggleExcludeMqtt = { viewModel.nodeFilterPreferences.toggleExcludeMqtt() }, - excludeUnheard = state.filter.excludeUnheard, - onToggleExcludeUnheard = { viewModel.nodeFilterPreferences.toggleExcludeUnheard() }, + modifier = Modifier.fillMaxWidth(), + toggles = + NodeFilterToggles( + includeUnknown = state.filter.includeUnknown, + onToggleIncludeUnknown = filterPrefs::toggleIncludeUnknown, + excludeInfrastructure = state.filter.excludeInfrastructure, + onToggleExcludeInfrastructure = filterPrefs::toggleExcludeInfrastructure, + onlyOnline = state.filter.onlyOnline, + onToggleOnlyOnline = filterPrefs::toggleOnlyOnline, + onlyDirect = state.filter.onlyDirect, + onToggleOnlyDirect = filterPrefs::toggleOnlyDirect, + showIgnored = state.filter.showIgnored, + onToggleShowIgnored = filterPrefs::toggleShowIgnored, + ignoredNodeCount = ignoredNodeCount, + excludeUnheard = state.filter.excludeUnheard, + onToggleExcludeUnheard = filterPrefs::toggleExcludeUnheard, + excludeMqtt = state.filter.excludeMqtt, + onToggleExcludeMqtt = filterPrefs::toggleExcludeMqtt, + onlySigned = state.filter.onlySigned, + onToggleOnlySigned = filterPrefs::toggleOnlySigned, + onlyEncrypted = state.filter.onlyEncrypted, + onToggleOnlyEncrypted = filterPrefs::toggleOnlyEncrypted, + ), ) } } diff --git a/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/list/NodeListViewModel.kt b/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/list/NodeListViewModel.kt index 57145c3877..8909463bc2 100644 --- a/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/list/NodeListViewModel.kt +++ b/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/list/NodeListViewModel.kt @@ -101,23 +101,6 @@ class NodeListViewModel( private val _nodeFilterText = savedStateHandle.getStateFlow(KEY_FILTER_TEXT, "") - private val filterToggles = - combine( - nodeFilterPreferences.includeUnknown, - nodeFilterPreferences.excludeInfrastructure, - nodeFilterPreferences.onlyOnline, - nodeFilterPreferences.onlyDirect, - nodeFilterPreferences.showIgnored, - ) { includeUnknown, excludeInfrastructure, onlyOnline, onlyDirect, showIgnored -> - NodeFilterToggles( - includeUnknown = includeUnknown, - excludeInfrastructure = excludeInfrastructure, - onlyOnline = onlyOnline, - onlyDirect = onlyDirect, - showIgnored = showIgnored, - ) - } - /** * The unheard filter may only narrow the list once the firmware has proven it reports the field AND the handshake's * NodeInfo install has completed. Between setFirmwareVersion flipping the capability and that install landing, @@ -129,23 +112,20 @@ class NodeListViewModel( reportsHeard && dbReady } + // Three flows regardless of how many filters exist: the store hands back all of them as one value. private val nodeFilter: Flow = - combine( - _nodeFilterText, - filterToggles, - nodeFilterPreferences.excludeMqtt, - nodeFilterPreferences.excludeUnheard, - unheardFilterAllowed, - ) { filterText, filterToggles, excludeMqtt, excludeUnheard, unheardAllowed -> + combine(_nodeFilterText, nodeFilterPreferences.filters, unheardFilterAllowed) { text, prefs, unheardAllowed -> NodeFilterState( - filterText = filterText, - includeUnknown = filterToggles.includeUnknown, - excludeInfrastructure = filterToggles.excludeInfrastructure, - onlyOnline = filterToggles.onlyOnline, - onlyDirect = filterToggles.onlyDirect, - showIgnored = filterToggles.showIgnored, - excludeMqtt = excludeMqtt, - excludeUnheard = excludeUnheard && unheardAllowed, + filterText = text, + includeUnknown = prefs.includeUnknown, + excludeInfrastructure = prefs.excludeInfrastructure, + onlyOnline = prefs.onlyOnline, + onlyDirect = prefs.onlyDirect, + showIgnored = prefs.showIgnored, + onlySigned = prefs.onlySigned, + onlyEncrypted = prefs.onlyEncrypted, + excludeMqtt = prefs.excludeMqtt, + excludeUnheard = prefs.excludeUnheard && unheardAllowed, ) } @@ -228,7 +208,7 @@ class NodeListViewModel( radioConfigRepository.replaceAllSettings(channelSet.settings) val newLoraConfig = channelSet.lora_config if (newLoraConfig != null) { - adminController.setLocalConfig(Config(lora = newLoraConfig)) + adminController.setLocalConfig(Config.Builder().also { wb -> wb.lora = newLoraConfig }.build()) } } @@ -276,6 +256,8 @@ data class NodeFilterState( val onlyOnline: Boolean = false, val onlyDirect: Boolean = false, val showIgnored: Boolean = false, + val onlySigned: Boolean = false, + val onlyEncrypted: Boolean = false, val excludeMqtt: Boolean = false, val excludeUnheard: Boolean = false, ) { @@ -287,14 +269,8 @@ data class NodeFilterState( excludeInfrastructure || onlyOnline || onlyDirect || + onlySigned || + onlyEncrypted || excludeMqtt || excludeUnheard } - -data class NodeFilterToggles( - val includeUnknown: Boolean = true, - val excludeInfrastructure: Boolean = false, - val onlyOnline: Boolean = false, - val onlyDirect: Boolean = false, - val showIgnored: Boolean = false, -) diff --git a/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/list/RemovableUnheardNodes.kt b/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/list/RemovableUnheardNodes.kt new file mode 100644 index 0000000000..94376ab9ee --- /dev/null +++ b/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/list/RemovableUnheardNodes.kt @@ -0,0 +1,36 @@ +/* + * 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 . + */ +package org.meshtastic.feature.node.list + +import org.meshtastic.core.model.Node + +/** + * The nodes the unheard banner counts and its one-tap clear would remove. + * + * Deliberately narrower than [Node.isUnheardOnCurrentLora] alone, because this one deletes: + * - **Favourites are exempt.** An explicit keep, and it is also what firmware marks a node added as a shared contact, + * which has never been heard over RF and would otherwise always qualify. + * - **The connected radio is exempt**, and nothing is offered at all until [ourNum] is known. It and the node list + * arrive on independent flows, so the list can hold the local node while [ourNum] is still null; offering then would + * risk removing the user's own node. + * - **MQTT nodes are exempt** via [Node.isUnheardOnCurrentLora]: they read unheard permanently, and removing one + * achieves nothing because the next uplinked packet brings it back. + */ +internal fun selectRemovableUnheardNodes(nodes: List, ourNum: Int?): List { + if (ourNum == null) return emptyList() + return nodes.filter { it.isUnheardOnCurrentLora && !it.isFavorite && it.num != ourNum } +} diff --git a/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/metrics/AirQualityMetrics.kt b/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/metrics/AirQualityMetrics.kt index 837dfa7f0d..47b83ba304 100644 --- a/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/metrics/AirQualityMetrics.kt +++ b/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/metrics/AirQualityMetrics.kt @@ -70,6 +70,8 @@ import org.meshtastic.core.resources.micrograms_per_cubic_meter import org.meshtastic.core.resources.pm10 import org.meshtastic.core.resources.pm1_0 import org.meshtastic.core.resources.pm2_5 +import org.meshtastic.core.resources.pm_sensor_status +import org.meshtastic.core.resources.pm_status_unknown_flags import org.meshtastic.core.resources.ppm import org.meshtastic.core.ui.component.Co2Severity import org.meshtastic.core.ui.component.PmAqiSeverity @@ -358,6 +360,27 @@ private fun AqiText(aqi: Int) { ) } +/** + * The PM sensor's decoded status register. A register at 0 is a healthy sensor and is not listed here (the CSV and the + * node DB export still carry it); a non-zero one names each fault it sets, plus any bits this build does not know. + */ +@Composable +private fun PmStatusText(flags: Int) { + val faults = remember(flags) { PmStatusFault.decode(flags) } + val unknownHex = remember(flags) { PmStatusFault.unknownBitsHex(flags) } + val parts = + faults.map { stringResource(it.labelRes) } + + listOfNotNull(unknownHex?.let { stringResource(Res.string.pm_status_unknown_flags, it) }) + // A warning-only register (fan speed) is not an error tone; unknown bits alone are not either. + val isFault = faults.any { !it.isWarning } + Text( + text = "${stringResource(Res.string.pm_sensor_status)}: ${parts.joinToString()}", + style = MaterialTheme.typography.bodySmall, + fontWeight = FontWeight.Medium, + color = if (isFault) MaterialTheme.colorScheme.error else MaterialTheme.colorScheme.onSurfaceVariant, + ) +} + @Composable private fun AirQualityMetricsCard( sample: AirQualitySample, @@ -421,6 +444,7 @@ private fun AirQualityMetricsCard( } } } + aq.pm_status_flags?.takeIf { it != 0 }?.let { PmStatusText(it) } } } } @@ -431,28 +455,50 @@ private fun AirQualityMetricsCard( fun PreviewAirQualityCards() { val readings = listOf( - Telemetry( - time = 1700000000, - air_quality_metrics = - AirQualityMetricsProto( - pm10_standard = 4, - pm25_standard = 9, - pm100_standard = 12, - co2 = 620, - co2_temperature = 21.5f, - co2_humidity = 58f, - ), - ) to "2023-11-14 20:13", - Telemetry( - time = 1700003600, - air_quality_metrics = - AirQualityMetricsProto(pm10_standard = 6, pm25_standard = 14, pm100_standard = 19, co2 = 1450), - ) to "2023-11-14 21:13", - Telemetry( - time = 1700007200, - air_quality_metrics = - AirQualityMetricsProto(pm10_standard = 11, pm25_standard = 25, pm100_standard = 33, co2 = 2300), - ) to "2023-11-14 22:13", + Telemetry.Builder() + .also { wb -> + wb.time = 1700000000 + wb.air_quality_metrics = + AirQualityMetricsProto.Builder() + .also { wb -> + wb.pm10_standard = 4 + wb.pm25_standard = 9 + wb.pm100_standard = 12 + wb.co2 = 620 + wb.co2_temperature = 21.5f + wb.co2_humidity = 58f + } + .build() + } + .build() to "2023-11-14 20:13", + Telemetry.Builder() + .also { wb -> + wb.time = 1700003600 + wb.air_quality_metrics = + AirQualityMetricsProto.Builder() + .also { wb -> + wb.pm10_standard = 6 + wb.pm25_standard = 14 + wb.pm100_standard = 19 + wb.co2 = 1450 + } + .build() + } + .build() to "2023-11-14 21:13", + Telemetry.Builder() + .also { wb -> + wb.time = 1700007200 + wb.air_quality_metrics = + AirQualityMetricsProto.Builder() + .also { wb -> + wb.pm10_standard = 11 + wb.pm25_standard = 25 + wb.pm100_standard = 33 + wb.co2 = 2300 + } + .build() + } + .build() to "2023-11-14 22:13", ) // Newest first, matching the list view; AQI is derived rather than read from the proto, so the first row has too // little history to show one. @@ -473,3 +519,39 @@ fun PreviewAirQualityCards() { } } } + +/** + * A log entry whose PM sensor reports a fault (fan, bit 4), a warning (fan speed, bit 21) and a bit this build does not + * name (bit 30), so the status line renders all three parts in its error tone. + */ +@PreviewLightDark +@Suppress("MagicNumber", "PreviewPublic") // fake data; public so :screenshot-tests can reference it +@Composable +fun PreviewAirQualityCardsStatus() { + val telemetry = + Telemetry.Builder() + .also { wb -> + wb.time = 1700000000 + wb.air_quality_metrics = + AirQualityMetricsProto.Builder() + .also { aq -> + aq.pm10_standard = 6 + aq.pm25_standard = 14 + aq.pm100_standard = 19 + aq.co2 = 820 + aq.pm_status_flags = (1 shl 4) or (1 shl 21) or (1 shl 30) + } + .build() + } + .build() + AppTheme { + Surface { + AirQualityMetricsCard( + sample = withNowCastAqi(listOf(telemetry)).single(), + isSelected = false, + onClick = {}, + timeTextOverride = "2023-11-14 22:13", + ) + } + } +} diff --git a/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/metrics/DeviceMetrics.kt b/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/metrics/DeviceMetrics.kt index 67bccc7498..4a7cc5a40a 100644 --- a/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/metrics/DeviceMetrics.kt +++ b/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/metrics/DeviceMetrics.kt @@ -378,17 +378,22 @@ private fun DeviceMetricsChartPreview() { val now = nowSeconds.toInt() val telemetries = List(20) { i -> - Telemetry( - time = now - (19 - i) * 60 * 60, // 1-hour intervals, oldest first - device_metrics = - org.meshtastic.proto.DeviceMetrics( - battery_level = 80 - i, - voltage = 3.7f - i * 0.02f, - channel_utilization = 10f + i * 2, - air_util_tx = 5f + i, - uptime_seconds = 3600 + i * 300, - ), - ) + Telemetry.Builder() + .also { wb -> + wb.time = now - (19 - i) * 60 * 60 + // 1-hour intervals, oldest first + wb.device_metrics = + org.meshtastic.proto.DeviceMetrics.Builder() + .also { wb -> + wb.battery_level = 80 - i + wb.voltage = 3.7f - i * 0.02f + wb.channel_utilization = 10f + i * 2 + wb.air_util_tx = 5f + i + wb.uptime_seconds = 3600 + i * 300 + } + .build() + } + .build() } AppTheme { DeviceMetricsChart( @@ -496,17 +501,21 @@ private fun DeviceMetricsCard( fun DeviceMetricsCardPreview() { val now = 1700000000 val telemetry = - Telemetry( - time = now, - device_metrics = - org.meshtastic.proto.DeviceMetrics( - battery_level = 75, - voltage = 3.65f, - channel_utilization = 22.5f, - air_util_tx = 12.0f, - uptime_seconds = 7200, - ), - ) + Telemetry.Builder() + .also { wb -> + wb.time = now + wb.device_metrics = + org.meshtastic.proto.DeviceMetrics.Builder() + .also { wb -> + wb.battery_level = 75 + wb.voltage = 3.65f + wb.channel_utilization = 22.5f + wb.air_util_tx = 12.0f + wb.uptime_seconds = 7200 + } + .build() + } + .build() AppTheme { DeviceMetricsCard( telemetry = telemetry, @@ -524,17 +533,22 @@ private fun DeviceMetricsScreenPreview() { val now = nowSeconds.toInt() val telemetries = List(24) { i -> - Telemetry( - time = now - (23 - i) * 60 * 60, // 1-hour intervals, oldest first - device_metrics = - org.meshtastic.proto.DeviceMetrics( - battery_level = 85 - i * 2, // Battery decreases over time - voltage = 3.8f - i * 0.01f, // Voltage decreases slightly - channel_utilization = 15f + i * 1.5f, // Channel utilization increases - air_util_tx = 8f + i * 0.8f, // Air utilization increases - uptime_seconds = 3600 + i * 3600, // Uptime increases by 1 hour each - ), - ) + Telemetry.Builder() + .also { wb -> + wb.time = now - (23 - i) * 60 * 60 + // 1-hour intervals, oldest first + wb.device_metrics = + org.meshtastic.proto.DeviceMetrics.Builder() + .also { wb -> + wb.battery_level = 85 - i * 2 // Battery decreases over time + wb.voltage = 3.8f - i * 0.01f // Voltage decreases slightly + wb.channel_utilization = 15f + i * 1.5f // Channel utilization increases + wb.air_util_tx = 8f + i * 0.8f // Air utilization increases + wb.uptime_seconds = 3600 + i * 3600 // Uptime increases by 1 hour each + } + .build() + } + .build() } AppTheme { diff --git a/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/metrics/EnvironmentMetrics.kt b/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/metrics/EnvironmentMetrics.kt index bfdf61d242..4f832a4570 100644 --- a/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/metrics/EnvironmentMetrics.kt +++ b/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/metrics/EnvironmentMetrics.kt @@ -51,6 +51,7 @@ import org.meshtastic.core.model.TelemetryType import org.meshtastic.core.model.util.TimeConstants.MS_PER_SEC import org.meshtastic.core.model.util.adcVoltage import org.meshtastic.core.model.util.oneWireTemperature +import org.meshtastic.core.model.util.toStormDistanceString import org.meshtastic.core.resources.Res import org.meshtastic.core.resources.adc_voltage import org.meshtastic.core.resources.current @@ -60,6 +61,8 @@ import org.meshtastic.core.resources.gas_resistance import org.meshtastic.core.resources.humidity import org.meshtastic.core.resources.iaq import org.meshtastic.core.resources.iaq_definition +import org.meshtastic.core.resources.lightning_distance +import org.meshtastic.core.resources.lightning_strikes_1h import org.meshtastic.core.resources.lux import org.meshtastic.core.resources.metric_channel_label import org.meshtastic.core.resources.one_wire_temperature @@ -506,6 +509,40 @@ private fun RainfallDisplay(envMetrics: org.meshtastic.proto.EnvironmentMetrics, } } +/** + * Strikes in the last hour and distance to the storm front, from the AS3935. Absent readings are null; 0 strikes is a + * real reading. + */ +@Composable +private fun LightningDisplay(envMetrics: org.meshtastic.proto.EnvironmentMetrics, isImperial: Boolean) { + val strikes = envMetrics.lightning_strike_count_1h + val distanceKm = envMetrics.lightning_distance_km?.takeIf { !it.isNaN() } + if (strikes == null && distanceKm == null) return + val system = if (isImperial) MeasurementSystem.IMPERIAL else MeasurementSystem.METRIC + + Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) { + strikes?.let { + Text( + text = formatString(LABELLED_VALUE, stringResource(Res.string.lightning_strikes_1h), it.toString()), + color = MaterialTheme.colorScheme.onSurface, + style = MaterialTheme.typography.labelLarge, + ) + } + distanceKm?.let { + Text( + text = + formatString( + LABELLED_VALUE, + stringResource(Res.string.lightning_distance), + it.toStormDistanceString(system), + ), + color = MaterialTheme.colorScheme.onSurface, + style = MaterialTheme.typography.labelLarge, + ) + } + } +} + /** * One row per reporting 1-Wire probe. Values arrive already converted to the display unit by the view model, so they * are only formatted here — a second conversion would double-count. An absent channel is `null`; 0°C is a real reading. @@ -579,7 +616,7 @@ private fun EnvironmentMetricsContent( isImperial: Boolean, timeTextOverride: String? = null, ) { - val envMetrics = telemetry.environment_metrics ?: org.meshtastic.proto.EnvironmentMetrics() + val envMetrics = telemetry.environment_metrics ?: org.meshtastic.proto.EnvironmentMetrics.Builder().build() val time = telemetry.time.toLong() * MS_PER_SEC Column(modifier = Modifier.fillMaxWidth().padding(12.dp)) { /* Time and Temperature */ @@ -606,6 +643,7 @@ private fun EnvironmentMetricsContent( RadiationDisplay(envMetrics) WindDisplay(envMetrics, isImperial) RainfallDisplay(envMetrics, isImperial) + LightningDisplay(envMetrics, isImperial) OneWireTemperatureDisplay(envMetrics, environmentDisplayFahrenheit) AdcVoltageDisplay(envMetrics) } @@ -616,27 +654,69 @@ private fun EnvironmentMetricsContent( @Composable fun PreviewEnvironmentMetricsContent() { val fakeEnvMetrics = - org.meshtastic.proto.EnvironmentMetrics( - temperature = 22.5f, - relative_humidity = 55.0f, - barometric_pressure = 1013.25f, - soil_moisture = 33, - soil_temperature = 18.0f, - lux = 100.0f, - uv_lux = 100.0f, - voltage = 3.7f, - current = 0.12f, - iaq = 100, - radiation = 0.15f, - gas_resistance = 1200.0f, - wind_speed = 5.2f, - wind_direction = 225, - wind_gust = 8.1f, - wind_lull = 2.3f, - rainfall_1h = 1.5f, - rainfall_24h = 12.3f, - ) - val fakeTelemetry = Telemetry(time = 1700000000, environment_metrics = fakeEnvMetrics) + org.meshtastic.proto.EnvironmentMetrics.Builder() + .also { wb -> + wb.temperature = 22.5f + wb.relative_humidity = 55.0f + wb.barometric_pressure = 1013.25f + wb.soil_moisture = 33 + wb.soil_temperature = 18.0f + wb.lux = 100.0f + wb.uv_lux = 100.0f + wb.voltage = 3.7f + wb.current = 0.12f + wb.iaq = 100 + wb.radiation = 0.15f + wb.gas_resistance = 1200.0f + wb.wind_speed = 5.2f + wb.wind_direction = 225 + wb.wind_gust = 8.1f + wb.wind_lull = 2.3f + wb.rainfall_1h = 1.5f + wb.rainfall_24h = 12.3f + } + .build() + val fakeTelemetry = + Telemetry.Builder() + .also { wb -> + wb.time = 1700000000 + wb.environment_metrics = fakeEnvMetrics + } + .build() + AppTheme { + Surface { + EnvironmentMetricsContent( + telemetry = fakeTelemetry, + environmentDisplayFahrenheit = false, + isImperial = false, + timeTextOverride = "2023-11-14 22:13", + ) + } + } +} + +/** A log entry from an AS3935-equipped node: strikes in the last hour and the distance to the storm front. */ +@PreviewLightDark +@Suppress("MagicNumber", "PreviewPublic") // fake data; public so :screenshot-tests can reference it +@Composable +fun PreviewEnvironmentMetricsContentLightning() { + val fakeEnvMetrics = + org.meshtastic.proto.EnvironmentMetrics.Builder() + .also { wb -> + wb.temperature = 19.4f + wb.relative_humidity = 71.0f + wb.barometric_pressure = 998.0f + wb.lightning_strike_count_1h = 3 + wb.lightning_distance_km = 12.0f + } + .build() + val fakeTelemetry = + Telemetry.Builder() + .also { wb -> + wb.time = 1700000000 + wb.environment_metrics = fakeEnvMetrics + } + .build() AppTheme { Surface { EnvironmentMetricsContent( diff --git a/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/metrics/MetricsViewModel.kt b/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/metrics/MetricsViewModel.kt index d6d445e490..9b48d3bb73 100644 --- a/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/metrics/MetricsViewModel.kt +++ b/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/metrics/MetricsViewModel.kt @@ -155,16 +155,19 @@ open class MetricsViewModel( // Each 1-Wire channel converts independently; an absent channel must stay null rather than // becoming a converted zero. var converted = - em.copy( - temperature = em.temperature?.let { UnitConversions.celsiusToFahrenheit(it) }, - soil_temperature = em.soil_temperature?.let { UnitConversions.celsiusToFahrenheit(it) }, - ) + em.newBuilder() + .also { wb -> + wb.temperature = em.temperature?.let { UnitConversions.celsiusToFahrenheit(it) } + wb.soil_temperature = + em.soil_temperature?.let { UnitConversions.celsiusToFahrenheit(it) } + } + .build() for (channel in 0 until TELEMETRY_CHANNEL_COUNT) { val celsius = em.oneWireTemperature(channel) ?: continue converted = converted.withOneWireTemperature(channel, UnitConversions.celsiusToFahrenheit(celsius)) } - telemetry.copy(environment_metrics = converted) + telemetry.newBuilder().also { wb -> wb.environment_metrics = converted }.build() } } else { data @@ -458,7 +461,8 @@ open class MetricsViewModel( header = "\"date\",\"time\",\"temperature\",\"relativeHumidity\",\"barometricPressure\"," + "\"gasResistance\",\"iaq\",\"windSpeed\",\"windDirection\",\"soilTemperature\"," + - "\"soilMoisture\",$oneWireHeaders,$adcHeaders\n", + "\"soilMoisture\",$oneWireHeaders,$adcHeaders," + + "\"lightningStrikeCount1h\",\"lightningDistanceKm\"\n", rows = data, epochSeconds = { it.time.toLong() }, ) { t -> @@ -476,7 +480,8 @@ open class MetricsViewModel( "\"${em?.barometric_pressure ?: ""}\",\"${em?.gas_resistance ?: ""}\"," + "\"${em?.iaq ?: ""}\",\"${em?.wind_speed ?: ""}\"," + "\"${em?.wind_direction ?: ""}\",\"${em?.soil_temperature ?: ""}\"," + - "\"${em?.soil_moisture ?: ""}\",$oneWireValues,$adcValues" + "\"${em?.soil_moisture ?: ""}\",$oneWireValues,$adcValues," + + "\"${em?.lightning_strike_count_1h ?: ""}\",\"${em?.lightning_distance_km ?: ""}\"" } } @@ -526,7 +531,7 @@ open class MetricsViewModel( "\"form_formaldehyde\",\"form_humidity\",\"form_temperature\"," + "\"pm40_standard\",\"particles_40um\"," + "\"pm_temperature\",\"pm_humidity\",\"pm_voc_idx\",\"pm_nox_idx\"," + - "\"particles_tps\"\n", + "\"particles_tps\",\"pm_status_flags\"\n", rows = data, epochSeconds = { it.telemetry.time.toLong() }, ) { sample -> @@ -558,7 +563,8 @@ open class MetricsViewModel( "\"${aq?.pm_humidity ?: ""}\"," + "\"${aq?.pm_voc_idx ?: ""}\"," + "\"${aq?.pm_nox_idx ?: ""}\"," + - "\"${aq?.particles_tps ?: ""}\"" + "\"${aq?.particles_tps ?: ""}\"," + + "\"${aq?.pm_status_flags ?: ""}\"" } } diff --git a/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/metrics/NeighborInfoLog.kt b/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/metrics/NeighborInfoLog.kt index a9a00ae9c0..f25941e556 100644 --- a/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/metrics/NeighborInfoLog.kt +++ b/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/metrics/NeighborInfoLog.kt @@ -50,7 +50,9 @@ import org.meshtastic.core.ui.icon.PersonOff import org.meshtastic.core.ui.icon.Refresh import org.meshtastic.core.ui.theme.StatusColors.StatusGreen import org.meshtastic.core.ui.theme.StatusColors.StatusOrange +import org.meshtastic.core.ui.theme.StatusColors.StatusRed import org.meshtastic.core.ui.theme.StatusColors.StatusYellow +import org.meshtastic.core.ui.util.LocalModemPreset import org.meshtastic.core.ui.util.annotateNeighborInfo import org.meshtastic.feature.node.component.CooldownIconButton @@ -65,6 +67,8 @@ fun NeighborInfoLogScreen(modifier: Modifier = Modifier, viewModel: MetricsViewM val statusGreen = MaterialTheme.colorScheme.StatusGreen val statusYellow = MaterialTheme.colorScheme.StatusYellow val statusOrange = MaterialTheme.colorScheme.StatusOrange + val statusRed = MaterialTheme.colorScheme.StatusRed + val modemPreset = LocalModemPreset.current Scaffold( topBar = { @@ -131,6 +135,8 @@ fun NeighborInfoLogScreen(modifier: Modifier = Modifier, viewModel: MetricsViewM statusGreen = statusGreen, statusYellow = statusYellow, statusOrange = statusOrange, + statusRed = statusRed, + modemPreset = modemPreset, ) viewModel.showLogDetail(Res.string.neighbor_info, message) } diff --git a/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/metrics/PmStatusFault.kt b/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/metrics/PmStatusFault.kt new file mode 100644 index 0000000000..5cb45ff249 --- /dev/null +++ b/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/metrics/PmStatusFault.kt @@ -0,0 +1,68 @@ +/* + * 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 . + */ +package org.meshtastic.feature.node.metrics + +import org.jetbrains.compose.resources.StringResource +import org.meshtastic.core.resources.Res +import org.meshtastic.core.resources.pm_status_co2_error +import org.meshtastic.core.resources.pm_status_fan_error +import org.meshtastic.core.resources.pm_status_fan_speed_warning +import org.meshtastic.core.resources.pm_status_gas_error +import org.meshtastic.core.resources.pm_status_hcho_error +import org.meshtastic.core.resources.pm_status_pm_error +import org.meshtastic.core.resources.pm_status_rht_error + +/** + * One bit of `AirQualityMetrics.pm_status_flags`, the PM sensor's raw status register. The bit layout is the SEN6X + * family's, as the proto documents it; the register is passed through unchanged, so other sensors may set other bits. + */ +@Suppress("MagicNumber") +internal enum class PmStatusFault(val bit: Int, val labelRes: StringResource) { + FAN_ERROR(4, Res.string.pm_status_fan_error), + RHT_ERROR(6, Res.string.pm_status_rht_error), + GAS_ERROR(7, Res.string.pm_status_gas_error), + CO2_ERROR_SEN66(9, Res.string.pm_status_co2_error), + HCHO_ERROR(10, Res.string.pm_status_hcho_error), + PM_ERROR(11, Res.string.pm_status_pm_error), + CO2_ERROR_SEN63C(12, Res.string.pm_status_co2_error), + FAN_SPEED_WARNING(21, Res.string.pm_status_fan_speed_warning), + ; + + val mask: Int + get() = 1 shl bit + + /** True for the bits the datasheet calls a warning rather than a fault; the sensor keeps reporting through them. */ + val isWarning: Boolean + get() = this == FAN_SPEED_WARNING + + companion object { + private const val HEX_RADIX = 16 + + /** The faults set in [flags], in bit order. Two bits sharing a label report that label once. */ + fun decode(flags: Int): List = + entries.filter { flags and it.mask != 0 }.distinctBy { it.labelRes } + + /** The bits of [flags] no [PmStatusFault] names, as a bitmask; 0 when every set bit is known. */ + fun unknownBits(flags: Int): Int = entries.fold(flags) { rest, fault -> rest and fault.mask.inv() } + + /** + * [unknownBits] as a hex bitmask for display, or null when every set bit is a named fault. The register is + * uint32 on the wire, so bit 31 renders as `80000000`, not a negative number. + */ + fun unknownBitsHex(flags: Int): String? = unknownBits(flags).takeIf { it != 0 }?.toUInt()?.toString(HEX_RADIX) + } +} diff --git a/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/metrics/PositionLogComponents.kt b/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/metrics/PositionLogComponents.kt index ddf843bbd6..ee37b6d576 100644 --- a/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/metrics/PositionLogComponents.kt +++ b/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/metrics/PositionLogComponents.kt @@ -14,8 +14,6 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -@file:OptIn(ExperimentalMaterial3ExpressiveApi::class) - package org.meshtastic.feature.node.metrics import androidx.compose.foundation.layout.Arrangement @@ -27,7 +25,6 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.width -import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable diff --git a/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/metrics/TimeFrameSelector.kt b/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/metrics/TimeFrameSelector.kt index 4289d14a8d..eb8722378b 100644 --- a/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/metrics/TimeFrameSelector.kt +++ b/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/metrics/TimeFrameSelector.kt @@ -37,6 +37,9 @@ fun TimeFrameSelector( ) { if (availableTimeFrames.size <= 1) return + // material3's ButtonGroup measures its overflow indicator with constraints.copy(maxWidth = remaining) + // without lowering minWidth, so under fillMaxWidth it throws once any chip overflows (#7141). + // Unfixed upstream: re-check that branch of ButtonGroup.kt before adopting it again. SingleChoiceSegmentedButtonRow(modifier = modifier.fillMaxWidth()) { availableTimeFrames.forEachIndexed { index, timeFrame -> val text = stringResource(timeFrame.strRes) diff --git a/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/metrics/TracerouteLog.kt b/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/metrics/TracerouteLog.kt index b38a244249..42d73f7cca 100644 --- a/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/metrics/TracerouteLog.kt +++ b/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/metrics/TracerouteLog.kt @@ -83,9 +83,12 @@ import org.meshtastic.core.ui.icon.Route import org.meshtastic.core.ui.theme.GraphColors import org.meshtastic.core.ui.theme.StatusColors.StatusGreen import org.meshtastic.core.ui.theme.StatusColors.StatusOrange +import org.meshtastic.core.ui.theme.StatusColors.StatusRed import org.meshtastic.core.ui.theme.StatusColors.StatusYellow +import org.meshtastic.core.ui.util.LocalModemPreset import org.meshtastic.core.ui.util.annotateTraceroute import org.meshtastic.feature.node.component.CooldownIconButton +import org.meshtastic.proto.Config.LoRaConfig.ModemPreset import org.meshtastic.proto.RouteDiscovery /** @@ -112,6 +115,8 @@ fun TracerouteLogScreen( val statusGreen = MaterialTheme.colorScheme.StatusGreen val statusYellow = MaterialTheme.colorScheme.StatusYellow val statusOrange = MaterialTheme.colorScheme.StatusOrange + val statusRed = MaterialTheme.colorScheme.StatusRed + val modemPreset = LocalModemPreset.current val headerTowardsStr = stringResource(Res.string.traceroute_route_towards_dest) val headerBackStr = stringResource(Res.string.traceroute_route_back_to_us) @@ -182,6 +187,8 @@ fun TracerouteLogScreen( statusGreen = statusGreen, statusYellow = statusYellow, statusOrange = statusOrange, + statusRed = statusRed, + modemPreset = modemPreset, onViewOnMap = onViewOnMap, ) }, @@ -320,6 +327,8 @@ private fun showTracerouteDetail( statusGreen: Color, statusYellow: Color, statusOrange: Color, + statusRed: Color, + modemPreset: ModemPreset?, onViewOnMap: (requestId: Int, responseLogUuid: String) -> Unit, ) { val result = point.result ?: return @@ -338,6 +347,8 @@ private fun showTracerouteDetail( statusGreen = statusGreen, statusYellow = statusYellow, statusOrange = statusOrange, + statusRed = statusRed, + modemPreset = modemPreset, ) val durationText = formatString(durationTemplate, NumberFormatter.format(seconds, 1)) buildAnnotatedString { diff --git a/feature/node/src/commonTest/kotlin/org/meshtastic/feature/node/compass/CompassViewModelTest.kt b/feature/node/src/commonTest/kotlin/org/meshtastic/feature/node/compass/CompassViewModelTest.kt index 19ccf37d87..2ecd57d412 100644 --- a/feature/node/src/commonTest/kotlin/org/meshtastic/feature/node/compass/CompassViewModelTest.kt +++ b/feature/node/src/commonTest/kotlin/org/meshtastic/feature/node/compass/CompassViewModelTest.kt @@ -85,7 +85,17 @@ class CompassViewModelTest { @Test fun `uiState reflects target node info after start`() = runTest { - val node = Node(num = 1234, user = User(id = "!1234", long_name = "Target Node")) + val node = + Node( + num = 1234, + user = + User.Builder() + .also { wb -> + wb.id = "!1234" + wb.long_name = "Target Node" + } + .build(), + ) viewModel.start(node, MeasurementSystem.METRIC) @@ -102,12 +112,14 @@ class CompassViewModelTest { val node = Node( num = 1234, - user = User(id = "!1234"), + user = User.Builder().also { wb -> wb.id = "!1234" }.build(), position = - org.meshtastic.proto.Position( - latitude_i = 10000000, - longitude_i = 10000000, - ), // 1 deg North, 1 deg East + org.meshtastic.proto.Position.Builder() + .also { wb -> + wb.latitude_i = 10000000 + wb.longitude_i = 10000000 + } + .build(), // 1 deg North, 1 deg East ) viewModel.start(node, MeasurementSystem.METRIC) @@ -145,13 +157,15 @@ class CompassViewModelTest { val state = startAndGetUiState( targetPosition = - org.meshtastic.proto.Position( - latitude_i = 10000000, - longitude_i = 10010000, - time = 1, - gps_accuracy = 5000, - PDOP = 250, - ), + org.meshtastic.proto.Position.Builder() + .also { wb -> + wb.latitude_i = 10000000 + wb.longitude_i = 10010000 + wb.time = 1 + wb.gps_accuracy = 5000 + wb.PDOP = 250 + } + .build(), ) assertEquals("± 12 m", state.errorRadiusText) @@ -162,14 +176,16 @@ class CompassViewModelTest { val state = startAndGetUiState( targetPosition = - org.meshtastic.proto.Position( - latitude_i = 10000000, - longitude_i = 10010000, - time = 1, - gps_accuracy = 5000, - HDOP = 300, - VDOP = 400, - ), + org.meshtastic.proto.Position.Builder() + .also { wb -> + wb.latitude_i = 10000000 + wb.longitude_i = 10010000 + wb.time = 1 + wb.gps_accuracy = 5000 + wb.HDOP = 300 + wb.VDOP = 400 + } + .build(), ) assertEquals("± 25 m", state.errorRadiusText) @@ -180,13 +196,15 @@ class CompassViewModelTest { val state = startAndGetUiState( targetPosition = - org.meshtastic.proto.Position( - latitude_i = 10000000, - longitude_i = 10010000, - time = 1, - gps_accuracy = 5000, - HDOP = 175, - ), + org.meshtastic.proto.Position.Builder() + .also { wb -> + wb.latitude_i = 10000000 + wb.longitude_i = 10010000 + wb.time = 1 + wb.gps_accuracy = 5000 + wb.HDOP = 175 + } + .build(), ) assertEquals("± 8 m", state.errorRadiusText) @@ -197,12 +215,14 @@ class CompassViewModelTest { val state = startAndGetUiState( targetPosition = - org.meshtastic.proto.Position( - latitude_i = 10000000, - longitude_i = 10010000, - time = 1, - precision_bits = 15, - ), + org.meshtastic.proto.Position.Builder() + .also { wb -> + wb.latitude_i = 10000000 + wb.longitude_i = 10010000 + wb.time = 1 + wb.precision_bits = 15 + } + .build(), ) assertEquals("± 729 m", state.errorRadiusText) @@ -213,12 +233,14 @@ class CompassViewModelTest { val state = startAndGetUiState( targetPosition = - org.meshtastic.proto.Position( - latitude_i = 10000000, - longitude_i = 10010000, - time = 1, - gps_accuracy = 5000, - ), + org.meshtastic.proto.Position.Builder() + .also { wb -> + wb.latitude_i = 10000000 + wb.longitude_i = 10010000 + wb.time = 1 + wb.gps_accuracy = 5000 + } + .build(), ) assertNull(state.errorRadiusText) @@ -230,13 +252,15 @@ class CompassViewModelTest { val state = startAndGetUiState( targetPosition = - org.meshtastic.proto.Position( - latitude_i = 10000000, - longitude_i = 10000000, - time = 1, - gps_accuracy = 5000, - PDOP = 250, - ), + org.meshtastic.proto.Position.Builder() + .also { wb -> + wb.latitude_i = 10000000 + wb.longitude_i = 10000000 + wb.time = 1 + wb.gps_accuracy = 5000 + wb.PDOP = 250 + } + .build(), location = PhoneLocation(1.0, 1.0, 0.0, 1000L), ) @@ -250,13 +274,15 @@ class CompassViewModelTest { val state = startAndGetUiState( targetPosition = - org.meshtastic.proto.Position( - latitude_i = 10000100, - longitude_i = 10000000, - time = 1, - gps_accuracy = 5000, - PDOP = 250, - ), + org.meshtastic.proto.Position.Builder() + .also { wb -> + wb.latitude_i = 10000100 + wb.longitude_i = 10000000 + wb.time = 1 + wb.gps_accuracy = 5000 + wb.PDOP = 250 + } + .build(), location = PhoneLocation(1.0, 1.0, 0.0, 1000L), ) @@ -270,7 +296,7 @@ class CompassViewModelTest { location: PhoneLocation = PhoneLocation(1.0, 1.0, 0.0, 1000L), ): CompassUiState { viewModel.start( - Node(num = 1234, user = User(id = "!1234"), position = targetPosition), + Node(num = 1234, user = User.Builder().also { wb -> wb.id = "!1234" }.build(), position = targetPosition), MeasurementSystem.METRIC, ) diff --git a/feature/node/src/commonTest/kotlin/org/meshtastic/feature/node/detail/HandleNodeActionTest.kt b/feature/node/src/commonTest/kotlin/org/meshtastic/feature/node/detail/HandleNodeActionTest.kt index f25cd9935b..9049872561 100644 --- a/feature/node/src/commonTest/kotlin/org/meshtastic/feature/node/detail/HandleNodeActionTest.kt +++ b/feature/node/src/commonTest/kotlin/org/meshtastic/feature/node/detail/HandleNodeActionTest.kt @@ -78,7 +78,7 @@ class HandleNodeActionTest { @Test fun `remove action delegates to viewModel and does not navigate up immediately`() = runTest(testDispatcher) { - val node = Node(num = 1234, user = User(id = "!1234")) + val node = Node(num = 1234, user = User.Builder().also { wb -> wb.id = "!1234" }.build()) every { nodeManagementActions.requestRemoveNode(any(), any(), any()) } returns Unit val viewModel = createViewModel() var navigateUpCalled = false @@ -100,7 +100,7 @@ class HandleNodeActionTest { fun `direct message to a keyless node with a thread opens that thread`() = runTest(testDispatcher) { // The message action is only visible for a keyless node because a thread exists. getDirectMessageRoute // would fall back to node.channel and open a different, empty conversation. - val node = Node(num = 1234, user = User(id = "!000004d2")) + val node = Node(num = 1234, user = User.Builder().also { wb -> wb.id = "!000004d2" }.build()) val viewModel = createViewModel() var route: String? = null @@ -118,8 +118,28 @@ class HandleNodeActionTest { @Test fun `direct message to a keyed node uses the computed route rather than an old thread`() = runTest(testDispatcher) { - val node = Node(num = 1234, user = User(id = "!000004d2", public_key = TEST_KEY)) - val ourNode = Node(num = 9999, user = User(id = "!0000270f", public_key = TEST_KEY)) + val node = + Node( + num = 1234, + user = + User.Builder() + .also { wb -> + wb.id = "!000004d2" + wb.public_key = TEST_KEY + } + .build(), + ) + val ourNode = + Node( + num = 9999, + user = + User.Builder() + .also { wb -> + wb.id = "!0000270f" + wb.public_key = TEST_KEY + } + .build(), + ) val viewModel = createViewModel() var route: String? = null diff --git a/feature/node/src/commonTest/kotlin/org/meshtastic/feature/node/detail/NodeDetailViewModelTest.kt b/feature/node/src/commonTest/kotlin/org/meshtastic/feature/node/detail/NodeDetailViewModelTest.kt index d27a78f588..03e1481f47 100644 --- a/feature/node/src/commonTest/kotlin/org/meshtastic/feature/node/detail/NodeDetailViewModelTest.kt +++ b/feature/node/src/commonTest/kotlin/org/meshtastic/feature/node/detail/NodeDetailViewModelTest.kt @@ -106,7 +106,7 @@ class NodeDetailViewModelTest { @Test fun `uiState emits updates from useCase`() = runTest(testDispatcher) { - val node = Node(num = 1234, user = User(id = "!1234")) + val node = Node(num = 1234, user = User.Builder().also { wb -> wb.id = "!1234" }.build()) val stateFlow = MutableStateFlow(NodeDetailUiState(node = node)) every { getNodeDetailsUseCase(1234) } returns stateFlow @@ -122,7 +122,7 @@ class NodeDetailViewModelTest { @Test fun `handleNodeMenuAction delegates to nodeManagementActions for Mute`() = runTest(testDispatcher) { - val node = Node(num = 1234, user = User(id = "!1234")) + val node = Node(num = 1234, user = User.Builder().also { wb -> wb.id = "!1234" }.build()) every { nodeManagementActions.requestMuteNode(any(), any()) } returns Unit viewModel.handleNodeMenuAction(NodeMenuAction.Mute(node)) @@ -132,7 +132,17 @@ class NodeDetailViewModelTest { @Test fun `handleNodeMenuAction delegates to nodeRequestActions for Traceroute`() = runTest(testDispatcher) { - val node = Node(num = 1234, user = User(id = "!1234", long_name = "Test Node")) + val node = + Node( + num = 1234, + user = + User.Builder() + .also { wb -> + wb.id = "!1234" + wb.long_name = "Test Node" + } + .build(), + ) everySuspend { nodeRequestActions.requestTraceroute(any(), any()) } returns Unit viewModel.handleNodeMenuAction(NodeMenuAction.TraceRoute(node)) diff --git a/feature/node/src/commonTest/kotlin/org/meshtastic/feature/node/detail/NodeManagementActionsTest.kt b/feature/node/src/commonTest/kotlin/org/meshtastic/feature/node/detail/NodeManagementActionsTest.kt index 36e07d6df3..08296d4415 100644 --- a/feature/node/src/commonTest/kotlin/org/meshtastic/feature/node/detail/NodeManagementActionsTest.kt +++ b/feature/node/src/commonTest/kotlin/org/meshtastic/feature/node/detail/NodeManagementActionsTest.kt @@ -74,7 +74,7 @@ class NodeManagementActionsTest { @Test fun requestRemoveNode_shows_confirmation_alert() { - val node = Node(num = 123, user = User(long_name = "Test Node")) + val node = Node(num = 123, user = User.Builder().also { wb -> wb.long_name = "Test Node" }.build()) actions.requestRemoveNode(testScope, node) @@ -97,7 +97,7 @@ class NodeManagementActionsTest { fun requestRemoveNode_invokes_onAfterRemove_when_user_confirms() { val realAlertManager = AlertManager() val actionsWithRealAlert = actionsWith(radioController, realAlertManager) - val node = Node(num = 123, user = User(long_name = "Test Node")) + val node = Node(num = 123, user = User.Builder().also { wb -> wb.long_name = "Test Node" }.build()) var afterRemoveCalled = false actionsWithRealAlert.requestRemoveNode(testScope, node) { afterRemoveCalled = true } @@ -111,7 +111,7 @@ class NodeManagementActionsTest { fun requestRemoveNode_success_callback_failure_is_not_reported_as_radio_rejection() { val realAlertManager = AlertManager() val actionsWithRealAlert = actionsWith(radioController, realAlertManager) - val node = Node(num = 123, user = User(long_name = "Test Node")) + val node = Node(num = 123, user = User.Builder().also { wb -> wb.long_name = "Test Node" }.build()) nodeRepository.setNodes(listOf(node)) actionsWithRealAlert.requestRemoveNode(testScope, node) { throw PacketQueueRejectedException("callback") } @@ -126,7 +126,7 @@ class NodeManagementActionsTest { val rejectedRadio = mock() val realAlertManager = AlertManager() val rejectedActions = actionsWith(rejectedRadio, realAlertManager) - val node = Node(num = 123, user = User(long_name = "Test Node")) + val node = Node(num = 123, user = User.Builder().also { wb -> wb.long_name = "Test Node" }.build()) var afterRemoveCalled = false nodeRepository.setNodes(listOf(node)) every { rejectedRadio.generatePacketId() } returns 7 @@ -146,7 +146,7 @@ class NodeManagementActionsTest { val unavailableRadio = mock() val realAlertManager = AlertManager() val unavailableActions = actionsWith(unavailableRadio, realAlertManager) - val node = Node(num = 123, user = User(long_name = "Test Node")) + val node = Node(num = 123, user = User.Builder().also { wb -> wb.long_name = "Test Node" }.build()) var afterRemoveCalled = false nodeRepository.setNodes(listOf(node)) every { unavailableRadio.generatePacketId() } returns 7 diff --git a/feature/node/src/commonTest/kotlin/org/meshtastic/feature/node/domain/usecase/GetFilteredNodesUseCaseTest.kt b/feature/node/src/commonTest/kotlin/org/meshtastic/feature/node/domain/usecase/GetFilteredNodesUseCaseTest.kt index 43a1bf6b90..65a069f402 100644 --- a/feature/node/src/commonTest/kotlin/org/meshtastic/feature/node/domain/usecase/GetFilteredNodesUseCaseTest.kt +++ b/feature/node/src/commonTest/kotlin/org/meshtastic/feature/node/domain/usecase/GetFilteredNodesUseCaseTest.kt @@ -24,7 +24,10 @@ import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.test.runTest +import okio.ByteString +import okio.ByteString.Companion.toByteString import org.meshtastic.core.model.Node +import org.meshtastic.core.model.Node.Companion.PUBLIC_KEY_SIZE import org.meshtastic.core.model.NodeSortOption import org.meshtastic.core.repository.NodeRepository import org.meshtastic.feature.node.list.NodeFilterState @@ -53,9 +56,28 @@ class GetFilteredNodesUseCaseTest { ignored: Boolean = false, name: String = "Node$num", viaMqtt: Boolean = false, + signsPackets: Boolean = false, + publicKey: ByteString? = null, + heardOnCurrentLora: Boolean = true, ): Node { - val user = User(id = "!$num", long_name = name, short_name = "N$num", role = role) - return Node(num = num, user = user, isIgnored = ignored, viaMqtt = viaMqtt) + val user = + User.Builder() + .also { wb -> + wb.id = "!$num" + wb.long_name = name + wb.short_name = "N$num" + wb.role = role + } + .build() + return Node( + num = num, + user = user, + isIgnored = ignored, + viaMqtt = viaMqtt, + signsPackets = signsPackets, + publicKey = publicKey, + heardOnCurrentLora = heardOnCurrentLora, + ) } @Test @@ -155,4 +177,53 @@ class GetFilteredNodesUseCaseTest { // Assert assertEquals(2, result.size) } + + @Test + fun `the signed filter keeps only nodes whose signature the radio verified`() = runTest { + val nodes = listOf(createNode(1), createNode(2, signsPackets = true)) + every { nodeRepository.getNodes() } returns flowOf(nodes) + + val result = useCase(NodeFilterState(onlySigned = true), NodeSortOption.LAST_HEARD).first() + + assertEquals(listOf(2), result.map { it.num }) + } + + @Test + fun `the encrypted filter keeps a key on file and drops one that stopped matching`() = runTest { + val nodes = + listOf( + createNode(1), + createNode(2, publicKey = ByteArray(PUBLIC_KEY_SIZE) { 1 }.toByteString()), + // A mismatch is not a key you can safely encrypt to, so it is excluded rather than counted. + createNode(3, publicKey = Node.ERROR_BYTE_STRING), + ) + every { nodeRepository.getNodes() } returns flowOf(nodes) + + val result = useCase(NodeFilterState(onlyEncrypted = true), NodeSortOption.LAST_HEARD).first() + + assertEquals(listOf(2), result.map { it.num }) + } + + @Test + fun `the unheard filter drops a node the radio can no longer reach`() = runTest { + val nodes = listOf(createNode(1), createNode(2, heardOnCurrentLora = false)) + every { nodeRepository.getNodes() } returns flowOf(nodes) + + val result = useCase(NodeFilterState(excludeUnheard = true), NodeSortOption.LAST_HEARD).first() + + assertEquals(listOf(1), result.map { it.num }) + } + + @Test + fun `the unheard filter keeps an MQTT node which reads unheard permanently`() = runTest { + // The radio only marks a node heard on the current LoRa config when it arrives over RF, so an MQTT-only node + // reads false permanently. That is not the same as "went unreachable when the settings changed", and hiding it + // would empty the list on an MQTT-uplinked mesh. + val nodes = listOf(createNode(1, viaMqtt = true, heardOnCurrentLora = false), createNode(2)) + every { nodeRepository.getNodes() } returns flowOf(nodes) + + val result = useCase(NodeFilterState(excludeUnheard = true), NodeSortOption.LAST_HEARD).first() + + assertEquals(listOf(1, 2), result.map { it.num }.sorted()) + } } diff --git a/feature/node/src/commonTest/kotlin/org/meshtastic/feature/node/list/NodeListViewModelTest.kt b/feature/node/src/commonTest/kotlin/org/meshtastic/feature/node/list/NodeListViewModelTest.kt index 4482149262..fd1981b408 100644 --- a/feature/node/src/commonTest/kotlin/org/meshtastic/feature/node/list/NodeListViewModelTest.kt +++ b/feature/node/src/commonTest/kotlin/org/meshtastic/feature/node/list/NodeListViewModelTest.kt @@ -31,6 +31,7 @@ import org.meshtastic.core.model.ConnectionState import org.meshtastic.core.model.Node import org.meshtastic.core.model.NodeSortOption import org.meshtastic.core.repository.ConnectionStateProvider +import org.meshtastic.core.repository.NodeFilterPrefs import org.meshtastic.core.repository.NodeManager import org.meshtastic.core.repository.RadioConfigRepository import org.meshtastic.core.testing.FakeDeviceHardwareRepository @@ -68,18 +69,14 @@ class NodeListViewModelTest { radioController = FakeRadioController() radioInterfaceService = FakeRadioInterfaceService() - every { radioConfigRepository.localConfigFlow } returns MutableStateFlow(org.meshtastic.proto.LocalConfig()) - every { radioConfigRepository.deviceProfileFlow } returns MutableStateFlow(org.meshtastic.proto.DeviceProfile()) + every { radioConfigRepository.localConfigFlow } returns + MutableStateFlow(org.meshtastic.proto.LocalConfig.Builder().build()) + every { radioConfigRepository.deviceProfileFlow } returns + MutableStateFlow(org.meshtastic.proto.DeviceProfile.Builder().build()) every { connectionStateProvider.connectionState } returns MutableStateFlow(ConnectionState.Disconnected) every { nodeFilterPreferences.nodeSortOption } returns MutableStateFlow(NodeSortOption.LAST_HEARD) - every { nodeFilterPreferences.includeUnknown } returns MutableStateFlow(true) - every { nodeFilterPreferences.excludeInfrastructure } returns MutableStateFlow(false) - every { nodeFilterPreferences.onlyOnline } returns MutableStateFlow(false) - every { nodeFilterPreferences.onlyDirect } returns MutableStateFlow(false) - every { nodeFilterPreferences.showIgnored } returns MutableStateFlow(false) - every { nodeFilterPreferences.excludeMqtt } returns MutableStateFlow(false) - every { nodeFilterPreferences.excludeUnheard } returns MutableStateFlow(false) + every { nodeFilterPreferences.filters } returns MutableStateFlow(NodeFilterPrefs()) every { nodeManager.reportsHeardOnCurrentLora } returns MutableStateFlow(true) every { nodeManager.isNodeDbReady } returns MutableStateFlow(true) diff --git a/feature/node/src/commonTest/kotlin/org/meshtastic/feature/node/list/RemovableUnheardNodesTest.kt b/feature/node/src/commonTest/kotlin/org/meshtastic/feature/node/list/RemovableUnheardNodesTest.kt new file mode 100644 index 0000000000..122259e08d --- /dev/null +++ b/feature/node/src/commonTest/kotlin/org/meshtastic/feature/node/list/RemovableUnheardNodesTest.kt @@ -0,0 +1,77 @@ +/* + * 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 . + */ +package org.meshtastic.feature.node.list + +import org.meshtastic.core.model.Node +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertTrue + +/** + * What the unheard banner counts and its one-tap clear would delete. This is the highest-consequence consumer of + * [Node.isUnheardOnCurrentLora] - it removes nodes - so each exemption is pinned separately. + */ +class RemovableUnheardNodesTest { + + private val ourNum = 1 + + private fun node( + num: Int, + heardOnCurrentLora: Boolean = false, + viaMqtt: Boolean = false, + isFavorite: Boolean = false, + ) = Node(num = num, heardOnCurrentLora = heardOnCurrentLora, viaMqtt = viaMqtt, isFavorite = isFavorite) + + @Test + fun offersANodeNotHeardOnTheConfigInForceNow() { + val nodes = listOf(node(2), node(3, heardOnCurrentLora = true)) + + assertEquals(listOf(2), selectRemovableUnheardNodes(nodes, ourNum).map { it.num }) + } + + @Test + fun neverOffersAnMqttNode() { + // Reads unheard for the life of the entry, and deleting it achieves nothing - the next uplinked packet + // brings it straight back. + val nodes = listOf(node(2, viaMqtt = true), node(3)) + + assertEquals(listOf(3), selectRemovableUnheardNodes(nodes, ourNum).map { it.num }) + } + + @Test + fun neverOffersAFavourite() { + // Also covers shared contacts: firmware marks a contact favourite, and one has never been heard over RF. + val nodes = listOf(node(2, isFavorite = true), node(3)) + + assertEquals(listOf(3), selectRemovableUnheardNodes(nodes, ourNum).map { it.num }) + } + + @Test + fun neverOffersTheConnectedRadio() { + val nodes = listOf(node(ourNum), node(3)) + + assertEquals(listOf(3), selectRemovableUnheardNodes(nodes, ourNum).map { it.num }) + } + + @Test + fun offersNothingUntilTheLocalNodeNumberIsKnown() { + // ourNode and the node list arrive on independent flows; offering here could delete the user's own node. + val nodes = listOf(node(2), node(3)) + + assertTrue(selectRemovableUnheardNodes(nodes, ourNum = null).isEmpty()) + } +} diff --git a/feature/node/src/commonTest/kotlin/org/meshtastic/feature/node/list/StatusMessageActionGateTest.kt b/feature/node/src/commonTest/kotlin/org/meshtastic/feature/node/list/StatusMessageActionGateTest.kt index b57adbeb52..a3ce85c8ab 100644 --- a/feature/node/src/commonTest/kotlin/org/meshtastic/feature/node/list/StatusMessageActionGateTest.kt +++ b/feature/node/src/commonTest/kotlin/org/meshtastic/feature/node/list/StatusMessageActionGateTest.kt @@ -29,8 +29,10 @@ import kotlin.test.assertTrue */ class StatusMessageActionGateTest { - private fun node(num: Int, firmware: String?) = - Node(num = num, metadata = firmware?.let { DeviceMetadata(firmware_version = it) }) + private fun node(num: Int, firmware: String?) = Node( + num = num, + metadata = firmware?.let { DeviceMetadata.Builder().also { wb -> wb.firmware_version = it }.build() }, + ) private val ourNode = node(num = 1, firmware = "2.8.0") diff --git a/feature/node/src/commonTest/kotlin/org/meshtastic/feature/node/metrics/AirQualityMetricsTest.kt b/feature/node/src/commonTest/kotlin/org/meshtastic/feature/node/metrics/AirQualityMetricsTest.kt index f97b8c2c4f..8db6b7cbd0 100644 --- a/feature/node/src/commonTest/kotlin/org/meshtastic/feature/node/metrics/AirQualityMetricsTest.kt +++ b/feature/node/src/commonTest/kotlin/org/meshtastic/feature/node/metrics/AirQualityMetricsTest.kt @@ -31,11 +31,22 @@ import kotlin.test.assertNull */ class AirQualityMetricsTest { - private fun telemetry(aq: AirQualityMetrics) = Telemetry(air_quality_metrics = aq) + private fun telemetry(aq: AirQualityMetrics) = + Telemetry.Builder().also { wb -> wb.air_quality_metrics = aq }.build() @Test fun `getValue returns present non-zero readings`() { - val t = telemetry(AirQualityMetrics(pm10_standard = 1, pm25_standard = 2, pm100_standard = 3, co2 = 800)) + val t = + telemetry( + AirQualityMetrics.Builder() + .also { wb -> + wb.pm10_standard = 1 + wb.pm25_standard = 2 + wb.pm100_standard = 3 + wb.co2 = 800 + } + .build(), + ) assertEquals(1f, AirQuality.PM1_0.getValue(t)) assertEquals(2f, AirQuality.PM2_5.getValue(t)) assertEquals(3f, AirQuality.PM10.getValue(t)) @@ -45,7 +56,17 @@ class AirQualityMetricsTest { @Test fun `getValue plots a present-zero reading instead of suppressing it`() { // BUG B regression: clean air reads 0 µg/m³ and must chart as 0f, not be dropped to null. - val t = telemetry(AirQualityMetrics(pm10_standard = 0, pm25_standard = 0, pm100_standard = 0, co2 = 0)) + val t = + telemetry( + AirQualityMetrics.Builder() + .also { wb -> + wb.pm10_standard = 0 + wb.pm25_standard = 0 + wb.pm100_standard = 0 + wb.co2 = 0 + } + .build(), + ) assertEquals(0f, AirQuality.PM1_0.getValue(t)) assertEquals(0f, AirQuality.PM2_5.getValue(t)) assertEquals(0f, AirQuality.PM10.getValue(t)) @@ -55,7 +76,7 @@ class AirQualityMetricsTest { @Test fun `getValue returns null for an absent field so a partial-sensor node does not chart spurious series`() { // A CO2-only node leaves the PM fields unset (Wire decodes an unset optional uint32 to null). - val t = telemetry(AirQualityMetrics(co2 = 450)) + val t = telemetry(AirQualityMetrics.Builder().also { wb -> wb.co2 = 450 }.build()) assertNull(AirQuality.PM1_0.getValue(t)) assertNull(AirQuality.PM2_5.getValue(t)) assertNull(AirQuality.PM10.getValue(t)) @@ -64,7 +85,7 @@ class AirQualityMetricsTest { @Test fun `getValue returns null for every series when there are no air quality metrics`() { - val t = Telemetry() + val t = Telemetry.Builder().build() AirQuality.entries.forEach { assertNull(it.getValue(t), "${it.name} should be null without air_quality_metrics") } @@ -74,13 +95,17 @@ class AirQualityMetricsTest { fun `metricsWithData drops selected series that have no reading so the legend is not ever-present`() { // Issue 5873, CO2-only node: PM2.5 is default-selected but never reported, so it must not survive into the // legend. - val co2Only = listOf(sample(AirQualityMetrics(co2 = 450))) + val co2Only = listOf(sample(AirQualityMetrics.Builder().also { wb -> wb.co2 = 450 }.build())) assertEquals(listOf(AirQuality.CO2), metricsWithData(listOf(AirQuality.PM2_5, AirQuality.CO2), co2Only)) } @Test fun `metricsWithData keeps a series once it has any reading in the frame`() { - val mixed = listOf(sample(AirQualityMetrics(co2 = 450)), sample(AirQualityMetrics(pm25_standard = 8))) + val mixed = + listOf( + sample(AirQualityMetrics.Builder().also { wb -> wb.co2 = 450 }.build()), + sample(AirQualityMetrics.Builder().also { wb -> wb.pm25_standard = 8 }.build()), + ) assertEquals( listOf(AirQuality.PM2_5, AirQuality.CO2), metricsWithData(listOf(AirQuality.PM2_5, AirQuality.CO2), mixed), @@ -91,7 +116,7 @@ class AirQualityMetricsTest { @Test fun `getValue for AQI comes from the sample derived value rather than the telemetry`() { - val t = telemetry(AirQualityMetrics(pm25_standard = 25)) + val t = telemetry(AirQualityMetrics.Builder().also { wb -> wb.pm25_standard = 25 }.build()) assertNull(AirQuality.AQI.getValue(t), "AQI is not a proto field, so a bare Telemetry has none") assertEquals(78f, AirQuality.AQI.getValue(AirQualitySample(t, aqi = 78))) assertNull(AirQuality.AQI.getValue(AirQualitySample(t, aqi = null))) @@ -100,7 +125,10 @@ class AirQualityMetricsTest { @Test fun `getValue for AQI plots a zero AQI instead of suppressing it`() { // Pristine air is AQI 0 - a real value, and the same present-and-zero rule the PM series follow. - assertEquals(0f, AirQuality.AQI.getValue(AirQualitySample(telemetry(AirQualityMetrics()), aqi = 0))) + assertEquals( + 0f, + AirQuality.AQI.getValue(AirQualitySample(telemetry(AirQualityMetrics.Builder().build()), aqi = 0)), + ) } @Test @@ -122,7 +150,14 @@ class AirQualityMetricsTest { @Test fun `withNowCastAqi gives a CO2-only node no AQI so the series is never offered`() { val co2Only = - (0..3).map { hour -> Telemetry(time = HOUR * hour, air_quality_metrics = AirQualityMetrics(co2 = 450)) } + (0..3).map { hour -> + Telemetry.Builder() + .also { wb -> + wb.time = HOUR * hour + wb.air_quality_metrics = AirQualityMetrics.Builder().also { wb -> wb.co2 = 450 }.build() + } + .build() + } val samples = withNowCastAqi(co2Only) assertEquals(emptyList(), samples.mapNotNull { it.aqi }) assertEquals(emptyList(), metricsWithData(listOf(AirQuality.AQI), samples)) @@ -134,7 +169,12 @@ class AirQualityMetricsTest { val telemetries = listOf( hourlyTelemetry(0, pm25 = 10), - Telemetry(time = HOUR, air_quality_metrics = AirQualityMetrics(co2 = 450)), + Telemetry.Builder() + .also { wb -> + wb.time = HOUR + wb.air_quality_metrics = AirQualityMetrics.Builder().also { wb -> wb.co2 = 450 }.build() + } + .build(), hourlyTelemetry(2, pm25 = 10), hourlyTelemetry(3, pm25 = 10), ) @@ -143,8 +183,12 @@ class AirQualityMetricsTest { assertEquals(53, samples[3].aqi) } - private fun hourlyTelemetry(hoursFromStart: Int, pm25: Int) = - Telemetry(time = HOUR * hoursFromStart, air_quality_metrics = AirQualityMetrics(pm25_standard = pm25)) + private fun hourlyTelemetry(hoursFromStart: Int, pm25: Int) = Telemetry.Builder() + .also { wb -> + wb.time = HOUR * hoursFromStart + wb.air_quality_metrics = AirQualityMetrics.Builder().also { wb -> wb.pm25_standard = pm25 }.build() + } + .build() private fun sample(aq: AirQualityMetrics, aqi: Int? = null) = AirQualitySample(telemetry(aq), aqi) diff --git a/feature/node/src/commonTest/kotlin/org/meshtastic/feature/node/metrics/DecodePaxFromLogTest.kt b/feature/node/src/commonTest/kotlin/org/meshtastic/feature/node/metrics/DecodePaxFromLogTest.kt index 98f7d3bbe9..7d94e41e98 100644 --- a/feature/node/src/commonTest/kotlin/org/meshtastic/feature/node/metrics/DecodePaxFromLogTest.kt +++ b/feature/node/src/commonTest/kotlin/org/meshtastic/feature/node/metrics/DecodePaxFromLogTest.kt @@ -52,7 +52,14 @@ class DecodePaxFromLogTest { @Test fun binaryProto_validPaxcount_decoded() { - val pax = ProtoPaxcount(wifi = 10, ble = 5, uptime = 3600) + val pax = + ProtoPaxcount.Builder() + .also { wb -> + wb.wifi = 10 + wb.ble = 5 + wb.uptime = 3600 + } + .build() val payload = ProtoPaxcount.ADAPTER.encode(pax) val log = meshLogWithPacket(payload, wantResponse = false) @@ -65,7 +72,14 @@ class DecodePaxFromLogTest { @Test fun binaryProto_wantResponse_returnsNull() { - val pax = ProtoPaxcount(wifi = 10, ble = 5, uptime = 100) + val pax = + ProtoPaxcount.Builder() + .also { wb -> + wb.wifi = 10 + wb.ble = 5 + wb.uptime = 100 + } + .build() val payload = ProtoPaxcount.ADAPTER.encode(pax) val log = meshLogWithPacket(payload, wantResponse = true) @@ -74,7 +88,14 @@ class DecodePaxFromLogTest { @Test fun binaryProto_allZeroValues_returnsNull() { - val pax = ProtoPaxcount(wifi = 0, ble = 0, uptime = 0) + val pax = + ProtoPaxcount.Builder() + .also { wb -> + wb.wifi = 0 + wb.ble = 0 + wb.uptime = 0 + } + .build() val payload = ProtoPaxcount.ADAPTER.encode(pax) val log = meshLogWithPacket(payload, wantResponse = false) @@ -83,7 +104,14 @@ class DecodePaxFromLogTest { @Test fun binaryProto_wrongPortNum_returnsNull() { - val pax = ProtoPaxcount(wifi = 10, ble = 5, uptime = 100) + val pax = + ProtoPaxcount.Builder() + .also { wb -> + wb.wifi = 10 + wb.ble = 5 + wb.uptime = 100 + } + .build() val payload = ProtoPaxcount.ADAPTER.encode(pax) val log = meshLogWithPacket(payload, wantResponse = false, portNum = PortNum.POSITION_APP) @@ -94,7 +122,14 @@ class DecodePaxFromLogTest { @Test fun base64Fallback_validPayload_decoded() { - val pax = ProtoPaxcount(wifi = 7, ble = 3, uptime = 500) + val pax = + ProtoPaxcount.Builder() + .also { wb -> + wb.wifi = 7 + wb.ble = 3 + wb.uptime = 500 + } + .build() val bytes = ProtoPaxcount.ADAPTER.encode(pax) val base64 = okio.ByteString.of(*bytes).base64() val log = MeshLog(uuid = "test", message_type = "pax", received_date = 0, raw_message = base64) @@ -135,9 +170,16 @@ class DecodePaxFromLogTest { wantResponse: Boolean, portNum: PortNum = PortNum.PAXCOUNTER_APP, ): MeshLog { - val data = Data(portnum = portNum, payload = payload.toByteString(), want_response = wantResponse) - val packet = MeshPacket(decoded = data) - val fromRadio = FromRadio(packet = packet) + val data = + Data.Builder() + .also { wb -> + wb.portnum = portNum + wb.payload = payload.toByteString() + wb.want_response = wantResponse + } + .build() + val packet = MeshPacket.Builder().also { wb -> wb.decoded = data }.build() + val fromRadio = FromRadio.Builder().also { wb -> wb.packet = packet }.build() return MeshLog( uuid = "test", message_type = "packet", diff --git a/feature/node/src/commonTest/kotlin/org/meshtastic/feature/node/metrics/EnvironmentChartUnitsTest.kt b/feature/node/src/commonTest/kotlin/org/meshtastic/feature/node/metrics/EnvironmentChartUnitsTest.kt index e4ef6481c0..753e49887a 100644 --- a/feature/node/src/commonTest/kotlin/org/meshtastic/feature/node/metrics/EnvironmentChartUnitsTest.kt +++ b/feature/node/src/commonTest/kotlin/org/meshtastic/feature/node/metrics/EnvironmentChartUnitsTest.kt @@ -26,20 +26,25 @@ import kotlin.test.assertNull @Suppress("MagicNumber") class EnvironmentChartUnitsTest { - private fun telemetry(env: EnvironmentMetrics) = Telemetry(time = nowSeconds.toInt(), environment_metrics = env) + private fun telemetry(env: EnvironmentMetrics) = Telemetry.Builder() + .also { wb -> + wb.time = nowSeconds.toInt() + wb.environment_metrics = env + } + .build() // ---- chartValue ---- @Test fun windSpeedMetricIsConvertedToKph() { - val t = telemetry(EnvironmentMetrics(wind_speed = 10f)) + val t = telemetry(EnvironmentMetrics.Builder().also { wb -> wb.wind_speed = 10f }.build()) assertEquals(36f, chartValue(Environment.WIND_SPEED, t, isImperial = false)!!, 0.001f) } @Test fun windSpeedImperialIsConvertedToMph() { - val t = telemetry(EnvironmentMetrics(wind_speed = 10f)) + val t = telemetry(EnvironmentMetrics.Builder().also { wb -> wb.wind_speed = 10f }.build()) assertEquals(22.3694f, chartValue(Environment.WIND_SPEED, t, isImperial = true)!!, 0.001f) } @@ -47,14 +52,14 @@ class EnvironmentChartUnitsTest { /** 0 m/s is a real reading (dead calm), so it must survive conversion rather than read as missing. */ @Test fun windSpeedZeroIsPreserved() { - val t = telemetry(EnvironmentMetrics(wind_speed = 0f)) + val t = telemetry(EnvironmentMetrics.Builder().also { wb -> wb.wind_speed = 0f }.build()) assertEquals(0f, chartValue(Environment.WIND_SPEED, t, isImperial = true)!!, 0.001f) } @Test fun missingWindSpeedStaysNull() { - val t = telemetry(EnvironmentMetrics()) + val t = telemetry(EnvironmentMetrics.Builder().build()) assertNull(chartValue(Environment.WIND_SPEED, t, isImperial = true)) } @@ -62,7 +67,7 @@ class EnvironmentChartUnitsTest { /** Temperatures are converted upstream by the view model, so the chart must not convert them again. */ @Test fun temperatureIsNotConvertedHere() { - val t = telemetry(EnvironmentMetrics(temperature = 20f)) + val t = telemetry(EnvironmentMetrics.Builder().also { wb -> wb.temperature = 20f }.build()) assertEquals(20f, chartValue(Environment.TEMPERATURE, t, isImperial = true)!!, 0.001f) } @@ -92,7 +97,7 @@ class EnvironmentChartUnitsTest { /** ADC readings are already in volts, so the chart must not unit-convert them. */ @Test fun adcVoltageIsNotConverted() { - val t = telemetry(EnvironmentMetrics(adc_voltage_ch0 = 3.3f)) + val t = telemetry(EnvironmentMetrics.Builder().also { wb -> wb.adc_voltage_ch0 = 3.3f }.build()) assertEquals(3.3f, chartValue(Environment.ADC_VOLTAGE_1, t, isImperial = true)!!, 0.001f) } diff --git a/feature/node/src/commonTest/kotlin/org/meshtastic/feature/node/metrics/EnvironmentMetricsForGraphingTest.kt b/feature/node/src/commonTest/kotlin/org/meshtastic/feature/node/metrics/EnvironmentMetricsForGraphingTest.kt index d1f0fa7f4b..ac354fa1a0 100644 --- a/feature/node/src/commonTest/kotlin/org/meshtastic/feature/node/metrics/EnvironmentMetricsForGraphingTest.kt +++ b/feature/node/src/commonTest/kotlin/org/meshtastic/feature/node/metrics/EnvironmentMetricsForGraphingTest.kt @@ -29,7 +29,12 @@ class EnvironmentMetricsForGraphingTest { private val now = nowSeconds.toInt() - private fun telemetry(time: Int = now, env: EnvironmentMetrics) = Telemetry(time = time, environment_metrics = env) + private fun telemetry(time: Int = now, env: EnvironmentMetrics) = Telemetry.Builder() + .also { wb -> + wb.time = time + wb.environment_metrics = env + } + .build() // ---- Empty input ---- @@ -48,8 +53,8 @@ class EnvironmentMetricsForGraphingTest { fun useFahrenheit_convertsTemperatureMinMax() { val metrics = listOf( - telemetry(env = EnvironmentMetrics(temperature = 0f)), - telemetry(env = EnvironmentMetrics(temperature = 100f)), + telemetry(env = EnvironmentMetrics.Builder().also { wb -> wb.temperature = 0f }.build()), + telemetry(env = EnvironmentMetrics.Builder().also { wb -> wb.temperature = 100f }.build()), ) val result = EnvironmentMetricsState(metrics).environmentMetricsForGraphing(useFahrenheit = true) @@ -63,8 +68,8 @@ class EnvironmentMetricsForGraphingTest { fun useFahrenheit_convertsSoilTemperature() { val metrics = listOf( - telemetry(env = EnvironmentMetrics(soil_temperature = 20f)), - telemetry(env = EnvironmentMetrics(soil_temperature = 30f)), + telemetry(env = EnvironmentMetrics.Builder().also { wb -> wb.soil_temperature = 20f }.build()), + telemetry(env = EnvironmentMetrics.Builder().also { wb -> wb.soil_temperature = 30f }.build()), ) val result = EnvironmentMetricsState(metrics).environmentMetricsForGraphing(useFahrenheit = true) @@ -78,7 +83,8 @@ class EnvironmentMetricsForGraphingTest { @Test fun humidity_zeroFilteredOut() { - val metrics = listOf(telemetry(env = EnvironmentMetrics(relative_humidity = 0.0f))) + val metrics = + listOf(telemetry(env = EnvironmentMetrics.Builder().also { wb -> wb.relative_humidity = 0.0f }.build())) val result = EnvironmentMetricsState(metrics).environmentMetricsForGraphing() assertFalse(result.shouldPlot[Environment.HUMIDITY.ordinal]) @@ -88,8 +94,8 @@ class EnvironmentMetricsForGraphingTest { fun humidity_nonZeroIncluded() { val metrics = listOf( - telemetry(env = EnvironmentMetrics(relative_humidity = 45f)), - telemetry(env = EnvironmentMetrics(relative_humidity = 65f)), + telemetry(env = EnvironmentMetrics.Builder().also { wb -> wb.relative_humidity = 45f }.build()), + telemetry(env = EnvironmentMetrics.Builder().also { wb -> wb.relative_humidity = 65f }.build()), ) val result = EnvironmentMetricsState(metrics).environmentMetricsForGraphing() @@ -102,7 +108,8 @@ class EnvironmentMetricsForGraphingTest { @Test fun iaq_intMinValueFilteredOut() { - val metrics = listOf(telemetry(env = EnvironmentMetrics(iaq = Int.MIN_VALUE))) + val metrics = + listOf(telemetry(env = EnvironmentMetrics.Builder().also { wb -> wb.iaq = Int.MIN_VALUE }.build())) val result = EnvironmentMetricsState(metrics).environmentMetricsForGraphing() assertFalse(result.shouldPlot[Environment.IAQ.ordinal]) @@ -111,7 +118,10 @@ class EnvironmentMetricsForGraphingTest { @Test fun iaq_validValueIncluded() { val metrics = - listOf(telemetry(env = EnvironmentMetrics(iaq = 50)), telemetry(env = EnvironmentMetrics(iaq = 150))) + listOf( + telemetry(env = EnvironmentMetrics.Builder().also { wb -> wb.iaq = 50 }.build()), + telemetry(env = EnvironmentMetrics.Builder().also { wb -> wb.iaq = 150 }.build()), + ) val result = EnvironmentMetricsState(metrics).environmentMetricsForGraphing() assertTrue(result.shouldPlot[Environment.IAQ.ordinal]) @@ -123,7 +133,10 @@ class EnvironmentMetricsForGraphingTest { @Test fun soilMoisture_intMinValueFilteredOut() { - val metrics = listOf(telemetry(env = EnvironmentMetrics(soil_moisture = Int.MIN_VALUE))) + val metrics = + listOf( + telemetry(env = EnvironmentMetrics.Builder().also { wb -> wb.soil_moisture = Int.MIN_VALUE }.build()), + ) val result = EnvironmentMetricsState(metrics).environmentMetricsForGraphing() assertFalse(result.shouldPlot[Environment.SOIL_MOISTURE.ordinal]) @@ -133,8 +146,8 @@ class EnvironmentMetricsForGraphingTest { fun soilMoisture_validValueIncluded() { val metrics = listOf( - telemetry(env = EnvironmentMetrics(soil_moisture = 30)), - telemetry(env = EnvironmentMetrics(soil_moisture = 70)), + telemetry(env = EnvironmentMetrics.Builder().also { wb -> wb.soil_moisture = 30 }.build()), + telemetry(env = EnvironmentMetrics.Builder().also { wb -> wb.soil_moisture = 70 }.build()), ) val result = EnvironmentMetricsState(metrics).environmentMetricsForGraphing() @@ -147,8 +160,8 @@ class EnvironmentMetricsForGraphingTest { fun barometricPressure_onLeftAxis() { val metrics = listOf( - telemetry(env = EnvironmentMetrics(barometric_pressure = 1013.25f)), - telemetry(env = EnvironmentMetrics(barometric_pressure = 1020.50f)), + telemetry(env = EnvironmentMetrics.Builder().also { wb -> wb.barometric_pressure = 1013.25f }.build()), + telemetry(env = EnvironmentMetrics.Builder().also { wb -> wb.barometric_pressure = 1020.50f }.build()), ) val result = EnvironmentMetricsState(metrics).environmentMetricsForGraphing() @@ -160,7 +173,10 @@ class EnvironmentMetricsForGraphingTest { @Test fun barometricPressure_doesNotAffectRightAxis() { // Only pressure, no other metrics - val metrics = listOf(telemetry(env = EnvironmentMetrics(barometric_pressure = 1013.25f))) + val metrics = + listOf( + telemetry(env = EnvironmentMetrics.Builder().also { wb -> wb.barometric_pressure = 1013.25f }.build()), + ) val result = EnvironmentMetricsState(metrics).environmentMetricsForGraphing() // rightMinMax should be 0/1 defaults since no right-axis metrics @@ -173,7 +189,10 @@ class EnvironmentMetricsForGraphingTest { @Test fun lux_plotted() { val metrics = - listOf(telemetry(env = EnvironmentMetrics(lux = 500f)), telemetry(env = EnvironmentMetrics(lux = 1200f))) + listOf( + telemetry(env = EnvironmentMetrics.Builder().also { wb -> wb.lux = 500f }.build()), + telemetry(env = EnvironmentMetrics.Builder().also { wb -> wb.lux = 1200f }.build()), + ) val result = EnvironmentMetricsState(metrics).environmentMetricsForGraphing() assertTrue(result.shouldPlot[Environment.LUX.ordinal]) @@ -184,7 +203,10 @@ class EnvironmentMetricsForGraphingTest { @Test fun uvLux_plotted() { val metrics = - listOf(telemetry(env = EnvironmentMetrics(uv_lux = 2f)), telemetry(env = EnvironmentMetrics(uv_lux = 8f))) + listOf( + telemetry(env = EnvironmentMetrics.Builder().also { wb -> wb.uv_lux = 2f }.build()), + telemetry(env = EnvironmentMetrics.Builder().also { wb -> wb.uv_lux = 8f }.build()), + ) val result = EnvironmentMetricsState(metrics).environmentMetricsForGraphing() assertTrue(result.shouldPlot[Environment.UV_LUX.ordinal]) @@ -194,8 +216,8 @@ class EnvironmentMetricsForGraphingTest { fun windSpeed_plotted() { val metrics = listOf( - telemetry(env = EnvironmentMetrics(wind_speed = 5f)), - telemetry(env = EnvironmentMetrics(wind_speed = 25f)), + telemetry(env = EnvironmentMetrics.Builder().also { wb -> wb.wind_speed = 5f }.build()), + telemetry(env = EnvironmentMetrics.Builder().also { wb -> wb.wind_speed = 25f }.build()), ) val result = EnvironmentMetricsState(metrics).environmentMetricsForGraphing() @@ -206,8 +228,8 @@ class EnvironmentMetricsForGraphingTest { fun radiation_positiveValuesOnly() { val metrics = listOf( - telemetry(env = EnvironmentMetrics(radiation = 0f)), - telemetry(env = EnvironmentMetrics(radiation = 0.15f)), + telemetry(env = EnvironmentMetrics.Builder().also { wb -> wb.radiation = 0f }.build()), + telemetry(env = EnvironmentMetrics.Builder().also { wb -> wb.radiation = 0.15f }.build()), ) val result = EnvironmentMetricsState(metrics).environmentMetricsForGraphing() @@ -222,7 +244,17 @@ class EnvironmentMetricsForGraphingTest { @Test fun oneWireChannels_plotIndependently() { val metrics = - listOf(telemetry(env = EnvironmentMetrics(one_wire_temperature_ch0 = 10f, one_wire_temperature_ch7 = 40f))) + listOf( + telemetry( + env = + EnvironmentMetrics.Builder() + .also { wb -> + wb.one_wire_temperature_ch0 = 10f + wb.one_wire_temperature_ch7 = 40f + } + .build(), + ), + ) val result = EnvironmentMetricsState(metrics).environmentMetricsForGraphing() assertTrue(result.shouldPlot[Environment.ONE_WIRE_TEMP_1.ordinal]) @@ -233,7 +265,10 @@ class EnvironmentMetricsForGraphingTest { @Test fun oneWireChannels_convertToFahrenheit() { - val metrics = listOf(telemetry(env = EnvironmentMetrics(one_wire_temperature_ch2 = 100f))) + val metrics = + listOf( + telemetry(env = EnvironmentMetrics.Builder().also { wb -> wb.one_wire_temperature_ch2 = 100f }.build()), + ) val result = EnvironmentMetricsState(metrics).environmentMetricsForGraphing(useFahrenheit = true) assertTrue(result.shouldPlot[Environment.ONE_WIRE_TEMP_3.ordinal]) @@ -243,7 +278,10 @@ class EnvironmentMetricsForGraphingTest { /** 0°C is a real probe reading, not a "no sensor" sentinel — the series must still plot. */ @Test fun oneWireChannel_zeroIsPlotted() { - val metrics = listOf(telemetry(env = EnvironmentMetrics(one_wire_temperature_ch0 = 0f))) + val metrics = + listOf( + telemetry(env = EnvironmentMetrics.Builder().also { wb -> wb.one_wire_temperature_ch0 = 0f }.build()), + ) val result = EnvironmentMetricsState(metrics).environmentMetricsForGraphing() assertTrue(result.shouldPlot[Environment.ONE_WIRE_TEMP_1.ordinal]) @@ -251,7 +289,18 @@ class EnvironmentMetricsForGraphingTest { @Test fun adcChannels_plotIndependently() { - val metrics = listOf(telemetry(env = EnvironmentMetrics(adc_voltage_ch0 = 3.3f, adc_voltage_ch7 = 1.8f))) + val metrics = + listOf( + telemetry( + env = + EnvironmentMetrics.Builder() + .also { wb -> + wb.adc_voltage_ch0 = 3.3f + wb.adc_voltage_ch7 = 1.8f + } + .build(), + ), + ) val result = EnvironmentMetricsState(metrics).environmentMetricsForGraphing() assertTrue(result.shouldPlot[Environment.ADC_VOLTAGE_1.ordinal]) @@ -262,7 +311,8 @@ class EnvironmentMetricsForGraphingTest { /** 0 V is a real reading on an unloaded ADC input — the series must still plot. */ @Test fun adcChannel_zeroIsPlotted() { - val metrics = listOf(telemetry(env = EnvironmentMetrics(adc_voltage_ch3 = 0f))) + val metrics = + listOf(telemetry(env = EnvironmentMetrics.Builder().also { wb -> wb.adc_voltage_ch3 = 0f }.build())) val result = EnvironmentMetricsState(metrics).environmentMetricsForGraphing() assertTrue(result.shouldPlot[Environment.ADC_VOLTAGE_4.ordinal]) @@ -272,7 +322,8 @@ class EnvironmentMetricsForGraphingTest { /** ADC voltages are volts already; the Fahrenheit setting must not touch them. */ @Test fun adcChannels_areNotUnitConverted() { - val metrics = listOf(telemetry(env = EnvironmentMetrics(adc_voltage_ch0 = 100f))) + val metrics = + listOf(telemetry(env = EnvironmentMetrics.Builder().also { wb -> wb.adc_voltage_ch0 = 100f }.build())) val result = EnvironmentMetricsState(metrics).environmentMetricsForGraphing(useFahrenheit = true) assertEquals(100f, result.rightMinMax.second, 0.01f) @@ -280,7 +331,8 @@ class EnvironmentMetricsForGraphingTest { @Test fun adcChannel_nanFilteredOut() { - val metrics = listOf(telemetry(env = EnvironmentMetrics(adc_voltage_ch0 = Float.NaN))) + val metrics = + listOf(telemetry(env = EnvironmentMetrics.Builder().also { wb -> wb.adc_voltage_ch0 = Float.NaN }.build())) val result = EnvironmentMetricsState(metrics).environmentMetricsForGraphing() assertFalse(result.shouldPlot[Environment.ADC_VOLTAGE_1.ordinal]) @@ -290,7 +342,8 @@ class EnvironmentMetricsForGraphingTest { @Test fun nanTemperature_filteredOut() { - val metrics = listOf(telemetry(env = EnvironmentMetrics(temperature = Float.NaN))) + val metrics = + listOf(telemetry(env = EnvironmentMetrics.Builder().also { wb -> wb.temperature = Float.NaN }.build())) val result = EnvironmentMetricsState(metrics).environmentMetricsForGraphing() assertFalse(result.shouldPlot[Environment.TEMPERATURE.ordinal]) @@ -298,7 +351,10 @@ class EnvironmentMetricsForGraphingTest { @Test fun nanHumidity_filteredOut() { - val metrics = listOf(telemetry(env = EnvironmentMetrics(relative_humidity = Float.NaN))) + val metrics = + listOf( + telemetry(env = EnvironmentMetrics.Builder().also { wb -> wb.relative_humidity = Float.NaN }.build()), + ) val result = EnvironmentMetricsState(metrics).environmentMetricsForGraphing() assertFalse(result.shouldPlot[Environment.HUMIDITY.ordinal]) @@ -306,7 +362,10 @@ class EnvironmentMetricsForGraphingTest { @Test fun nanPressure_filteredOut() { - val metrics = listOf(telemetry(env = EnvironmentMetrics(barometric_pressure = Float.NaN))) + val metrics = + listOf( + telemetry(env = EnvironmentMetrics.Builder().also { wb -> wb.barometric_pressure = Float.NaN }.build()), + ) val result = EnvironmentMetricsState(metrics).environmentMetricsForGraphing() assertFalse(result.shouldPlot[Environment.BAROMETRIC_PRESSURE.ordinal]) @@ -320,22 +379,33 @@ class EnvironmentMetricsForGraphingTest { listOf( telemetry( env = - EnvironmentMetrics( - temperature = Float.NaN, - relative_humidity = 50f, - barometric_pressure = Float.NaN, - ), + EnvironmentMetrics.Builder() + .also { wb -> + wb.temperature = Float.NaN + wb.relative_humidity = 50f + wb.barometric_pressure = Float.NaN + } + .build(), ), telemetry( env = - EnvironmentMetrics( - temperature = 20f, - relative_humidity = Float.NaN, - barometric_pressure = 1015f, - ), + EnvironmentMetrics.Builder() + .also { wb -> + wb.temperature = 20f + wb.relative_humidity = Float.NaN + wb.barometric_pressure = 1015f + } + .build(), ), telemetry( - env = EnvironmentMetrics(temperature = 25f, relative_humidity = 60f, barometric_pressure = 1020f), + env = + EnvironmentMetrics.Builder() + .also { wb -> + wb.temperature = 25f + wb.relative_humidity = 60f + wb.barometric_pressure = 1020f + } + .build(), ), ) val result = EnvironmentMetricsState(metrics).environmentMetricsForGraphing() @@ -355,19 +425,23 @@ class EnvironmentMetricsForGraphingTest { listOf( telemetry( env = - EnvironmentMetrics( - temperature = Float.NaN, - relative_humidity = Float.NaN, - barometric_pressure = Float.NaN, - ), + EnvironmentMetrics.Builder() + .also { wb -> + wb.temperature = Float.NaN + wb.relative_humidity = Float.NaN + wb.barometric_pressure = Float.NaN + } + .build(), ), telemetry( env = - EnvironmentMetrics( - temperature = Float.NaN, - relative_humidity = Float.NaN, - barometric_pressure = Float.NaN, - ), + EnvironmentMetrics.Builder() + .also { wb -> + wb.temperature = Float.NaN + wb.relative_humidity = Float.NaN + wb.barometric_pressure = Float.NaN + } + .build(), ), ) val result = EnvironmentMetricsState(metrics).environmentMetricsForGraphing() @@ -385,8 +459,24 @@ class EnvironmentMetricsForGraphingTest { fun multipleMetrics_rightAxisMinMaxSpansAll() { val metrics = listOf( - telemetry(env = EnvironmentMetrics(temperature = 10f, relative_humidity = 80f)), - telemetry(env = EnvironmentMetrics(temperature = 30f, relative_humidity = 40f)), + telemetry( + env = + EnvironmentMetrics.Builder() + .also { wb -> + wb.temperature = 10f + wb.relative_humidity = 80f + } + .build(), + ), + telemetry( + env = + EnvironmentMetrics.Builder() + .also { wb -> + wb.temperature = 30f + wb.relative_humidity = 40f + } + .build(), + ), ) val result = EnvironmentMetricsState(metrics).environmentMetricsForGraphing() @@ -407,8 +497,8 @@ class EnvironmentMetricsForGraphingTest { // does not have explicit handling for it. This test documents that current behavior. val metrics = listOf( - telemetry(env = EnvironmentMetrics(gas_resistance = 100f)), - telemetry(env = EnvironmentMetrics(gas_resistance = 500f)), + telemetry(env = EnvironmentMetrics.Builder().also { wb -> wb.gas_resistance = 100f }.build()), + telemetry(env = EnvironmentMetrics.Builder().also { wb -> wb.gas_resistance = 500f }.build()), ) val result = EnvironmentMetricsState(metrics).environmentMetricsForGraphing() diff --git a/feature/node/src/commonTest/kotlin/org/meshtastic/feature/node/metrics/EnvironmentMetricsStateTest.kt b/feature/node/src/commonTest/kotlin/org/meshtastic/feature/node/metrics/EnvironmentMetricsStateTest.kt index 1800582f49..8c4a47bc2d 100644 --- a/feature/node/src/commonTest/kotlin/org/meshtastic/feature/node/metrics/EnvironmentMetricsStateTest.kt +++ b/feature/node/src/commonTest/kotlin/org/meshtastic/feature/node/metrics/EnvironmentMetricsStateTest.kt @@ -30,9 +30,27 @@ class EnvironmentMetricsStateTest { val now = nowSeconds.toInt() val metrics = listOf( - Telemetry(time = now - 100, environment_metrics = EnvironmentMetrics(temperature = 20f)), - Telemetry(time = now - 50, environment_metrics = EnvironmentMetrics(temperature = 22f)), - Telemetry(time = now, environment_metrics = EnvironmentMetrics(temperature = 21f)), + Telemetry.Builder() + .also { wb -> + wb.time = now - 100 + wb.environment_metrics = + EnvironmentMetrics.Builder().also { wb -> wb.temperature = 20f }.build() + } + .build(), + Telemetry.Builder() + .also { wb -> + wb.time = now - 50 + wb.environment_metrics = + EnvironmentMetrics.Builder().also { wb -> wb.temperature = 22f }.build() + } + .build(), + Telemetry.Builder() + .also { wb -> + wb.time = now + wb.environment_metrics = + EnvironmentMetrics.Builder().also { wb -> wb.temperature = 21f }.build() + } + .build(), ) val state = EnvironmentMetricsState(metrics) val result = state.environmentMetricsForGraphing() @@ -44,7 +62,16 @@ class EnvironmentMetricsStateTest { @Test fun environmentMetricsForGraphing_handles_valid_zero_temperatures() { val now = nowSeconds.toInt() - val metrics = listOf(Telemetry(time = now, environment_metrics = EnvironmentMetrics(temperature = 0.0f))) + val metrics = + listOf( + Telemetry.Builder() + .also { wb -> + wb.time = now + wb.environment_metrics = + EnvironmentMetrics.Builder().also { wb -> wb.temperature = 0.0f }.build() + } + .build(), + ) val state = EnvironmentMetricsState(metrics) val result = state.environmentMetricsForGraphing() diff --git a/feature/node/src/commonTest/kotlin/org/meshtastic/feature/node/metrics/HostMetricsTest.kt b/feature/node/src/commonTest/kotlin/org/meshtastic/feature/node/metrics/HostMetricsTest.kt index 4b769f1577..82b34dbc99 100644 --- a/feature/node/src/commonTest/kotlin/org/meshtastic/feature/node/metrics/HostMetricsTest.kt +++ b/feature/node/src/commonTest/kotlin/org/meshtastic/feature/node/metrics/HostMetricsTest.kt @@ -26,8 +26,12 @@ import kotlin.test.assertTrue @Suppress("MagicNumber") class HostMetricsTest { - private fun telemetry(time: Int, hostMetrics: HostMetrics? = null) = - Telemetry(time = time, host_metrics = hostMetrics) + private fun telemetry(time: Int, hostMetrics: HostMetrics? = null) = Telemetry.Builder() + .also { wb -> + wb.time = time + wb.host_metrics = hostMetrics + } + .build() @Test fun buildHostMetricsChartData_filters_missing_and_non_positive_values() { @@ -36,9 +40,27 @@ class HostMetricsTest { listOf( telemetry( time = 100, - hostMetrics = HostMetrics(load1 = 150, load5 = 0, load15 = 225, freemem_bytes = 2_097_152L), + hostMetrics = + HostMetrics.Builder() + .also { wb -> + wb.load1 = 150 + wb.load5 = 0 + wb.load15 = 225 + wb.freemem_bytes = 2_097_152L + } + .build(), + ), + telemetry( + time = 200, + hostMetrics = + HostMetrics.Builder() + .also { wb -> + wb.load1 = 0 + wb.load5 = 320 + wb.freemem_bytes = 0L + } + .build(), ), - telemetry(time = 200, hostMetrics = HostMetrics(load1 = 0, load5 = 320, freemem_bytes = 0L)), telemetry(time = 300, hostMetrics = null), ), ) @@ -57,9 +79,17 @@ class HostMetricsTest { listOf( telemetry( time = 100, - hostMetrics = HostMetrics(load1 = 0, load5 = 0, load15 = 0, freemem_bytes = 0L), + hostMetrics = + HostMetrics.Builder() + .also { wb -> + wb.load1 = 0 + wb.load5 = 0 + wb.load15 = 0 + wb.freemem_bytes = 0L + } + .build(), ), - telemetry(time = 200, hostMetrics = HostMetrics()), + telemetry(time = 200, hostMetrics = HostMetrics.Builder().build()), telemetry(time = 300, hostMetrics = null), ), ) diff --git a/feature/node/src/commonTest/kotlin/org/meshtastic/feature/node/metrics/MetricsViewModelTest.kt b/feature/node/src/commonTest/kotlin/org/meshtastic/feature/node/metrics/MetricsViewModelTest.kt index 9e25dc3347..022c8c1bdb 100644 --- a/feature/node/src/commonTest/kotlin/org/meshtastic/feature/node/metrics/MetricsViewModelTest.kt +++ b/feature/node/src/commonTest/kotlin/org/meshtastic/feature/node/metrics/MetricsViewModelTest.kt @@ -47,6 +47,7 @@ import org.meshtastic.feature.node.detail.NodeRequestActions import org.meshtastic.feature.node.domain.usecase.GetNodeDetailsUseCase import org.meshtastic.feature.node.model.MetricsState import org.meshtastic.feature.node.model.TimeFrame +import org.meshtastic.proto.AirQualityMetrics import org.meshtastic.proto.DeviceMetrics import org.meshtastic.proto.EnvironmentMetrics import org.meshtastic.proto.LocalStats @@ -151,9 +152,15 @@ class MetricsViewModelTest { nodeDetailFlow.value = NodeDetailUiState( node = - org.meshtastic.core.model.Node(num = 1234, user = org.meshtastic.proto.User(id = "!1234")), + org.meshtastic.core.model.Node( + num = 1234, + user = org.meshtastic.proto.User.Builder().also { wb -> wb.id = "!1234" }.build(), + ), environmentState = - EnvironmentMetricsState(environmentMetrics = listOf(Telemetry(time = twoHoursAgo.toInt()))), + EnvironmentMetricsState( + environmentMetrics = + listOf(Telemetry.Builder().also { wb -> wb.time = twoHoursAgo.toInt() }.build()), + ), ) // We might get multiple emissions as flows propagate @@ -170,10 +177,14 @@ class MetricsViewModelTest { nodeDetailFlow.value = NodeDetailUiState( node = - org.meshtastic.core.model.Node(num = 1234, user = org.meshtastic.proto.User(id = "!1234")), + org.meshtastic.core.model.Node( + num = 1234, + user = org.meshtastic.proto.User.Builder().also { wb -> wb.id = "!1234" }.build(), + ), environmentState = EnvironmentMetricsState( - environmentMetrics = listOf(Telemetry(time = eightDaysAgo.toInt())), + environmentMetrics = + listOf(Telemetry.Builder().also { wb -> wb.time = eightDaysAgo.toInt() }.build()), ), ) @@ -187,15 +198,17 @@ class MetricsViewModelTest { @Test fun `savePositionCSV writes correct data`() = runTest(testDispatcher) { val testPosition = - Position( - latitude_i = 123456789, - longitude_i = -987654321, - altitude = 100, - sats_in_view = 5, - ground_speed = 10, - ground_track = 123456, - time = 1700000000, - ) + Position.Builder() + .also { wb -> + wb.latitude_i = 123456789 + wb.longitude_i = -987654321 + wb.altitude = 100 + wb.sats_in_view = 5 + wb.ground_speed = 10 + wb.ground_track = 123456 + wb.time = 1700000000 + } + .build() val nodeDetailFlow = MutableStateFlow(NodeDetailUiState(metricsState = MetricsState(positionLogs = listOf(testPosition)))) @@ -234,17 +247,21 @@ class MetricsViewModelTest { @Test fun `saveDeviceMetricsCSV writes correct data`() = runTest(testDispatcher) { val testTelemetry = - Telemetry( - time = 1700000000, - device_metrics = - DeviceMetrics( - battery_level = 80, - voltage = 4.1f, - channel_utilization = 12.5f, - air_util_tx = 3.25f, - uptime_seconds = 3600, - ), - ) + Telemetry.Builder() + .also { wb -> + wb.time = 1700000000 + wb.device_metrics = + DeviceMetrics.Builder() + .also { wb -> + wb.battery_level = 80 + wb.voltage = 4.1f + wb.channel_utilization = 12.5f + wb.air_util_tx = 3.25f + wb.uptime_seconds = 3600 + } + .build() + } + .build() val nodeDetailFlow = MutableStateFlow(NodeDetailUiState(metricsState = MetricsState(deviceMetrics = listOf(testTelemetry)))) @@ -284,27 +301,33 @@ class MetricsViewModelTest { @Test fun `saveEnvironmentMetricsCSV writes correct data`() = runTest(testDispatcher) { val testTelemetry = - Telemetry( - time = 1700000000, - environment_metrics = - EnvironmentMetrics( - temperature = 21.5f, - relative_humidity = 55.5f, - barometric_pressure = 1013.25f, - gas_resistance = 12.3f, - iaq = 42, - wind_speed = 5.5f, - wind_direction = 180, - soil_temperature = 18.75f, - soil_moisture = 65, - one_wire_temperature_ch0 = 1f, - one_wire_temperature_ch1 = 2f, - one_wire_temperature_ch2 = 3f, - // 0 V is a real reading on an unloaded ADC input, so it exports as 0.0, not empty. - adc_voltage_ch0 = 3.3f, - adc_voltage_ch1 = 0f, - ), - ) + Telemetry.Builder() + .also { wb -> + wb.time = 1700000000 + wb.environment_metrics = + EnvironmentMetrics.Builder() + .also { wb -> + wb.temperature = 21.5f + wb.relative_humidity = 55.5f + wb.barometric_pressure = 1013.25f + wb.gas_resistance = 12.3f + wb.iaq = 42 + wb.wind_speed = 5.5f + wb.wind_direction = 180 + wb.soil_temperature = 18.75f + wb.soil_moisture = 65 + wb.one_wire_temperature_ch0 = 1f + wb.one_wire_temperature_ch1 = 2f + wb.one_wire_temperature_ch2 = 3f + // 0 V is a real reading on an unloaded ADC input, so it exports as 0.0, not empty. + wb.adc_voltage_ch0 = 3.3f + wb.adc_voltage_ch1 = 0f + wb.lightning_strike_count_1h = 3 + wb.lightning_distance_km = 12f + } + .build() + } + .build() val nodeDetailFlow = MutableStateFlow( @@ -336,12 +359,12 @@ class MetricsViewModelTest { val csvOutput = buffer.readUtf8() assertTrue( csvOutput.startsWith( - "\"date\",\"time\",\"temperature\",\"relativeHumidity\",\"barometricPressure\",\"gasResistance\",\"iaq\",\"windSpeed\",\"windDirection\",\"soilTemperature\",\"soilMoisture\",\"oneWireTemp1\",\"oneWireTemp2\",\"oneWireTemp3\",\"oneWireTemp4\",\"oneWireTemp5\",\"oneWireTemp6\",\"oneWireTemp7\",\"oneWireTemp8\",\"adcVoltage1\",\"adcVoltage2\",\"adcVoltage3\",\"adcVoltage4\",\"adcVoltage5\",\"adcVoltage6\",\"adcVoltage7\",\"adcVoltage8\"", + "\"date\",\"time\",\"temperature\",\"relativeHumidity\",\"barometricPressure\",\"gasResistance\",\"iaq\",\"windSpeed\",\"windDirection\",\"soilTemperature\",\"soilMoisture\",\"oneWireTemp1\",\"oneWireTemp2\",\"oneWireTemp3\",\"oneWireTemp4\",\"oneWireTemp5\",\"oneWireTemp6\",\"oneWireTemp7\",\"oneWireTemp8\",\"adcVoltage1\",\"adcVoltage2\",\"adcVoltage3\",\"adcVoltage4\",\"adcVoltage5\",\"adcVoltage6\",\"adcVoltage7\",\"adcVoltage8\",\"lightningStrikeCount1h\",\"lightningDistanceKm\"\n", ), ) assertTrue( csvOutput.contains( - "\"21.5\",\"55.5\",\"1013.25\",\"12.3\",\"42\",\"5.5\",\"180\",\"18.75\",\"65\",\"1.0\",\"2.0\",\"3.0\",\"\",\"\",\"\",\"\",\"\",\"3.3\",\"0.0\",\"\",\"\",\"\",\"\",\"\",\"\"", + "\"21.5\",\"55.5\",\"1013.25\",\"12.3\",\"42\",\"5.5\",\"180\",\"18.75\",\"65\",\"1.0\",\"2.0\",\"3.0\",\"\",\"\",\"\",\"\",\"\",\"3.3\",\"0.0\",\"\",\"\",\"\",\"\",\"\",\"\",\"3\",\"12.0\"\n", ), ) @@ -349,9 +372,68 @@ class MetricsViewModelTest { } } + /** A status register of 0 is a healthy sensor, and 0 is a reading: it exports as "0", never as an empty cell. */ + @Test + fun `saveAirQualityMetricsCSV exports the raw pm status register`() = runTest(testDispatcher) { + val testTelemetry = + Telemetry.Builder() + .also { wb -> + wb.time = 1700000000 + wb.air_quality_metrics = + AirQualityMetrics.Builder() + .also { wb -> + wb.pm10_standard = 5 + wb.pm_status_flags = 0 + } + .build() + } + .build() + + val nodeDetailFlow = + MutableStateFlow( + NodeDetailUiState(metricsState = MetricsState(airQualityMetrics = listOf(testTelemetry))), + ) + every { getNodeDetailsUseCase(1234) } returns nodeDetailFlow.asStateFlow() + + val buffer = Buffer() + everySuspend { fileService.write(any(), any()) } calls + { args -> + val block = args.arg Unit>(1) + block(buffer) + true + } + + val vm = createViewModel() + vm.state.test { + awaitItem() + awaitItem() + + val uri = CommonUri.parse("content://test") + vm.saveAirQualityMetricsCSV(uri, listOf(AirQualitySample(testTelemetry, aqi = null))) + runCurrent() + + verifySuspend { fileService.write(uri, any()) } + + val lines = buffer.readUtf8().lines() + assertTrue(lines[0].startsWith("\"date\",\"time\",\"aqi\",\"pm10_standard\",")) + assertTrue(lines[0].endsWith("\"particles_tps\",\"pm_status_flags\"")) + assertTrue(lines[1].contains("\"\",\"5\",\"\","), "no AQI, then the PM1.0 reading") + assertTrue(lines[1].endsWith("\"\",\"0\""), "absent particles_tps, then the zero register") + + cancelAndIgnoreRemainingEvents() + } + } + @Test fun `saveSignalMetricsCSV writes correct data`() = runTest(testDispatcher) { - val testPacket = MeshPacket(rx_time = 1700000000, rx_rssi = -105, rx_snr = 7.5f) + val testPacket = + MeshPacket.Builder() + .also { wb -> + wb.rx_time = 1700000000 + wb.rx_rssi = -105 + wb.rx_snr = 7.5f + } + .build() val nodeDetailFlow = MutableStateFlow(NodeDetailUiState(metricsState = MetricsState(signalMetrics = listOf(testPacket)))) @@ -387,26 +469,41 @@ class MetricsViewModelTest { @Test fun `saveLocalStatsCSV writes only provided visible data`() = runTest(testDispatcher) { val visibleTelemetry = - Telemetry( - time = 1700000000, - local_stats = - LocalStats( - noise_floor = -112, - uptime_seconds = 3600, - channel_utilization = 12.5f, - air_util_tx = 3.25f, - num_packets_tx = 2, - num_packets_rx = 3, - num_packets_rx_bad = 1, - num_rx_dupe = 4, - num_tx_relay = 5, - num_tx_relay_canceled = 6, - num_online_nodes = 7, - num_total_nodes = 8, - ), - ) + Telemetry.Builder() + .also { wb -> + wb.time = 1700000000 + wb.local_stats = + LocalStats.Builder() + .also { wb -> + wb.noise_floor = -112 + wb.uptime_seconds = 3600 + wb.channel_utilization = 12.5f + wb.air_util_tx = 3.25f + wb.num_packets_tx = 2 + wb.num_packets_rx = 3 + wb.num_packets_rx_bad = 1 + wb.num_rx_dupe = 4 + wb.num_tx_relay = 5 + wb.num_tx_relay_canceled = 6 + wb.num_online_nodes = 7 + wb.num_total_nodes = 8 + } + .build() + } + .build() val hiddenTelemetry = - Telemetry(time = 1600000000, local_stats = LocalStats(noise_floor = -99, uptime_seconds = 10)) + Telemetry.Builder() + .also { wb -> + wb.time = 1600000000 + wb.local_stats = + LocalStats.Builder() + .also { wb -> + wb.noise_floor = -99 + wb.uptime_seconds = 10 + } + .build() + } + .build() val nodeDetailFlow = MutableStateFlow( @@ -459,18 +556,22 @@ class MetricsViewModelTest { @Test fun `savePowerMetricsCSV writes correct data`() = runTest(testDispatcher) { val testTelemetry = - Telemetry( - time = 1700000000, - power_metrics = - PowerMetrics( - ch1_voltage = 3.3f, - ch1_current = 0.1f, - ch2_voltage = 5.0f, - ch2_current = 0.2f, - ch3_voltage = 12.0f, - ch3_current = 0.3f, - ), - ) + Telemetry.Builder() + .also { wb -> + wb.time = 1700000000 + wb.power_metrics = + PowerMetrics.Builder() + .also { wb -> + wb.ch1_voltage = 3.3f + wb.ch1_current = 0.1f + wb.ch2_voltage = 5.0f + wb.ch2_current = 0.2f + wb.ch3_voltage = 12.0f + wb.ch3_current = 0.3f + } + .build() + } + .build() val nodeDetailFlow = MutableStateFlow(NodeDetailUiState(metricsState = MetricsState(powerMetrics = listOf(testTelemetry)))) diff --git a/feature/node/src/commonTest/kotlin/org/meshtastic/feature/node/metrics/PmStatusFaultTest.kt b/feature/node/src/commonTest/kotlin/org/meshtastic/feature/node/metrics/PmStatusFaultTest.kt new file mode 100644 index 0000000000..ecc3ac4e9d --- /dev/null +++ b/feature/node/src/commonTest/kotlin/org/meshtastic/feature/node/metrics/PmStatusFaultTest.kt @@ -0,0 +1,68 @@ +/* + * 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 . + */ +package org.meshtastic.feature.node.metrics + +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertNull + +/** Pins the SEN6X bit layout the proto documents for `pm_status_flags`, and the pass-through of bits it does not. */ +class PmStatusFaultTest { + + @Test + fun `a clean register decodes to no faults`() { + assertEquals(emptyList(), PmStatusFault.decode(0)) + assertEquals(0, PmStatusFault.unknownBits(0)) + assertNull(PmStatusFault.unknownBitsHex(0)) + } + + @Test + fun `each documented bit decodes to its fault in bit order`() { + val flags = (1 shl 21) or (1 shl 11) or (1 shl 4) + assertEquals( + listOf(PmStatusFault.FAN_ERROR, PmStatusFault.PM_ERROR, PmStatusFault.FAN_SPEED_WARNING), + PmStatusFault.decode(flags), + ) + assertEquals(0, PmStatusFault.unknownBits(flags)) + } + + @Test + fun `the two CO2 error bits report one fault`() { + val flags = (1 shl 9) or (1 shl 12) + assertEquals(listOf(PmStatusFault.CO2_ERROR_SEN66), PmStatusFault.decode(flags)) + } + + @Test + fun `bits no sensor documents are kept as unknown`() { + val flags = (1 shl 0) or (1 shl 6) or (1 shl 30) + assertEquals(listOf(PmStatusFault.RHT_ERROR), PmStatusFault.decode(flags)) + assertEquals((1 shl 0) or (1 shl 30), PmStatusFault.unknownBits(flags)) + assertEquals("40000001", PmStatusFault.unknownBitsHex(flags)) + } + + @Test + fun `bit 31 renders as an unsigned register`() { + val flags = 1 shl 31 + assertEquals(emptyList(), PmStatusFault.decode(flags)) + assertEquals("80000000", PmStatusFault.unknownBitsHex(flags)) + } + + @Test + fun `only the fan speed bit is a warning`() { + assertEquals(listOf(PmStatusFault.FAN_SPEED_WARNING), PmStatusFault.entries.filter { it.isWarning }) + } +} diff --git a/feature/node/src/commonTest/kotlin/org/meshtastic/feature/node/metrics/SignalLogKeyTest.kt b/feature/node/src/commonTest/kotlin/org/meshtastic/feature/node/metrics/SignalLogKeyTest.kt index e8e13682df..7a37359ccf 100644 --- a/feature/node/src/commonTest/kotlin/org/meshtastic/feature/node/metrics/SignalLogKeyTest.kt +++ b/feature/node/src/commonTest/kotlin/org/meshtastic/feature/node/metrics/SignalLogKeyTest.kt @@ -70,7 +70,12 @@ class SignalLogKeyTest { assertEquals(listOf(30, 20, 10), log.map { it.timeSeconds }) } - private fun packet(id: Int, rxTime: Int = 0) = MeshPacket(id = id, rx_time = rxTime) + private fun packet(id: Int, rxTime: Int = 0) = MeshPacket.Builder() + .also { wb -> + wb.id = id + wb.rx_time = rxTime + } + .build() - private fun stats(time: Int) = Telemetry(time = time) + private fun stats(time: Int) = Telemetry.Builder().also { wb -> wb.time = time }.build() } diff --git a/feature/node/src/commonTest/kotlin/org/meshtastic/feature/node/metrics/TracerouteChartTest.kt b/feature/node/src/commonTest/kotlin/org/meshtastic/feature/node/metrics/TracerouteChartTest.kt index a80b2172ed..b25e840bd4 100644 --- a/feature/node/src/commonTest/kotlin/org/meshtastic/feature/node/metrics/TracerouteChartTest.kt +++ b/feature/node/src/commonTest/kotlin/org/meshtastic/feature/node/metrics/TracerouteChartTest.kt @@ -63,15 +63,25 @@ class TracerouteChartTest { received_date = receivedDateMillis, raw_message = "", fromRadio = - FromRadio( - packet = - MeshPacket( - id = id, - from = LOCAL_NODE, - to = REMOTE_NODE, - decoded = Data(portnum = PortNum.TRACEROUTE_APP, want_response = true), - ), - ), + FromRadio.Builder() + .also { wb -> + wb.packet = + MeshPacket.Builder() + .also { wb -> + wb.id = id + wb.from = LOCAL_NODE + wb.to = REMOTE_NODE + wb.decoded = + Data.Builder() + .also { wb -> + wb.portnum = PortNum.TRACEROUTE_APP + wb.want_response = true + } + .build() + } + .build() + } + .build(), ) /** @@ -93,31 +103,39 @@ class TracerouteChartTest { // snr_back must have one entry per node in route_back for fullRouteDiscovery to wrap it val snrBack = intermediateRouteBack?.map { DUMMY_SNR } ?: emptyList() val rd = - RouteDiscovery( - route = intermediateRoute, - route_back = intermediateRouteBack ?: emptyList(), - snr_back = snrBack, - ) + RouteDiscovery.Builder() + .also { wb -> + wb.route = intermediateRoute + wb.route_back = intermediateRouteBack ?: emptyList() + wb.snr_back = snrBack + } + .build() return MeshLog( uuid = "res-$requestId", message_type = "TRACEROUTE", received_date = receivedDateMillis, raw_message = "", fromRadio = - FromRadio( - packet = - MeshPacket( - from = REMOTE_NODE, - to = LOCAL_NODE, - hop_start = hopStart, - decoded = - Data( - portnum = PortNum.TRACEROUTE_APP, - request_id = requestId, - payload = RouteDiscovery.ADAPTER.encode(rd).toByteString(), - ), - ), - ), + FromRadio.Builder() + .also { wb -> + wb.packet = + MeshPacket.Builder() + .also { wb -> + wb.from = REMOTE_NODE + wb.to = LOCAL_NODE + wb.hop_start = hopStart + wb.decoded = + Data.Builder() + .also { wb -> + wb.portnum = PortNum.TRACEROUTE_APP + wb.request_id = requestId + wb.payload = RouteDiscovery.ADAPTER.encode(rd).toByteString() + } + .build() + } + .build() + } + .build(), ) } diff --git a/feature/node/src/commonTest/kotlin/org/meshtastic/feature/node/model/CanDirectMessageTest.kt b/feature/node/src/commonTest/kotlin/org/meshtastic/feature/node/model/CanDirectMessageTest.kt index 5d1bffd49c..4a69503713 100644 --- a/feature/node/src/commonTest/kotlin/org/meshtastic/feature/node/model/CanDirectMessageTest.kt +++ b/feature/node/src/commonTest/kotlin/org/meshtastic/feature/node/model/CanDirectMessageTest.kt @@ -38,7 +38,15 @@ class CanDirectMessageTest { role: Config.DeviceConfig.Role = Config.DeviceConfig.Role.CLIENT, ) = Node( num = 1, - user = User(id = "!00000001", public_key = publicKey, is_unmessagable = isUnmessagable, role = role), + user = + User.Builder() + .also { wb -> + wb.id = "!00000001" + wb.public_key = publicKey + wb.is_unmessagable = isUnmessagable + wb.role = role + } + .build(), ) @Test diff --git a/feature/node/src/commonTest/kotlin/org/meshtastic/feature/node/model/MetricsStateTest.kt b/feature/node/src/commonTest/kotlin/org/meshtastic/feature/node/model/MetricsStateTest.kt index 69b0fa2698..50a2221139 100644 --- a/feature/node/src/commonTest/kotlin/org/meshtastic/feature/node/model/MetricsStateTest.kt +++ b/feature/node/src/commonTest/kotlin/org/meshtastic/feature/node/model/MetricsStateTest.kt @@ -35,12 +35,17 @@ class MetricsStateTest { fun oldestTimestampIncludesLocalStatsTelemetry() { val state = MetricsState( - deviceMetrics = listOf(Telemetry(time = 200)), + deviceMetrics = listOf(Telemetry.Builder().also { wb -> wb.time = 200 }.build()), localStats = listOf(localStatsTelemetry(time = 100)), ) assertEquals(100L, state.oldestTimestampSeconds()) } - private fun localStatsTelemetry(time: Int) = Telemetry(time = time, local_stats = LocalStats(noise_floor = -101)) + private fun localStatsTelemetry(time: Int) = Telemetry.Builder() + .also { wb -> + wb.time = time + wb.local_stats = LocalStats.Builder().also { wb -> wb.noise_floor = -101 }.build() + } + .build() } diff --git a/feature/node/src/jvmTest/kotlin/org/meshtastic/feature/node/component/EnvironmentChannelMetricsTest.kt b/feature/node/src/jvmTest/kotlin/org/meshtastic/feature/node/component/EnvironmentChannelMetricsTest.kt index f62c4fe4f8..f32e04eab9 100644 --- a/feature/node/src/jvmTest/kotlin/org/meshtastic/feature/node/component/EnvironmentChannelMetricsTest.kt +++ b/feature/node/src/jvmTest/kotlin/org/meshtastic/feature/node/component/EnvironmentChannelMetricsTest.kt @@ -37,21 +37,28 @@ class EnvironmentChannelMetricsTest { @Test fun zeroOneWireTemperatureIsShown() = runComposeUiTest { - setEnvironmentMetrics(EnvironmentMetricsProto(one_wire_temperature_ch0 = 0f)) + setEnvironmentMetrics(EnvironmentMetricsProto.Builder().also { wb -> wb.one_wire_temperature_ch0 = 0f }.build()) onNodeWithText("1-Wire Temp 1").assertIsDisplayed() onNodeWithText("0°C").assertIsDisplayed() } @Test fun absentOneWireChannelIsHidden() = runComposeUiTest { - setEnvironmentMetrics(EnvironmentMetricsProto()) + setEnvironmentMetrics(EnvironmentMetricsProto.Builder().build()) onNodeWithText("1-Wire Temp 1").assertDoesNotExist() } @Test fun oneWireChannelsKeepTheirChannelNumber() = runComposeUiTest { // A gap in the middle must not renumber the channels above it. - setEnvironmentMetrics(EnvironmentMetricsProto(one_wire_temperature_ch0 = 10f, one_wire_temperature_ch2 = 30f)) + setEnvironmentMetrics( + EnvironmentMetricsProto.Builder() + .also { wb -> + wb.one_wire_temperature_ch0 = 10f + wb.one_wire_temperature_ch2 = 30f + } + .build(), + ) onNodeWithText("1-Wire Temp 1").assertIsDisplayed() onNodeWithText("1-Wire Temp 3").assertIsDisplayed() onNodeWithText("1-Wire Temp 2").assertDoesNotExist() @@ -59,20 +66,27 @@ class EnvironmentChannelMetricsTest { @Test fun zeroAdcVoltageIsShown() = runComposeUiTest { - setEnvironmentMetrics(EnvironmentMetricsProto(adc_voltage_ch0 = 0f)) + setEnvironmentMetrics(EnvironmentMetricsProto.Builder().also { wb -> wb.adc_voltage_ch0 = 0f }.build()) onNodeWithText("ADC Voltage 1").assertIsDisplayed() onNodeWithText("0.00 V").assertIsDisplayed() } @Test fun absentAdcChannelIsHidden() = runComposeUiTest { - setEnvironmentMetrics(EnvironmentMetricsProto()) + setEnvironmentMetrics(EnvironmentMetricsProto.Builder().build()) onNodeWithText("ADC Voltage 1").assertDoesNotExist() } @Test fun adcChannelsKeepTheirChannelNumber() = runComposeUiTest { - setEnvironmentMetrics(EnvironmentMetricsProto(adc_voltage_ch1 = 3.3f, adc_voltage_ch7 = 1.8f)) + setEnvironmentMetrics( + EnvironmentMetricsProto.Builder() + .also { wb -> + wb.adc_voltage_ch1 = 3.3f + wb.adc_voltage_ch7 = 1.8f + } + .build(), + ) onNodeWithText("ADC Voltage 2").assertIsDisplayed() onNodeWithText("3.30 V").assertIsDisplayed() onNodeWithText("ADC Voltage 8").assertIsDisplayed() diff --git a/feature/node/src/jvmTest/kotlin/org/meshtastic/feature/node/component/PowerMetricsZeroVoltageTest.kt b/feature/node/src/jvmTest/kotlin/org/meshtastic/feature/node/component/PowerMetricsZeroVoltageTest.kt index f591a6780e..9ae6cb0ee9 100644 --- a/feature/node/src/jvmTest/kotlin/org/meshtastic/feature/node/component/PowerMetricsZeroVoltageTest.kt +++ b/feature/node/src/jvmTest/kotlin/org/meshtastic/feature/node/component/PowerMetricsZeroVoltageTest.kt @@ -35,21 +35,28 @@ class PowerMetricsZeroVoltageTest { @Test fun zeroVoltageChannelIsShown() = runComposeUiTest { - setPowerMetrics(PowerMetricsProto(ch1_voltage = 0f, ch1_current = 0f)) + setPowerMetrics( + PowerMetricsProto.Builder() + .also { wb -> + wb.ch1_voltage = 0f + wb.ch1_current = 0f + } + .build(), + ) onNodeWithText("0.00V").assertIsDisplayed() onNodeWithText("0.0mA").assertIsDisplayed() } @Test fun absentChannelIsHidden() = runComposeUiTest { - setPowerMetrics(PowerMetricsProto()) + setPowerMetrics(PowerMetricsProto.Builder().build()) onNodeWithText("0.00V").assertDoesNotExist() onNodeWithText("0.0mA").assertDoesNotExist() } @Test fun reportedVoltageWithoutCurrentShowsVoltageOnly() = runComposeUiTest { - setPowerMetrics(PowerMetricsProto(ch1_voltage = 3.7f)) + setPowerMetrics(PowerMetricsProto.Builder().also { wb -> wb.ch1_voltage = 3.7f }.build()) onNodeWithText("3.70V").assertIsDisplayed() onNodeWithText("0.0mA").assertDoesNotExist() } diff --git a/feature/node/src/jvmTest/kotlin/org/meshtastic/feature/node/detail/NodeDetailCompassLifecycleTest.kt b/feature/node/src/jvmTest/kotlin/org/meshtastic/feature/node/detail/NodeDetailCompassLifecycleTest.kt index c509760ce2..a5949c42aa 100644 --- a/feature/node/src/jvmTest/kotlin/org/meshtastic/feature/node/detail/NodeDetailCompassLifecycleTest.kt +++ b/feature/node/src/jvmTest/kotlin/org/meshtastic/feature/node/detail/NodeDetailCompassLifecycleTest.kt @@ -98,8 +98,20 @@ class NodeDetailCompassLifecycleTest { val node = Node( num = 1234, - user = User(id = "!000004d2", long_name = "Compass target"), - position = Position(latitude_i = 10000000, longitude_i = 10000000), + user = + User.Builder() + .also { wb -> + wb.id = "!000004d2" + wb.long_name = "Compass target" + } + .build(), + position = + Position.Builder() + .also { wb -> + wb.latitude_i = 10000000 + wb.longitude_i = 10000000 + } + .build(), ) val nodeDetailViewModel = createNodeDetailViewModel(node) viewModelStore.put("node-detail", nodeDetailViewModel) diff --git a/feature/node/src/jvmTest/kotlin/org/meshtastic/feature/node/metrics/ExportLocaleInvarianceTest.kt b/feature/node/src/jvmTest/kotlin/org/meshtastic/feature/node/metrics/ExportLocaleInvarianceTest.kt index 1f821c87be..5a3fc0bf50 100644 --- a/feature/node/src/jvmTest/kotlin/org/meshtastic/feature/node/metrics/ExportLocaleInvarianceTest.kt +++ b/feature/node/src/jvmTest/kotlin/org/meshtastic/feature/node/metrics/ExportLocaleInvarianceTest.kt @@ -41,7 +41,18 @@ class ExportLocaleInvarianceTest { fun `gpx coordinates keep a dot separator on a comma locale`() { Locale.setDefault(Locale.GERMANY) - val gpx = buildGpx(listOf(Position(latitude_i = 525_200_000, longitude_i = 134_050_000)), "Berlin") + val gpx = + buildGpx( + listOf( + Position.Builder() + .also { wb -> + wb.latitude_i = 525_200_000 + wb.longitude_i = 134_050_000 + } + .build(), + ), + "Berlin", + ) assertTrue(gpx.contains("lat=\"52.52"), "latitude was localized:\n$gpx") assertTrue(gpx.contains("lon=\"13.40"), "longitude was localized:\n$gpx") diff --git a/feature/settings/src/androidMain/kotlin/org/meshtastic/feature/settings/SettingsScreen.kt b/feature/settings/src/androidMain/kotlin/org/meshtastic/feature/settings/SettingsScreen.kt index 0d3aef21e3..82e59f84c0 100644 --- a/feature/settings/src/androidMain/kotlin/org/meshtastic/feature/settings/SettingsScreen.kt +++ b/feature/settings/src/androidMain/kotlin/org/meshtastic/feature/settings/SettingsScreen.kt @@ -89,6 +89,7 @@ import org.meshtastic.feature.settings.radio.RadioConfigViewModel import org.meshtastic.feature.settings.radio.component.EditDeviceProfileDialog import org.meshtastic.feature.settings.util.LanguageUtils import org.meshtastic.feature.settings.util.LanguageUtils.languageMap +import org.meshtastic.feature.settings.util.deviceProfileExportFileName import org.meshtastic.proto.DeviceProfile import kotlin.time.Instant.Companion.fromEpochMilliseconds @@ -146,7 +147,6 @@ fun SettingsScreen( viewModel.installProfile(it) } else { deviceProfile = it - val nodeName = (it.short_name ?: "").ifBlank { "node" } val dateStr = fromEpochMilliseconds(nowMillis) .toLocalDateTime(TimeZone.currentSystemDefault()) @@ -157,7 +157,7 @@ fun SettingsScreen( day() }, ) - val fileName = "Meshtastic_${nodeName}_${dateStr}_nodeConfig.cfg" + val fileName = deviceProfileExportFileName(it.long_name, it.short_name, dateStr) val intent = Intent(Intent.ACTION_CREATE_DOCUMENT).apply { addCategory(Intent.CATEGORY_OPENABLE) diff --git a/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/AboutScreen.kt b/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/AboutScreen.kt index 296f748446..fe68859d8e 100644 --- a/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/AboutScreen.kt +++ b/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/AboutScreen.kt @@ -60,6 +60,8 @@ import org.meshtastic.core.resources.apps import org.meshtastic.core.resources.copyright_notice import org.meshtastic.core.resources.documentation import org.meshtastic.core.resources.github_repository +import org.meshtastic.core.resources.license +import org.meshtastic.core.resources.license_notice import org.meshtastic.core.resources.need_hardware import org.meshtastic.core.resources.need_hardware_description import org.meshtastic.core.resources.project_information @@ -75,6 +77,7 @@ import org.meshtastic.core.ui.icon.Info import org.meshtastic.core.ui.icon.Language import org.meshtastic.core.ui.icon.Memory import org.meshtastic.core.ui.icon.MeshtasticIcons +import org.meshtastic.core.ui.icon.Notes import org.meshtastic.core.ui.theme.AppTheme import org.meshtastic.feature.settings.component.ExpressiveSection @@ -87,6 +90,7 @@ private const val HARDWARE_URL = "https://meshtastic.org/#hardware" private const val GITHUB_REPO_URL = "https://github.com/meshtastic/Meshtastic-Android" private const val WEBSITE_URL = "https://meshtastic.org" private const val DOCS_URL = "https://meshtastic.org/docs/getting-started" +private const val LICENSE_URL = "https://www.gnu.org/licenses/gpl-3.0.html" private data class PopularDevice(val name: String, val svgFileName: String) @@ -146,6 +150,7 @@ fun AboutScreen( ProjectInformationSection( onOpenWebsite = { uriHandler.openUri(WEBSITE_URL) }, onOpenDocs = { uriHandler.openUri(DOCS_URL) }, + onOpenLicense = { uriHandler.openUri(LICENSE_URL) }, ) CopyrightFooter() } @@ -199,6 +204,7 @@ private fun AppsSection( private fun ProjectInformationSection( onOpenWebsite: () -> Unit, onOpenDocs: () -> Unit, + onOpenLicense: () -> Unit, modifier: Modifier = Modifier, ) { ExpressiveSection(title = stringResource(Res.string.project_information), modifier = modifier) { @@ -214,18 +220,38 @@ private fun ProjectInformationSection( trailingIcon = MeshtasticIcons.ChevronRight, onClick = onOpenDocs, ) + ListItem( + text = stringResource(Res.string.license), + leadingIcon = MeshtasticIcons.Notes, + trailingIcon = MeshtasticIcons.ChevronRight, + onClick = onOpenLicense, + ) } } +// GPL section 5(d): this screen shows the copyright notice, the no-warranty statement and where the +// license is, so a modified version has to keep showing them too. Keep all three together here. @Composable private fun CopyrightFooter(modifier: Modifier = Modifier) { - Text( - text = stringResource(Res.string.copyright_notice), + Column( modifier = modifier.fillMaxWidth().padding(vertical = 8.dp), - style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.onSurfaceVariant, - textAlign = TextAlign.Center, - ) + verticalArrangement = Arrangement.spacedBy(4.dp), + ) { + Text( + text = stringResource(Res.string.copyright_notice), + modifier = Modifier.fillMaxWidth(), + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + textAlign = TextAlign.Center, + ) + Text( + text = stringResource(Res.string.license_notice), + modifier = Modifier.fillMaxWidth(), + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + textAlign = TextAlign.Center, + ) + } } @Composable diff --git a/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/AcknowledgementsScreen.kt b/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/AcknowledgementsScreen.kt index 7de6d9c02f..c9c2e57448 100644 --- a/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/AcknowledgementsScreen.kt +++ b/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/AcknowledgementsScreen.kt @@ -14,8 +14,6 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -@file:OptIn(ExperimentalMaterial3ExpressiveApi::class) - package org.meshtastic.feature.settings import androidx.compose.foundation.layout.Box @@ -23,7 +21,6 @@ import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding -import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Scaffold diff --git a/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/NodeListScreen.kt b/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/NodeListScreen.kt index a5a02e9a58..75aa239675 100644 --- a/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/NodeListScreen.kt +++ b/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/NodeListScreen.kt @@ -14,8 +14,6 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -@file:OptIn(ExperimentalMaterial3ExpressiveApi::class) - package org.meshtastic.feature.settings import androidx.compose.foundation.layout.Arrangement @@ -23,7 +21,6 @@ import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.padding import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll -import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi import androidx.compose.material3.Scaffold import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue diff --git a/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/SettingsViewModel.kt b/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/SettingsViewModel.kt index fe1baf035a..2ae3499b41 100644 --- a/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/SettingsViewModel.kt +++ b/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/SettingsViewModel.kt @@ -81,7 +81,7 @@ class SettingsViewModel( .stateInWhileSubscribed(initialValue = false) val localConfig: StateFlow = - radioConfigRepository.localConfigFlow.stateInWhileSubscribed(initialValue = LocalConfig()) + radioConfigRepository.localConfigFlow.stateInWhileSubscribed(initialValue = LocalConfig.Builder().build()) val provideLocation: StateFlow = myNodeInfo diff --git a/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/channel/ChannelViewModel.kt b/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/channel/ChannelViewModel.kt index 78ade787a7..8ad07ebb1d 100644 --- a/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/channel/ChannelViewModel.kt +++ b/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/channel/ChannelViewModel.kt @@ -43,9 +43,11 @@ class ChannelViewModel( val connectionState = radioController.connectionState - val localConfig = radioConfigRepository.localConfigFlow.stateInWhileSubscribed(initialValue = LocalConfig()) + val localConfig = + radioConfigRepository.localConfigFlow.stateInWhileSubscribed(initialValue = LocalConfig.Builder().build()) - val channels = radioConfigRepository.channelSetFlow.stateInWhileSubscribed(initialValue = ChannelSet()) + val channels = + radioConfigRepository.channelSetFlow.stateInWhileSubscribed(initialValue = ChannelSet.Builder().build()) // managed mode disables all access to configuration val isManaged: Boolean @@ -54,13 +56,13 @@ class ChannelViewModel( var txEnabled: Boolean get() = localConfig.value.lora?.tx_enabled == true set(value) { - updateLoraConfig { it.copy(tx_enabled = value) } + updateLoraConfig { it.newBuilder().also { wb -> wb.tx_enabled = value }.build() } } var region: Config.LoRaConfig.RegionCode get() = localConfig.value.lora?.region ?: Config.LoRaConfig.RegionCode.UNSET set(value) { - updateLoraConfig { it.copy(region = value) } + updateLoraConfig { it.newBuilder().also { wb -> wb.region = value }.build() } } private val _requestChannelSet = MutableStateFlow(null) @@ -100,7 +102,7 @@ class ChannelViewModel( } private inline fun updateLoraConfig(crossinline body: (Config.LoRaConfig) -> Config.LoRaConfig) { - val data = body(localConfig.value.lora ?: Config.LoRaConfig()) - setConfig(Config(lora = data)) + val data = body(localConfig.value.lora ?: Config.LoRaConfig.Builder().build()) + setConfig(Config.Builder().also { wb -> wb.lora = data }.build()) } } diff --git a/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/component/ExpressiveSection.kt b/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/component/ExpressiveSection.kt index 9305ee6ba4..c93ca1570b 100644 --- a/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/component/ExpressiveSection.kt +++ b/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/component/ExpressiveSection.kt @@ -14,8 +14,6 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -@file:OptIn(ExperimentalMaterial3ExpressiveApi::class) - package org.meshtastic.feature.settings.component import androidx.compose.foundation.layout.Arrangement @@ -25,7 +23,6 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.material3.Card import androidx.compose.material3.CardDefaults -import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable diff --git a/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/component/NodeLayoutSettings.kt b/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/component/NodeLayoutSettings.kt index 491460e98d..d7a90330c0 100644 --- a/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/component/NodeLayoutSettings.kt +++ b/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/component/NodeLayoutSettings.kt @@ -235,29 +235,47 @@ fun NodeLayoutSettings( internal fun previewSampleNode(hopsAway: Int = 1): Node = Node( num = 0x1A2B3C4D, user = - User( - id = "!1a2b3c4d", - long_name = "Solar Hilltop", - short_name = "SoHi", - hw_model = HardwareModel.TBEAM, - role = Config.DeviceConfig.Role.ROUTER, - public_key = ByteArray(32) { (it * 7).toByte() }.toByteString(), - ), - position = Position(latitude_i = 338125110, longitude_i = -1179189760, altitude = 138, sats_in_view = 8), + User.Builder() + .also { wb -> + wb.id = "!1a2b3c4d" + wb.long_name = "Solar Hilltop" + wb.short_name = "SoHi" + wb.hw_model = HardwareModel.TBEAM + wb.role = Config.DeviceConfig.Role.ROUTER + wb.public_key = ByteArray(32) { (it * 7).toByte() }.toByteString() + } + .build(), + position = + Position.Builder() + .also { wb -> + wb.latitude_i = 338125110 + wb.longitude_i = -1179189760 + wb.altitude = 138 + wb.sats_in_view = 8 + } + .build(), lastHeard = (nowSeconds - 300).toInt(), channel = 1, snr = 10.25F, rssi = -67, deviceMetrics = - DeviceMetrics( - channel_utilization = 3.2F, - air_util_tx = 1.8F, - battery_level = 92, - voltage = 4.1F, - uptime_seconds = 86400, - ), + DeviceMetrics.Builder() + .also { wb -> + wb.channel_utilization = 3.2F + wb.air_util_tx = 1.8F + wb.battery_level = 92 + wb.voltage = 4.1F + wb.uptime_seconds = 86400 + } + .build(), environmentMetrics = - EnvironmentMetrics(temperature = 24.5F, relative_humidity = 45.0F, barometric_pressure = 1013.25F), + EnvironmentMetrics.Builder() + .also { wb -> + wb.temperature = 24.5F + wb.relative_humidity = 45.0F + wb.barometric_pressure = 1013.25F + } + .build(), isFavorite = true, hopsAway = hopsAway, ) @@ -267,13 +285,23 @@ internal fun previewSampleNode(hopsAway: Int = 1): Node = Node( internal fun previewLocalNode(): Node = Node( num = 0xDEADBEEF.toInt(), user = - User( - id = "!deadbeef", - long_name = "My Radio", - short_name = "MyRd", - hw_model = HardwareModel.HELTEC_V3, - role = Config.DeviceConfig.Role.CLIENT, - ), - position = Position(latitude_i = 338000000, longitude_i = -1179000000, altitude = 50, sats_in_view = 10), + User.Builder() + .also { wb -> + wb.id = "!deadbeef" + wb.long_name = "My Radio" + wb.short_name = "MyRd" + wb.hw_model = HardwareModel.HELTEC_V3 + wb.role = Config.DeviceConfig.Role.CLIENT + } + .build(), + position = + Position.Builder() + .also { wb -> + wb.latitude_i = 338000000 + wb.longitude_i = -1179000000 + wb.altitude = 50 + wb.sats_in_view = 10 + } + .build(), lastHeard = (nowSeconds - 30).toInt(), ) diff --git a/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/debugging/DebugViewModel.kt b/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/debugging/DebugViewModel.kt index d1c69b5791..b81520c3ee 100644 --- a/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/debugging/DebugViewModel.kt +++ b/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/debugging/DebugViewModel.kt @@ -355,7 +355,7 @@ class DebugViewModel( private fun annotatePacketLog(packet: MeshPacket, nodeList: List, myNodeNum: Int?): String { val decoded = packet.decoded - val basePacket = packet.copy(decoded = null) + val basePacket = packet.newBuilder().also { wb -> wb.decoded = null }.build() val baseText = basePacket.toString().trimEnd() var result = if (decoded != null) { diff --git a/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/filter/FilterSettingsScreen.kt b/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/filter/FilterSettingsScreen.kt index 7ee3f603e3..621316fb10 100644 --- a/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/filter/FilterSettingsScreen.kt +++ b/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/filter/FilterSettingsScreen.kt @@ -14,8 +14,6 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -@file:OptIn(ExperimentalMaterial3ExpressiveApi::class) - package org.meshtastic.feature.settings.filter import androidx.compose.foundation.layout.Arrangement @@ -28,7 +26,6 @@ import androidx.compose.foundation.lazy.items import androidx.compose.foundation.text.KeyboardActions import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.material3.Card -import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme diff --git a/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/RadioConfigViewModel.kt b/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/RadioConfigViewModel.kt index cc51cf79a2..54bb2c4fb2 100644 --- a/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/RadioConfigViewModel.kt +++ b/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/RadioConfigViewModel.kt @@ -77,7 +77,6 @@ import org.meshtastic.core.repository.MeshConnectionManager import org.meshtastic.core.repository.MqttManager import org.meshtastic.core.repository.NodeRepository import org.meshtastic.core.repository.NodeRestartTracker -import org.meshtastic.core.repository.PacketRepository import org.meshtastic.core.repository.PlatformAnalytics import org.meshtastic.core.repository.RadioConfigRepository import org.meshtastic.core.repository.SecurityKeyBackupStore @@ -131,10 +130,10 @@ data class RadioConfigState( val connected: Boolean = false, val route: String = "", val metadata: DeviceMetadata? = null, - val userConfig: User = User(), + val userConfig: User = User.Builder().build(), val channelList: List = emptyList(), - val radioConfig: LocalConfig = LocalConfig(), - val moduleConfig: LocalModuleConfig = LocalModuleConfig(), + val radioConfig: LocalConfig = LocalConfig.Builder().build(), + val moduleConfig: LocalModuleConfig = LocalModuleConfig.Builder().build(), val ringtone: String = "", val cannedMessageMessages: String = "", val deviceConnectionStatus: DeviceConnectionStatus? = null, @@ -155,7 +154,6 @@ data class RadioConfigState( open class RadioConfigViewModel( @InjectedParam initialDestNum: Int?, private val radioConfigRepository: RadioConfigRepository, - private val packetRepository: PacketRepository, private val serviceRepository: ServiceRepository, private val nodeRepository: NodeRepository, private val locationRepository: LocationRepository, @@ -307,7 +305,7 @@ open class RadioConfigViewModel( _radioConfigState.update { it.copy(nodeDbResetPreserveFavorites = preserveFavorites) } } - private val _currentDeviceProfile = MutableStateFlow(DeviceProfile()) + private val _currentDeviceProfile = MutableStateFlow(DeviceProfile.Builder().build()) val currentDeviceProfile get() = _currentDeviceProfile.value @@ -363,8 +361,8 @@ open class RadioConfigViewModel( isLocal = false, pioEnv = null, channelList = emptyList(), - radioConfig = LocalConfig(), - moduleConfig = LocalModuleConfig(), + radioConfig = LocalConfig.Builder().build(), + moduleConfig = LocalModuleConfig.Builder().build(), ) } } @@ -464,7 +462,13 @@ open class RadioConfigViewModel( expectRestartIfLocal(RebootBehavior.ALWAYS) radioConfigUseCase.setHamMode( destNum, - HamParameters(call_sign = callSign, short_name = user.short_name, long_name = longName), + HamParameters.Builder() + .also { wb -> + wb.call_sign = callSign + wb.short_name = user.short_name + wb.long_name = longName + } + .build(), onRequestId = ::registerWriteRequestId, ) } @@ -516,13 +520,12 @@ open class RadioConfigViewModel( onInterrupted = { result -> reconcileInterruptedManualChannelUpdate( destNum = destNum, - oldSettings = current, appliedSettings = result.appliedSettings, ) }, ) currentCoroutineContext().ensureActive() - commitManualChannelSettings(destNum = destNum, oldSettings = current, newSettings = new) + commitManualChannelSettings(destNum = destNum, newSettings = new) finishManualChannelBatch() } catch (e: CancellationException) { abortManualChannelBatch(batchRequestIds) @@ -543,29 +546,21 @@ open class RadioConfigViewModel( private fun getManualChannelUpdatePlan(new: List, old: List): List = getChannelList(new, old).sortedBy { it.index } - private suspend fun commitManualChannelSettings( - destNum: Int, - oldSettings: List, - newSettings: List, - ) { + private suspend fun commitManualChannelSettings(destNum: Int, newSettings: List) { withContext(NonCancellable) { + // replaceAllSettings re-keys the conversations itself; remote-admin of another node's channels must not + // touch this device's conversations, which is what the destNum gate protects. if (destNum == myNodeNum) { - packetRepository.migrateChannelsByPSK(oldSettings, newSettings) radioConfigRepository.replaceAllSettings(newSettings) } _radioConfigState.update { it.copy(channelList = newSettings) } } } - private suspend fun reconcileInterruptedManualChannelUpdate( - destNum: Int, - oldSettings: List, - appliedSettings: List, - ) { + private suspend fun reconcileInterruptedManualChannelUpdate(destNum: Int, appliedSettings: List) { withContext(NonCancellable) { Logger.w { "Reconciling interrupted manual channel update appliedSettings=${appliedSettings.size}" } if (destNum == myNodeNum) { - packetRepository.migrateChannelsByPSK(oldSettings, appliedSettings) radioConfigRepository.replaceAllSettings(appliedSettings) } _radioConfigState.update { it.copy(channelList = appliedSettings) } @@ -578,18 +573,23 @@ open class RadioConfigViewModel( _radioConfigState.update { state -> state.copy( radioConfig = - state.radioConfig.copy( - device = config.device ?: state.radioConfig.device, - position = config.position ?: state.radioConfig.position, - power = config.power ?: state.radioConfig.power, - network = config.network ?: state.radioConfig.network, - display = config.display ?: state.radioConfig.display, - // LoRa is intentionally NOT applied optimistically: the firmware can clamp or region-swap - // (e.g. EU sibling) a LoRa write and applies it live, so the form must reflect the device's - // actual value. It is re-read from the device when the LoRa screen is next opened. - bluetooth = config.bluetooth ?: state.radioConfig.bluetooth, - security = config.security ?: state.radioConfig.security, - ), + state.radioConfig + .newBuilder() + .also { wb -> + wb.device = config.device ?: state.radioConfig.device + wb.position = config.position ?: state.radioConfig.position + wb.power = config.power ?: state.radioConfig.power + wb.network = config.network ?: state.radioConfig.network + wb.display = config.display ?: state.radioConfig.display + // LoRa is intentionally NOT applied optimistically: the firmware can clamp or + // region-swap + // (e.g. EU sibling) a LoRa write and applies it live, so the form must reflect the + // device's + // actual value. It is re-read from the device when the LoRa screen is next opened. + wb.bluetooth = config.bluetooth ?: state.radioConfig.bluetooth + wb.security = config.security ?: state.radioConfig.security + } + .build(), ) } expectRestartIfLocal(config.saveRebootBehavior()) @@ -604,25 +604,28 @@ open class RadioConfigViewModel( _radioConfigState.update { state -> state.copy( moduleConfig = - state.moduleConfig.copy( - mqtt = config.mqtt ?: state.moduleConfig.mqtt, - serial = config.serial ?: state.moduleConfig.serial, - external_notification = - config.external_notification ?: state.moduleConfig.external_notification, - store_forward = config.store_forward ?: state.moduleConfig.store_forward, - range_test = config.range_test ?: state.moduleConfig.range_test, - telemetry = config.telemetry ?: state.moduleConfig.telemetry, - canned_message = config.canned_message ?: state.moduleConfig.canned_message, - audio = config.audio ?: state.moduleConfig.audio, - remote_hardware = config.remote_hardware ?: state.moduleConfig.remote_hardware, - neighbor_info = config.neighbor_info ?: state.moduleConfig.neighbor_info, - ambient_lighting = config.ambient_lighting ?: state.moduleConfig.ambient_lighting, - detection_sensor = config.detection_sensor ?: state.moduleConfig.detection_sensor, - paxcounter = config.paxcounter ?: state.moduleConfig.paxcounter, - statusmessage = config.statusmessage ?: state.moduleConfig.statusmessage, - tak = config.tak ?: state.moduleConfig.tak, - mesh_beacon = config.mesh_beacon ?: state.moduleConfig.mesh_beacon, - ), + state.moduleConfig + .newBuilder() + .also { wb -> + wb.mqtt = config.mqtt ?: state.moduleConfig.mqtt + wb.serial = config.serial ?: state.moduleConfig.serial + wb.external_notification = + config.external_notification ?: state.moduleConfig.external_notification + wb.store_forward = config.store_forward ?: state.moduleConfig.store_forward + wb.range_test = config.range_test ?: state.moduleConfig.range_test + wb.telemetry = config.telemetry ?: state.moduleConfig.telemetry + wb.canned_message = config.canned_message ?: state.moduleConfig.canned_message + wb.audio = config.audio ?: state.moduleConfig.audio + wb.remote_hardware = config.remote_hardware ?: state.moduleConfig.remote_hardware + wb.neighbor_info = config.neighbor_info ?: state.moduleConfig.neighbor_info + wb.ambient_lighting = config.ambient_lighting ?: state.moduleConfig.ambient_lighting + wb.detection_sensor = config.detection_sensor ?: state.moduleConfig.detection_sensor + wb.paxcounter = config.paxcounter ?: state.moduleConfig.paxcounter + wb.statusmessage = config.statusmessage ?: state.moduleConfig.statusmessage + wb.tak = config.tak ?: state.moduleConfig.tak + wb.mesh_beacon = config.mesh_beacon ?: state.moduleConfig.mesh_beacon + } + .build(), ) } expectRestartIfLocal(config.saveRebootBehavior()) @@ -767,7 +770,7 @@ open class RadioConfigViewModel( } importSecurityConfigUseCase(stored) .onSuccess { - setConfig(Config(security = it)) + setConfig(Config.Builder().also { wb -> wb.security = it }.build()) snackbarManager.showSnackbar(message = UiText.Resource(Res.string.key_backup_restored).resolve()) } .onFailure { @@ -1285,9 +1288,9 @@ open class RadioConfigViewModel( channelList = state.channelList.toMutableList().apply { val index = response.index - val settings = response.settings ?: ChannelSettings() + val settings = response.settings ?: ChannelSettings.Builder().build() // Make sure list is large enough - while (size <= index) add(ChannelSettings()) + while (size <= index) add(ChannelSettings.Builder().build()) set(index, settings) }, ) @@ -1316,16 +1319,19 @@ open class RadioConfigViewModel( _radioConfigState.update { state -> state.copy( radioConfig = - state.radioConfig.copy( - device = response.device ?: state.radioConfig.device, - position = response.position ?: state.radioConfig.position, - power = response.power ?: state.radioConfig.power, - network = response.network ?: state.radioConfig.network, - display = response.display ?: state.radioConfig.display, - lora = response.lora ?: state.radioConfig.lora, - bluetooth = response.bluetooth ?: state.radioConfig.bluetooth, - security = response.security ?: state.radioConfig.security, - ), + state.radioConfig + .newBuilder() + .also { wb -> + wb.device = response.device ?: state.radioConfig.device + wb.position = response.position ?: state.radioConfig.position + wb.power = response.power ?: state.radioConfig.power + wb.network = response.network ?: state.radioConfig.network + wb.display = response.display ?: state.radioConfig.display + wb.lora = response.lora ?: state.radioConfig.lora + wb.bluetooth = response.bluetooth ?: state.radioConfig.bluetooth + wb.security = response.security ?: state.radioConfig.security + } + .build(), ) } if (!isLateRemoteRead) incrementCompleted() @@ -1336,25 +1342,30 @@ open class RadioConfigViewModel( _radioConfigState.update { state -> state.copy( moduleConfig = - state.moduleConfig.copy( - mqtt = response.mqtt ?: state.moduleConfig.mqtt, - serial = response.serial ?: state.moduleConfig.serial, - external_notification = - response.external_notification ?: state.moduleConfig.external_notification, - store_forward = response.store_forward ?: state.moduleConfig.store_forward, - range_test = response.range_test ?: state.moduleConfig.range_test, - telemetry = response.telemetry ?: state.moduleConfig.telemetry, - canned_message = response.canned_message ?: state.moduleConfig.canned_message, - audio = response.audio ?: state.moduleConfig.audio, - remote_hardware = response.remote_hardware ?: state.moduleConfig.remote_hardware, - neighbor_info = response.neighbor_info ?: state.moduleConfig.neighbor_info, - ambient_lighting = response.ambient_lighting ?: state.moduleConfig.ambient_lighting, - detection_sensor = response.detection_sensor ?: state.moduleConfig.detection_sensor, - paxcounter = response.paxcounter ?: state.moduleConfig.paxcounter, - statusmessage = response.statusmessage ?: state.moduleConfig.statusmessage, - tak = response.tak ?: state.moduleConfig.tak, - mesh_beacon = response.mesh_beacon ?: state.moduleConfig.mesh_beacon, - ), + state.moduleConfig + .newBuilder() + .also { wb -> + wb.mqtt = response.mqtt ?: state.moduleConfig.mqtt + wb.serial = response.serial ?: state.moduleConfig.serial + wb.external_notification = + response.external_notification ?: state.moduleConfig.external_notification + wb.store_forward = response.store_forward ?: state.moduleConfig.store_forward + wb.range_test = response.range_test ?: state.moduleConfig.range_test + wb.telemetry = response.telemetry ?: state.moduleConfig.telemetry + wb.canned_message = response.canned_message ?: state.moduleConfig.canned_message + wb.audio = response.audio ?: state.moduleConfig.audio + wb.remote_hardware = response.remote_hardware ?: state.moduleConfig.remote_hardware + wb.neighbor_info = response.neighbor_info ?: state.moduleConfig.neighbor_info + wb.ambient_lighting = + response.ambient_lighting ?: state.moduleConfig.ambient_lighting + wb.detection_sensor = + response.detection_sensor ?: state.moduleConfig.detection_sensor + wb.paxcounter = response.paxcounter ?: state.moduleConfig.paxcounter + wb.statusmessage = response.statusmessage ?: state.moduleConfig.statusmessage + wb.tak = response.tak ?: state.moduleConfig.tak + wb.mesh_beacon = response.mesh_beacon ?: state.moduleConfig.mesh_beacon + } + .build(), ) } if (!isLateRemoteRead) incrementCompleted() @@ -1495,13 +1506,13 @@ internal suspend fun applyManualChannelUpdatePlan( private fun MutableList.applyManualChannelWrite(channel: Channel) { while (size <= channel.index) { - add(ChannelSettings()) + add(ChannelSettings.Builder().build()) } this[channel.index] = if (channel.role == Channel.Role.DISABLED) { - ChannelSettings() + ChannelSettings.Builder().build() } else { - channel.settings ?: ChannelSettings() + channel.settings ?: ChannelSettings.Builder().build() } } diff --git a/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/channel/ChannelConfigScreen.kt b/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/channel/ChannelConfigScreen.kt index 6e6b2cacc7..fd2c455c9f 100644 --- a/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/channel/ChannelConfigScreen.kt +++ b/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/channel/ChannelConfigScreen.kt @@ -89,7 +89,7 @@ fun ChannelConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> Unit) { title = stringResource(Res.string.channels), onBack = onBack, settingsList = state.channelList, - loraConfig = state.radioConfig.lora ?: Config.LoRaConfig(), + loraConfig = state.radioConfig.lora ?: Config.LoRaConfig.Builder().build(), maxChannels = viewModel.maxChannels, firmwareVersion = state.metadata?.firmware_version ?: "0.0.0", enabled = state.connected, @@ -168,7 +168,7 @@ private fun ChannelConfigScreen( if (showEditChannelDialog != null) { val index = showEditChannelDialog ?: return EditChannelDialog( - channelSettings = settingsListInput.getOrNull(index) ?: ChannelSettings(), + channelSettings = settingsListInput.getOrNull(index) ?: ChannelSettings.Builder().build(), modemPresetName = modemPresetName, initialPskEditState = pskEditStatesInput.getOrElse(index) { ChannelPskEditState() }, onAddClick = { settings, pskEditState -> diff --git a/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/channel/ChannelScreen.kt b/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/channel/ChannelScreen.kt index 1f80618c28..7a0da443fc 100644 --- a/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/channel/ChannelScreen.kt +++ b/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/channel/ChannelScreen.kt @@ -63,7 +63,7 @@ import org.jetbrains.compose.resources.stringResource import org.koin.compose.viewmodel.koinViewModel import org.meshtastic.core.model.Channel import org.meshtastic.core.model.ConnectionState -import org.meshtastic.core.model.defaultPresetFor +import org.meshtastic.core.model.firstSetupDefaultFor import org.meshtastic.core.navigation.Route import org.meshtastic.core.navigation.SettingsRoute import org.meshtastic.core.resources.Res @@ -79,7 +79,10 @@ import org.meshtastic.core.resources.navigate_into_label import org.meshtastic.core.resources.replace import org.meshtastic.core.resources.reset import org.meshtastic.core.resources.reset_to_defaults -import org.meshtastic.core.resources.share_channels_qr +import org.meshtastic.core.resources.share_channels +import org.meshtastic.core.resources.share_channels_add_hint +import org.meshtastic.core.resources.share_channels_replace_hint +import org.meshtastic.core.resources.share_channels_subject import org.meshtastic.core.ui.component.AdaptiveTwoPane import org.meshtastic.core.ui.component.ChannelSelection import org.meshtastic.core.ui.component.MainAppBar @@ -123,7 +126,9 @@ fun ChannelScreen( val channels by viewModel.channels.collectAsStateWithLifecycle() var channelSet by remember(channels) { mutableStateOf(channels) } val modemPresetName by - remember(channels) { mutableStateOf(Channel(loraConfig = channels.lora_config ?: Config.LoRaConfig()).name) } + remember(channels) { + mutableStateOf(Channel(loraConfig = channels.lora_config ?: Config.LoRaConfig.Builder().build()).name) + } var showResetDialog by rememberSaveable { mutableStateOf(false) } @@ -164,7 +169,12 @@ fun ChannelScreen( } val selectedChannelSet = - channelSet.copy(settings = channelSet.settings.filterIndexed { i, _ -> channelSelections.getOrNull(i) == true }) + channelSet + .newBuilder() + .also { wb -> + wb.settings = channelSet.settings.filterIndexed { i, _ -> channelSelections.getOrNull(i) == true } + } + .build() val scope = rememberCoroutineScope() val showToast = rememberShowToastResource() @@ -187,7 +197,13 @@ fun ChannelScreen( } fun installSettings(newChannel: ChannelSettings, newLoRaConfig: Config.LoRaConfig) { - val newSet = ChannelSet(settings = listOf(newChannel), lora_config = newLoRaConfig) + val newSet = + ChannelSet.Builder() + .also { wb -> + wb.settings = listOf(newChannel) + wb.lora_config = newLoRaConfig + } + .build() installSettings(newSet) } @@ -201,15 +217,20 @@ fun ChannelScreen( messageRes = Res.string.are_you_sure_change_default, onConfirm = { Logger.d { "Switching back to default channel" } - // The default channel takes the region's preferred preset (e.g. US -> LongTurbo) so its derived name, - // hash, and frequency match what a freshly-set-up node in that region would use. - val preset = defaultPresetFor(viewModel.region) ?: Channel.default.loraConfig.modem_preset + // The default channel takes the region's first-setup preset (e.g. US -> LongTurbo) so its derived + // name, hash, and frequency match what a freshly-set-up node in that region would use. + val preset = + radioConfigState.loraRegionPresetMap.firstSetupDefaultFor(viewModel.region) + ?: Channel.default.loraConfig.modem_preset val lora = - Channel.default.loraConfig.copy( - region = viewModel.region, - modem_preset = preset, - tx_enabled = viewModel.txEnabled, - ) + Channel.default.loraConfig + .newBuilder() + .also { wb -> + wb.region = viewModel.region + wb.modem_preset = preset + wb.tx_enabled = viewModel.txEnabled + } + .build() installSettings(Channel.default.settings, lora) showResetDialog = false }, @@ -280,6 +301,22 @@ fun ChannelScreen( ) } } + item { + // Replace is destructive on the recipient's radio, and the two words alone do not say so. + Text( + text = + stringResource( + if (channelShareState.shouldAdd) { + Res.string.share_channels_add_hint + } else { + Res.string.share_channels_replace_hint + }, + ), + modifier = Modifier.fillMaxWidth().padding(horizontal = 16.dp), + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + } item { ModemPresetInfo( modemPresetName = modemPresetName, @@ -309,7 +346,12 @@ fun ChannelScreen( @Composable private fun ChannelShareDialog(uriString: String, onDismiss: () -> Unit) { - QrDialog(title = stringResource(Res.string.share_channels_qr), uriString = uriString, onDismiss = onDismiss) + QrDialog( + title = stringResource(Res.string.share_channels), + uriString = uriString, + onDismiss = onDismiss, + shareSubject = stringResource(Res.string.share_channels_subject), + ) } @Composable @@ -322,12 +364,17 @@ private fun ChannelListView( onClickShare: () -> Unit = {}, ) { val selectedChannelSet = - channelSet.copy(settings = channelSet.settings.filterIndexed { i, _ -> channelSelections.getOrNull(i) == true }) + channelSet + .newBuilder() + .also { wb -> + wb.settings = channelSet.settings.filterIndexed { i, _ -> channelSelections.getOrNull(i) == true } + } + .build() AdaptiveTwoPane( first = { channelSet.settings.forEachIndexed { index, channel -> - val channelObj = Channel(channel, channelSet.lora_config ?: Config.LoRaConfig()) + val channelObj = Channel(channel, channelSet.lora_config ?: Config.LoRaConfig.Builder().build()) val displayTitle = if (channel.name.isEmpty()) modemPresetName else channel.name ChannelSelection( diff --git a/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/channel/component/EditChannelDialog.kt b/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/channel/component/EditChannelDialog.kt index ecd3901fb8..44b14ef893 100644 --- a/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/channel/component/EditChannelDialog.kt +++ b/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/channel/component/EditChannelDialog.kt @@ -102,15 +102,15 @@ fun EditChannelDialog( enabled = true, keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }), onValueChange = { - val fullPsk = Channel(ChannelSettings(psk = it)).psk + val fullPsk = Channel(ChannelSettings.Builder().also { wb -> wb.psk = it }.build()).psk if (fullPsk.size in setOf(0, 16, 32)) { pskEditState = pskEditState.copy(generatedPskForName = false, pskExplicitlyEdited = true) - channelInput = channelInput.copy(psk = it) + channelInput = channelInput.newBuilder().also { wb -> wb.psk = it }.build() } }, onGenerateKey = { pskEditState = pskEditState.copy(generatedPskForName = false, pskExplicitlyEdited = true) - channelInput = channelInput.copy(psk = Channel.getRandomKey()) + channelInput = channelInput.newBuilder().also { wb -> wb.psk = Channel.getRandomKey() }.build() }, ) @@ -118,7 +118,9 @@ fun EditChannelDialog( title = stringResource(Res.string.uplink_enabled), checked = channelInput.uplink_enabled, enabled = true, - onCheckedChange = { channelInput = channelInput.copy(uplink_enabled = it) }, + onCheckedChange = { + channelInput = channelInput.newBuilder().also { wb -> wb.uplink_enabled = it }.build() + }, padding = PaddingValues(0.dp), ) @@ -126,17 +128,21 @@ fun EditChannelDialog( title = stringResource(Res.string.downlink_enabled), checked = channelInput.downlink_enabled, enabled = true, - onCheckedChange = { channelInput = channelInput.copy(downlink_enabled = it) }, + onCheckedChange = { + channelInput = channelInput.newBuilder().also { wb -> wb.downlink_enabled = it }.build() + }, padding = PaddingValues(0.dp), ) - val moduleSettings = channelInput.module_settings ?: ModuleSettings() + val moduleSettings = channelInput.module_settings ?: ModuleSettings.Builder().build() PositionPrecisionPreference( enabled = true, value = moduleSettings.position_precision, onValueChanged = { - val updatedModule = moduleSettings.copy(position_precision = it) - channelInput = channelInput.copy(module_settings = updatedModule) + val updatedModule = + moduleSettings.newBuilder().also { wb -> wb.position_precision = it }.build() + channelInput = + channelInput.newBuilder().also { wb -> wb.module_settings = updatedModule }.build() }, ) } @@ -177,7 +183,13 @@ internal fun ChannelSettings.applyChannelNameEdit( else -> pskEditState.generatedPskForName } return ChannelNameUpdate( - settings = copy(name = name, psk = nextPsk), + settings = + this.newBuilder() + .also { wb -> + wb.name = name + wb.psk = nextPsk + } + .build(), pskEditState = pskEditState.copy(generatedPskForName = nextGeneratedPskForName), ) } diff --git a/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/component/AmbientLightingConfigItemList.kt b/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/component/AmbientLightingConfigItemList.kt index fef18d822f..dbe3f30ca6 100644 --- a/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/component/AmbientLightingConfigItemList.kt +++ b/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/component/AmbientLightingConfigItemList.kt @@ -33,20 +33,21 @@ import org.meshtastic.core.resources.current import org.meshtastic.core.resources.green import org.meshtastic.core.resources.led_state import org.meshtastic.core.resources.red -import org.meshtastic.core.ui.component.EditTextPreference import org.meshtastic.core.ui.component.SwitchPreference import org.meshtastic.core.ui.component.TitledCard import org.meshtastic.feature.settings.radio.RadioConfigViewModel import org.meshtastic.feature.settings.radio.RebootBehavior import org.meshtastic.proto.ModuleConfig - -private const val MAX_LED_CURRENT = 31 -private const val MAX_RGB_VALUE = 255 +import org.meshtastic.proto.blue +import org.meshtastic.proto.current +import org.meshtastic.proto.green +import org.meshtastic.proto.red @Composable fun AmbientLightingConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> Unit) { val state by viewModel.radioConfigState.collectAsStateWithLifecycle() - val ambientLightingConfig = state.moduleConfig.ambient_lighting ?: ModuleConfig.AmbientLightingConfig() + val ambientLightingConfig = + state.moduleConfig.ambient_lighting ?: ModuleConfig.AmbientLightingConfig.Builder().build() val formState = rememberConfigState(initialValue = ambientLightingConfig) val focusManager = LocalFocusManager.current @@ -59,7 +60,7 @@ fun AmbientLightingConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> U responseState = state.responseState, onDismissPacketResponse = viewModel::clearPacketResponse, onSave = { - val config = ModuleConfig(ambient_lighting = it) + val config = ModuleConfig.Builder().also { wb -> wb.ambient_lighting = it }.build() viewModel.setModuleConfig(config) }, ) { @@ -69,7 +70,9 @@ fun AmbientLightingConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> U title = stringResource(Res.string.led_state), checked = formState.value.led_state, enabled = state.connected, - onCheckedChange = { formState.value = formState.value.copy(led_state = it) }, + onCheckedChange = { + formState.value = formState.value.newBuilder().also { wb -> wb.led_state = it }.build() + }, containerColor = CardDefaults.cardColors().containerColor, ) HorizontalDivider() @@ -92,53 +95,37 @@ private fun LedColorFields( onConfigChange: (ModuleConfig.AmbientLightingConfig) -> Unit, ) { androidx.compose.foundation.layout.Column { - EditTextPreference( + BoundedIntEditTextPreference( title = stringResource(Res.string.current), value = config.current, + metadata = ModuleConfig.AmbientLightingConfig.current, enabled = enabled, - isError = config.current !in 0..MAX_LED_CURRENT, keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }), - onValueChanged = { - if (it in 0..MAX_LED_CURRENT) { - onConfigChange(config.copy(current = it)) - } - }, + onValueChange = { onConfigChange(config.newBuilder().also { wb -> wb.current = it }.build()) }, ) - EditTextPreference( + BoundedIntEditTextPreference( title = stringResource(Res.string.red), value = config.red, + metadata = ModuleConfig.AmbientLightingConfig.red, enabled = enabled, - isError = config.red !in 0..MAX_RGB_VALUE, keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }), - onValueChanged = { - if (it in 0..MAX_RGB_VALUE) { - onConfigChange(config.copy(red = it)) - } - }, + onValueChange = { onConfigChange(config.newBuilder().also { wb -> wb.red = it }.build()) }, ) - EditTextPreference( + BoundedIntEditTextPreference( title = stringResource(Res.string.green), value = config.green, + metadata = ModuleConfig.AmbientLightingConfig.green, enabled = enabled, - isError = config.green !in 0..MAX_RGB_VALUE, keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }), - onValueChanged = { - if (it in 0..MAX_RGB_VALUE) { - onConfigChange(config.copy(green = it)) - } - }, + onValueChange = { onConfigChange(config.newBuilder().also { wb -> wb.green = it }.build()) }, ) - EditTextPreference( + BoundedIntEditTextPreference( title = stringResource(Res.string.blue), value = config.blue, + metadata = ModuleConfig.AmbientLightingConfig.blue, enabled = enabled, - isError = config.blue !in 0..MAX_RGB_VALUE, keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }), - onValueChanged = { - if (it in 0..MAX_RGB_VALUE) { - onConfigChange(config.copy(blue = it)) - } - }, + onValueChange = { onConfigChange(config.newBuilder().also { wb -> wb.blue = it }.build()) }, ) } } diff --git a/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/component/AudioConfigItemList.kt b/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/component/AudioConfigItemList.kt index 1c1b36a721..f519bd2844 100644 --- a/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/component/AudioConfigItemList.kt +++ b/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/component/AudioConfigItemList.kt @@ -45,7 +45,7 @@ import org.meshtastic.proto.ModuleConfig @Composable fun AudioConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> Unit) { val state by viewModel.radioConfigState.collectAsStateWithLifecycle() - val audioConfig = state.moduleConfig.audio ?: ModuleConfig.AudioConfig() + val audioConfig = state.moduleConfig.audio ?: ModuleConfig.AudioConfig.Builder().build() val formState = rememberConfigState(initialValue = audioConfig) val focusManager = LocalFocusManager.current @@ -58,7 +58,7 @@ fun AudioConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> Unit) { responseState = state.responseState, onDismissPacketResponse = viewModel::clearPacketResponse, onSave = { - val config = ModuleConfig(audio = it) + val config = ModuleConfig.Builder().also { wb -> wb.audio = it }.build() viewModel.setModuleConfig(config) }, ) { @@ -68,7 +68,9 @@ fun AudioConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> Unit) { title = stringResource(Res.string.codec_2_enabled), checked = formState.value.codec2_enabled, enabled = state.connected, - onCheckedChange = { formState.value = formState.value.copy(codec2_enabled = it) }, + onCheckedChange = { + formState.value = formState.value.newBuilder().also { wb -> wb.codec2_enabled = it }.build() + }, containerColor = CardDefaults.cardColors().containerColor, ) HorizontalDivider() @@ -77,14 +79,18 @@ fun AudioConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> Unit) { value = formState.value.ptt_pin, enabled = state.connected, keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }), - onValueChanged = { formState.value = formState.value.copy(ptt_pin = it) }, + onValueChanged = { + formState.value = formState.value.newBuilder().also { wb -> wb.ptt_pin = it }.build() + }, ) DropDownPreference( title = stringResource(Res.string.codec2_sample_rate), enabled = state.connected, items = ModuleConfig.AudioConfig.Audio_Baud.entries.map { it to it.name }, selectedItem = formState.value.bitrate, - onItemSelected = { formState.value = formState.value.copy(bitrate = it) }, + onItemSelected = { + formState.value = formState.value.newBuilder().also { wb -> wb.bitrate = it }.build() + }, ) HorizontalDivider() EditTextPreference( @@ -92,28 +98,36 @@ fun AudioConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> Unit) { value = formState.value.i2s_ws, enabled = state.connected, keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }), - onValueChanged = { formState.value = formState.value.copy(i2s_ws = it) }, + onValueChanged = { + formState.value = formState.value.newBuilder().also { wb -> wb.i2s_ws = it }.build() + }, ) EditTextPreference( title = stringResource(Res.string.i2s_data_in), value = formState.value.i2s_sd, enabled = state.connected, keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }), - onValueChanged = { formState.value = formState.value.copy(i2s_sd = it) }, + onValueChanged = { + formState.value = formState.value.newBuilder().also { wb -> wb.i2s_sd = it }.build() + }, ) EditTextPreference( title = stringResource(Res.string.i2s_data_out), value = formState.value.i2s_din, enabled = state.connected, keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }), - onValueChanged = { formState.value = formState.value.copy(i2s_din = it) }, + onValueChanged = { + formState.value = formState.value.newBuilder().also { wb -> wb.i2s_din = it }.build() + }, ) EditTextPreference( title = stringResource(Res.string.i2s_clock), value = formState.value.i2s_sck, enabled = state.connected, keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }), - onValueChanged = { formState.value = formState.value.copy(i2s_sck = it) }, + onValueChanged = { + formState.value = formState.value.newBuilder().also { wb -> wb.i2s_sck = it }.build() + }, ) } } diff --git a/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/component/BluetoothConfigItemList.kt b/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/component/BluetoothConfigItemList.kt index 14138edaa4..228dd2ec82 100644 --- a/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/component/BluetoothConfigItemList.kt +++ b/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/component/BluetoothConfigItemList.kt @@ -49,7 +49,7 @@ private const val PIN_LENGTH = 6 @Composable fun BluetoothConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> Unit) { val state by viewModel.radioConfigState.collectAsStateWithLifecycle() - val bluetoothConfig = state.radioConfig.bluetooth ?: Config.BluetoothConfig() + val bluetoothConfig = state.radioConfig.bluetooth ?: Config.BluetoothConfig.Builder().build() val formState = rememberConfigState(initialValue = bluetoothConfig) val focusManager = LocalFocusManager.current @@ -62,7 +62,7 @@ fun BluetoothConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> Unit) { responseState = state.responseState, onDismissPacketResponse = viewModel::clearPacketResponse, onSave = { - val config = Config(bluetooth = it) + val config = Config.Builder().also { wb -> wb.bluetooth = it }.build() viewModel.setConfig(config) }, ) { @@ -72,7 +72,9 @@ fun BluetoothConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> Unit) { title = stringResource(Res.string.bluetooth_enabled), checked = formState.value.enabled, enabled = state.connected, - onCheckedChange = { formState.value = formState.value.copy(enabled = it) }, + onCheckedChange = { + formState.value = formState.value.newBuilder().also { wb -> wb.enabled = it }.build() + }, containerColor = CardDefaults.cardColors().containerColor, ) HorizontalDivider() @@ -86,14 +88,18 @@ fun BluetoothConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> Unit) { selectedItem = formState.value.mode.takeUnless { it.name == "UNRECOGNIZED" } ?: Config.BluetoothConfig.PairingMode.RANDOM_PIN, - onItemSelected = { formState.value = formState.value.copy(mode = it) }, + onItemSelected = { + formState.value = formState.value.newBuilder().also { wb -> wb.mode = it }.build() + }, ) HorizontalDivider() FixedPinPreference( pinValue = formState.value.fixed_pin, enabled = state.connected, focusManager = focusManager, - onPinChange = { formState.value = formState.value.copy(fixed_pin = it) }, + onPinChange = { + formState.value = formState.value.newBuilder().also { wb -> wb.fixed_pin = it }.build() + }, ) } } diff --git a/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/component/BoundedIntEditTextPreference.kt b/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/component/BoundedIntEditTextPreference.kt new file mode 100644 index 0000000000..6d15aadd78 --- /dev/null +++ b/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/component/BoundedIntEditTextPreference.kt @@ -0,0 +1,58 @@ +/* + * 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 . + */ +package org.meshtastic.feature.settings.radio.component + +import androidx.compose.foundation.text.KeyboardActions +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.focus.FocusState +import org.meshtastic.core.ui.component.EditTextPreference +import org.meshtastic.feature.settings.util.intRange +import org.meshtastic.proto.FieldMetadata + +/** + * An integer field bounded by its proto field metadata: an out-of-range value shows as an error and is not applied. A + * field the registry leaves unbounded accepts any value. + */ +@Composable +fun BoundedIntEditTextPreference( + title: String, + value: Int, + metadata: FieldMetadata, + enabled: Boolean, + keyboardActions: KeyboardActions, + onValueChange: (Int) -> Unit, + modifier: Modifier = Modifier, + summary: String? = null, + onFocusChanged: (FocusState) -> Unit = {}, + trailingIcon: (@Composable () -> Unit)? = null, +) { + val range = remember(metadata) { metadata.intRange } + EditTextPreference( + title = title, + value = value, + enabled = enabled, + isError = range != null && value !in range, + keyboardActions = keyboardActions, + onValueChanged = { if (range == null || it in range) onValueChange(it) }, + modifier = modifier, + summary = summary, + onFocusChanged = onFocusChanged, + trailingIcon = trailingIcon, + ) +} diff --git a/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/component/CannedMessageConfigItemList.kt b/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/component/CannedMessageConfigItemList.kt index 35fa61ff02..ca7661f264 100644 --- a/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/component/CannedMessageConfigItemList.kt +++ b/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/component/CannedMessageConfigItemList.kt @@ -57,7 +57,7 @@ import org.meshtastic.proto.ModuleConfig @Composable fun CannedMessageConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> Unit) { val state by viewModel.radioConfigState.collectAsStateWithLifecycle() - val cannedMessageConfig = state.moduleConfig.canned_message ?: ModuleConfig.CannedMessageConfig() + val cannedMessageConfig = state.moduleConfig.canned_message ?: ModuleConfig.CannedMessageConfig.Builder().build() val messages = state.cannedMessageMessages val formState = rememberConfigState(initialValue = cannedMessageConfig) var messagesInput by rememberSaveable(messages) { mutableStateOf(messages) } @@ -78,7 +78,7 @@ fun CannedMessageConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> Uni viewModel.setCannedMessages(messagesInput) } if (formState.value != cannedMessageConfig) { - val config = ModuleConfig(canned_message = formState.value) + val config = ModuleConfig.Builder().also { wb -> wb.canned_message = formState.value }.build() viewModel.setModuleConfig(config) } }, @@ -89,7 +89,9 @@ fun CannedMessageConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> Uni title = stringResource(Res.string.canned_message_enabled), checked = formState.value.enabled, enabled = state.connected, - onCheckedChange = { formState.value = formState.value.copy(enabled = it) }, + onCheckedChange = { + formState.value = formState.value.newBuilder().also { wb -> wb.enabled = it }.build() + }, containerColor = CardDefaults.cardColors().containerColor, ) HorizontalDivider() @@ -97,7 +99,9 @@ fun CannedMessageConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> Uni title = stringResource(Res.string.rotary_encoder_1_enabled), checked = formState.value.rotary1_enabled, enabled = state.connected, - onCheckedChange = { formState.value = formState.value.copy(rotary1_enabled = it) }, + onCheckedChange = { + formState.value = formState.value.newBuilder().also { wb -> wb.rotary1_enabled = it }.build() + }, containerColor = CardDefaults.cardColors().containerColor, ) HorizontalDivider() @@ -106,28 +110,38 @@ fun CannedMessageConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> Uni value = formState.value.inputbroker_pin_a, enabled = state.connected, keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }), - onValueChanged = { formState.value = formState.value.copy(inputbroker_pin_a = it) }, + onValueChanged = { + formState.value = formState.value.newBuilder().also { wb -> wb.inputbroker_pin_a = it }.build() + }, ) EditTextPreference( title = stringResource(Res.string.gpio_pin_for_rotary_encoder_b_port), value = formState.value.inputbroker_pin_b, enabled = state.connected, keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }), - onValueChanged = { formState.value = formState.value.copy(inputbroker_pin_b = it) }, + onValueChanged = { + formState.value = formState.value.newBuilder().also { wb -> wb.inputbroker_pin_b = it }.build() + }, ) EditTextPreference( title = stringResource(Res.string.gpio_pin_for_rotary_encoder_press_port), value = formState.value.inputbroker_pin_press, enabled = state.connected, keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }), - onValueChanged = { formState.value = formState.value.copy(inputbroker_pin_press = it) }, + onValueChanged = { + formState.value = + formState.value.newBuilder().also { wb -> wb.inputbroker_pin_press = it }.build() + }, ) DropDownPreference( title = stringResource(Res.string.generate_input_event_on_press), enabled = state.connected, items = ModuleConfig.CannedMessageConfig.InputEventChar.entries.map { it to it.name }, selectedItem = formState.value.inputbroker_event_press, - onItemSelected = { formState.value = formState.value.copy(inputbroker_event_press = it) }, + onItemSelected = { + formState.value = + formState.value.newBuilder().also { wb -> wb.inputbroker_event_press = it }.build() + }, ) HorizontalDivider() DropDownPreference( @@ -135,7 +149,10 @@ fun CannedMessageConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> Uni enabled = state.connected, items = ModuleConfig.CannedMessageConfig.InputEventChar.entries.map { it to it.name }, selectedItem = formState.value.inputbroker_event_cw, - onItemSelected = { formState.value = formState.value.copy(inputbroker_event_cw = it) }, + onItemSelected = { + formState.value = + formState.value.newBuilder().also { wb -> wb.inputbroker_event_cw = it }.build() + }, ) HorizontalDivider() DropDownPreference( @@ -143,14 +160,19 @@ fun CannedMessageConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> Uni enabled = state.connected, items = ModuleConfig.CannedMessageConfig.InputEventChar.entries.map { it to it.name }, selectedItem = formState.value.inputbroker_event_ccw, - onItemSelected = { formState.value = formState.value.copy(inputbroker_event_ccw = it) }, + onItemSelected = { + formState.value = + formState.value.newBuilder().also { wb -> wb.inputbroker_event_ccw = it }.build() + }, ) HorizontalDivider() SwitchPreference( title = stringResource(Res.string.up_down_select_input_enabled), checked = formState.value.updown1_enabled, enabled = state.connected, - onCheckedChange = { formState.value = formState.value.copy(updown1_enabled = it) }, + onCheckedChange = { + formState.value = formState.value.newBuilder().also { wb -> wb.updown1_enabled = it }.build() + }, containerColor = CardDefaults.cardColors().containerColor, ) HorizontalDivider() @@ -163,13 +185,17 @@ fun CannedMessageConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> Uni keyboardOptions = KeyboardOptions.Default.copy(keyboardType = KeyboardType.Text, imeAction = ImeAction.Done), keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }), - onValueChanged = { formState.value = formState.value.copy(allow_input_source = it) }, + onValueChanged = { + formState.value = formState.value.newBuilder().also { wb -> wb.allow_input_source = it }.build() + }, ) SwitchPreference( title = stringResource(Res.string.send_bell), checked = formState.value.send_bell, enabled = state.connected, - onCheckedChange = { formState.value = formState.value.copy(send_bell = it) }, + onCheckedChange = { + formState.value = formState.value.newBuilder().also { wb -> wb.send_bell = it }.build() + }, containerColor = CardDefaults.cardColors().containerColor, ) HorizontalDivider() diff --git a/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/component/DetectionSensorConfigItemList.kt b/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/component/DetectionSensorConfigItemList.kt index 0fa5801d7f..36ffc06fba 100644 --- a/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/component/DetectionSensorConfigItemList.kt +++ b/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/component/DetectionSensorConfigItemList.kt @@ -53,7 +53,8 @@ import org.meshtastic.proto.ModuleConfig @Composable fun DetectionSensorConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> Unit) { val state by viewModel.radioConfigState.collectAsStateWithLifecycle() - val detectionSensorConfig = state.moduleConfig.detection_sensor ?: ModuleConfig.DetectionSensorConfig() + val detectionSensorConfig = + state.moduleConfig.detection_sensor ?: ModuleConfig.DetectionSensorConfig.Builder().build() val formState = rememberConfigState(initialValue = detectionSensorConfig) val focusManager = LocalFocusManager.current @@ -66,7 +67,7 @@ fun DetectionSensorConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> U responseState = state.responseState, onDismissPacketResponse = viewModel::clearPacketResponse, onSave = { - val config = ModuleConfig(detection_sensor = it) + val config = ModuleConfig.Builder().also { wb -> wb.detection_sensor = it }.build() viewModel.setModuleConfig(config) }, ) { @@ -76,7 +77,9 @@ fun DetectionSensorConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> U title = stringResource(Res.string.detection_sensor_enabled), checked = formState.value.enabled, enabled = state.connected, - onCheckedChange = { formState.value = formState.value.copy(enabled = it) }, + onCheckedChange = { + formState.value = formState.value.newBuilder().also { wb -> wb.enabled = it }.build() + }, containerColor = CardDefaults.cardColors().containerColor, ) HorizontalDivider() @@ -88,7 +91,10 @@ fun DetectionSensorConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> U selectedItem = formState.value.minimum_broadcast_secs.toLong(), enabled = state.connected, items = minimumBroadcastIntervals.map { it.value to it.toDisplayString() }, - onItemSelected = { formState.value = formState.value.copy(minimum_broadcast_secs = it.toInt()) }, + onItemSelected = { + formState.value = + formState.value.newBuilder().also { wb -> wb.minimum_broadcast_secs = it.toInt() }.build() + }, ) val stateBroadcastIntervals = remember { IntervalConfiguration.DETECTION_SENSOR_STATE.allowedIntervals } @@ -97,14 +103,19 @@ fun DetectionSensorConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> U selectedItem = formState.value.state_broadcast_secs.toLong(), enabled = state.connected, items = stateBroadcastIntervals.map { it.value to it.toDisplayString() }, - onItemSelected = { formState.value = formState.value.copy(state_broadcast_secs = it.toInt()) }, + onItemSelected = { + formState.value = + formState.value.newBuilder().also { wb -> wb.state_broadcast_secs = it.toInt() }.build() + }, ) HorizontalDivider() SwitchPreference( title = stringResource(Res.string.send_bell_with_alert_message), checked = formState.value.send_bell, enabled = state.connected, - onCheckedChange = { formState.value = formState.value.copy(send_bell = it) }, + onCheckedChange = { + formState.value = formState.value.newBuilder().also { wb -> wb.send_bell = it }.build() + }, containerColor = CardDefaults.cardColors().containerColor, ) HorizontalDivider() @@ -117,7 +128,9 @@ fun DetectionSensorConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> U keyboardOptions = KeyboardOptions.Default.copy(keyboardType = KeyboardType.Text, imeAction = ImeAction.Done), keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }), - onValueChanged = { formState.value = formState.value.copy(name = it) }, + onValueChanged = { + formState.value = formState.value.newBuilder().also { wb -> wb.name = it }.build() + }, ) HorizontalDivider() val pins = remember { gpioPins } @@ -126,7 +139,9 @@ fun DetectionSensorConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> U items = pins, selectedItem = formState.value.monitor_pin, enabled = state.connected, - onItemSelected = { formState.value = formState.value.copy(monitor_pin = it) }, + onItemSelected = { + formState.value = formState.value.newBuilder().also { wb -> wb.monitor_pin = it }.build() + }, ) HorizontalDivider() DropDownPreference( @@ -134,14 +149,19 @@ fun DetectionSensorConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> U enabled = state.connected, items = ModuleConfig.DetectionSensorConfig.TriggerType.entries.map { it to it.name }, selectedItem = formState.value.detection_trigger_type, - onItemSelected = { formState.value = formState.value.copy(detection_trigger_type = it) }, + onItemSelected = { + formState.value = + formState.value.newBuilder().also { wb -> wb.detection_trigger_type = it }.build() + }, ) HorizontalDivider() SwitchPreference( title = stringResource(Res.string.use_input_pullup_mode), checked = formState.value.use_pullup, enabled = state.connected, - onCheckedChange = { formState.value = formState.value.copy(use_pullup = it) }, + onCheckedChange = { + formState.value = formState.value.newBuilder().also { wb -> wb.use_pullup = it }.build() + }, containerColor = CardDefaults.cardColors().containerColor, ) HorizontalDivider() diff --git a/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/component/DeviceConfigScreen.kt b/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/component/DeviceConfigScreen.kt index 83c424dd8c..193bd1c32b 100644 --- a/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/component/DeviceConfigScreen.kt +++ b/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/component/DeviceConfigScreen.kt @@ -159,7 +159,7 @@ private val Config.DeviceConfig.RebroadcastMode.description: StringResource @Composable fun DeviceConfigScreenCommon(viewModel: RadioConfigViewModel, onBack: () -> Unit) { val state by viewModel.radioConfigState.collectAsStateWithLifecycle() - val deviceConfig = state.radioConfig.device ?: Config.DeviceConfig() + val deviceConfig = state.radioConfig.device ?: Config.DeviceConfig.Builder().build() val formState = rememberConfigState(initialValue = deviceConfig) var selectedRole by rememberSaveable(formState.value.role) { mutableStateOf(formState.value.role) } val infrastructureRoles = @@ -168,10 +168,12 @@ fun DeviceConfigScreenCommon(viewModel: RadioConfigViewModel, onBack: () -> Unit if (selectedRole in infrastructureRoles) { RouterRoleConfirmationDialog( onDismiss = { selectedRole = formState.value.role }, - onConfirm = { formState.value = formState.value.copy(role = selectedRole) }, + onConfirm = { + formState.value = formState.value.newBuilder().also { wb -> wb.role = selectedRole }.build() + }, ) } else { - formState.value = formState.value.copy(role = selectedRole) + formState.value = formState.value.newBuilder().also { wb -> wb.role = selectedRole }.build() } } val focusManager = LocalFocusManager.current @@ -183,7 +185,7 @@ fun DeviceConfigScreenCommon(viewModel: RadioConfigViewModel, onBack: () -> Unit responseState = state.responseState, onDismissPacketResponse = viewModel::clearPacketResponse, onSave = { - val config = Config(device = it) + val config = Config.Builder().also { wb -> wb.device = it }.build() viewModel.setConfig(config) }, ) { @@ -207,7 +209,9 @@ fun DeviceConfigScreenCommon(viewModel: RadioConfigViewModel, onBack: () -> Unit title = stringResource(Res.string.rebroadcast_mode), enabled = state.connected, selectedItem = currentRebroadcastMode, - onItemSelected = { formState.value = formState.value.copy(rebroadcast_mode = it) }, + onItemSelected = { + formState.value = formState.value.newBuilder().also { wb -> wb.rebroadcast_mode = it }.build() + }, summary = stringResource(currentRebroadcastMode.description), ) @@ -219,7 +223,10 @@ fun DeviceConfigScreenCommon(viewModel: RadioConfigViewModel, onBack: () -> Unit selectedItem = formState.value.node_info_broadcast_secs.toLong(), enabled = state.connected, items = nodeInfoBroadcastIntervals.map { it.value to it.toDisplayString() }, - onItemSelected = { formState.value = formState.value.copy(node_info_broadcast_secs = it.toInt()) }, + onItemSelected = { + formState.value = + formState.value.newBuilder().also { wb -> wb.node_info_broadcast_secs = it.toInt() }.build() + }, ) } } @@ -231,7 +238,10 @@ fun DeviceConfigScreenCommon(viewModel: RadioConfigViewModel, onBack: () -> Unit summary = stringResource(Res.string.config_device_doubleTapAsButtonPress_summary), checked = formState.value.double_tap_as_button_press, enabled = state.connected, - onCheckedChange = { formState.value = formState.value.copy(double_tap_as_button_press = it) }, + onCheckedChange = { + formState.value = + formState.value.newBuilder().also { wb -> wb.double_tap_as_button_press = it }.build() + }, containerColor = CardDefaults.cardColors().containerColor, ) @@ -242,7 +252,10 @@ fun DeviceConfigScreenCommon(viewModel: RadioConfigViewModel, onBack: () -> Unit summary = stringResource(Res.string.config_device_tripleClickAsAdHocPing_summary), checked = !formState.value.disable_triple_click, enabled = state.connected, - onCheckedChange = { formState.value = formState.value.copy(disable_triple_click = !it) }, + onCheckedChange = { + formState.value = + formState.value.newBuilder().also { wb -> wb.disable_triple_click = !it }.build() + }, containerColor = CardDefaults.cardColors().containerColor, ) @@ -253,7 +266,10 @@ fun DeviceConfigScreenCommon(viewModel: RadioConfigViewModel, onBack: () -> Unit summary = stringResource(Res.string.config_device_ledHeartbeatEnabled_summary), checked = !formState.value.led_heartbeat_disabled, enabled = state.connected, - onCheckedChange = { formState.value = formState.value.copy(led_heartbeat_disabled = !it) }, + onCheckedChange = { + formState.value = + formState.value.newBuilder().also { wb -> wb.led_heartbeat_disabled = !it }.build() + }, containerColor = CardDefaults.cardColors().containerColor, ) } @@ -272,9 +288,15 @@ fun DeviceConfigScreenCommon(viewModel: RadioConfigViewModel, onBack: () -> Unit keyboardOptions = KeyboardOptions.Default.copy(keyboardType = KeyboardType.Text, imeAction = ImeAction.Done), keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }), - onValueChanged = { formState.value = formState.value.copy(tzdef = it) }, + onValueChanged = { + formState.value = formState.value.newBuilder().also { wb -> wb.tzdef = it }.build() + }, trailingIcon = { - IconButton(onClick = { formState.value = formState.value.copy(tzdef = "") }) { + IconButton( + onClick = { + formState.value = formState.value.newBuilder().also { wb -> wb.tzdef = "" }.build() + }, + ) { Icon( imageVector = MeshtasticIcons.Close, contentDescription = stringResource(Res.string.clear_time_zone), @@ -289,7 +311,10 @@ fun DeviceConfigScreenCommon(viewModel: RadioConfigViewModel, onBack: () -> Unit modifier = Modifier.fillMaxWidth(), enabled = state.connected, shape = RectangleShape, - onClick = { formState.value = formState.value.copy(tzdef = appTzPosixString) }, + onClick = { + formState.value = + formState.value.newBuilder().also { wb -> wb.tzdef = appTzPosixString }.build() + }, ) { Icon( imageVector = MeshtasticIcons.PhoneAndroid, @@ -310,7 +335,9 @@ fun DeviceConfigScreenCommon(viewModel: RadioConfigViewModel, onBack: () -> Unit value = formState.value.button_gpio, enabled = state.connected, keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }), - onValueChanged = { formState.value = formState.value.copy(button_gpio = it) }, + onValueChanged = { + formState.value = formState.value.newBuilder().also { wb -> wb.button_gpio = it }.build() + }, ) HorizontalDivider() @@ -320,7 +347,9 @@ fun DeviceConfigScreenCommon(viewModel: RadioConfigViewModel, onBack: () -> Unit value = formState.value.buzzer_gpio, enabled = state.connected, keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }), - onValueChanged = { formState.value = formState.value.copy(buzzer_gpio = it) }, + onValueChanged = { + formState.value = formState.value.newBuilder().also { wb -> wb.buzzer_gpio = it }.build() + }, ) } } diff --git a/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/component/DisplayConfigItemList.kt b/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/component/DisplayConfigItemList.kt index 9d859f21bc..f4b76e63e3 100644 --- a/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/component/DisplayConfigItemList.kt +++ b/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/component/DisplayConfigItemList.kt @@ -60,7 +60,7 @@ import org.meshtastic.proto.Config @Composable fun DisplayConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> Unit) { val state by viewModel.radioConfigState.collectAsStateWithLifecycle() - val displayConfig = state.radioConfig.display ?: Config.DisplayConfig() + val displayConfig = state.radioConfig.display ?: Config.DisplayConfig.Builder().build() val formState = rememberConfigState(initialValue = displayConfig) RadioConfigScreenList( @@ -71,7 +71,7 @@ fun DisplayConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> Unit) { responseState = state.responseState, onDismissPacketResponse = viewModel::clearPacketResponse, onSave = { - val config = Config(display = it) + val config = Config.Builder().also { wb -> wb.display = it }.build() viewModel.setConfig(config) }, ) { @@ -82,7 +82,9 @@ fun DisplayConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> Unit) { summary = stringResource(Res.string.config_display_compass_north_top_summary), checked = formState.value.compass_north_top, enabled = state.connected, - onCheckedChange = { formState.value = formState.value.copy(compass_north_top = it) }, + onCheckedChange = { + formState.value = formState.value.newBuilder().also { wb -> wb.compass_north_top = it }.build() + }, containerColor = CardDefaults.cardColors().containerColor, ) HorizontalDivider() @@ -91,7 +93,9 @@ fun DisplayConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> Unit) { summary = stringResource(Res.string.display_time_in_12h_format), enabled = state.connected, checked = formState.value.use_12h_clock, - onCheckedChange = { formState.value = formState.value.copy(use_12h_clock = it) }, + onCheckedChange = { + formState.value = formState.value.newBuilder().also { wb -> wb.use_12h_clock = it }.build() + }, containerColor = CardDefaults.cardColors().containerColor, ) HorizontalDivider() @@ -100,7 +104,9 @@ fun DisplayConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> Unit) { summary = stringResource(Res.string.config_display_heading_bold_summary), checked = formState.value.heading_bold, enabled = state.connected, - onCheckedChange = { formState.value = formState.value.copy(heading_bold = it) }, + onCheckedChange = { + formState.value = formState.value.newBuilder().also { wb -> wb.heading_bold = it }.build() + }, containerColor = CardDefaults.cardColors().containerColor, ) HorizontalDivider() @@ -110,7 +116,9 @@ fun DisplayConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> Unit) { enabled = state.connected, items = Config.DisplayConfig.DisplayUnits.entries.map { it to it.name }, selectedItem = formState.value.units, - onItemSelected = { formState.value = formState.value.copy(units = it) }, + onItemSelected = { + formState.value = formState.value.newBuilder().also { wb -> wb.units = it }.build() + }, ) } } @@ -126,7 +134,10 @@ fun DisplayConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> Unit) { selectedItem = screenOnIntervals.find { it.value == formState.value.screen_on_secs.toLong() } ?: screenOnIntervals.first(), - onItemSelected = { formState.value = formState.value.copy(screen_on_secs = it.value.toInt()) }, + onItemSelected = { + formState.value = + formState.value.newBuilder().also { wb -> wb.screen_on_secs = it.value.toInt() }.build() + }, ) HorizontalDivider() DropDownPreference( @@ -138,7 +149,11 @@ fun DisplayConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> Unit) { carouselIntervals.find { it.value == formState.value.auto_screen_carousel_secs.toLong() } ?: carouselIntervals.first(), onItemSelected = { - formState.value = formState.value.copy(auto_screen_carousel_secs = it.value.toInt()) + formState.value = + formState.value + .newBuilder() + .also { wb -> wb.auto_screen_carousel_secs = it.value.toInt() } + .build() }, ) HorizontalDivider() @@ -147,7 +162,10 @@ fun DisplayConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> Unit) { summary = stringResource(Res.string.config_display_wake_on_tap_or_motion_summary), checked = formState.value.wake_on_tap_or_motion, enabled = state.connected, - onCheckedChange = { formState.value = formState.value.copy(wake_on_tap_or_motion = it) }, + onCheckedChange = { + formState.value = + formState.value.newBuilder().also { wb -> wb.wake_on_tap_or_motion = it }.build() + }, containerColor = CardDefaults.cardColors().containerColor, ) HorizontalDivider() @@ -156,7 +174,9 @@ fun DisplayConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> Unit) { summary = stringResource(Res.string.config_display_flip_screen_summary), checked = formState.value.flip_screen, enabled = state.connected, - onCheckedChange = { formState.value = formState.value.copy(flip_screen = it) }, + onCheckedChange = { + formState.value = formState.value.newBuilder().also { wb -> wb.flip_screen = it }.build() + }, containerColor = CardDefaults.cardColors().containerColor, ) HorizontalDivider() @@ -166,7 +186,9 @@ fun DisplayConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> Unit) { enabled = state.connected, items = Config.DisplayConfig.DisplayMode.entries.map { it to it.name }, selectedItem = formState.value.displaymode, - onItemSelected = { formState.value = formState.value.copy(displaymode = it) }, + onItemSelected = { + formState.value = formState.value.newBuilder().also { wb -> wb.displaymode = it }.build() + }, ) HorizontalDivider() DropDownPreference( @@ -175,7 +197,9 @@ fun DisplayConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> Unit) { enabled = state.connected, items = Config.DisplayConfig.OledType.entries.map { it to it.name }, selectedItem = formState.value.oled, - onItemSelected = { formState.value = formState.value.copy(oled = it) }, + onItemSelected = { + formState.value = formState.value.newBuilder().also { wb -> wb.oled = it }.build() + }, ) HorizontalDivider() DropDownPreference( @@ -183,7 +207,10 @@ fun DisplayConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> Unit) { enabled = state.connected, items = Config.DisplayConfig.CompassOrientation.entries.map { it to it.name }, selectedItem = formState.value.compass_orientation, - onItemSelected = { formState.value = formState.value.copy(compass_orientation = it) }, + onItemSelected = { + formState.value = + formState.value.newBuilder().also { wb -> wb.compass_orientation = it }.build() + }, ) } } diff --git a/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/component/EditDeviceProfileDialog.kt b/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/component/EditDeviceProfileDialog.kt index d2ebfdff44..3921f242bf 100644 --- a/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/component/EditDeviceProfileDialog.kt +++ b/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/component/EditDeviceProfileDialog.kt @@ -91,27 +91,30 @@ fun EditDeviceProfileDialog( confirmText = stringResource(Res.string.save), onConfirm = { val result = - DeviceProfile( - long_name = if (state[ProfileField.LONG_NAME] == true) deviceProfile.long_name else null, - short_name = if (state[ProfileField.SHORT_NAME] == true) deviceProfile.short_name else null, - channel_url = if (state[ProfileField.CHANNEL_URL] == true) deviceProfile.channel_url else null, - config = if (state[ProfileField.CONFIG] == true) deviceProfile.config else null, - module_config = - if (state[ProfileField.MODULE_CONFIG] == true) { - deviceProfile.module_config - } else { - null - }, - fixed_position = - if (state[ProfileField.FIXED_POSITION] == true) { - deviceProfile.fixed_position - } else { - null - }, - is_unmessagable = - if (state[ProfileField.UNMESSAGABLE] == true) deviceProfile.is_unmessagable else null, - is_licensed = if (state[ProfileField.LICENSED] == true) deviceProfile.is_licensed else null, - ) + DeviceProfile.Builder() + .also { wb -> + wb.long_name = if (state[ProfileField.LONG_NAME] == true) deviceProfile.long_name else null + wb.short_name = if (state[ProfileField.SHORT_NAME] == true) deviceProfile.short_name else null + wb.channel_url = + if (state[ProfileField.CHANNEL_URL] == true) deviceProfile.channel_url else null + wb.config = if (state[ProfileField.CONFIG] == true) deviceProfile.config else null + wb.module_config = + if (state[ProfileField.MODULE_CONFIG] == true) { + deviceProfile.module_config + } else { + null + } + wb.fixed_position = + if (state[ProfileField.FIXED_POSITION] == true) { + deviceProfile.fixed_position + } else { + null + } + wb.is_unmessagable = + if (state[ProfileField.UNMESSAGABLE] == true) deviceProfile.is_unmessagable else null + wb.is_licensed = if (state[ProfileField.LICENSED] == true) deviceProfile.is_licensed else null + } + .build() onConfirm(result) }, modifier = modifier, diff --git a/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/component/ExternalNotificationConfigScreen.kt b/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/component/ExternalNotificationConfigScreen.kt index 7862492765..1f6a2b926f 100644 --- a/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/component/ExternalNotificationConfigScreen.kt +++ b/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/component/ExternalNotificationConfigScreen.kt @@ -76,7 +76,8 @@ fun ExternalNotificationConfigScreenCommon( viewModel: RadioConfigViewModel, ) { val state by viewModel.radioConfigState.collectAsStateWithLifecycle() - val extNotificationConfig = state.moduleConfig.external_notification ?: ModuleConfig.ExternalNotificationConfig() + val extNotificationConfig = + state.moduleConfig.external_notification ?: ModuleConfig.ExternalNotificationConfig.Builder().build() val ringtone = state.ringtone val formState = rememberConfigState(initialValue = extNotificationConfig) var ringtoneInput by rememberSaveable(ringtone) { mutableStateOf(ringtone) } @@ -97,7 +98,7 @@ fun ExternalNotificationConfigScreenCommon( viewModel.setRingtone(ringtoneInput) } if (formState.value != extNotificationConfig) { - val config = ModuleConfig(external_notification = formState.value) + val config = ModuleConfig.Builder().also { wb -> wb.external_notification = formState.value }.build() viewModel.setModuleConfig(config) } }, @@ -108,7 +109,9 @@ fun ExternalNotificationConfigScreenCommon( title = stringResource(Res.string.external_notification_enabled), checked = formState.value.enabled, enabled = state.connected, - onCheckedChange = { formState.value = formState.value.copy(enabled = it) }, + onCheckedChange = { + formState.value = formState.value.newBuilder().also { wb -> wb.enabled = it }.build() + }, containerColor = CardDefaults.cardColors().containerColor, ) } @@ -120,7 +123,9 @@ fun ExternalNotificationConfigScreenCommon( title = stringResource(Res.string.alert_message_led), checked = formState.value.alert_message, enabled = state.connected, - onCheckedChange = { formState.value = formState.value.copy(alert_message = it) }, + onCheckedChange = { + formState.value = formState.value.newBuilder().also { wb -> wb.alert_message = it }.build() + }, containerColor = CardDefaults.cardColors().containerColor, ) HorizontalDivider() @@ -128,7 +133,10 @@ fun ExternalNotificationConfigScreenCommon( title = stringResource(Res.string.alert_message_buzzer), checked = formState.value.alert_message_buzzer, enabled = state.connected, - onCheckedChange = { formState.value = formState.value.copy(alert_message_buzzer = it) }, + onCheckedChange = { + formState.value = + formState.value.newBuilder().also { wb -> wb.alert_message_buzzer = it }.build() + }, containerColor = CardDefaults.cardColors().containerColor, ) HorizontalDivider() @@ -136,7 +144,10 @@ fun ExternalNotificationConfigScreenCommon( title = stringResource(Res.string.alert_message_vibra), checked = formState.value.alert_message_vibra, enabled = state.connected, - onCheckedChange = { formState.value = formState.value.copy(alert_message_vibra = it) }, + onCheckedChange = { + formState.value = + formState.value.newBuilder().also { wb -> wb.alert_message_vibra = it }.build() + }, containerColor = CardDefaults.cardColors().containerColor, ) } @@ -148,7 +159,9 @@ fun ExternalNotificationConfigScreenCommon( title = stringResource(Res.string.alert_bell_led), checked = formState.value.alert_bell, enabled = state.connected, - onCheckedChange = { formState.value = formState.value.copy(alert_bell = it) }, + onCheckedChange = { + formState.value = formState.value.newBuilder().also { wb -> wb.alert_bell = it }.build() + }, containerColor = CardDefaults.cardColors().containerColor, ) HorizontalDivider() @@ -156,7 +169,9 @@ fun ExternalNotificationConfigScreenCommon( title = stringResource(Res.string.alert_bell_buzzer), checked = formState.value.alert_bell_buzzer, enabled = state.connected, - onCheckedChange = { formState.value = formState.value.copy(alert_bell_buzzer = it) }, + onCheckedChange = { + formState.value = formState.value.newBuilder().also { wb -> wb.alert_bell_buzzer = it }.build() + }, containerColor = CardDefaults.cardColors().containerColor, ) HorizontalDivider() @@ -164,7 +179,9 @@ fun ExternalNotificationConfigScreenCommon( title = stringResource(Res.string.alert_bell_vibra), checked = formState.value.alert_bell_vibra, enabled = state.connected, - onCheckedChange = { formState.value = formState.value.copy(alert_bell_vibra = it) }, + onCheckedChange = { + formState.value = formState.value.newBuilder().also { wb -> wb.alert_bell_vibra = it }.build() + }, containerColor = CardDefaults.cardColors().containerColor, ) } @@ -178,7 +195,9 @@ fun ExternalNotificationConfigScreenCommon( items = gpio, selectedItem = formState.value.output.toLong(), enabled = state.connected, - onItemSelected = { formState.value = formState.value.copy(output = it.toInt()) }, + onItemSelected = { + formState.value = formState.value.newBuilder().also { wb -> wb.output = it.toInt() }.build() + }, ) if (formState.value.output != 0) { HorizontalDivider() @@ -186,7 +205,9 @@ fun ExternalNotificationConfigScreenCommon( title = stringResource(Res.string.output_led_active_high), checked = formState.value.active, enabled = state.connected, - onCheckedChange = { formState.value = formState.value.copy(active = it) }, + onCheckedChange = { + formState.value = formState.value.newBuilder().also { wb -> wb.active = it }.build() + }, containerColor = CardDefaults.cardColors().containerColor, ) } @@ -196,7 +217,10 @@ fun ExternalNotificationConfigScreenCommon( items = gpio, selectedItem = formState.value.output_buzzer.toLong(), enabled = state.connected, - onItemSelected = { formState.value = formState.value.copy(output_buzzer = it.toInt()) }, + onItemSelected = { + formState.value = + formState.value.newBuilder().also { wb -> wb.output_buzzer = it.toInt() }.build() + }, ) if (formState.value.output_buzzer != 0) { HorizontalDivider() @@ -204,7 +228,9 @@ fun ExternalNotificationConfigScreenCommon( title = stringResource(Res.string.use_pwm_buzzer), checked = formState.value.use_pwm, enabled = state.connected, - onCheckedChange = { formState.value = formState.value.copy(use_pwm = it) }, + onCheckedChange = { + formState.value = formState.value.newBuilder().also { wb -> wb.use_pwm = it }.build() + }, containerColor = CardDefaults.cardColors().containerColor, ) } @@ -214,7 +240,10 @@ fun ExternalNotificationConfigScreenCommon( items = gpio, selectedItem = formState.value.output_vibra.toLong(), enabled = state.connected, - onItemSelected = { formState.value = formState.value.copy(output_vibra = it.toInt()) }, + onItemSelected = { + formState.value = + formState.value.newBuilder().also { wb -> wb.output_vibra = it.toInt() }.build() + }, ) HorizontalDivider() val outputItems = remember { IntervalConfiguration.OUTPUT.allowedIntervals } @@ -223,7 +252,9 @@ fun ExternalNotificationConfigScreenCommon( items = outputItems.map { it.value to it.toDisplayString() }, selectedItem = formState.value.output_ms.toLong(), enabled = state.connected, - onItemSelected = { formState.value = formState.value.copy(output_ms = it.toInt()) }, + onItemSelected = { + formState.value = formState.value.newBuilder().also { wb -> wb.output_ms = it.toInt() }.build() + }, ) HorizontalDivider() val nagItems = remember { IntervalConfiguration.NAG_TIMEOUT.allowedIntervals } @@ -232,7 +263,10 @@ fun ExternalNotificationConfigScreenCommon( items = nagItems.map { it.value to it.toDisplayString() }, selectedItem = formState.value.nag_timeout.toLong(), enabled = state.connected, - onItemSelected = { formState.value = formState.value.copy(nag_timeout = it.toInt()) }, + onItemSelected = { + formState.value = + formState.value.newBuilder().also { wb -> wb.nag_timeout = it.toInt() }.build() + }, ) HorizontalDivider() EditTextPreference( @@ -258,7 +292,9 @@ fun ExternalNotificationConfigScreenCommon( title = stringResource(Res.string.use_i2s_as_buzzer), checked = formState.value.use_i2s_as_buzzer, enabled = state.connected, - onCheckedChange = { formState.value = formState.value.copy(use_i2s_as_buzzer = it) }, + onCheckedChange = { + formState.value = formState.value.newBuilder().also { wb -> wb.use_i2s_as_buzzer = it }.build() + }, containerColor = CardDefaults.cardColors().containerColor, ) } diff --git a/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/component/LoRaConfigItemList.kt b/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/component/LoRaConfigItemList.kt index 281dcd18e9..eacf7a8204 100644 --- a/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/component/LoRaConfigItemList.kt +++ b/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/component/LoRaConfigItemList.kt @@ -71,12 +71,13 @@ import org.meshtastic.core.ui.component.SignedIntegerEditTextPreference import org.meshtastic.core.ui.component.SwitchPreference import org.meshtastic.core.ui.component.TitledCard import org.meshtastic.feature.settings.radio.RadioConfigViewModel -import org.meshtastic.feature.settings.util.hopLimits +import org.meshtastic.feature.settings.util.intRange import org.meshtastic.proto.Config import org.meshtastic.proto.Config.LoRaConfig.ModemPreset import org.meshtastic.proto.Config.LoRaConfig.RegionCode +import org.meshtastic.proto.hop_limit -private val SPREAD_FACTOR_RANGE = 7..12 +private val SPREAD_FACTOR_RANGE = 5..12 private val CODING_RATE_RANGE = 5..8 /** @@ -118,7 +119,7 @@ private fun buildRegionItems(capabilities: Capabilities, selectedRegion: RegionC @Composable fun LoRaConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> Unit) { val state by viewModel.radioConfigState.collectAsStateWithLifecycle() - val loraConfig = state.radioConfig.lora ?: Config.LoRaConfig() + val loraConfig = state.radioConfig.lora ?: Config.LoRaConfig.Builder().build() val primarySettings = state.channelList.getOrNull(0) if (primarySettings == null) { @@ -156,7 +157,7 @@ fun LoRaConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> Unit) { responseState = state.responseState, onDismissPacketResponse = viewModel::clearPacketResponse, onSave = { - val config = Config(lora = it) + val config = Config.Builder().also { wb -> wb.lora = it }.build() viewModel.setConfig(config) }, ) { @@ -196,7 +197,14 @@ fun LoRaConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> Unit) { newRegion = region, current = formState.value.modem_preset, ) - formState.value = formState.value.copy(region = region, modem_preset = preset) + formState.value = + formState.value + .newBuilder() + .also { wb -> + wb.region = region + wb.modem_preset = preset + } + .build() }, ) HorizontalDivider() @@ -204,7 +212,9 @@ fun LoRaConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> Unit) { title = stringResource(Res.string.use_modem_preset), checked = formState.value.use_preset, enabled = state.connected, - onCheckedChange = { formState.value = formState.value.copy(use_preset = it) }, + onCheckedChange = { + formState.value = formState.value.newBuilder().also { wb -> wb.use_preset = it }.build() + }, containerColor = CardDefaults.cardColors().containerColor, ) HorizontalDivider() @@ -228,7 +238,9 @@ fun LoRaConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> Unit) { enabled = state.connected, items = presetItems, selectedItem = formState.value.modem_preset, - onItemSelected = { formState.value = formState.value.copy(modem_preset = it) }, + onItemSelected = { + formState.value = formState.value.newBuilder().also { wb -> wb.modem_preset = it }.build() + }, ) } else { ManualModemSettings( @@ -248,7 +260,9 @@ fun LoRaConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> Unit) { title = stringResource(Res.string.ignore_mqtt), checked = formState.value.ignore_mqtt, enabled = state.connected, - onCheckedChange = { formState.value = formState.value.copy(ignore_mqtt = it) }, + onCheckedChange = { + formState.value = formState.value.newBuilder().also { wb -> wb.ignore_mqtt = it }.build() + }, containerColor = CardDefaults.cardColors().containerColor, ) HorizontalDivider() @@ -256,7 +270,9 @@ fun LoRaConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> Unit) { title = stringResource(Res.string.ok_to_mqtt), checked = formState.value.config_ok_to_mqtt, enabled = state.connected, - onCheckedChange = { formState.value = formState.value.copy(config_ok_to_mqtt = it) }, + onCheckedChange = { + formState.value = formState.value.newBuilder().also { wb -> wb.config_ok_to_mqtt = it }.build() + }, containerColor = CardDefaults.cardColors().containerColor, ) HorizontalDivider() @@ -264,7 +280,9 @@ fun LoRaConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> Unit) { title = stringResource(Res.string.tx_enabled), checked = formState.value.tx_enabled, enabled = state.connected, - onCheckedChange = { formState.value = formState.value.copy(tx_enabled = it) }, + onCheckedChange = { + formState.value = formState.value.newBuilder().also { wb -> wb.tx_enabled = it }.build() + }, containerColor = CardDefaults.cardColors().containerColor, ) HorizontalDivider() @@ -272,17 +290,25 @@ fun LoRaConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> Unit) { title = stringResource(Res.string.override_duty_cycle), checked = formState.value.override_duty_cycle, enabled = state.connected, - onCheckedChange = { formState.value = formState.value.copy(override_duty_cycle = it) }, + onCheckedChange = { + formState.value = + formState.value.newBuilder().also { wb -> wb.override_duty_cycle = it }.build() + }, containerColor = CardDefaults.cardColors().containerColor, ) HorizontalDivider() - val hopLimitItems = remember { hopLimits } + // The schema declares this bound; a pin that drops it must fail here rather than unbound the picker. + val hopLimitItems = remember { + requireNotNull(Config.LoRaConfig.hop_limit.intRange).map { it to it.toString() } + } DropDownPreference( title = stringResource(Res.string.hop_limit), summary = stringResource(Res.string.config_lora_hop_limit_summary), items = hopLimitItems, selectedItem = formState.value.hop_limit, - onItemSelected = { formState.value = formState.value.copy(hop_limit = it) }, + onItemSelected = { + formState.value = formState.value.newBuilder().also { wb -> wb.hop_limit = it }.build() + }, enabled = state.connected, ) HorizontalDivider() @@ -301,7 +327,7 @@ fun LoRaConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> Unit) { onFocusChanged = { isFocusedSlot = it.isFocused }, onValueChanged = { if (it <= formState.value.numChannels) { // total num of LoRa channels - formState.value = formState.value.copy(channel_num = it) + formState.value = formState.value.newBuilder().also { wb -> wb.channel_num = it }.build() } }, ) @@ -310,7 +336,10 @@ fun LoRaConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> Unit) { title = stringResource(Res.string.sx126x_rx_boosted_gain), checked = formState.value.sx126x_rx_boosted_gain, enabled = state.connected, - onCheckedChange = { formState.value = formState.value.copy(sx126x_rx_boosted_gain = it) }, + onCheckedChange = { + formState.value = + formState.value.newBuilder().also { wb -> wb.sx126x_rx_boosted_gain = it }.build() + }, containerColor = CardDefaults.cardColors().containerColor, ) HorizontalDivider() @@ -326,7 +355,9 @@ fun LoRaConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> Unit) { enabled = state.connected, keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }), onFocusChanged = { isFocusedOverride = it.isFocused }, - onValueChanged = { formState.value = formState.value.copy(override_frequency = it) }, + onValueChanged = { + formState.value = formState.value.newBuilder().also { wb -> wb.override_frequency = it }.build() + }, ) HorizontalDivider() SignedIntegerEditTextPreference( @@ -334,7 +365,9 @@ fun LoRaConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> Unit) { value = formState.value.tx_power, enabled = state.connected, keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }), - onValueChanged = { formState.value = formState.value.copy(tx_power = it) }, + onValueChanged = { + formState.value = formState.value.newBuilder().also { wb -> wb.tx_power = it }.build() + }, ) if (viewModel.hasPaFan) { HorizontalDivider() @@ -342,7 +375,10 @@ fun LoRaConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> Unit) { title = stringResource(Res.string.pa_fan_disabled), checked = formState.value.pa_fan_disabled, enabled = state.connected, - onCheckedChange = { formState.value = formState.value.copy(pa_fan_disabled = it) }, + onCheckedChange = { + formState.value = + formState.value.newBuilder().also { wb -> wb.pa_fan_disabled = it }.build() + }, containerColor = CardDefaults.cardColors().containerColor, ) } @@ -376,7 +412,7 @@ private fun ManualModemSettings( keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }), onValueChanged = { if (it in SPREAD_FACTOR_RANGE) { - onConfigChange(config.copy(spread_factor = it)) + onConfigChange(config.newBuilder().also { wb -> wb.spread_factor = it }.build()) } }, ) @@ -389,7 +425,7 @@ private fun ManualModemSettings( keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }), onValueChanged = { if (it in CODING_RATE_RANGE) { - onConfigChange(config.copy(coding_rate = it)) + onConfigChange(config.newBuilder().also { wb -> wb.coding_rate = it }.build()) } }, ) @@ -411,7 +447,7 @@ internal fun LoRaBandwidthPreference( value = config.bandwidth, enabled = enabled, keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }), - onValueChanged = { onConfigChange(config.copy(bandwidth = it)) }, + onValueChanged = { onConfigChange(config.newBuilder().also { wb -> wb.bandwidth = it }.build()) }, ) return } @@ -447,6 +483,6 @@ internal fun LoRaBandwidthPreference( enabled = enabled, items = items, selectedItem = config.bandwidth, - onItemSelected = { onConfigChange(config.copy(bandwidth = it)) }, + onItemSelected = { onConfigChange(config.newBuilder().also { wb -> wb.bandwidth = it }.build()) }, ) } diff --git a/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/component/MQTTConfigItemList.kt b/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/component/MQTTConfigItemList.kt index fb3a18b5d4..34c7762d83 100644 --- a/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/component/MQTTConfigItemList.kt +++ b/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/component/MQTTConfigItemList.kt @@ -101,14 +101,18 @@ fun MQTTConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> Unit) { val mqttProxyActive by viewModel.mqttProxyActive.collectAsStateWithLifecycle() val probeStatus by viewModel.mqttProbeStatus.collectAsStateWithLifecycle() val destNum = destNode?.num - val mqttConfig = state.moduleConfig.mqtt ?: ModuleConfig.MQTTConfig() + val mqttConfig = state.moduleConfig.mqtt ?: ModuleConfig.MQTTConfig.Builder().build() val formState = rememberConfigState(initialValue = mqttConfig) - val currentMapReportSettings = formState.value.map_report_settings ?: ModuleConfig.MapReportSettings() + val currentMapReportSettings = + formState.value.map_report_settings ?: ModuleConfig.MapReportSettings.Builder().build() if (!currentMapReportSettings.should_report_location) { val settings = - currentMapReportSettings.copy(should_report_location = viewModel.shouldReportLocation(destNum).value) - formState.value = formState.value.copy(map_report_settings = settings) + currentMapReportSettings + .newBuilder() + .also { wb -> wb.should_report_location = viewModel.shouldReportLocation(destNum).value } + .build() + formState.value = formState.value.newBuilder().also { wb -> wb.map_report_settings = settings }.build() } val consentValid = @@ -129,7 +133,7 @@ fun MQTTConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> Unit) { responseState = state.responseState, onDismissPacketResponse = viewModel::clearPacketResponse, onSave = { - val config = ModuleConfig(mqtt = it) + val config = ModuleConfig.Builder().also { wb -> wb.mqtt = it }.build() viewModel.setModuleConfig(config) }, ) { @@ -161,7 +165,9 @@ fun MQTTConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> Unit) { title = stringResource(Res.string.mqtt_enabled), checked = formState.value.enabled, enabled = state.connected, - onCheckedChange = { formState.value = formState.value.copy(enabled = it) }, + onCheckedChange = { + formState.value = formState.value.newBuilder().also { wb -> wb.enabled = it }.build() + }, containerColor = CardDefaults.cardColors().containerColor, ) HorizontalDivider() @@ -183,7 +189,9 @@ fun MQTTConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> Unit) { keyboardOptions = KeyboardOptions.Default.copy(keyboardType = KeyboardType.Text, imeAction = ImeAction.Done), keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }), - onValueChanged = { formState.value = formState.value.copy(username = it) }, + onValueChanged = { + formState.value = formState.value.newBuilder().also { wb -> wb.username = it }.build() + }, ) HorizontalDivider() EditPasswordPreference( @@ -192,14 +200,18 @@ fun MQTTConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> Unit) { maxSize = 63, // password max_size:64 enabled = state.connected, keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }), - onValueChanged = { formState.value = formState.value.copy(password = it) }, + onValueChanged = { + formState.value = formState.value.newBuilder().also { wb -> wb.password = it }.build() + }, ) HorizontalDivider() SwitchPreference( title = stringResource(Res.string.encryption_enabled), checked = formState.value.encryption_enabled, enabled = state.connected, - onCheckedChange = { formState.value = formState.value.copy(encryption_enabled = it) }, + onCheckedChange = { + formState.value = formState.value.newBuilder().also { wb -> wb.encryption_enabled = it }.build() + }, containerColor = CardDefaults.cardColors().containerColor, ) HorizontalDivider() @@ -207,7 +219,9 @@ fun MQTTConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> Unit) { title = stringResource(Res.string.json_output_enabled), checked = formState.value.json_enabled, enabled = state.connected, - onCheckedChange = { formState.value = formState.value.copy(json_enabled = it) }, + onCheckedChange = { + formState.value = formState.value.newBuilder().also { wb -> wb.json_enabled = it }.build() + }, containerColor = CardDefaults.cardColors().containerColor, ) HorizontalDivider() @@ -217,7 +231,9 @@ fun MQTTConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> Unit) { title = stringResource(Res.string.tls_enabled), checked = formState.value.tls_enabled || enforceTls, enabled = state.connected && !enforceTls, - onCheckedChange = { formState.value = formState.value.copy(tls_enabled = it) }, + onCheckedChange = { + formState.value = formState.value.newBuilder().also { wb -> wb.tls_enabled = it }.build() + }, containerColor = CardDefaults.cardColors().containerColor, ) HorizontalDivider() @@ -230,14 +246,19 @@ fun MQTTConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> Unit) { keyboardOptions = KeyboardOptions.Default.copy(keyboardType = KeyboardType.Text, imeAction = ImeAction.Done), keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }), - onValueChanged = { formState.value = formState.value.copy(root = it) }, + onValueChanged = { + formState.value = formState.value.newBuilder().also { wb -> wb.root = it }.build() + }, ) HorizontalDivider() SwitchPreference( title = stringResource(Res.string.proxy_to_client_enabled), checked = formState.value.proxy_to_client_enabled, enabled = state.connected, - onCheckedChange = { formState.value = formState.value.copy(proxy_to_client_enabled = it) }, + onCheckedChange = { + formState.value = + formState.value.newBuilder().also { wb -> wb.proxy_to_client_enabled = it }.build() + }, containerColor = CardDefaults.cardColors().containerColor, ) } @@ -245,27 +266,34 @@ fun MQTTConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> Unit) { item { TitledCard(title = stringResource(Res.string.map_reporting)) { - val mapReportSettings = formState.value.map_report_settings ?: ModuleConfig.MapReportSettings() + val mapReportSettings = + formState.value.map_report_settings ?: ModuleConfig.MapReportSettings.Builder().build() MapReportingPreference( mapReportingEnabled = formState.value.map_reporting_enabled, onMapReportingEnabledChanged = { - formState.value = formState.value.copy(map_reporting_enabled = it) + formState.value = + formState.value.newBuilder().also { wb -> wb.map_reporting_enabled = it }.build() }, shouldReportLocation = mapReportSettings.should_report_location, onShouldReportLocationChanged = { viewModel.setShouldReportLocation(destNum, it) - val settings = mapReportSettings.copy(should_report_location = it) - formState.value = formState.value.copy(map_report_settings = settings) + val settings = + mapReportSettings.newBuilder().also { wb -> wb.should_report_location = it }.build() + formState.value = + formState.value.newBuilder().also { wb -> wb.map_report_settings = settings }.build() }, positionPrecision = mapReportSettings.position_precision, onPositionPrecisionChanged = { - val settings = mapReportSettings.copy(position_precision = it) - formState.value = formState.value.copy(map_report_settings = settings) + val settings = mapReportSettings.newBuilder().also { wb -> wb.position_precision = it }.build() + formState.value = + formState.value.newBuilder().also { wb -> wb.map_report_settings = settings }.build() }, publishIntervalSecs = mapReportSettings.publish_interval_secs, onPublishIntervalSecsChanged = { - val settings = mapReportSettings.copy(publish_interval_secs = it) - formState.value = formState.value.copy(map_report_settings = settings) + val settings = + mapReportSettings.newBuilder().also { wb -> wb.publish_interval_secs = it }.build() + formState.value = + formState.value.newBuilder().also { wb -> wb.map_report_settings = settings }.build() }, enabled = state.connected, ) @@ -340,7 +368,7 @@ private fun MqttAddressAndProbe( keyboardOptions = KeyboardOptions.Default.copy(keyboardType = KeyboardType.Text, imeAction = ImeAction.Done), keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }), onValueChanged = { - formState.value = formState.value.copy(address = it) + formState.value = formState.value.newBuilder().also { wb -> wb.address = it }.build() onClearProbe() }, ) diff --git a/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/component/MeshBeaconConfigItemList.kt b/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/component/MeshBeaconConfigItemList.kt index 2db71ec3a5..80559fa785 100644 --- a/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/component/MeshBeaconConfigItemList.kt +++ b/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/component/MeshBeaconConfigItemList.kt @@ -108,7 +108,7 @@ private fun Int.hasFlag(flag: Int): Boolean = (this and flag) != 0 @Composable fun MeshBeaconConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> Unit, modifier: Modifier = Modifier) { val state by viewModel.radioConfigState.collectAsStateWithLifecycle() - val meshBeaconConfig = state.moduleConfig.mesh_beacon ?: MeshBeaconConfig() + val meshBeaconConfig = state.moduleConfig.mesh_beacon ?: MeshBeaconConfig.Builder().build() val radioLora = state.radioConfig.lora if (radioLora == null || radioLora.region == RegionCode.UNSET) { @@ -184,7 +184,7 @@ fun MeshBeaconConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> Unit, onDismissPacketResponse = viewModel::clearPacketResponse, onSave = { val stamped = stampBeaconConfigForSave(it, meshBeaconConfig, radioLora, state.channelList) - viewModel.setModuleConfig(ModuleConfig(mesh_beacon = stamped)) + viewModel.setModuleConfig(ModuleConfig.Builder().also { wb -> wb.mesh_beacon = stamped }.build()) }, ) { item { @@ -204,7 +204,11 @@ fun MeshBeaconConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> Unit, checked = formState.value.flags.hasFlag(listenFlag), enabled = state.connected, onCheckedChange = { - formState.value = formState.value.copy(flags = formState.value.flags.withFlag(listenFlag, it)) + formState.value = + formState.value + .newBuilder() + .also { wb -> wb.flags = formState.value.flags.withFlag(listenFlag, it) } + .build() }, containerColor = CardDefaults.cardColors().containerColor, ) @@ -216,7 +220,10 @@ fun MeshBeaconConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> Unit, enabled = broadcastGate.toggleEnabled, onCheckedChange = { formState.value = - formState.value.copy(flags = formState.value.flags.withFlag(broadcastFlag, it)) + formState.value + .newBuilder() + .also { wb -> wb.flags = formState.value.flags.withFlag(broadcastFlag, it) } + .build() }, containerColor = CardDefaults.cardColors().containerColor, ) @@ -238,7 +245,10 @@ fun MeshBeaconConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> Unit, isError = false, keyboardOptions = KeyboardOptions.Default, keyboardActions = KeyboardActions.Default, - onValueChanged = { formState.value = formState.value.copy(broadcast_message = it) }, + onValueChanged = { + formState.value = + formState.value.newBuilder().also { wb -> wb.broadcast_message = it }.build() + }, ) HorizontalDivider() val intervalOptions = remember { IntervalConfiguration.MESH_BEACON_BROADCAST.allowedIntervals } @@ -275,7 +285,11 @@ fun MeshBeaconConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> Unit, selectedItem = storedIntervalSecs, enabled = broadcastGate.sectionsEnabled, onItemSelected = { - formState.value = formState.value.copy(broadcast_interval_secs = it.toInt()) + formState.value = + formState.value + .newBuilder() + .also { wb -> wb.broadcast_interval_secs = it.toInt() } + .build() }, ) } @@ -292,7 +306,8 @@ fun MeshBeaconConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> Unit, channelItems = channelItems, enabled = broadcastGate.sectionsEnabled, onChannelSelect = { chosen -> - formState.value = formState.value.copy(broadcast_offer_channel = chosen) + formState.value = + formState.value.newBuilder().also { wb -> wb.broadcast_offer_channel = chosen }.build() }, ) } @@ -306,7 +321,9 @@ fun MeshBeaconConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> Unit, presetConstraint = presetConstraint, presetsGated = presetsGated, capabilities = capabilities, - onChange = { formState.value = formState.value.copy(broadcast_targets = it) }, + onChange = { + formState.value = formState.value.newBuilder().also { wb -> wb.broadcast_targets = it }.build() + }, ) } } @@ -401,7 +418,10 @@ internal fun BroadcastTargetsCard( ) } HorizontalDivider() - TextButton(onClick = { onChange(targets + MeshBeaconConfig.BroadcastTarget()) }, enabled = enabled) { + TextButton( + onClick = { onChange(targets + MeshBeaconConfig.BroadcastTarget.Builder().build()) }, + enabled = enabled, + ) { Text(stringResource(Res.string.mesh_beacon_target_add)) } } @@ -471,7 +491,7 @@ private fun BroadcastTargetRow( items = nullablePresetItems, selectedItem = rowPreset, enabled = enabled, - onItemSelected = { sel -> onChange { it.copy(preset = sel) } }, + onItemSelected = { sel -> onChange { it.newBuilder().also { wb -> wb.preset = sel }.build() } }, ) TextButton(onClick = onRemove, enabled = enabled) { Text(stringResource(Res.string.mesh_beacon_target_remove)) } } diff --git a/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/component/MeshBeaconConfigPolicy.kt b/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/component/MeshBeaconConfigPolicy.kt index f4fcb652d6..beb1cbc858 100644 --- a/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/component/MeshBeaconConfigPolicy.kt +++ b/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/component/MeshBeaconConfigPolicy.kt @@ -100,24 +100,36 @@ internal fun stampBeaconConfigForSave( radioLora: Config.LoRaConfig, channelList: List, ): MeshBeaconConfig = if (radioLora.use_preset) { - form.copy( - broadcast_offer_region = radioLora.region, - broadcast_offer_preset = radioLora.modem_preset, - broadcast_offer_channel = - (form.broadcast_offer_channel ?: channelList.getOrNull(0))?.let { - ChannelSettings(name = it.name, psk = it.psk) - }, - broadcast_targets = form.broadcast_targets.map { it.copy(region = radioLora.region) }, - ) + form + .newBuilder() + .also { wb -> + wb.broadcast_offer_region = radioLora.region + wb.broadcast_offer_preset = radioLora.modem_preset + wb.broadcast_offer_channel = + (form.broadcast_offer_channel ?: channelList.getOrNull(0))?.let { + ChannelSettings.Builder() + .also { wb -> + wb.name = it.name + wb.psk = it.psk + } + .build() + } + wb.broadcast_targets = + form.broadcast_targets.map { it.newBuilder().also { wb -> wb.region = radioLora.region }.build() } + } + .build() } else { - form.copy( - broadcast_message = stored.broadcast_message, - broadcast_interval_secs = stored.broadcast_interval_secs, - broadcast_offer_region = stored.broadcast_offer_region, - broadcast_offer_preset = stored.broadcast_offer_preset, - broadcast_offer_channel = stored.broadcast_offer_channel, - broadcast_targets = stored.broadcast_targets, - ) + form + .newBuilder() + .also { wb -> + wb.broadcast_message = stored.broadcast_message + wb.broadcast_interval_secs = stored.broadcast_interval_secs + wb.broadcast_offer_region = stored.broadcast_offer_region + wb.broadcast_offer_preset = stored.broadcast_offer_preset + wb.broadcast_offer_channel = stored.broadcast_offer_channel + wb.broadcast_targets = stored.broadcast_targets + } + .build() } /** @@ -130,10 +142,13 @@ internal fun selectBeaconTargetChannel( target: MeshBeaconConfig.BroadcastTarget, channelIndex: Int?, currentPreset: ModemPreset, -): MeshBeaconConfig.BroadcastTarget = target.copy( - channel_index = channelIndex, - preset = if (channelIndex != null) target.preset ?: currentPreset else target.preset, -) +): MeshBeaconConfig.BroadcastTarget = target + .newBuilder() + .also { wb -> + wb.channel_index = channelIndex + wb.preset = if (channelIndex != null) target.preset ?: currentPreset else target.preset + } + .build() /** * Seeds an empty stored `broadcast_targets` list with one default row (design#140 behavior 6: "one row saves as the @@ -143,7 +158,7 @@ internal fun selectBeaconTargetChannel( * stored list is returned unchanged. */ internal fun seedBeaconTargets(stored: List): List = - stored.ifEmpty { listOf(MeshBeaconConfig.BroadcastTarget()) } + stored.ifEmpty { listOf(MeshBeaconConfig.BroadcastTarget.Builder().build()) } /** * [MeshBeaconConfigScreen]'s actual `formState` initial value: [seedBeaconTargets] applied to the loaded config's @@ -152,7 +167,7 @@ internal fun seedBeaconTargets(stored: List): * [seedBeaconTargets] and only asserting against it in isolation. */ internal fun initialBeaconFormState(loaded: MeshBeaconConfig): MeshBeaconConfig = - loaded.copy(broadcast_targets = seedBeaconTargets(loaded.broadcast_targets)) + loaded.newBuilder().also { wb -> wb.broadcast_targets = seedBeaconTargets(loaded.broadcast_targets) }.build() /** * Removes the target row at [index], keeping the list at a floor of one row (design#140 behavior 6: "no UI that changes @@ -164,7 +179,7 @@ internal fun removeBeaconTarget( targets: List, index: Int, ): List = - targets.filterIndexed { i, _ -> i != index }.ifEmpty { listOf(MeshBeaconConfig.BroadcastTarget()) } + targets.filterIndexed { i, _ -> i != index }.ifEmpty { listOf(MeshBeaconConfig.BroadcastTarget.Builder().build()) } /** The three broadcast-half gating decisions design#140 Q1 hangs off `radioLora.use_preset` and the STORED flag. */ internal data class BeaconBroadcastGate( diff --git a/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/component/NeighborInfoConfigItemList.kt b/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/component/NeighborInfoConfigItemList.kt index 841e2fdaab..5a27efa87d 100644 --- a/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/component/NeighborInfoConfigItemList.kt +++ b/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/component/NeighborInfoConfigItemList.kt @@ -41,7 +41,7 @@ import org.meshtastic.proto.ModuleConfig @Composable fun NeighborInfoConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> Unit) { val state by viewModel.radioConfigState.collectAsStateWithLifecycle() - val neighborInfoConfig = state.moduleConfig.neighbor_info ?: ModuleConfig.NeighborInfoConfig() + val neighborInfoConfig = state.moduleConfig.neighbor_info ?: ModuleConfig.NeighborInfoConfig.Builder().build() val formState = rememberConfigState(initialValue = neighborInfoConfig) val focusManager = LocalFocusManager.current @@ -54,7 +54,7 @@ fun NeighborInfoConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> Unit responseState = state.responseState, onDismissPacketResponse = viewModel::clearPacketResponse, onSave = { - val config = ModuleConfig(neighbor_info = it) + val config = ModuleConfig.Builder().also { wb -> wb.neighbor_info = it }.build() viewModel.setModuleConfig(config) }, ) { @@ -64,7 +64,9 @@ fun NeighborInfoConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> Unit title = stringResource(Res.string.neighbor_info_enabled), checked = formState.value.enabled, enabled = state.connected, - onCheckedChange = { formState.value = formState.value.copy(enabled = it) }, + onCheckedChange = { + formState.value = formState.value.newBuilder().also { wb -> wb.enabled = it }.build() + }, containerColor = CardDefaults.cardColors().containerColor, ) HorizontalDivider() @@ -73,7 +75,9 @@ fun NeighborInfoConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> Unit value = formState.value.update_interval, enabled = state.connected, keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }), - onValueChanged = { formState.value = formState.value.copy(update_interval = it) }, + onValueChanged = { + formState.value = formState.value.newBuilder().also { wb -> wb.update_interval = it }.build() + }, ) HorizontalDivider() SwitchPreference( @@ -81,7 +85,9 @@ fun NeighborInfoConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> Unit summary = stringResource(Res.string.config_device_transmitOverLora_summary), checked = formState.value.transmit_over_lora, enabled = state.connected, - onCheckedChange = { formState.value = formState.value.copy(transmit_over_lora = it) }, + onCheckedChange = { + formState.value = formState.value.newBuilder().also { wb -> wb.transmit_over_lora = it }.build() + }, containerColor = CardDefaults.cardColors().containerColor, ) } diff --git a/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/component/NetworkConfigItemList.kt b/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/component/NetworkConfigItemList.kt index 1ae65715d5..1bc285d3f8 100644 --- a/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/component/NetworkConfigItemList.kt +++ b/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/component/NetworkConfigItemList.kt @@ -23,7 +23,6 @@ import androidx.compose.foundation.text.KeyboardActions import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.material3.Button import androidx.compose.material3.ButtonDefaults -import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Text import androidx.compose.runtime.Composable @@ -99,7 +98,7 @@ private fun formatIpAddress(ipAddress: Int): String = "${(ipAddress) and 0xFF}." @Composable fun NetworkConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> Unit, onOpenNfcSettings: () -> Unit = {}) { val state by viewModel.radioConfigState.collectAsStateWithLifecycle() - val networkConfig = state.radioConfig.network ?: Config.NetworkConfig() + val networkConfig = state.radioConfig.network ?: Config.NetworkConfig.Builder().build() val formState = rememberConfigState(initialValue = networkConfig) var showScanErrorDialog: Boolean by rememberSaveable { mutableStateOf(false) } @@ -134,7 +133,14 @@ fun NetworkConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> Unit, onO if (!handled) { val (ssid, psk) = extractWifiCredentials(contents) if (ssid != null && psk != null) { - formState.value = formState.value.copy(wifi_ssid = ssid, wifi_psk = psk) + formState.value = + formState.value + .newBuilder() + .also { wb -> + wb.wifi_ssid = ssid + wb.wifi_psk = psk + } + .build() } else { showScanErrorDialog = true } @@ -158,7 +164,7 @@ fun NetworkConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> Unit, onO responseState = state.responseState, onDismissPacketResponse = viewModel::clearPacketResponse, onSave = { - val config = Config(network = it) + val config = Config.Builder().also { wb -> wb.network = it }.build() viewModel.setConfig(config) }, ) { @@ -198,7 +204,9 @@ fun NetworkConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> Unit, onO title = stringResource(Res.string.wifi_enabled), summary = stringResource(Res.string.config_network_wifi_enabled_summary), checked = formState.value.wifi_enabled, - onCheckedChange = { formState.value = formState.value.copy(wifi_enabled = it) }, + onCheckedChange = { + formState.value = formState.value.newBuilder().also { wb -> wb.wifi_enabled = it }.build() + }, enabled = state.connected, ) if (formState.value.wifi_enabled) { @@ -212,7 +220,9 @@ fun NetworkConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> Unit, onO keyboardOptions = KeyboardOptions.Default.copy(keyboardType = KeyboardType.Text, imeAction = ImeAction.Done), keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }), - onValueChanged = { formState.value = formState.value.copy(wifi_ssid = it) }, + onValueChanged = { + formState.value = formState.value.newBuilder().also { wb -> wb.wifi_ssid = it }.build() + }, ) HorizontalDivider() EditPasswordPreference( @@ -221,12 +231,12 @@ fun NetworkConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> Unit, onO maxSize = 64, // wifi_psk max_size:65 enabled = state.connected, keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }), - onValueChanged = { formState.value = formState.value.copy(wifi_psk = it) }, + onValueChanged = { + formState.value = formState.value.newBuilder().also { wb -> wb.wifi_psk = it }.build() + }, ) HorizontalDivider() - @OptIn(ExperimentalMaterial3ExpressiveApi::class) val mediumHeight = ButtonDefaults.MediumContainerHeight - @OptIn(ExperimentalMaterial3ExpressiveApi::class) Button( onClick = { barcodeScanner.startScan() }, shapes = ButtonDefaults.shapesFor(mediumHeight), @@ -248,7 +258,9 @@ fun NetworkConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> Unit, onO title = stringResource(Res.string.ethernet_enabled), summary = stringResource(Res.string.config_network_eth_enabled_summary), checked = formState.value.eth_enabled, - onCheckedChange = { formState.value = formState.value.copy(eth_enabled = it) }, + onCheckedChange = { + formState.value = formState.value.newBuilder().also { wb -> wb.eth_enabled = it }.build() + }, enabled = state.connected, ) } @@ -265,7 +277,9 @@ fun NetworkConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> Unit, onO keyboardOptions = KeyboardOptions.Default.copy(keyboardType = KeyboardType.Uri, imeAction = ImeAction.Done), keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }), - onValueChanged = { formState.value = formState.value.copy(ntp_server = it) }, + onValueChanged = { + formState.value = formState.value.newBuilder().also { wb -> wb.ntp_server = it }.build() + }, ) HorizontalDivider() EditTextPreference( @@ -277,7 +291,9 @@ fun NetworkConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> Unit, onO keyboardOptions = KeyboardOptions.Default.copy(keyboardType = KeyboardType.Uri, imeAction = ImeAction.Done), keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }), - onValueChanged = { formState.value = formState.value.copy(rsyslog_server = it) }, + onValueChanged = { + formState.value = formState.value.newBuilder().also { wb -> wb.rsyslog_server = it }.build() + }, ) HorizontalDivider() SwitchPreference( @@ -295,7 +311,8 @@ fun NetworkConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> Unit, onO formState.value.enabled_protocols and Config.NetworkConfig.ProtocolFlags.UDP_BROADCAST.value.inv() } - formState.value = formState.value.copy(enabled_protocols = flags) + formState.value = + formState.value.newBuilder().also { wb -> wb.enabled_protocols = flags }.build() }, enabled = state.connected, ) @@ -304,17 +321,25 @@ fun NetworkConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> Unit, onO title = stringResource(Res.string.ipv4_mode), enabled = state.connected, selectedItem = formState.value.address_mode, - onItemSelected = { formState.value = formState.value.copy(address_mode = it) }, + onItemSelected = { + formState.value = formState.value.newBuilder().also { wb -> wb.address_mode = it }.build() + }, itemLabel = { it.name }, ) if (formState.value.address_mode == Config.NetworkConfig.AddressMode.STATIC) { HorizontalDivider() - val ipv4 = formState.value.ipv4_config ?: Config.NetworkConfig.IpV4Config() + val ipv4 = formState.value.ipv4_config ?: Config.NetworkConfig.IpV4Config.Builder().build() EditIPv4Preference( title = stringResource(Res.string.wifi_ip), value = ipv4.ip, enabled = state.connected, - onValueChanged = { formState.value = formState.value.copy(ipv4_config = ipv4.copy(ip = it)) }, + onValueChanged = { + formState.value = + formState.value + .newBuilder() + .also { wb -> wb.ipv4_config = ipv4.newBuilder().also { wb -> wb.ip = it }.build() } + .build() + }, keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }), ) HorizontalDivider() @@ -323,7 +348,13 @@ fun NetworkConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> Unit, onO value = ipv4.gateway, enabled = state.connected, onValueChanged = { - formState.value = formState.value.copy(ipv4_config = ipv4.copy(gateway = it)) + formState.value = + formState.value + .newBuilder() + .also { wb -> + wb.ipv4_config = ipv4.newBuilder().also { wb -> wb.gateway = it }.build() + } + .build() }, keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }), ) @@ -333,7 +364,13 @@ fun NetworkConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> Unit, onO value = ipv4.subnet, enabled = state.connected, onValueChanged = { - formState.value = formState.value.copy(ipv4_config = ipv4.copy(subnet = it)) + formState.value = + formState.value + .newBuilder() + .also { wb -> + wb.ipv4_config = ipv4.newBuilder().also { wb -> wb.subnet = it }.build() + } + .build() }, keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }), ) @@ -342,7 +379,15 @@ fun NetworkConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> Unit, onO title = stringResource(Res.string.dns), value = ipv4.dns, enabled = state.connected, - onValueChanged = { formState.value = formState.value.copy(ipv4_config = ipv4.copy(dns = it)) }, + onValueChanged = { + formState.value = + formState.value + .newBuilder() + .also { wb -> + wb.ipv4_config = ipv4.newBuilder().also { wb -> wb.dns = it }.build() + } + .build() + }, keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }), ) } diff --git a/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/component/NodeActionButton.kt b/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/component/NodeActionButton.kt index b895178662..0943908ad9 100644 --- a/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/component/NodeActionButton.kt +++ b/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/component/NodeActionButton.kt @@ -25,7 +25,6 @@ import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width import androidx.compose.material3.Button import androidx.compose.material3.ButtonDefaults -import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi import androidx.compose.material3.Icon import androidx.compose.material3.LocalContentColor import androidx.compose.material3.Text @@ -45,9 +44,7 @@ fun NodeActionButton( iconTint: Color? = null, onClick: () -> Unit, ) { - @OptIn(ExperimentalMaterial3ExpressiveApi::class) val mediumHeight = ButtonDefaults.MediumContainerHeight - @OptIn(ExperimentalMaterial3ExpressiveApi::class) Button( onClick = { onClick() }, shapes = ButtonDefaults.shapesFor(mediumHeight), diff --git a/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/component/PacketAuthenticityPreviews.kt b/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/component/PacketAuthenticityPreviews.kt index 578eee42be..cc5f27152a 100644 --- a/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/component/PacketAuthenticityPreviews.kt +++ b/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/component/PacketAuthenticityPreviews.kt @@ -30,7 +30,7 @@ import org.meshtastic.proto.Config @PreviewLightDark @Composable fun PacketAuthenticityDefaultPreview() { - PacketAuthenticitySettingPreview(Config.SecurityConfig().packet_signature_policy) + PacketAuthenticitySettingPreview(Config.SecurityConfig.Builder().build().packet_signature_policy) } @PreviewLightDark diff --git a/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/component/PaxcounterConfigItemList.kt b/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/component/PaxcounterConfigItemList.kt index b754ef97a2..7d8e0b4a90 100644 --- a/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/component/PaxcounterConfigItemList.kt +++ b/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/component/PaxcounterConfigItemList.kt @@ -45,7 +45,7 @@ import org.meshtastic.proto.ModuleConfig @Composable fun PaxcounterConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> Unit) { val state by viewModel.radioConfigState.collectAsStateWithLifecycle() - val paxcounterConfig = state.moduleConfig.paxcounter ?: ModuleConfig.PaxcounterConfig() + val paxcounterConfig = state.moduleConfig.paxcounter ?: ModuleConfig.PaxcounterConfig.Builder().build() val formState = rememberConfigState(initialValue = paxcounterConfig) val focusManager = LocalFocusManager.current @@ -58,7 +58,7 @@ fun PaxcounterConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> Unit) responseState = state.responseState, onDismissPacketResponse = viewModel::clearPacketResponse, onSave = { - val config = ModuleConfig(paxcounter = it) + val config = ModuleConfig.Builder().also { wb -> wb.paxcounter = it }.build() viewModel.setModuleConfig(config) }, ) { @@ -68,7 +68,9 @@ fun PaxcounterConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> Unit) title = stringResource(Res.string.paxcounter_enabled), checked = formState.value.enabled, enabled = state.connected, - onCheckedChange = { formState.value = formState.value.copy(enabled = it) }, + onCheckedChange = { + formState.value = formState.value.newBuilder().also { wb -> wb.enabled = it }.build() + }, containerColor = CardDefaults.cardColors().containerColor, ) HorizontalDivider() @@ -79,7 +81,11 @@ fun PaxcounterConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> Unit) enabled = state.connected, items = items.map { it.value to it.toDisplayString() }, onItemSelected = { - formState.value = formState.value.copy(paxcounter_update_interval = it.toInt()) + formState.value = + formState.value + .newBuilder() + .also { wb -> wb.paxcounter_update_interval = it.toInt() } + .build() }, ) HorizontalDivider() @@ -88,7 +94,9 @@ fun PaxcounterConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> Unit) value = formState.value.wifi_threshold, enabled = state.connected, keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }), - onValueChanged = { formState.value = formState.value.copy(wifi_threshold = it) }, + onValueChanged = { + formState.value = formState.value.newBuilder().also { wb -> wb.wifi_threshold = it }.build() + }, ) HorizontalDivider() SignedIntegerEditTextPreference( @@ -96,7 +104,9 @@ fun PaxcounterConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> Unit) value = formState.value.ble_threshold, enabled = state.connected, keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }), - onValueChanged = { formState.value = formState.value.copy(ble_threshold = it) }, + onValueChanged = { + formState.value = formState.value.newBuilder().also { wb -> wb.ble_threshold = it }.build() + }, ) } } diff --git a/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/component/PositionConfigScreen.kt b/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/component/PositionConfigScreen.kt index 8db0f5170d..cbf408418c 100644 --- a/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/component/PositionConfigScreen.kt +++ b/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/component/PositionConfigScreen.kt @@ -113,21 +113,34 @@ fun PositionConfigScreenCommon(viewModel: RadioConfigViewModel, onBack: () -> Un altitude = node?.position?.altitude ?: 0, time = 1, // ignore time for fixed_position ) - val positionConfig = state.radioConfig.position ?: Config.PositionConfig() + val positionConfig = state.radioConfig.position ?: Config.PositionConfig.Builder().build() val sanitizedPositionConfig = remember(positionConfig) { val positionItems = IntervalConfiguration.POSITION.allowedIntervals val smartBroadcastItems = IntervalConfiguration.SMART_BROADCAST_MINIMUM.allowedIntervals var updated = positionConfig if (FixedUpdateIntervals.fromValue(updated.position_broadcast_secs.toLong()) == null) { - updated = updated.copy(position_broadcast_secs = positionItems.first().value.toInt()) + updated = + updated + .newBuilder() + .also { wb -> wb.position_broadcast_secs = positionItems.first().value.toInt() } + .build() } if (FixedUpdateIntervals.fromValue(updated.broadcast_smart_minimum_interval_secs.toLong()) == null) { updated = - updated.copy(broadcast_smart_minimum_interval_secs = smartBroadcastItems.first().value.toInt()) + updated + .newBuilder() + .also { wb -> + wb.broadcast_smart_minimum_interval_secs = smartBroadcastItems.first().value.toInt() + } + .build() } if (FixedUpdateIntervals.fromValue(updated.gps_update_interval.toLong()) == null) { - updated = updated.copy(gps_update_interval = positionItems.first().value.toInt()) + updated = + updated + .newBuilder() + .also { wb -> wb.gps_update_interval = positionItems.first().value.toInt() } + .build() } updated } @@ -158,7 +171,7 @@ fun PositionConfigScreenCommon(viewModel: RadioConfigViewModel, onBack: () -> Un viewModel.removeFixedPosition() } } - val config = Config(position = it) + val config = Config.Builder().also { wb -> wb.position = it }.build() viewModel.setConfig(config) }, ) { @@ -174,7 +187,11 @@ fun PositionConfigScreenCommon(viewModel: RadioConfigViewModel, onBack: () -> Un FixedUpdateIntervals.fromValue(formState.value.position_broadcast_secs.toLong()) ?: items.first(), onItemSelected = { - formState.value = formState.value.copy(position_broadcast_secs = it.value.toInt()) + formState.value = + formState.value + .newBuilder() + .also { wb -> wb.position_broadcast_secs = it.value.toInt() } + .build() }, ) HorizontalDivider() @@ -182,7 +199,10 @@ fun PositionConfigScreenCommon(viewModel: RadioConfigViewModel, onBack: () -> Un title = stringResource(Res.string.smart_position), checked = formState.value.position_broadcast_smart_enabled, enabled = state.connected, - onCheckedChange = { formState.value = formState.value.copy(position_broadcast_smart_enabled = it) }, + onCheckedChange = { + formState.value = + formState.value.newBuilder().also { wb -> wb.position_broadcast_smart_enabled = it }.build() + }, containerColor = CardDefaults.cardColors().containerColor, ) if (formState.value.position_broadcast_smart_enabled) { @@ -200,7 +220,10 @@ fun PositionConfigScreenCommon(viewModel: RadioConfigViewModel, onBack: () -> Un ) ?: smartItems.first(), onItemSelected = { formState.value = - formState.value.copy(broadcast_smart_minimum_interval_secs = it.value.toInt()) + formState.value + .newBuilder() + .also { wb -> wb.broadcast_smart_minimum_interval_secs = it.value.toInt() } + .build() }, ) HorizontalDivider() @@ -211,7 +234,11 @@ fun PositionConfigScreenCommon(viewModel: RadioConfigViewModel, onBack: () -> Un enabled = state.connected, keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }), onValueChanged = { - formState.value = formState.value.copy(broadcast_smart_minimum_distance = it) + formState.value = + formState.value + .newBuilder() + .also { wb -> wb.broadcast_smart_minimum_distance = it } + .build() }, ) } @@ -223,7 +250,9 @@ fun PositionConfigScreenCommon(viewModel: RadioConfigViewModel, onBack: () -> Un title = stringResource(Res.string.fixed_position), checked = formState.value.fixed_position, enabled = state.connected, - onCheckedChange = { formState.value = formState.value.copy(fixed_position = it) }, + onCheckedChange = { + formState.value = formState.value.newBuilder().also { wb -> wb.fixed_position = it }.build() + }, containerColor = CardDefaults.cardColors().containerColor, ) if (formState.value.fixed_position) { @@ -272,7 +301,9 @@ fun PositionConfigScreenCommon(viewModel: RadioConfigViewModel, onBack: () -> Un enabled = state.connected, items = Config.PositionConfig.GpsMode.entries.map { it to it.name }, selectedItem = formState.value.gps_mode, - onItemSelected = { formState.value = formState.value.copy(gps_mode = it) }, + onItemSelected = { + formState.value = formState.value.newBuilder().also { wb -> wb.gps_mode = it }.build() + }, ) HorizontalDivider() val items = remember { IntervalConfiguration.GPS_UPDATE.allowedIntervals } @@ -285,7 +316,11 @@ fun PositionConfigScreenCommon(viewModel: RadioConfigViewModel, onBack: () -> Un FixedUpdateIntervals.fromValue(formState.value.gps_update_interval.toLong()) ?: items.first(), onItemSelected = { - formState.value = formState.value.copy(gps_update_interval = it.value.toInt()) + formState.value = + formState.value + .newBuilder() + .also { wb -> wb.gps_update_interval = it.value.toInt() } + .build() }, ) } @@ -302,7 +337,9 @@ fun PositionConfigScreenCommon(viewModel: RadioConfigViewModel, onBack: () -> Un Config.PositionConfig.PositionFlags.entries .filter { it != Config.PositionConfig.PositionFlags.UNSET } .map { it.value to it.name }, - onItemSelected = { formState.value = formState.value.copy(position_flags = it) }, + onItemSelected = { + formState.value = formState.value.newBuilder().also { wb -> wb.position_flags = it }.build() + }, ) } } @@ -314,7 +351,9 @@ fun PositionConfigScreenCommon(viewModel: RadioConfigViewModel, onBack: () -> Un enabled = state.connected, items = pins, selectedItem = formState.value.rx_gpio, - onItemSelected = { formState.value = formState.value.copy(rx_gpio = it) }, + onItemSelected = { + formState.value = formState.value.newBuilder().also { wb -> wb.rx_gpio = it }.build() + }, ) HorizontalDivider() DropDownPreference( @@ -322,7 +361,9 @@ fun PositionConfigScreenCommon(viewModel: RadioConfigViewModel, onBack: () -> Un enabled = state.connected, items = pins, selectedItem = formState.value.tx_gpio, - onItemSelected = { formState.value = formState.value.copy(tx_gpio = it) }, + onItemSelected = { + formState.value = formState.value.newBuilder().also { wb -> wb.tx_gpio = it }.build() + }, ) HorizontalDivider() DropDownPreference( @@ -330,7 +371,9 @@ fun PositionConfigScreenCommon(viewModel: RadioConfigViewModel, onBack: () -> Un enabled = state.connected, items = pins, selectedItem = formState.value.gps_en_gpio, - onItemSelected = { formState.value = formState.value.copy(gps_en_gpio = it) }, + onItemSelected = { + formState.value = formState.value.newBuilder().also { wb -> wb.gps_en_gpio = it }.build() + }, ) } } diff --git a/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/component/PowerConfigItemList.kt b/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/component/PowerConfigItemList.kt index 56e6e9b189..72393fa662 100644 --- a/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/component/PowerConfigItemList.kt +++ b/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/component/PowerConfigItemList.kt @@ -49,7 +49,7 @@ import org.meshtastic.proto.Config @Composable fun PowerConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> Unit) { val state by viewModel.radioConfigState.collectAsStateWithLifecycle() - val powerConfig = state.radioConfig.power ?: Config.PowerConfig() + val powerConfig = state.radioConfig.power ?: Config.PowerConfig.Builder().build() val formState = rememberConfigState(initialValue = powerConfig) val focusManager = LocalFocusManager.current @@ -61,7 +61,7 @@ fun PowerConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> Unit) { responseState = state.responseState, onDismissPacketResponse = viewModel::clearPacketResponse, onSave = { - val config = Config(power = it) + val config = Config.Builder().also { wb -> wb.power = it }.build() viewModel.setConfig(config) }, ) { @@ -72,7 +72,9 @@ fun PowerConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> Unit) { summary = stringResource(Res.string.config_power_is_power_saving_summary), checked = formState.value.is_power_saving, enabled = state.connected, - onCheckedChange = { formState.value = formState.value.copy(is_power_saving = it) }, + onCheckedChange = { + formState.value = formState.value.newBuilder().also { wb -> wb.is_power_saving = it }.build() + }, containerColor = CardDefaults.cardColors().containerColor, ) HorizontalDivider() @@ -83,7 +85,11 @@ fun PowerConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> Unit) { enabled = state.connected, items = items.map { it.value to it.toDisplayString() }, onItemSelected = { - formState.value = formState.value.copy(on_battery_shutdown_after_secs = it.toInt()) + formState.value = + formState.value + .newBuilder() + .also { wb -> wb.on_battery_shutdown_after_secs = it.toInt() } + .build() }, ) HorizontalDivider() @@ -92,7 +98,11 @@ fun PowerConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> Unit) { checked = formState.value.adc_multiplier_override > 0f, enabled = state.connected, onCheckedChange = { - formState.value = formState.value.copy(adc_multiplier_override = if (it) 1.0f else 0.0f) + formState.value = + formState.value + .newBuilder() + .also { wb -> wb.adc_multiplier_override = if (it) 1.0f else 0.0f } + .build() }, containerColor = CardDefaults.cardColors().containerColor, ) @@ -103,7 +113,10 @@ fun PowerConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> Unit) { value = formState.value.adc_multiplier_override, enabled = state.connected, keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }), - onValueChanged = { formState.value = formState.value.copy(adc_multiplier_override = it) }, + onValueChanged = { + formState.value = + formState.value.newBuilder().also { wb -> wb.adc_multiplier_override = it }.build() + }, ) } HorizontalDivider() @@ -113,14 +126,19 @@ fun PowerConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> Unit) { selectedItem = formState.value.wait_bluetooth_secs.toLong(), enabled = state.connected, items = waitBluetoothItems.map { it.value to it.toDisplayString() }, - onItemSelected = { formState.value = formState.value.copy(wait_bluetooth_secs = it.toInt()) }, + onItemSelected = { + formState.value = + formState.value.newBuilder().also { wb -> wb.wait_bluetooth_secs = it.toInt() }.build() + }, ) HorizontalDivider() val sdsSecsItems = remember { IntervalConfiguration.ALL.allowedIntervals } DropDownPreference( title = stringResource(Res.string.super_deep_sleep_duration_seconds), selectedItem = formState.value.sds_secs.toLong(), - onItemSelected = { formState.value = formState.value.copy(sds_secs = it.toInt()) }, + onItemSelected = { + formState.value = formState.value.newBuilder().also { wb -> wb.sds_secs = it.toInt() }.build() + }, enabled = state.connected, items = sdsSecsItems.map { it.value to it.toDisplayString() }, ) @@ -131,7 +149,10 @@ fun PowerConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> Unit) { selectedItem = formState.value.min_wake_secs.toLong(), enabled = state.connected, items = minWakeItems.map { it.value to it.toDisplayString() }, - onItemSelected = { formState.value = formState.value.copy(min_wake_secs = it.toInt()) }, + onItemSelected = { + formState.value = + formState.value.newBuilder().also { wb -> wb.min_wake_secs = it.toInt() }.build() + }, ) HorizontalDivider() EditTextPreference( @@ -139,7 +160,10 @@ fun PowerConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> Unit) { value = formState.value.device_battery_ina_address, enabled = state.connected, keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }), - onValueChanged = { formState.value = formState.value.copy(device_battery_ina_address = it) }, + onValueChanged = { + formState.value = + formState.value.newBuilder().also { wb -> wb.device_battery_ina_address = it }.build() + }, ) } } diff --git a/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/component/RangeTestConfigItemList.kt b/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/component/RangeTestConfigItemList.kt index 174ce63b4f..bb3b521af7 100644 --- a/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/component/RangeTestConfigItemList.kt +++ b/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/component/RangeTestConfigItemList.kt @@ -41,7 +41,7 @@ import org.meshtastic.proto.ModuleConfig @Composable fun RangeTestConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> Unit) { val state by viewModel.radioConfigState.collectAsStateWithLifecycle() - val rangeTestConfig = state.moduleConfig.range_test ?: ModuleConfig.RangeTestConfig() + val rangeTestConfig = state.moduleConfig.range_test ?: ModuleConfig.RangeTestConfig.Builder().build() val formState = rememberConfigState(initialValue = rangeTestConfig) val isPublicPrimaryChannel = (state.channelList.firstOrNull()?.psk?.size ?: 0) < 2 @@ -56,8 +56,9 @@ fun RangeTestConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> Unit) { responseState = state.responseState, onDismissPacketResponse = viewModel::clearPacketResponse, onSave = { - val safeConfig = if (isPublicPrimaryChannel) it.copy(enabled = false) else it - val config = ModuleConfig(range_test = safeConfig) + val safeConfig = + if (isPublicPrimaryChannel) it.newBuilder().also { wb -> wb.enabled = false }.build() else it + val config = ModuleConfig.Builder().also { wb -> wb.range_test = safeConfig }.build() viewModel.setModuleConfig(config) }, ) { @@ -67,7 +68,9 @@ fun RangeTestConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> Unit) { title = stringResource(Res.string.range_test_enabled), checked = formState.value.enabled, enabled = canConfigure || formState.value.enabled, - onCheckedChange = { formState.value = formState.value.copy(enabled = it) }, + onCheckedChange = { + formState.value = formState.value.newBuilder().also { wb -> wb.enabled = it }.build() + }, containerColor = CardDefaults.cardColors().containerColor, ) HorizontalDivider() @@ -77,14 +80,18 @@ fun RangeTestConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> Unit) { selectedItem = (formState.value.sender).toLong(), enabled = canConfigure, items = rangeItems.map { it.value to it.toDisplayString() }, - onItemSelected = { formState.value = formState.value.copy(sender = it.toInt()) }, + onItemSelected = { + formState.value = formState.value.newBuilder().also { wb -> wb.sender = it.toInt() }.build() + }, ) HorizontalDivider() SwitchPreference( title = stringResource(Res.string.save_csv_in_storage_esp32_only), checked = formState.value.save, enabled = canConfigure, - onCheckedChange = { formState.value = formState.value.copy(save = it) }, + onCheckedChange = { + formState.value = formState.value.newBuilder().also { wb -> wb.save = it }.build() + }, containerColor = CardDefaults.cardColors().containerColor, ) } diff --git a/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/component/RemoteHardwareConfigItemList.kt b/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/component/RemoteHardwareConfigItemList.kt index a43a713851..e656d93483 100644 --- a/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/component/RemoteHardwareConfigItemList.kt +++ b/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/component/RemoteHardwareConfigItemList.kt @@ -40,7 +40,7 @@ import org.meshtastic.proto.ModuleConfig @Composable fun RemoteHardwareConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> Unit) { val state by viewModel.radioConfigState.collectAsStateWithLifecycle() - val remoteHardwareConfig = state.moduleConfig.remote_hardware ?: ModuleConfig.RemoteHardwareConfig() + val remoteHardwareConfig = state.moduleConfig.remote_hardware ?: ModuleConfig.RemoteHardwareConfig.Builder().build() val formState = rememberConfigState(initialValue = remoteHardwareConfig) val focusManager = LocalFocusManager.current @@ -53,7 +53,7 @@ fun RemoteHardwareConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> Un responseState = state.responseState, onDismissPacketResponse = viewModel::clearPacketResponse, onSave = { - val config = ModuleConfig(remote_hardware = it) + val config = ModuleConfig.Builder().also { wb -> wb.remote_hardware = it }.build() viewModel.setModuleConfig(config) }, ) { @@ -63,7 +63,9 @@ fun RemoteHardwareConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> Un title = stringResource(Res.string.remote_hardware_enabled), checked = formState.value.enabled, enabled = state.connected, - onCheckedChange = { formState.value = formState.value.copy(enabled = it) }, + onCheckedChange = { + formState.value = formState.value.newBuilder().also { wb -> wb.enabled = it }.build() + }, containerColor = CardDefaults.cardColors().containerColor, ) HorizontalDivider() @@ -71,7 +73,10 @@ fun RemoteHardwareConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> Un title = stringResource(Res.string.allow_undefined_pin_access), checked = formState.value.allow_undefined_pin_access, enabled = state.connected, - onCheckedChange = { formState.value = formState.value.copy(allow_undefined_pin_access = it) }, + onCheckedChange = { + formState.value = + formState.value.newBuilder().also { wb -> wb.allow_undefined_pin_access = it }.build() + }, containerColor = CardDefaults.cardColors().containerColor, ) HorizontalDivider() @@ -81,7 +86,9 @@ fun RemoteHardwareConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> Un maxCount = 4, // available_pins max_count:4 enabled = state.connected, keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }), - onValuesChanged = { list -> formState.value = formState.value.copy(available_pins = list) }, + onValuesChanged = { list -> + formState.value = formState.value.newBuilder().also { wb -> wb.available_pins = list }.build() + }, ) } } diff --git a/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/component/SecurityConfigScreen.kt b/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/component/SecurityConfigScreen.kt index 365e2b73d0..f01d61af65 100644 --- a/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/component/SecurityConfigScreen.kt +++ b/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/component/SecurityConfigScreen.kt @@ -85,7 +85,7 @@ fun SecurityConfigScreenCommon(viewModel: RadioConfigViewModel, onBack: () -> Un val state by viewModel.radioConfigState.collectAsStateWithLifecycle() val firmwareVersion = state.metadata?.firmware_version val capabilities = remember(firmwareVersion) { Capabilities(firmwareVersion) } - val securityConfig = state.radioConfig.security ?: Config.SecurityConfig() + val securityConfig = state.radioConfig.security ?: Config.SecurityConfig.Builder().build() val formState = rememberConfigState(initialValue = securityConfig) var showKeyGenerationDialog by rememberSaveable { mutableStateOf(false) } @@ -94,7 +94,7 @@ fun SecurityConfigScreenCommon(viewModel: RadioConfigViewModel, onBack: () -> Un onConfirm = { formState.value = it showKeyGenerationDialog = false - val config = Config(security = formState.value) + val config = Config.Builder().also { wb -> wb.security = formState.value }.build() viewModel.setConfig(config) }, onDismiss = { showKeyGenerationDialog = false }, @@ -110,7 +110,7 @@ fun SecurityConfigScreenCommon(viewModel: RadioConfigViewModel, onBack: () -> Un responseState = state.responseState, onDismissPacketResponse = viewModel::clearPacketResponse, onSave = { - val config = Config(security = it) + val config = Config.Builder().also { wb -> wb.security = it }.build() viewModel.setConfig(config) }, ) { @@ -119,7 +119,10 @@ fun SecurityConfigScreenCommon(viewModel: RadioConfigViewModel, onBack: () -> Un selectedPolicy = formState.value.packet_signature_policy, connected = state.connected, supported = state.metadata?.has_xeddsa, - onPolicyChange = { policy -> formState.value = formState.value.copy(packet_signature_policy = policy) }, + onPolicyChange = { policy -> + formState.value = + formState.value.newBuilder().also { wb -> wb.packet_signature_policy = policy }.build() + }, ) } item { @@ -164,7 +167,9 @@ fun SecurityConfigScreenCommon(viewModel: RadioConfigViewModel, onBack: () -> Un maxCount = 3, enabled = state.connected, keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }), - onValuesChanged = { formState.value = formState.value.copy(admin_key = it) }, + onValuesChanged = { + formState.value = formState.value.newBuilder().also { wb -> wb.admin_key = it }.build() + }, ) } } @@ -175,7 +180,9 @@ fun SecurityConfigScreenCommon(viewModel: RadioConfigViewModel, onBack: () -> Un summary = stringResource(Res.string.config_security_serial_enabled), checked = formState.value.serial_enabled, enabled = state.connected, - onCheckedChange = { formState.value = formState.value.copy(serial_enabled = it) }, + onCheckedChange = { + formState.value = formState.value.newBuilder().also { wb -> wb.serial_enabled = it }.build() + }, containerColor = CardDefaults.cardColors().containerColor, ) HorizontalDivider() @@ -184,7 +191,10 @@ fun SecurityConfigScreenCommon(viewModel: RadioConfigViewModel, onBack: () -> Un summary = stringResource(Res.string.config_security_debug_log_api_enabled), checked = formState.value.debug_log_api_enabled, enabled = state.connected, - onCheckedChange = { formState.value = formState.value.copy(debug_log_api_enabled = it) }, + onCheckedChange = { + formState.value = + formState.value.newBuilder().also { wb -> wb.debug_log_api_enabled = it }.build() + }, containerColor = CardDefaults.cardColors().containerColor, ) } @@ -196,7 +206,9 @@ fun SecurityConfigScreenCommon(viewModel: RadioConfigViewModel, onBack: () -> Un summary = stringResource(Res.string.config_security_is_managed), checked = formState.value.is_managed, enabled = state.connected && formState.value.admin_key.isNotEmpty(), - onCheckedChange = { formState.value = formState.value.copy(is_managed = it) }, + onCheckedChange = { + formState.value = formState.value.newBuilder().also { wb -> wb.is_managed = it }.build() + }, containerColor = CardDefaults.cardColors().containerColor, ) HorizontalDivider() @@ -259,7 +271,7 @@ internal fun SecurityPublicKeyPreference( keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }), onValueChange = { if (it.size == 32) { - formState.value = formState.value.copy(public_key = it) + formState.value = formState.value.newBuilder().also { wb -> wb.public_key = it }.build() } }, trailingIcon = { publicKeyCopyButton(publicKey) }, @@ -298,7 +310,7 @@ internal fun SecurityPrivateKeyPreference( keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }), onValueChange = { if (it.size == PRIVATE_KEY_SIZE) { - formState.value = formState.value.copy(private_key = it) + formState.value = formState.value.newBuilder().also { wb -> wb.private_key = it }.build() } }, trailingIcon = @@ -332,7 +344,13 @@ fun PrivateKeyRegenerateDialog( // and set the second to left-most bit of f[31]. f[0] = (f[0].toInt() and 0xF8).toByte() f[31] = ((f[31].toInt() and 0x7F) or 0x40).toByte() - val securityInput = Config.SecurityConfig(private_key = f.toByteString(), public_key = ByteString.EMPTY) + val securityInput = + Config.SecurityConfig.Builder() + .also { wb -> + wb.private_key = f.toByteString() + wb.public_key = ByteString.EMPTY + } + .build() onConfirm(securityInput) }, ) diff --git a/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/component/SerialConfigItemList.kt b/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/component/SerialConfigItemList.kt index 2c9c8f6b0c..28d68aee5b 100644 --- a/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/component/SerialConfigItemList.kt +++ b/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/component/SerialConfigItemList.kt @@ -46,7 +46,7 @@ import org.meshtastic.proto.ModuleConfig @Composable fun SerialConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> Unit) { val state by viewModel.radioConfigState.collectAsStateWithLifecycle() - val serialConfig = state.moduleConfig.serial ?: ModuleConfig.SerialConfig() + val serialConfig = state.moduleConfig.serial ?: ModuleConfig.SerialConfig.Builder().build() val formState = rememberConfigState(initialValue = serialConfig) val focusManager = LocalFocusManager.current @@ -59,7 +59,7 @@ fun SerialConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> Unit) { responseState = state.responseState, onDismissPacketResponse = viewModel::clearPacketResponse, onSave = { - val config = ModuleConfig(serial = it) + val config = ModuleConfig.Builder().also { wb -> wb.serial = it }.build() viewModel.setModuleConfig(config) }, ) { @@ -69,7 +69,9 @@ fun SerialConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> Unit) { title = stringResource(Res.string.serial_enabled), checked = formState.value.enabled, enabled = state.connected, - onCheckedChange = { formState.value = formState.value.copy(enabled = it) }, + onCheckedChange = { + formState.value = formState.value.newBuilder().also { wb -> wb.enabled = it }.build() + }, containerColor = CardDefaults.cardColors().containerColor, ) HorizontalDivider() @@ -77,7 +79,9 @@ fun SerialConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> Unit) { title = stringResource(Res.string.echo_enabled), checked = formState.value.echo, enabled = state.connected, - onCheckedChange = { formState.value = formState.value.copy(echo = it) }, + onCheckedChange = { + formState.value = formState.value.newBuilder().also { wb -> wb.echo = it }.build() + }, containerColor = CardDefaults.cardColors().containerColor, ) HorizontalDivider() @@ -86,7 +90,9 @@ fun SerialConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> Unit) { value = formState.value.rxd, enabled = state.connected, keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }), - onValueChanged = { formState.value = formState.value.copy(rxd = it) }, + onValueChanged = { + formState.value = formState.value.newBuilder().also { wb -> wb.rxd = it }.build() + }, ) HorizontalDivider() EditTextPreference( @@ -94,7 +100,9 @@ fun SerialConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> Unit) { value = formState.value.txd, enabled = state.connected, keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }), - onValueChanged = { formState.value = formState.value.copy(txd = it) }, + onValueChanged = { + formState.value = formState.value.newBuilder().also { wb -> wb.txd = it }.build() + }, ) HorizontalDivider() DropDownPreference( @@ -102,7 +110,9 @@ fun SerialConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> Unit) { enabled = state.connected, items = ModuleConfig.SerialConfig.Serial_Baud.entries.map { it to it.name }, selectedItem = formState.value.baud, - onItemSelected = { formState.value = formState.value.copy(baud = it) }, + onItemSelected = { + formState.value = formState.value.newBuilder().also { wb -> wb.baud = it }.build() + }, ) HorizontalDivider() EditTextPreference( @@ -110,7 +120,9 @@ fun SerialConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> Unit) { value = formState.value.timeout, enabled = state.connected, keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }), - onValueChanged = { formState.value = formState.value.copy(timeout = it) }, + onValueChanged = { + formState.value = formState.value.newBuilder().also { wb -> wb.timeout = it }.build() + }, ) HorizontalDivider() DropDownPreference( @@ -118,14 +130,19 @@ fun SerialConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> Unit) { enabled = state.connected, items = ModuleConfig.SerialConfig.Serial_Mode.entries.map { it to it.name }, selectedItem = formState.value.mode, - onItemSelected = { formState.value = formState.value.copy(mode = it) }, + onItemSelected = { + formState.value = formState.value.newBuilder().also { wb -> wb.mode = it }.build() + }, ) HorizontalDivider() SwitchPreference( title = stringResource(Res.string.override_console_serial_port), checked = formState.value.override_console_serial_port, enabled = state.connected, - onCheckedChange = { formState.value = formState.value.copy(override_console_serial_port = it) }, + onCheckedChange = { + formState.value = + formState.value.newBuilder().also { wb -> wb.override_console_serial_port = it }.build() + }, containerColor = CardDefaults.cardColors().containerColor, ) } diff --git a/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/component/StoreForwardConfigItemList.kt b/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/component/StoreForwardConfigItemList.kt index 606e16429a..66aa49c096 100644 --- a/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/component/StoreForwardConfigItemList.kt +++ b/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/component/StoreForwardConfigItemList.kt @@ -43,7 +43,7 @@ import org.meshtastic.proto.ModuleConfig @Composable fun StoreForwardConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> Unit) { val state by viewModel.radioConfigState.collectAsStateWithLifecycle() - val storeForwardConfig = state.moduleConfig.store_forward ?: ModuleConfig.StoreForwardConfig() + val storeForwardConfig = state.moduleConfig.store_forward ?: ModuleConfig.StoreForwardConfig.Builder().build() val formState = rememberConfigState(initialValue = storeForwardConfig) val focusManager = LocalFocusManager.current @@ -56,7 +56,7 @@ fun StoreForwardConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> Unit responseState = state.responseState, onDismissPacketResponse = viewModel::clearPacketResponse, onSave = { - val config = ModuleConfig(store_forward = it) + val config = ModuleConfig.Builder().also { wb -> wb.store_forward = it }.build() viewModel.setModuleConfig(config) }, ) { @@ -66,7 +66,9 @@ fun StoreForwardConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> Unit title = stringResource(Res.string.store_forward_enabled), checked = formState.value.enabled, enabled = state.connected, - onCheckedChange = { formState.value = formState.value.copy(enabled = it) }, + onCheckedChange = { + formState.value = formState.value.newBuilder().also { wb -> wb.enabled = it }.build() + }, containerColor = CardDefaults.cardColors().containerColor, ) HorizontalDivider() @@ -74,7 +76,9 @@ fun StoreForwardConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> Unit title = stringResource(Res.string.heartbeat), checked = formState.value.heartbeat, enabled = state.connected, - onCheckedChange = { formState.value = formState.value.copy(heartbeat = it) }, + onCheckedChange = { + formState.value = formState.value.newBuilder().also { wb -> wb.heartbeat = it }.build() + }, containerColor = CardDefaults.cardColors().containerColor, ) HorizontalDivider() @@ -83,7 +87,9 @@ fun StoreForwardConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> Unit value = formState.value.records, enabled = state.connected, keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }), - onValueChanged = { formState.value = formState.value.copy(records = it) }, + onValueChanged = { + formState.value = formState.value.newBuilder().also { wb -> wb.records = it }.build() + }, ) HorizontalDivider() EditTextPreference( @@ -91,7 +97,9 @@ fun StoreForwardConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> Unit value = formState.value.history_return_max, enabled = state.connected, keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }), - onValueChanged = { formState.value = formState.value.copy(history_return_max = it) }, + onValueChanged = { + formState.value = formState.value.newBuilder().also { wb -> wb.history_return_max = it }.build() + }, ) HorizontalDivider() EditTextPreference( @@ -99,14 +107,19 @@ fun StoreForwardConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> Unit value = formState.value.history_return_window, enabled = state.connected, keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }), - onValueChanged = { formState.value = formState.value.copy(history_return_window = it) }, + onValueChanged = { + formState.value = + formState.value.newBuilder().also { wb -> wb.history_return_window = it }.build() + }, ) HorizontalDivider() SwitchPreference( title = stringResource(Res.string.server), checked = formState.value.is_server, enabled = state.connected, - onCheckedChange = { formState.value = formState.value.copy(is_server = it) }, + onCheckedChange = { + formState.value = formState.value.newBuilder().also { wb -> wb.is_server = it }.build() + }, containerColor = CardDefaults.cardColors().containerColor, ) } diff --git a/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/component/TAKConfigItemList.kt b/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/component/TAKConfigItemList.kt index 6c0a2e78b8..7b806de83c 100644 --- a/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/component/TAKConfigItemList.kt +++ b/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/component/TAKConfigItemList.kt @@ -126,7 +126,7 @@ import org.meshtastic.proto.Team @Composable fun TAKConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> Unit) { val state by viewModel.radioConfigState.collectAsStateWithLifecycle() - val takConfig = state.moduleConfig.tak ?: ModuleConfig.TAKConfig() + val takConfig = state.moduleConfig.tak ?: ModuleConfig.TAKConfig.Builder().build() val formState = rememberConfigState(initialValue = takConfig) val effectiveResponseState = @@ -144,7 +144,7 @@ fun TAKConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> Unit) { responseState = effectiveResponseState, onDismissPacketResponse = viewModel::clearPacketResponse, onSave = { - val config = ModuleConfig(tak = it) + val config = ModuleConfig.Builder().also { wb -> wb.tak = it }.build() viewModel.setModuleConfig(config) }, ) { @@ -153,8 +153,8 @@ fun TAKConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> Unit) { team = formState.value.team, role = formState.value.role, enabled = state.connected, - onTeamSelected = { formState.value = formState.value.copy(team = it) }, - onRoleSelected = { formState.value = formState.value.copy(role = it) }, + onTeamSelected = { formState.value = formState.value.newBuilder().also { wb -> wb.team = it }.build() }, + onRoleSelected = { formState.value = formState.value.newBuilder().also { wb -> wb.role = it }.build() }, ) } } @@ -212,7 +212,8 @@ fun TakServerScreen(onBack: () -> Unit) { val isTakServerEnabled by takPrefs.isTakServerEnabled.collectAsStateWithLifecycle() val isMeshToCotEnabled by takPrefs.isMeshToCotEnabled.collectAsStateWithLifecycle() val takServerChannel by takPrefs.takServerChannel.collectAsStateWithLifecycle() - val channelSet by radioConfigRepository.channelSetFlow.collectAsStateWithLifecycle(initialValue = ChannelSet()) + val channelSet by + radioConfigRepository.channelSetFlow.collectAsStateWithLifecycle(initialValue = ChannelSet.Builder().build()) // Options come from the radio's configured channels; the persisted index is appended as a bare // number when it isn't among them (no radio connected yet, or the channel list shrank) so the // dropdown never renders an empty selection. diff --git a/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/component/TelemetryConfigItemList.kt b/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/component/TelemetryConfigItemList.kt index 75d4238a1c..13b85be7d7 100644 --- a/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/component/TelemetryConfigItemList.kt +++ b/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/component/TelemetryConfigItemList.kt @@ -51,7 +51,7 @@ import org.meshtastic.proto.ModuleConfig @Composable fun TelemetryConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> Unit) { val state by viewModel.radioConfigState.collectAsStateWithLifecycle() - val telemetryConfig = state.moduleConfig.telemetry ?: ModuleConfig.TelemetryConfig() + val telemetryConfig = state.moduleConfig.telemetry ?: ModuleConfig.TelemetryConfig.Builder().build() val formState = rememberConfigState(initialValue = telemetryConfig) val firmwareVersion = state.metadata?.firmware_version @@ -66,7 +66,7 @@ fun TelemetryConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> Unit) { responseState = state.responseState, onDismissPacketResponse = viewModel::clearPacketResponse, onSave = { - val config = ModuleConfig(telemetry = it) + val config = ModuleConfig.Builder().also { wb -> wb.telemetry = it }.build() viewModel.setModuleConfig(config) }, ) { @@ -78,7 +78,10 @@ fun TelemetryConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> Unit) { summary = stringResource(Res.string.device_telemetry_enabled_summary), checked = formState.value.device_telemetry_enabled, enabled = state.connected, - onCheckedChange = { formState.value = formState.value.copy(device_telemetry_enabled = it) }, + onCheckedChange = { + formState.value = + formState.value.newBuilder().also { wb -> wb.device_telemetry_enabled = it }.build() + }, containerColor = CardDefaults.cardColors().containerColor, ) HorizontalDivider() @@ -89,14 +92,20 @@ fun TelemetryConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> Unit) { selectedItem = formState.value.device_update_interval.toLong(), enabled = state.connected, items = items.map { it.value to it.toDisplayString() }, - onItemSelected = { formState.value = formState.value.copy(device_update_interval = it.toInt()) }, + onItemSelected = { + formState.value = + formState.value.newBuilder().also { wb -> wb.device_update_interval = it.toInt() }.build() + }, ) HorizontalDivider() SwitchPreference( title = stringResource(Res.string.environment_metrics_module_enabled), checked = formState.value.environment_measurement_enabled, enabled = state.connected, - onCheckedChange = { formState.value = formState.value.copy(environment_measurement_enabled = it) }, + onCheckedChange = { + formState.value = + formState.value.newBuilder().also { wb -> wb.environment_measurement_enabled = it }.build() + }, containerColor = CardDefaults.cardColors().containerColor, ) HorizontalDivider() @@ -107,7 +116,11 @@ fun TelemetryConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> Unit) { enabled = state.connected, items = envItems.map { it.value to it.toDisplayString() }, onItemSelected = { - formState.value = formState.value.copy(environment_update_interval = it.toInt()) + formState.value = + formState.value + .newBuilder() + .also { wb -> wb.environment_update_interval = it.toInt() } + .build() }, ) HorizontalDivider() @@ -115,7 +128,10 @@ fun TelemetryConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> Unit) { title = stringResource(Res.string.environment_metrics_on_screen_enabled), checked = formState.value.environment_screen_enabled, enabled = state.connected, - onCheckedChange = { formState.value = formState.value.copy(environment_screen_enabled = it) }, + onCheckedChange = { + formState.value = + formState.value.newBuilder().also { wb -> wb.environment_screen_enabled = it }.build() + }, containerColor = CardDefaults.cardColors().containerColor, ) HorizontalDivider() @@ -123,7 +139,10 @@ fun TelemetryConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> Unit) { title = stringResource(Res.string.environment_metrics_use_fahrenheit), checked = formState.value.environment_display_fahrenheit, enabled = state.connected, - onCheckedChange = { formState.value = formState.value.copy(environment_display_fahrenheit = it) }, + onCheckedChange = { + formState.value = + formState.value.newBuilder().also { wb -> wb.environment_display_fahrenheit = it }.build() + }, containerColor = CardDefaults.cardColors().containerColor, ) HorizontalDivider() @@ -131,7 +150,10 @@ fun TelemetryConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> Unit) { title = stringResource(Res.string.air_quality_metrics_module_enabled), checked = formState.value.air_quality_enabled, enabled = state.connected, - onCheckedChange = { formState.value = formState.value.copy(air_quality_enabled = it) }, + onCheckedChange = { + formState.value = + formState.value.newBuilder().also { wb -> wb.air_quality_enabled = it }.build() + }, containerColor = CardDefaults.cardColors().containerColor, ) HorizontalDivider() @@ -141,14 +163,20 @@ fun TelemetryConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> Unit) { selectedItem = formState.value.air_quality_interval.toLong(), enabled = state.connected, items = airItems.map { it.value to it.toDisplayString() }, - onItemSelected = { formState.value = formState.value.copy(air_quality_interval = it.toInt()) }, + onItemSelected = { + formState.value = + formState.value.newBuilder().also { wb -> wb.air_quality_interval = it.toInt() }.build() + }, ) HorizontalDivider() SwitchPreference( title = stringResource(Res.string.power_metrics_module_enabled), checked = formState.value.power_measurement_enabled, enabled = state.connected, - onCheckedChange = { formState.value = formState.value.copy(power_measurement_enabled = it) }, + onCheckedChange = { + formState.value = + formState.value.newBuilder().also { wb -> wb.power_measurement_enabled = it }.build() + }, containerColor = CardDefaults.cardColors().containerColor, ) HorizontalDivider() @@ -158,14 +186,20 @@ fun TelemetryConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> Unit) { selectedItem = formState.value.power_update_interval.toLong(), enabled = state.connected, items = powerItems.map { it.value to it.toDisplayString() }, - onItemSelected = { formState.value = formState.value.copy(power_update_interval = it.toInt()) }, + onItemSelected = { + formState.value = + formState.value.newBuilder().also { wb -> wb.power_update_interval = it.toInt() }.build() + }, ) HorizontalDivider() SwitchPreference( title = stringResource(Res.string.power_metrics_on_screen_enabled), checked = formState.value.power_screen_enabled, enabled = state.connected, - onCheckedChange = { formState.value = formState.value.copy(power_screen_enabled = it) }, + onCheckedChange = { + formState.value = + formState.value.newBuilder().also { wb -> wb.power_screen_enabled = it }.build() + }, containerColor = CardDefaults.cardColors().containerColor, ) } diff --git a/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/component/UserConfigItemList.kt b/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/component/UserConfigItemList.kt index e8cf8945c2..6ea2f639c1 100644 --- a/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/component/UserConfigItemList.kt +++ b/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/component/UserConfigItemList.kt @@ -168,7 +168,9 @@ fun UserConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> Unit, focusS (formState.value.is_unmessagable ?: false) || (!capabilities.canToggleUnmessageable && formState.value.role.isUnmessageableRole()), enabled = formState.value.is_unmessagable != null || capabilities.canToggleUnmessageable, - onCheckedChange = { formState.value = formState.value.copy(is_unmessagable = it) }, + onCheckedChange = { + formState.value = formState.value.newBuilder().also { wb -> wb.is_unmessagable = it }.build() + }, containerColor = CardDefaults.cardColors().containerColor, ) HorizontalDivider() @@ -182,15 +184,18 @@ fun UserConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> Unit, focusS // being entered: one too wide to be a callsign is demoted to the ham long name rather than // discarded, and abandoning onboarding does not leave a stray separator behind. formState.value = - formState.value.copy( - is_licensed = licensed, - long_name = - when { - !state.isLocal -> longName - licensed -> HamName.forOnboarding(longName) - else -> HamName.forUnlicensing(longName) - }, - ) + formState.value + .newBuilder() + .also { wb -> + wb.is_licensed = licensed + wb.long_name = + when { + !state.isLocal -> longName + licensed -> HamName.forOnboarding(longName) + else -> HamName.forUnlicensing(longName) + } + } + .build() }, ) } @@ -232,7 +237,14 @@ private fun statusMessagePrefill(config: ModuleConfig.StatusMessageConfig?, broa private fun RadioConfigViewModel.save(user: User, userDirty: Boolean, statusMessage: String, statusDirty: Boolean) { if (userDirty) saveUserConfig(user) if (statusDirty) { - setModuleConfig(ModuleConfig(statusmessage = ModuleConfig.StatusMessageConfig(node_status = statusMessage))) + setModuleConfig( + ModuleConfig.Builder() + .also { wb -> + wb.statusmessage = + ModuleConfig.StatusMessageConfig.Builder().also { wb -> wb.node_status = statusMessage }.build() + } + .build(), + ) } } @@ -320,7 +332,7 @@ internal fun UserNameFields( modifier = Modifier.testTag(USER_LONG_NAME_TEST_TAG), onValueChanged = { val longName = if (hamMode) HamName.compose(it, hamLongName) else it - formState.value = formState.value.copy(long_name = longName) + formState.value = formState.value.newBuilder().also { wb -> wb.long_name = longName }.build() }, ) if (hamMode) { @@ -336,7 +348,10 @@ internal fun UserNameFields( keyboardOptions = keyboardOptions, keyboardActions = keyboardActions, modifier = Modifier.testTag(HAM_LONG_NAME_TEST_TAG), - onValueChanged = { formState.value = formState.value.copy(long_name = HamName.compose(callSign, it)) }, + onValueChanged = { + formState.value = + formState.value.newBuilder().also { wb -> wb.long_name = HamName.compose(callSign, it) }.build() + }, ) } HorizontalDivider() @@ -349,7 +364,9 @@ internal fun UserNameFields( keyboardOptions = keyboardOptions, keyboardActions = keyboardActions, modifier = Modifier.testTag(USER_SHORT_NAME_TEST_TAG), - onValueChanged = { formState.value = formState.value.copy(short_name = it) }, + onValueChanged = { + formState.value = formState.value.newBuilder().also { wb -> wb.short_name = it }.build() + }, ) } } diff --git a/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/util/DeviceProfileExportFileName.kt b/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/util/DeviceProfileExportFileName.kt new file mode 100644 index 0000000000..4e64897fdb --- /dev/null +++ b/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/util/DeviceProfileExportFileName.kt @@ -0,0 +1,95 @@ +/* + * 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 . + */ +package org.meshtastic.feature.settings.util + +private const val FALLBACK_NODE_NAME = "node" + +/** Upper bound on the name segment so the whole file name stays well inside filesystem limits. */ +private const val MAX_NAME_LENGTH = 48 + +private const val SEPARATOR = '_' + +/** + * Letters and digits of any script are kept, in any Unicode plane, so a node called `Küche`, `東京` or `𐐀` keeps its + * name. Only characters a storage provider would actually choke on — separators, the Windows reserved set, whitespace, + * control characters, emoji — are replaced. + */ +private fun Char.isFileNameSafe() = isLetterOrDigit() || this == SEPARATOR || this == '-' + +/** + * Reduces a free-text node name to something safe to hand to a file picker as a suggested name. + * + * A run of unsafe characters collapses to one separator rather than one per character, so `Roof // Node` becomes + * `Roof_Node`. Returns `null` when nothing usable survives (an all-emoji name, for example) so the caller can fall back + * to another name. + */ +internal fun sanitizeExportNameSegment(name: String?): String? { + if (name == null) return null + val sanitized = StringBuilder(name.length) + var index = 0 + // Steps whole code points: a supplementary-plane letter is two UTF-16 units, and testing either half alone + // reports a lone surrogate rather than the letter it belongs to. + while (index < name.length) { + val high = name[index] + val paired = high.isHighSurrogate() && index + 1 < name.length && name[index + 1].isLowSurrogate() + val width = if (paired) 2 else 1 + val safe = if (paired) isSupplementaryNameChar(codePointAt(name, index)) else high.isFileNameSafe() + when { + safe -> sanitized.append(name, index, index + width) + + // Collapse a run of unsafe characters, and never open the name with a separator. + sanitized.isNotEmpty() && sanitized.last() != SEPARATOR -> sanitized.append(SEPARATOR) + } + index += width + } + return sanitized.toString().take(MAX_NAME_LENGTH).trimEnd(SEPARATOR).takeIf { it.isNotEmpty() } +} + +private const val SURROGATE_SHIFT = 10 +private const val SURROGATE_OFFSET = 0x10000 +private const val HIGH_SURROGATE_BASE = 0xD800 +private const val LOW_SURROGATE_BASE = 0xDC00 + +/** + * Whether a supplementary-plane code point belongs in a name. + * + * `Char.isLetterOrDigit()` only classifies one UTF-16 unit, and the stdlib has no code-point form in `commonMain`. + * Ranges beat a platform lookup here: the pictographic blocks are what a name has to shed, and everything else above + * the basic plane is a script — Deseret, Linear B, the CJK extensions — that a node may legitimately be named in. + */ +private fun isSupplementaryNameChar(codePoint: Int): Boolean = PICTOGRAPHIC_RANGES.none { codePoint in it } + +/** Musical notation, emoji and pictographs, and the legacy-computing symbols. */ +private val PICTOGRAPHIC_RANGES = listOf(0x1D000..0x1D1FF, 0x1F000..0x1FAFF, 0x1FB00..0x1FBFF) + +/** The code point of the surrogate pair starting at [index]. */ +private fun codePointAt(text: String, index: Int): Int = SURROGATE_OFFSET + + ((text[index].code - HIGH_SURROGATE_BASE) shl SURROGATE_SHIFT) + + (text[index + 1].code - LOW_SURROGATE_BASE) + +/** + * Builds the suggested file name for a device profile ("node config") export. + * + * Prefers the long name, which since firmware 2.8 is short enough to be practical and is the field that actually + * distinguishes a person's nodes from each other — short names are routinely identical across them (see #7082). Falls + * back to the short name, then to a generic placeholder, when the preferred name is absent (the export dialog can + * exclude it) or has no file-name-safe characters. + */ +internal fun deviceProfileExportFileName(longName: String?, shortName: String?, dateStamp: String): String { + val nodeName = sanitizeExportNameSegment(longName) ?: sanitizeExportNameSegment(shortName) ?: FALLBACK_NODE_NAME + return "Meshtastic_${nodeName}_${dateStamp}_nodeConfig.cfg" +} diff --git a/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/util/FieldMetadataBounds.kt b/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/util/FieldMetadataBounds.kt new file mode 100644 index 0000000000..9ff3dbaf26 --- /dev/null +++ b/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/util/FieldMetadataBounds.kt @@ -0,0 +1,36 @@ +/* + * 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 . + */ +package org.meshtastic.feature.settings.util + +import org.meshtastic.proto.FieldMetadata +import kotlin.math.ceil +import kotlin.math.floor + +/** + * The inclusive range a proto field declares for presentation, or null when the schema leaves either bound open. + * Fractional bounds round inward, so the integer range never admits a value the schema excludes. + */ +val FieldMetadata.intRange: IntRange? + get() { + val min = min_value ?: return null + val max = max_value ?: return null + val low = ceil(min) + val high = floor(max) + // Double.toInt() saturates, so a range that misses the Int domain would read as a single bogus value. + if (high < Int.MIN_VALUE.toDouble() || low > Int.MAX_VALUE.toDouble()) return null + return low.coerceAtLeast(Int.MIN_VALUE.toDouble()).toInt()..high.coerceAtMost(Int.MAX_VALUE.toDouble()).toInt() + } diff --git a/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/util/SettingsIntervals.kt b/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/util/SettingsIntervals.kt index 5a9299e2a0..f9288e6297 100644 --- a/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/util/SettingsIntervals.kt +++ b/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/util/SettingsIntervals.kt @@ -17,4 +17,3 @@ package org.meshtastic.feature.settings.util val gpioPins = (0..48).map { it to "Pin $it" } -val hopLimits = (0..7).map { it to it.toString() } diff --git a/feature/settings/src/commonTest/kotlin/org/meshtastic/feature/settings/AboutScreenTest.kt b/feature/settings/src/commonTest/kotlin/org/meshtastic/feature/settings/AboutScreenTest.kt index e6d2569293..16ed5b58a7 100644 --- a/feature/settings/src/commonTest/kotlin/org/meshtastic/feature/settings/AboutScreenTest.kt +++ b/feature/settings/src/commonTest/kotlin/org/meshtastic/feature/settings/AboutScreenTest.kt @@ -16,14 +16,19 @@ */ package org.meshtastic.feature.settings +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.ui.platform.LocalUriHandler +import androidx.compose.ui.platform.UriHandler import androidx.compose.ui.test.ExperimentalTestApi import androidx.compose.ui.test.assertIsDisplayed import androidx.compose.ui.test.onNodeWithContentDescription import androidx.compose.ui.test.onNodeWithText import androidx.compose.ui.test.performClick +import androidx.compose.ui.test.performScrollTo import androidx.compose.ui.test.v2.runComposeUiTest import org.meshtastic.core.ui.theme.AppTheme import kotlin.test.Test +import kotlin.test.assertEquals import kotlin.test.assertTrue @OptIn(ExperimentalTestApi::class) @@ -71,9 +76,16 @@ class AboutScreenTest { onNodeWithText("Project information").assertIsDisplayed() onNodeWithText("Website").assertIsDisplayed() onNodeWithText("Documentation").assertIsDisplayed() + onNodeWithText("License").performScrollTo().assertIsDisplayed() // Copyright footer - onNodeWithText("Meshtastic® Copyright Meshtastic LLC").assertIsDisplayed() + onNodeWithText("Meshtastic® Copyright Meshtastic LLC").performScrollTo().assertIsDisplayed() + onNodeWithText( + "Free software under the GNU General Public License v3, with no warranty. " + + "You may redistribute it under the same license.", + ) + .performScrollTo() + .assertIsDisplayed() onNodeWithText("Acknowledgements").performClick() assertTrue(navigatedToAcknowledgements) @@ -81,4 +93,25 @@ class AboutScreenTest { onNodeWithContentDescription("Navigate Back").performClick() assertTrue(navigatedUp) } + + @Test + fun `license row opens the GPLv3 license`() = runComposeUiTest { + val openedUris = mutableListOf() + val uriHandler = + object : UriHandler { + override fun openUri(uri: String) { + openedUris += uri + } + } + + setContent { + CompositionLocalProvider(LocalUriHandler provides uriHandler) { + AppTheme { AboutScreen(appVersionName = "2.5.0", onNavigateUp = {}, onNavigateToAcknowledgements = {}) } + } + } + + onNodeWithText("License").performScrollTo().performClick() + + assertEquals(listOf("https://www.gnu.org/licenses/gpl-3.0.html"), openedUris) + } } diff --git a/feature/settings/src/commonTest/kotlin/org/meshtastic/feature/settings/SettingsViewModelTest.kt b/feature/settings/src/commonTest/kotlin/org/meshtastic/feature/settings/SettingsViewModelTest.kt index 26fc7a0756..83c2c037a9 100644 --- a/feature/settings/src/commonTest/kotlin/org/meshtastic/feature/settings/SettingsViewModelTest.kt +++ b/feature/settings/src/commonTest/kotlin/org/meshtastic/feature/settings/SettingsViewModelTest.kt @@ -98,7 +98,7 @@ class SettingsViewModelTest { databaseManager = FakeDatabaseManager() notificationPrefs = FakeNotificationPrefs() - every { radioConfigRepository.localConfigFlow } returns MutableStateFlow(LocalConfig()) + every { radioConfigRepository.localConfigFlow } returns MutableStateFlow(LocalConfig.Builder().build()) every { buildConfigProvider.versionName } returns "3.0.0-test" val isOtaCapableUseCase: IsOtaCapableUseCase = mock(MockMode.autofill) @@ -279,18 +279,24 @@ class SettingsViewModelTest { fromNum = senderNodeNum, portNum = PortNum.TEXT_MESSAGE_APP.value, fromRadio = - FromRadio( - packet = - MeshPacket( - from = senderNodeNum, - rx_snr = 5.0f, - decoded = - Data( - portnum = PortNum.TEXT_MESSAGE_APP, - payload = "Hello settings".encodeUtf8(), - ), - ), - ), + FromRadio.Builder() + .also { wb -> + wb.packet = + MeshPacket.Builder() + .also { wb -> + wb.from = senderNodeNum + wb.rx_snr = 5.0f + wb.decoded = + Data.Builder() + .also { wb -> + wb.portnum = PortNum.TEXT_MESSAGE_APP + wb.payload = "Hello settings".encodeUtf8() + } + .build() + } + .build() + } + .build(), ), MeshLog( uuid = "filtered-out", @@ -300,17 +306,24 @@ class SettingsViewModelTest { fromNum = senderNodeNum, portNum = PortNum.RANGE_TEST_APP.value, fromRadio = - FromRadio( - packet = - MeshPacket( - from = senderNodeNum, - rx_snr = 6.0f, - decoded = Data( - portnum = PortNum.RANGE_TEST_APP, - payload = "Ignore me".encodeUtf8(), - ), - ), - ), + FromRadio.Builder() + .also { wb -> + wb.packet = + MeshPacket.Builder() + .also { wb -> + wb.from = senderNodeNum + wb.rx_snr = 6.0f + wb.decoded = + Data.Builder() + .also { wb -> + wb.portnum = PortNum.RANGE_TEST_APP + wb.payload = "Ignore me".encodeUtf8() + } + .build() + } + .build() + } + .build(), ), ), ) diff --git a/feature/settings/src/commonTest/kotlin/org/meshtastic/feature/settings/channel/CommonChannelViewModelTest.kt b/feature/settings/src/commonTest/kotlin/org/meshtastic/feature/settings/channel/CommonChannelViewModelTest.kt index 81d60ece32..83047cd499 100644 --- a/feature/settings/src/commonTest/kotlin/org/meshtastic/feature/settings/channel/CommonChannelViewModelTest.kt +++ b/feature/settings/src/commonTest/kotlin/org/meshtastic/feature/settings/channel/CommonChannelViewModelTest.kt @@ -56,8 +56,8 @@ abstract class CommonChannelViewModelTest { @BeforeTest fun setupRepo() { Dispatchers.setMain(testDispatcher) - every { radioConfigRepository.localConfigFlow } returns MutableStateFlow(LocalConfig()) - every { radioConfigRepository.channelSetFlow } returns MutableStateFlow(ChannelSet()) + every { radioConfigRepository.localConfigFlow } returns MutableStateFlow(LocalConfig.Builder().build()) + every { radioConfigRepository.channelSetFlow } returns MutableStateFlow(ChannelSet.Builder().build()) viewModel = ChannelViewModel(radioController, radioConfigRepository, analytics) } @@ -69,7 +69,12 @@ abstract class CommonChannelViewModelTest { @Test fun `isManaged returns true when security is managed`() = runTest { - val config = LocalConfig(security = Config.SecurityConfig(is_managed = true)) + val config = + LocalConfig.Builder() + .also { wb -> + wb.security = Config.SecurityConfig.Builder().also { wb -> wb.is_managed = true }.build() + } + .build() every { radioConfigRepository.localConfigFlow } returns MutableStateFlow(config) viewModel = ChannelViewModel(radioController, radioConfigRepository, analytics) diff --git a/feature/settings/src/commonTest/kotlin/org/meshtastic/feature/settings/debugging/DebugViewModelTest.kt b/feature/settings/src/commonTest/kotlin/org/meshtastic/feature/settings/debugging/DebugViewModelTest.kt index 34bfff44e7..4708f07b0d 100644 --- a/feature/settings/src/commonTest/kotlin/org/meshtastic/feature/settings/debugging/DebugViewModelTest.kt +++ b/feature/settings/src/commonTest/kotlin/org/meshtastic/feature/settings/debugging/DebugViewModelTest.kt @@ -210,14 +210,22 @@ class DebugViewModelTest { @Test fun `decodePayloadFromMeshLog decodes various portnums`() { - val position = org.meshtastic.proto.Position(latitude_i = 10000000, longitude_i = 20000000) + val position = + org.meshtastic.proto.Position.Builder() + .also { wb -> + wb.latitude_i = 10000000 + wb.longitude_i = 20000000 + } + .build() val packet = org.meshtastic.core.testing.TestDataFactory.createTestPacket( decoded = - org.meshtastic.proto.Data( - portnum = org.meshtastic.proto.PortNum.POSITION_APP, - payload = okio.ByteString.Companion.of(*position.encode()), - ), + org.meshtastic.proto.Data.Builder() + .also { wb -> + wb.portnum = org.meshtastic.proto.PortNum.POSITION_APP + wb.payload = okio.ByteString.Companion.of(*position.encode()) + } + .build(), ) val log = org.meshtastic.core.model.MeshLog( @@ -225,7 +233,7 @@ class DebugViewModelTest { message_type = "Packet", received_date = 1L, raw_message = "raw", - fromRadio = org.meshtastic.proto.FromRadio(packet = packet), + fromRadio = org.meshtastic.proto.FromRadio.Builder().also { wb -> wb.packet = packet }.build(), ) // This is a private method but we can test it via toUiState @@ -252,7 +260,7 @@ class DebugViewModelTest { message_type = "Packet", received_date = 1L, raw_message = "", - fromRadio = org.meshtastic.proto.FromRadio(packet = packet), + fromRadio = org.meshtastic.proto.FromRadio.Builder().also { wb -> wb.packet = packet }.build(), ), ) @@ -282,7 +290,7 @@ class DebugViewModelTest { message_type = "Packet", received_date = 1L, raw_message = "", - fromRadio = org.meshtastic.proto.FromRadio(packet = packet), + fromRadio = org.meshtastic.proto.FromRadio.Builder().also { wb -> wb.packet = packet }.build(), ), ) @@ -300,7 +308,7 @@ class DebugViewModelTest { message_type = "Packet", received_date = 1L, raw_message = "", - fromRadio = org.meshtastic.proto.FromRadio(packet = packet), + fromRadio = org.meshtastic.proto.FromRadio.Builder().also { wb -> wb.packet = packet }.build(), ), ) @@ -318,7 +326,14 @@ class DebugViewModelTest { received_date = 1L, raw_message = "node_info {\n num: 3735928559\n}", fromRadio = - org.meshtastic.proto.FromRadio(node_info = org.meshtastic.proto.NodeInfo(num = 0xDEADBEEF.toInt())), + org.meshtastic.proto.FromRadio.Builder() + .also { wb -> + wb.node_info = + org.meshtastic.proto.NodeInfo.Builder() + .also { wb -> wb.num = 0xDEADBEEF.toInt() } + .build() + } + .build(), ), ) diff --git a/feature/settings/src/commonTest/kotlin/org/meshtastic/feature/settings/radio/CleanNodeDatabaseViewModelTest.kt b/feature/settings/src/commonTest/kotlin/org/meshtastic/feature/settings/radio/CleanNodeDatabaseViewModelTest.kt index 80648a7ef5..feae113cd1 100644 --- a/feature/settings/src/commonTest/kotlin/org/meshtastic/feature/settings/radio/CleanNodeDatabaseViewModelTest.kt +++ b/feature/settings/src/commonTest/kotlin/org/meshtastic/feature/settings/radio/CleanNodeDatabaseViewModelTest.kt @@ -72,7 +72,8 @@ class CleanNodeDatabaseViewModelTest { @Test fun `getNodesToDelete calls useCase and updates state`() = runTest { - val nodes = listOf(Node(num = 1, user = org.meshtastic.proto.User(id = "!1"))) + val nodes = + listOf(Node(num = 1, user = org.meshtastic.proto.User.Builder().also { wb -> wb.id = "!1" }.build())) everySuspend { cleanNodeDatabaseUseCase.getNodesToClean(any(), any(), any()) } returns nodes viewModel.getNodesToDelete() @@ -83,7 +84,8 @@ class CleanNodeDatabaseViewModelTest { @Test fun `cleanNodes calls useCase and clears state`() = runTest { // First set some nodes to delete - val nodes = listOf(Node(num = 1, user = org.meshtastic.proto.User(id = "!1"))) + val nodes = + listOf(Node(num = 1, user = org.meshtastic.proto.User.Builder().also { wb -> wb.id = "!1" }.build())) everySuspend { cleanNodeDatabaseUseCase.getNodesToClean(any(), any(), any()) } returns nodes viewModel.getNodesToDelete() diff --git a/feature/settings/src/commonTest/kotlin/org/meshtastic/feature/settings/radio/ProfileRoundTripTest.kt b/feature/settings/src/commonTest/kotlin/org/meshtastic/feature/settings/radio/ProfileRoundTripTest.kt index f8222fe645..e2b564a301 100644 --- a/feature/settings/src/commonTest/kotlin/org/meshtastic/feature/settings/radio/ProfileRoundTripTest.kt +++ b/feature/settings/src/commonTest/kotlin/org/meshtastic/feature/settings/radio/ProfileRoundTripTest.kt @@ -52,7 +52,6 @@ import org.meshtastic.core.repository.LocationService import org.meshtastic.core.repository.MapConsentPrefs import org.meshtastic.core.repository.MqttManager import org.meshtastic.core.repository.NodeRestartTracker -import org.meshtastic.core.repository.PacketRepository import org.meshtastic.core.repository.RadioConfigRepository import org.meshtastic.core.repository.SecurityKeyBackupStore import org.meshtastic.core.repository.ServiceRepository @@ -79,7 +78,6 @@ class ProfileRoundTripTest { private val testDispatcher = UnconfinedTestDispatcher() private val radioConfigRepository: RadioConfigRepository = mock(MockMode.autofill) - private val packetRepository: PacketRepository = mock(MockMode.autofill) private val serviceRepository: ServiceRepository = mock(MockMode.autofill) private val nodeRepository = FakeNodeRepository() private val locationRepository: LocationRepository = mock(MockMode.autofill) @@ -104,10 +102,10 @@ class ProfileRoundTripTest { Dispatchers.setMain(testDispatcher) fileService = InMemoryFileService() - every { radioConfigRepository.deviceProfileFlow } returns MutableStateFlow(DeviceProfile()) - every { radioConfigRepository.localConfigFlow } returns MutableStateFlow(LocalConfig()) - every { radioConfigRepository.channelSetFlow } returns MutableStateFlow(ChannelSet()) - every { radioConfigRepository.moduleConfigFlow } returns MutableStateFlow(LocalModuleConfig()) + every { radioConfigRepository.deviceProfileFlow } returns MutableStateFlow(DeviceProfile.Builder().build()) + every { radioConfigRepository.localConfigFlow } returns MutableStateFlow(LocalConfig.Builder().build()) + every { radioConfigRepository.channelSetFlow } returns MutableStateFlow(ChannelSet.Builder().build()) + every { radioConfigRepository.moduleConfigFlow } returns MutableStateFlow(LocalModuleConfig.Builder().build()) every { radioConfigRepository.deviceUIConfigFlow } returns MutableStateFlow(null) every { radioConfigRepository.fileManifestFlow } returns MutableStateFlow(emptyList()) every { radioConfigRepository.loraRegionPresetMapFlow } returns MutableStateFlow(null) @@ -124,7 +122,6 @@ class ProfileRoundTripTest { RadioConfigViewModel( initialDestNum = null, radioConfigRepository = radioConfigRepository, - packetRepository = packetRepository, serviceRepository = serviceRepository, nodeRepository = nodeRepository, locationRepository = locationRepository, @@ -158,65 +155,118 @@ class ProfileRoundTripTest { @Test fun `profile export then import round trips representative DeviceProfile`() = runTest { assertRoundTrip( - DeviceProfile( - long_name = "Round Trip Node", - short_name = "RTN", - channel_url = "https://meshtastic.org/e/#CgMSAQESBggBQANIAQ", - config = - LocalConfig( - device = Config.DeviceConfig(role = Config.DeviceConfig.Role.ROUTER, button_gpio = 7), - lora = Config.LoRaConfig(hop_limit = 5, use_preset = true), - power = Config.PowerConfig(is_power_saving = true, ls_secs = 300), - network = - Config.NetworkConfig( - wifi_enabled = true, - wifi_ssid = "mesh-ssid", - wifi_psk = "mesh-pass", - ntp_server = "meshtastic.pool.ntp.org", - ), - ), - module_config = - LocalModuleConfig( - mqtt = - ModuleConfig.MQTTConfig( - enabled = true, - proxy_to_client_enabled = true, - root = "msh/US/test", - json_enabled = true, - ), - telemetry = - ModuleConfig.TelemetryConfig( - device_update_interval = 300, - environment_measurement_enabled = true, - power_measurement_enabled = true, - ), - canned_message = - ModuleConfig.CannedMessageConfig( - rotary1_enabled = true, - inputbroker_pin_a = 12, - inputbroker_pin_b = 13, - send_bell = true, - ), - statusmessage = ModuleConfig.StatusMessageConfig(node_status = "Ready to mesh"), - ), - fixed_position = Position(latitude_i = 327766650, longitude_i = -967969890, altitude = 138), - ringtone = "tones/notify.mp3", - canned_messages = "Alpha|Bravo|Charlie", - ), + DeviceProfile.Builder() + .also { wb -> + wb.long_name = "Round Trip Node" + wb.short_name = "RTN" + wb.channel_url = "https://meshtastic.org/e/#CgMSAQESBggBQANIAQ" + wb.config = + LocalConfig.Builder() + .also { wb -> + wb.device = + Config.DeviceConfig.Builder() + .also { wb -> + wb.role = Config.DeviceConfig.Role.ROUTER + wb.button_gpio = 7 + } + .build() + wb.lora = + Config.LoRaConfig.Builder() + .also { wb -> + wb.hop_limit = 5 + wb.use_preset = true + } + .build() + wb.power = + Config.PowerConfig.Builder() + .also { wb -> + wb.is_power_saving = true + wb.ls_secs = 300 + } + .build() + wb.network = + Config.NetworkConfig.Builder() + .also { wb -> + wb.wifi_enabled = true + wb.wifi_ssid = "mesh-ssid" + wb.wifi_psk = "mesh-pass" + wb.ntp_server = "meshtastic.pool.ntp.org" + } + .build() + } + .build() + wb.module_config = + LocalModuleConfig.Builder() + .also { wb -> + wb.mqtt = + ModuleConfig.MQTTConfig.Builder() + .also { wb -> + wb.enabled = true + wb.proxy_to_client_enabled = true + wb.root = "msh/US/test" + wb.json_enabled = true + } + .build() + wb.telemetry = + ModuleConfig.TelemetryConfig.Builder() + .also { wb -> + wb.device_update_interval = 300 + wb.environment_measurement_enabled = true + wb.power_measurement_enabled = true + } + .build() + wb.canned_message = + ModuleConfig.CannedMessageConfig.Builder() + .also { wb -> + wb.rotary1_enabled = true + wb.inputbroker_pin_a = 12 + wb.inputbroker_pin_b = 13 + wb.send_bell = true + } + .build() + wb.statusmessage = + ModuleConfig.StatusMessageConfig.Builder() + .also { wb -> wb.node_status = "Ready to mesh" } + .build() + } + .build() + wb.fixed_position = + Position.Builder() + .also { wb -> + wb.latitude_i = 327766650 + wb.longitude_i = -967969890 + wb.altitude = 138 + } + .build() + wb.ringtone = "tones/notify.mp3" + wb.canned_messages = "Alpha|Bravo|Charlie" + } + .build(), ) } @Test - fun `profile export then import round trips empty DeviceProfile`() = runTest { assertRoundTrip(DeviceProfile()) } + fun `profile export then import round trips empty DeviceProfile`() = runTest { + assertRoundTrip(DeviceProfile.Builder().build()) + } @Test fun `profile export then import round trips partially populated DeviceProfile`() = runTest { assertRoundTrip( - DeviceProfile( - long_name = "Partial Node", - module_config = - LocalModuleConfig(statusmessage = ModuleConfig.StatusMessageConfig(node_status = "Standing by")), - ), + DeviceProfile.Builder() + .also { wb -> + wb.long_name = "Partial Node" + wb.module_config = + LocalModuleConfig.Builder() + .also { wb -> + wb.statusmessage = + ModuleConfig.StatusMessageConfig.Builder() + .also { wb -> wb.node_status = "Standing by" } + .build() + } + .build() + } + .build(), ) } diff --git a/feature/settings/src/commonTest/kotlin/org/meshtastic/feature/settings/radio/RadioConfigViewModelTest.kt b/feature/settings/src/commonTest/kotlin/org/meshtastic/feature/settings/radio/RadioConfigViewModelTest.kt index f8748e05d9..138cd86e8f 100644 --- a/feature/settings/src/commonTest/kotlin/org/meshtastic/feature/settings/radio/RadioConfigViewModelTest.kt +++ b/feature/settings/src/commonTest/kotlin/org/meshtastic/feature/settings/radio/RadioConfigViewModelTest.kt @@ -68,7 +68,6 @@ import org.meshtastic.core.repository.MapConsentPrefs import org.meshtastic.core.repository.MeshConnectionManager import org.meshtastic.core.repository.MqttManager import org.meshtastic.core.repository.NodeRestartTracker -import org.meshtastic.core.repository.PacketRepository import org.meshtastic.core.repository.RadioConfigRepository import org.meshtastic.core.repository.SecurityKeyBackupStore import org.meshtastic.core.repository.ServiceRepository @@ -127,7 +126,12 @@ class RadioConfigViewModelTest { @Test fun `USER route reads the status message config on capable firmware`() = runTest { - val node = Node(num = 123, user = User(id = "!123"), metadata = DeviceMetadata(firmware_version = "2.8.0")) + val node = + Node( + num = 123, + user = User.Builder().also { wb -> wb.id = "!123" }.build(), + metadata = DeviceMetadata.Builder().also { wb -> wb.firmware_version = "2.8.0" }.build(), + ) nodeRepository.setNodes(listOf(node)) viewModel = createViewModel(destNum = 123) @@ -141,7 +145,12 @@ class RadioConfigViewModelTest { @Test fun `USER route skips the status message config on firmware without the module`() = runTest { - val node = Node(num = 123, user = User(id = "!123"), metadata = DeviceMetadata(firmware_version = "2.7.21")) + val node = + Node( + num = 123, + user = User.Builder().also { wb -> wb.id = "!123" }.build(), + metadata = DeviceMetadata.Builder().also { wb -> wb.firmware_version = "2.7.21" }.build(), + ) nodeRepository.setNodes(listOf(node)) viewModel = createViewModel(destNum = 123) @@ -153,9 +162,13 @@ class RadioConfigViewModelTest { @Test fun `MESH_BEACON route on a remote node reads the beacon module config plus LoRa config and channel 0`() = runTest { - val localNode = Node(num = 100, user = User(id = "!100")) + val localNode = Node(num = 100, user = User.Builder().also { wb -> wb.id = "!100" }.build()) val remoteNode = - Node(num = 456, user = User(id = "!456"), metadata = DeviceMetadata(firmware_version = "2.8.0")) + Node( + num = 456, + user = User.Builder().also { wb -> wb.id = "!456" }.build(), + metadata = DeviceMetadata.Builder().also { wb -> wb.firmware_version = "2.8.0" }.build(), + ) nodeRepository.setNodes(listOf(localNode, remoteNode)) nodeRepository.setMyNodeInfo(myNodeInfo(myNodeNum = 100)) viewModel = createViewModel(destNum = 456) @@ -178,9 +191,13 @@ class RadioConfigViewModelTest { @Test fun `MESH_BEACON route skips the module get on firmware without the module but still reads LoRa and channel 0`() = runTest { - val localNode = Node(num = 100, user = User(id = "!100")) + val localNode = Node(num = 100, user = User.Builder().also { wb -> wb.id = "!100" }.build()) val remoteNode = - Node(num = 456, user = User(id = "!456"), metadata = DeviceMetadata(firmware_version = "2.7.21")) + Node( + num = 456, + user = User.Builder().also { wb -> wb.id = "!456" }.build(), + metadata = DeviceMetadata.Builder().also { wb -> wb.firmware_version = "2.7.21" }.build(), + ) nodeRepository.setNodes(listOf(localNode, remoteNode)) nodeRepository.setMyNodeInfo(myNodeInfo(myNodeNum = 100)) viewModel = createViewModel(destNum = 456) @@ -197,12 +214,21 @@ class RadioConfigViewModelTest { @Test fun `MESH_BEACON remote read completes only after all three responses land`() = runTest { - val localNode = Node(num = 100, user = User(id = "!100")) + val localNode = Node(num = 100, user = User.Builder().also { wb -> wb.id = "!100" }.build()) val remoteNode = - Node(num = 456, user = User(id = "!456"), metadata = DeviceMetadata(firmware_version = "2.8.0")) + Node( + num = 456, + user = User.Builder().also { wb -> wb.id = "!456" }.build(), + metadata = DeviceMetadata.Builder().also { wb -> wb.firmware_version = "2.8.0" }.build(), + ) val packetFlow = MutableSharedFlow() val beacon = - MeshBeaconConfig(broadcast_offer_region = Config.LoRaConfig.RegionCode.US, broadcast_message = "join us") + MeshBeaconConfig.Builder() + .also { wb -> + wb.broadcast_offer_region = Config.LoRaConfig.RegionCode.US + wb.broadcast_message = "join us" + } + .build() every { serviceRepository.meshPacketFlow } returns packetFlow everySuspend { radioConfigUseCase.getChannel(any(), any(), any()) } calls { @@ -228,19 +254,31 @@ class RadioConfigViewModelTest { when (requestId?.takeIf { id -> id in pendingRequestIds }) { 41 -> RadioResponseResult.ChannelResponse( - Channel( - index = 0, - role = Channel.Role.PRIMARY, - settings = ChannelSettings(name = "LongFast"), - ), + Channel.Builder() + .also { wb -> + wb.index = 0 + wb.role = Channel.Role.PRIMARY + wb.settings = ChannelSettings.Builder().also { wb -> wb.name = "LongFast" }.build() + } + .build(), ) 42 -> RadioResponseResult.ConfigResponse( - Config(lora = Config.LoRaConfig(region = Config.LoRaConfig.RegionCode.US)), + Config.Builder() + .also { wb -> + wb.lora = + Config.LoRaConfig.Builder() + .also { wb -> wb.region = Config.LoRaConfig.RegionCode.US } + .build() + } + .build(), ) - 43 -> RadioResponseResult.ModuleConfigResponse(ModuleConfig(mesh_beacon = beacon)) + 43 -> + RadioResponseResult.ModuleConfigResponse( + ModuleConfig.Builder().also { wb -> wb.mesh_beacon = beacon }.build(), + ) else -> null } @@ -253,8 +291,16 @@ class RadioConfigViewModelTest { runCurrent() assertEquals(3, (viewModel.radioConfigState.value.responseState as ResponseState.Loading).total) - packetFlow.emit(MeshPacket(decoded = Data(request_id = 41))) - packetFlow.emit(MeshPacket(decoded = Data(request_id = 42))) + packetFlow.emit( + MeshPacket.Builder() + .also { wb -> wb.decoded = Data.Builder().also { wb -> wb.request_id = 41 }.build() } + .build(), + ) + packetFlow.emit( + MeshPacket.Builder() + .also { wb -> wb.decoded = Data.Builder().also { wb -> wb.request_id = 42 }.build() } + .build(), + ) runCurrent() assertTrue( viewModel.radioConfigState.value.responseState is ResponseState.Loading, @@ -262,7 +308,11 @@ class RadioConfigViewModelTest { ) assertEquals(3, (viewModel.radioConfigState.value.responseState as ResponseState.Loading).total) - packetFlow.emit(MeshPacket(decoded = Data(request_id = 43))) + packetFlow.emit( + MeshPacket.Builder() + .also { wb -> wb.decoded = Data.Builder().also { wb -> wb.request_id = 43 }.build() } + .build(), + ) runCurrent() assertEquals(ResponseState.Empty, viewModel.radioConfigState.value.responseState) @@ -274,25 +324,39 @@ class RadioConfigViewModelTest { @Test fun `ModuleConfigResponse carrying mesh_beacon is merged and survives a later response without it`() = runTest { - val node = Node(num = 123, user = User(id = "!123")) + val node = Node(num = 123, user = User.Builder().also { wb -> wb.id = "!123" }.build()) nodeRepository.setNodes(listOf(node)) val packetFlow = MutableSharedFlow() every { serviceRepository.meshPacketFlow } returns packetFlow viewModel = createViewModel() val beacon = - MeshBeaconConfig(broadcast_offer_region = Config.LoRaConfig.RegionCode.EU_868, broadcast_message = "hi") + MeshBeaconConfig.Builder() + .also { wb -> + wb.broadcast_offer_region = Config.LoRaConfig.RegionCode.EU_868 + wb.broadcast_message = "hi" + } + .build() every { processRadioResponseUseCase(any(), 123, any()) } returns - RadioResponseResult.ModuleConfigResponse(ModuleConfig(mesh_beacon = beacon)) - packetFlow.emit(MeshPacket()) + RadioResponseResult.ModuleConfigResponse( + ModuleConfig.Builder().also { wb -> wb.mesh_beacon = beacon }.build(), + ) + packetFlow.emit(MeshPacket.Builder().build()) // Pins the mesh_beacon merge line: without it the reply is dropped and the editor renders defaults. assertEquals(beacon, viewModel.radioConfigState.value.moduleConfig.mesh_beacon) every { processRadioResponseUseCase(any(), 123, any()) } returns RadioResponseResult.ModuleConfigResponse( - ModuleConfig(telemetry = ModuleConfig.TelemetryConfig(device_update_interval = 300)), + ModuleConfig.Builder() + .also { wb -> + wb.telemetry = + ModuleConfig.TelemetryConfig.Builder() + .also { wb -> wb.device_update_interval = 300 } + .build() + } + .build(), ) - packetFlow.emit(MeshPacket()) + packetFlow.emit(MeshPacket.Builder().build()) // Pins the `?: state.moduleConfig.mesh_beacon` fallback: a reply for another module keeps the beacon config. assertEquals(beacon, viewModel.radioConfigState.value.moduleConfig.mesh_beacon) assertEquals(300, viewModel.radioConfigState.value.moduleConfig.telemetry?.device_update_interval) @@ -301,7 +365,6 @@ class RadioConfigViewModelTest { private val testDispatcher = UnconfinedTestDispatcher() private val radioConfigRepository: RadioConfigRepository = mock(MockMode.autofill) - private val packetRepository: PacketRepository = mock(MockMode.autofill) private val serviceRepository: ServiceRepository = mock(MockMode.autofill) private val nodeRepository = FakeNodeRepository() private val locationRepository: LocationRepository = mock(MockMode.autofill) @@ -339,10 +402,10 @@ class RadioConfigViewModelTest { fun setUp() { Dispatchers.setMain(testDispatcher) - every { radioConfigRepository.deviceProfileFlow } returns MutableStateFlow(DeviceProfile()) - every { radioConfigRepository.localConfigFlow } returns MutableStateFlow(LocalConfig()) - every { radioConfigRepository.channelSetFlow } returns MutableStateFlow(ChannelSet()) - every { radioConfigRepository.moduleConfigFlow } returns MutableStateFlow(LocalModuleConfig()) + every { radioConfigRepository.deviceProfileFlow } returns MutableStateFlow(DeviceProfile.Builder().build()) + every { radioConfigRepository.localConfigFlow } returns MutableStateFlow(LocalConfig.Builder().build()) + every { radioConfigRepository.channelSetFlow } returns MutableStateFlow(ChannelSet.Builder().build()) + every { radioConfigRepository.moduleConfigFlow } returns MutableStateFlow(LocalModuleConfig.Builder().build()) every { radioConfigRepository.deviceUIConfigFlow } returns MutableStateFlow(null) every { radioConfigRepository.fileManifestFlow } returns MutableStateFlow(emptyList()) every { radioConfigRepository.loraRegionPresetMapFlow } returns MutableStateFlow(null) @@ -382,7 +445,6 @@ class RadioConfigViewModelTest { ) = RadioConfigViewModel( initialDestNum = destNum, radioConfigRepository = radioConfigRepository, - packetRepository = packetRepository, serviceRepository = serviceRepository, nodeRepository = nodeRepository, locationRepository = locationRepository, @@ -410,7 +472,7 @@ class RadioConfigViewModelTest { @Test fun `local save acked by a renumbered radio re-runs the handshake and completes the save`() = runTest { - val node = Node(num = 123, user = User(id = "!123")) + val node = Node(num = 123, user = User.Builder().also { wb -> wb.id = "!123" }.build()) nodeRepository.setNodes(listOf(node)) nodeRepository.setMyNodeInfo(myNodeInfo(myNodeNum = 123)) val packetFlow = MutableSharedFlow() @@ -422,7 +484,14 @@ class RadioConfigViewModelTest { } viewModel = createViewModel(processRadioResponseUseCase = ProcessRadioResponseUseCase()) - viewModel.setConfig(Config(lora = Config.LoRaConfig(region = Config.LoRaConfig.RegionCode.US))) + viewModel.setConfig( + Config.Builder() + .also { wb -> + wb.lora = + Config.LoRaConfig.Builder().also { wb -> wb.region = Config.LoRaConfig.RegionCode.US }.build() + } + .build(), + ) runCurrent() assertTrue(viewModel.radioConfigState.value.responseState is ResponseState.Loading) @@ -436,7 +505,7 @@ class RadioConfigViewModelTest { @Test fun `a session opened on the connected node by number follows it after the renumber`() = runTest { - nodeRepository.setNodes(listOf(Node(num = 123, user = User(id = "!123")))) + nodeRepository.setNodes(listOf(Node(num = 123, user = User.Builder().also { wb -> wb.id = "!123" }.build()))) nodeRepository.setMyNodeInfo(myNodeInfo(myNodeNum = 123)) val packetFlow = MutableSharedFlow() every { serviceRepository.meshPacketFlow } returns packetFlow @@ -453,7 +522,14 @@ class RadioConfigViewModelTest { viewModel = createViewModel(destNum = 123, processRadioResponseUseCase = ProcessRadioResponseUseCase()) assertTrue(viewModel.radioConfigState.value.isLocal) - viewModel.setConfig(Config(lora = Config.LoRaConfig(region = Config.LoRaConfig.RegionCode.US))) + viewModel.setConfig( + Config.Builder() + .also { wb -> + wb.lora = + Config.LoRaConfig.Builder().also { wb -> wb.region = Config.LoRaConfig.RegionCode.US }.build() + } + .build(), + ) runCurrent() packetFlow.emit(routingAck(requestId = 91, from = 456)) runCurrent() @@ -462,11 +538,18 @@ class RadioConfigViewModelTest { // The re-handshake lands: the radio reports its new number and is installed under it. nodeRepository.setMyNodeInfo(myNodeInfo(myNodeNum = 456)) - nodeRepository.setNodes(listOf(Node(num = 456, user = User(id = "!456")))) + nodeRepository.setNodes(listOf(Node(num = 456, user = User.Builder().also { wb -> wb.id = "!456" }.build()))) runCurrent() assertTrue(viewModel.radioConfigState.value.isLocal) - viewModel.setConfig(Config(lora = Config.LoRaConfig(region = Config.LoRaConfig.RegionCode.US))) + viewModel.setConfig( + Config.Builder() + .also { wb -> + wb.lora = + Config.LoRaConfig.Builder().also { wb -> wb.region = Config.LoRaConfig.RegionCode.US }.build() + } + .build(), + ) runCurrent() assertEquals(listOf(123, 456), writeTargets) } @@ -474,7 +557,10 @@ class RadioConfigViewModelTest { @Test fun `remote save acked by an unexpected node neither re-handshakes nor completes`() = runTest { nodeRepository.setNodes( - listOf(Node(num = 123, user = User(id = "!123")), Node(num = 200, user = User(id = "!200"))), + listOf( + Node(num = 123, user = User.Builder().also { wb -> wb.id = "!123" }.build()), + Node(num = 200, user = User.Builder().also { wb -> wb.id = "!200" }.build()), + ), ) nodeRepository.setMyNodeInfo(myNodeInfo(myNodeNum = 123)) val packetFlow = MutableSharedFlow() @@ -486,7 +572,14 @@ class RadioConfigViewModelTest { } viewModel = createViewModel(destNum = 200, processRadioResponseUseCase = ProcessRadioResponseUseCase()) - viewModel.setConfig(Config(lora = Config.LoRaConfig(region = Config.LoRaConfig.RegionCode.US))) + viewModel.setConfig( + Config.Builder() + .also { wb -> + wb.lora = + Config.LoRaConfig.Builder().also { wb -> wb.region = Config.LoRaConfig.RegionCode.US }.build() + } + .build(), + ) runCurrent() assertTrue(viewModel.radioConfigState.value.responseState is ResponseState.Loading) @@ -503,11 +596,17 @@ class RadioConfigViewModelTest { @Test fun `setConfig calls useCase`() = runTest { - val node = Node(num = 123, user = User(id = "!123")) + val node = Node(num = 123, user = User.Builder().also { wb -> wb.id = "!123" }.build()) nodeRepository.setNodes(listOf(node)) viewModel = createViewModel() - val config = Config(device = Config.DeviceConfig(role = Config.DeviceConfig.Role.ROUTER)) + val config = + Config.Builder() + .also { wb -> + wb.device = + Config.DeviceConfig.Builder().also { wb -> wb.role = Config.DeviceConfig.Role.ROUTER }.build() + } + .build() everySuspend { radioConfigUseCase.setConfig(any(), any(), any()) } returns 42 viewModel.setConfig(config) @@ -543,11 +642,11 @@ class RadioConfigViewModelTest { @Test fun `processPacketResponse updates state on metadata result`() = runTest { - val node = Node(num = 123, user = User(id = "!123")) + val node = Node(num = 123, user = User.Builder().also { wb -> wb.id = "!123" }.build()) nodeRepository.setNodes(listOf(node)) - val packet = MeshPacket() - val metadata = DeviceMetadata(firmware_version = "3.0.0") + val packet = MeshPacket.Builder().build() + val metadata = DeviceMetadata.Builder().also { wb -> wb.firmware_version = "3.0.0" }.build() val packetFlow = MutableSharedFlow() every { serviceRepository.meshPacketFlow } returns packetFlow @@ -621,9 +720,17 @@ class RadioConfigViewModelTest { fun `setMqttProxyActive true restarts proxy using current device module config`() = runTest { every { radioConfigRepository.moduleConfigFlow } returns MutableStateFlow( - LocalModuleConfig( - mqtt = org.meshtastic.proto.ModuleConfig.MQTTConfig(enabled = true, proxy_to_client_enabled = true), - ), + LocalModuleConfig.Builder() + .also { wb -> + wb.mqtt = + org.meshtastic.proto.ModuleConfig.MQTTConfig.Builder() + .also { wb -> + wb.enabled = true + wb.proxy_to_client_enabled = true + } + .build() + } + .build(), ) every { mqttManager.startProxy(any(), any()) } returns Unit viewModel = createViewModel() @@ -637,7 +744,7 @@ class RadioConfigViewModelTest { @Test fun `setMqttProxyActive true is a no-op start when device MQTT is disabled`() = runTest { - every { radioConfigRepository.moduleConfigFlow } returns MutableStateFlow(LocalModuleConfig()) + every { radioConfigRepository.moduleConfigFlow } returns MutableStateFlow(LocalModuleConfig.Builder().build()) every { mqttManager.startProxy(any(), any()) } returns Unit viewModel = createViewModel() runCurrent() @@ -670,12 +777,12 @@ class RadioConfigViewModelTest { @Test fun `updateChannels calls useCase for each changed channel`() = runTest { - val node = Node(num = 123, user = User(id = "!123")) + val node = Node(num = 123, user = User.Builder().also { wb -> wb.id = "!123" }.build()) nodeRepository.setNodes(listOf(node)) viewModel = createViewModel() - val old = listOf(ChannelSettings(name = "Old")) - val new = listOf(ChannelSettings(name = "New")) + val old = listOf(ChannelSettings.Builder().also { wb -> wb.name = "Old" }.build()) + val new = listOf(ChannelSettings.Builder().also { wb -> wb.name = "New" }.build()) everySuspend { radioConfigUseCase.setRemoteChannel(any(), any(), any()) } returns 42 @@ -687,13 +794,13 @@ class RadioConfigViewModelTest { @Test fun `updateChannels writes changed channels sequentially in index order`() = runTest { - val node = Node(num = 123, user = User(id = "!123")) + val node = Node(num = 123, user = User.Builder().also { wb -> wb.id = "!123" }.build()) nodeRepository.setNodes(listOf(node)) viewModel = createViewModel() - val channelA = ChannelSettings(name = "A") - val channelB = ChannelSettings(name = "B") - val channelC = ChannelSettings(name = "C") + val channelA = ChannelSettings.Builder().also { wb -> wb.name = "A" }.build() + val channelB = ChannelSettings.Builder().also { wb -> wb.name = "B" }.build() + val channelC = ChannelSettings.Builder().also { wb -> wb.name = "C" }.build() val old = listOf(channelA, channelB, channelC) val new = listOf(channelA, channelC, channelB) val writtenIndexes = mutableListOf() @@ -722,16 +829,16 @@ class RadioConfigViewModelTest { @Test fun `updateChannels waits for all ordered writes before success`() = runTest { - val node = Node(num = 123, user = User(id = "!123")) + val node = Node(num = 123, user = User.Builder().also { wb -> wb.id = "!123" }.build()) val packetFlow = MutableSharedFlow() every { serviceRepository.meshPacketFlow } returns packetFlow every { processRadioResponseUseCase(any(), 123, any()) } returns RadioResponseResult.Success nodeRepository.setNodes(listOf(node)) viewModel = createViewModel() - val channelA = ChannelSettings(name = "A") - val channelB = ChannelSettings(name = "B") - val channelC = ChannelSettings(name = "C") + val channelA = ChannelSettings.Builder().also { wb -> wb.name = "A" }.build() + val channelB = ChannelSettings.Builder().also { wb -> wb.name = "B" }.build() + val channelC = ChannelSettings.Builder().also { wb -> wb.name = "C" }.build() val old = listOf(channelA, channelB, channelC) val new = listOf(channelA, channelC, channelB) var nextPacketId = 40 @@ -746,7 +853,11 @@ class RadioConfigViewModelTest { viewModel.updateChannels(new, old) runCurrent() - packetFlow.emit(MeshPacket(decoded = Data(request_id = 41))) + packetFlow.emit( + MeshPacket.Builder() + .also { wb -> wb.decoded = Data.Builder().also { wb -> wb.request_id = 41 }.build() } + .build(), + ) runCurrent() val midBatchState = viewModel.radioConfigState.value.responseState @@ -757,7 +868,11 @@ class RadioConfigViewModelTest { advanceTimeBy(MANUAL_CHANNEL_WRITE_DELAY.inWholeMilliseconds) runCurrent() - packetFlow.emit(MeshPacket(decoded = Data(request_id = 42))) + packetFlow.emit( + MeshPacket.Builder() + .also { wb -> wb.decoded = Data.Builder().also { wb -> wb.request_id = 42 }.build() } + .build(), + ) runCurrent() assertTrue(viewModel.radioConfigState.value.responseState is ResponseState.Success) @@ -765,9 +880,14 @@ class RadioConfigViewModelTest { @Test fun `routing error stops remaining manual channel writes`() = runTest { - val node = Node(num = 123, user = User(id = "!123")) + val node = Node(num = 123, user = User.Builder().also { wb -> wb.id = "!123" }.build()) val packetFlow = MutableSharedFlow() - val old = listOf(ChannelSettings(name = "A"), ChannelSettings(name = "B"), ChannelSettings(name = "C")) + val old = + listOf( + ChannelSettings.Builder().also { wb -> wb.name = "A" }.build(), + ChannelSettings.Builder().also { wb -> wb.name = "B" }.build(), + ChannelSettings.Builder().also { wb -> wb.name = "C" }.build(), + ) val new = listOf(old[0], old[2], old[1]) val writtenIndexes = mutableListOf() @@ -789,7 +909,11 @@ class RadioConfigViewModelTest { runCurrent() assertEquals(listOf(1), writtenIndexes) - packetFlow.emit(MeshPacket(decoded = Data(request_id = 41))) + packetFlow.emit( + MeshPacket.Builder() + .also { wb -> wb.decoded = Data.Builder().also { wb -> wb.request_id = 41 }.build() } + .build(), + ) runCurrent() advanceTimeBy(MANUAL_CHANNEL_WRITE_DELAY.inWholeMilliseconds + 1) runCurrent() @@ -800,11 +924,16 @@ class RadioConfigViewModelTest { @Test fun `routing error cancels manual channel batches queued behind the active batch`() = runTest { - val node = Node(num = 123, user = User(id = "!123")) + val node = Node(num = 123, user = User.Builder().also { wb -> wb.id = "!123" }.build()) val packetFlow = MutableSharedFlow() - val old = listOf(ChannelSettings(name = "A"), ChannelSettings(name = "B"), ChannelSettings(name = "C")) + val old = + listOf( + ChannelSettings.Builder().also { wb -> wb.name = "A" }.build(), + ChannelSettings.Builder().also { wb -> wb.name = "B" }.build(), + ChannelSettings.Builder().also { wb -> wb.name = "C" }.build(), + ) val firstUpdate = listOf(old[0], old[2], old[1]) - val secondUpdate = listOf(old[0], ChannelSettings(name = "D"), old[2]) + val secondUpdate = listOf(old[0], ChannelSettings.Builder().also { wb -> wb.name = "D" }.build(), old[2]) val writtenIndexes = mutableListOf() every { serviceRepository.meshPacketFlow } returns packetFlow @@ -826,7 +955,11 @@ class RadioConfigViewModelTest { viewModel.updateChannels(secondUpdate, old) runCurrent() - packetFlow.emit(MeshPacket(decoded = Data(request_id = 41))) + packetFlow.emit( + MeshPacket.Builder() + .also { wb -> wb.decoded = Data.Builder().also { wb -> wb.request_id = 41 }.build() } + .build(), + ) runCurrent() advanceUntilIdle() @@ -836,14 +969,15 @@ class RadioConfigViewModelTest { @Test fun `updateChannels reconciles applied channel writes when ordered write fails`() = runTest { - val node = Node(num = 123, user = User(id = "!123")) + val node = Node(num = 123, user = User.Builder().also { wb -> wb.id = "!123" }.build()) val old = fourChannelFixture() val (channelA, _, _, channelD) = old val new = listOf(channelA, channelD) val partiallyApplied = listOf(channelA, channelD, old[2], old[3]) val writtenIndexes = mutableListOf() - every { radioConfigRepository.channelSetFlow } returns MutableStateFlow(ChannelSet(settings = old)) + every { radioConfigRepository.channelSetFlow } returns + MutableStateFlow(ChannelSet.Builder().also { wb -> wb.settings = old }.build()) nodeRepository.setNodes(listOf(node)) nodeRepository.setMyNodeInfo(myNodeInfo(myNodeNum = 123)) viewModel = createViewModel() @@ -865,13 +999,13 @@ class RadioConfigViewModelTest { assertEquals(listOf(1, 2), writtenIndexes) assertEquals(partiallyApplied, viewModel.radioConfigState.value.channelList) assertTrue(viewModel.radioConfigState.value.responseState is ResponseState.Error) - verifySuspend { packetRepository.migrateChannelsByPSK(old, partiallyApplied) } + // replaceAllSettings re-keys the conversations itself, so the partial set must still reach it. verifySuspend { radioConfigRepository.replaceAllSettings(partiallyApplied) } } @Test fun `updateChannels serializes overlapping channel saves`() = runTest { - val node = Node(num = 123, user = User(id = "!123")) + val node = Node(num = 123, user = User.Builder().also { wb -> wb.id = "!123" }.build()) val old = fourChannelFixture() val (channelA, _, channelC, channelD) = old val firstNew = listOf(channelA, channelD) @@ -879,7 +1013,8 @@ class RadioConfigViewModelTest { val writtenChannels = mutableListOf() var firstWrite = true - every { radioConfigRepository.channelSetFlow } returns MutableStateFlow(ChannelSet(settings = old)) + every { radioConfigRepository.channelSetFlow } returns + MutableStateFlow(ChannelSet.Builder().also { wb -> wb.settings = old }.build()) nodeRepository.setNodes(listOf(node)) viewModel = createViewModel() runCurrent() @@ -906,12 +1041,12 @@ class RadioConfigViewModelTest { @Test fun `updateChannels does not start manual batch while another radio request is pending`() = runTest { - val node = Node(num = 123, user = User(id = "!123")) + val node = Node(num = 123, user = User.Builder().also { wb -> wb.id = "!123" }.build()) nodeRepository.setNodes(listOf(node)) viewModel = createViewModel() - val old = listOf(ChannelSettings(name = "Old")) - val new = listOf(ChannelSettings(name = "New")) + val old = listOf(ChannelSettings.Builder().also { wb -> wb.name = "Old" }.build()) + val new = listOf(ChannelSettings.Builder().also { wb -> wb.name = "New" }.build()) everySuspend { radioConfigUseCase.getOwner(any(), any()) } calls { @@ -936,12 +1071,18 @@ class RadioConfigViewModelTest { @Test fun `aborted manual batch preserves unrelated pending request`() = runTest { - val node = Node(num = 123, user = User(id = "!123")) + val node = Node(num = 123, user = User.Builder().also { wb -> wb.id = "!123" }.build()) val packetFlow = MutableSharedFlow() val old = fourChannelFixture() val (channelA, _, _, channelD) = old val new = listOf(channelA, channelD) - val owner = User(id = "!123", long_name = "Updated") + val owner = + User.Builder() + .also { wb -> + wb.id = "!123" + wb.long_name = "Updated" + } + .build() var writeCount = 0 every { serviceRepository.meshPacketFlow } returns packetFlow @@ -974,7 +1115,11 @@ class RadioConfigViewModelTest { advanceTimeBy(MANUAL_CHANNEL_WRITE_DELAY.inWholeMilliseconds) runCurrent() - packetFlow.emit(MeshPacket(decoded = Data(request_id = 42))) + packetFlow.emit( + MeshPacket.Builder() + .also { wb -> wb.decoded = Data.Builder().also { wb -> wb.request_id = 42 }.build() } + .build(), + ) runCurrent() assertEquals(owner, viewModel.radioConfigState.value.userConfig) @@ -982,7 +1127,7 @@ class RadioConfigViewModelTest { @Test fun `aborted manual batch cancels batch timeout before packet id reuse`() = runTest { - val node = Node(num = 123, user = User(id = "!123")) + val node = Node(num = 123, user = User.Builder().also { wb -> wb.id = "!123" }.build()) val old = fourChannelFixture() val (channelA, _, _, channelD) = old val new = listOf(channelA, channelD) @@ -1020,9 +1165,30 @@ class RadioConfigViewModelTest { @Test fun `applyManualChannelUpdatePlan paces writes except after final channel`() = runTest { - val channelA = Channel(index = 1, role = Channel.Role.SECONDARY, settings = ChannelSettings(name = "A")) - val channelB = Channel(index = 2, role = Channel.Role.DISABLED, settings = ChannelSettings()) - val channelC = Channel(index = 3, role = Channel.Role.DISABLED, settings = ChannelSettings()) + val channelA = + Channel.Builder() + .also { wb -> + wb.index = 1 + wb.role = Channel.Role.SECONDARY + wb.settings = ChannelSettings.Builder().also { wb -> wb.name = "A" }.build() + } + .build() + val channelB = + Channel.Builder() + .also { wb -> + wb.index = 2 + wb.role = Channel.Role.DISABLED + wb.settings = ChannelSettings.Builder().build() + } + .build() + val channelC = + Channel.Builder() + .also { wb -> + wb.index = 3 + wb.role = Channel.Role.DISABLED + wb.settings = ChannelSettings.Builder().build() + } + .build() val writtenIndexes = mutableListOf() val registeredRequestIds = mutableListOf() val delays = mutableListOf() @@ -1030,8 +1196,8 @@ class RadioConfigViewModelTest { val result = applyManualChannelUpdatePlan( updatePlan = listOf(channelA, channelB, channelC), - currentSettings = listOf(ChannelSettings(name = "old")), - finalSettings = listOf(ChannelSettings(name = "new")), + currentSettings = listOf(ChannelSettings.Builder().also { wb -> wb.name = "old" }.build()), + finalSettings = listOf(ChannelSettings.Builder().also { wb -> wb.name = "new" }.build()), writeChannel = { channel, onRequestId -> writtenIndexes.add(channel.index) val id = channel.index + 100 @@ -1044,16 +1210,37 @@ class RadioConfigViewModelTest { assertEquals(listOf(1, 2, 3), writtenIndexes) assertEquals(listOf(101, 102, 103), result.packetIds) - assertEquals(listOf(ChannelSettings(name = "new")), result.finalSettings) + assertEquals(listOf(ChannelSettings.Builder().also { wb -> wb.name = "new" }.build()), result.finalSettings) assertEquals(listOf(101, 102, 103), registeredRequestIds) assertEquals(listOf(MANUAL_CHANNEL_WRITE_DELAY, MANUAL_CHANNEL_WRITE_DELAY), delays) } @Test fun `applyManualChannelUpdatePlan invokes onInterrupted when writeChannel fails mid-plan`() = runTest { - val channelA = Channel(index = 1, role = Channel.Role.SECONDARY, settings = ChannelSettings(name = "A")) - val channelB = Channel(index = 2, role = Channel.Role.SECONDARY, settings = ChannelSettings(name = "B")) - val channelC = Channel(index = 3, role = Channel.Role.SECONDARY, settings = ChannelSettings(name = "C")) + val channelA = + Channel.Builder() + .also { wb -> + wb.index = 1 + wb.role = Channel.Role.SECONDARY + wb.settings = ChannelSettings.Builder().also { wb -> wb.name = "A" }.build() + } + .build() + val channelB = + Channel.Builder() + .also { wb -> + wb.index = 2 + wb.role = Channel.Role.SECONDARY + wb.settings = ChannelSettings.Builder().also { wb -> wb.name = "B" }.build() + } + .build() + val channelC = + Channel.Builder() + .also { wb -> + wb.index = 3 + wb.role = Channel.Role.SECONDARY + wb.settings = ChannelSettings.Builder().also { wb -> wb.name = "C" }.build() + } + .build() val writtenIndexes = mutableListOf() var interrupted: InterruptedManualChannelUpdate? = null @@ -1062,8 +1249,8 @@ class RadioConfigViewModelTest { assertFailsWith { applyManualChannelUpdatePlan( updatePlan = listOf(channelA, channelB, channelC), - currentSettings = listOf(ChannelSettings(name = "old")), - finalSettings = listOf(ChannelSettings(name = "new")), + currentSettings = listOf(ChannelSettings.Builder().also { wb -> wb.name = "old" }.build()), + finalSettings = listOf(ChannelSettings.Builder().also { wb -> wb.name = "new" }.build()), writeChannel = { channel, onRequestId -> writtenIndexes.add(channel.index) if (channel.index == 2) throw IllegalStateException("boom") @@ -1087,14 +1274,15 @@ class RadioConfigViewModelTest { @Test fun `setResponseStateLoading for REBOOT calls useCase after config response`() = runTest { - val node = Node(num = 123, user = User(id = "!123")) + val node = Node(num = 123, user = User.Builder().also { wb -> wb.id = "!123" }.build()) nodeRepository.setNodes(listOf(node)) val packetFlow = MutableSharedFlow() every { serviceRepository.meshPacketFlow } returns packetFlow // AdminRoute first sends a session key config request; the admin action fires // only after the actual ConfigResponse (not a routing ACK / Success). - every { processRadioResponseUseCase(any(), any(), any()) } returns RadioResponseResult.ConfigResponse(Config()) + every { processRadioResponseUseCase(any(), any(), any()) } returns + RadioResponseResult.ConfigResponse(Config.Builder().build()) viewModel = createViewModel() @@ -1103,21 +1291,22 @@ class RadioConfigViewModelTest { viewModel.setResponseStateLoading(AdminRoute.REBOOT) // Emit a config response packet to trigger processPacketResponse -> sendAdminRequest - packetFlow.emit(MeshPacket()) + packetFlow.emit(MeshPacket.Builder().build()) verifySuspend { adminActionsUseCase.reboot(123, any()) } } @Test fun `setResponseStateLoading for FACTORY_RESET calls useCase after config response`() = runTest { - val node = Node(num = 123, user = User(id = "!123")) + val node = Node(num = 123, user = User.Builder().also { wb -> wb.id = "!123" }.build()) nodeRepository.setNodes(listOf(node)) val packetFlow = MutableSharedFlow() every { serviceRepository.meshPacketFlow } returns packetFlow // AdminRoute first sends a session key config request; the admin action fires // only after the actual ConfigResponse (not a routing ACK / Success). - every { processRadioResponseUseCase(any(), any(), any()) } returns RadioResponseResult.ConfigResponse(Config()) + every { processRadioResponseUseCase(any(), any(), any()) } returns + RadioResponseResult.ConfigResponse(Config.Builder().build()) viewModel = createViewModel() @@ -1126,7 +1315,7 @@ class RadioConfigViewModelTest { viewModel.setResponseStateLoading(AdminRoute.FACTORY_RESET) // Emit a config response packet to trigger processPacketResponse -> sendAdminRequest - packetFlow.emit(MeshPacket()) + packetFlow.emit(MeshPacket.Builder().build()) verifySuspend { adminActionsUseCase.factoryReset(123, any(), any()) } } @@ -1135,13 +1324,14 @@ class RadioConfigViewModelTest { fun `NODEDB_RESET marks an expected local restart`() = runTest { // Firmware reboots after a nodedb reset, so a local reset must open the restart window (like FACTORY_RESET) // or the ensuing transport drop surfaces as a surprise disconnect instead of "restarting". - val node = Node(num = 123, user = User(id = "!123")) + val node = Node(num = 123, user = User.Builder().also { wb -> wb.id = "!123" }.build()) nodeRepository.setNodes(listOf(node)) nodeRepository.setMyNodeInfo(myNodeInfo(myNodeNum = 123)) val packetFlow = MutableSharedFlow() every { serviceRepository.meshPacketFlow } returns packetFlow - every { processRadioResponseUseCase(any(), any(), any()) } returns RadioResponseResult.ConfigResponse(Config()) + every { processRadioResponseUseCase(any(), any(), any()) } returns + RadioResponseResult.ConfigResponse(Config.Builder().build()) viewModel = createViewModel() runCurrent() @@ -1149,7 +1339,7 @@ class RadioConfigViewModelTest { everySuspend { adminActionsUseCase.nodedbReset(any(), any(), any(), any()) } returns 42 viewModel.setResponseStateLoading(AdminRoute.NODEDB_RESET) - packetFlow.emit(MeshPacket()) + packetFlow.emit(MeshPacket.Builder().build()) advanceUntilIdle() verifySuspend { adminActionsUseCase.nodedbReset(123, any(), any(), any()) } @@ -1162,7 +1352,7 @@ class RadioConfigViewModelTest { // Connected, so onConnected never re-fired to close it early). A manual channel batch shares the save shape // (empty route + Loading); a transient drop inside the stale window must not flip the incomplete batch to // success and silently misreport a partial channel write. - val node = Node(num = 123, user = User(id = "!123")) + val node = Node(num = 123, user = User.Builder().also { wb -> wb.id = "!123" }.build()) nodeRepository.setNodes(listOf(node)) nodeRepository.setMyNodeInfo(myNodeInfo(myNodeNum = 123)) @@ -1179,7 +1369,10 @@ class RadioConfigViewModelTest { it.args.onRequestIdArg()(77) 77 } - viewModel.updateChannels(listOf(ChannelSettings(name = "New")), listOf(ChannelSettings(name = "Old"))) + viewModel.updateChannels( + listOf(ChannelSettings.Builder().also { wb -> wb.name = "New" }.build()), + listOf(ChannelSettings.Builder().also { wb -> wb.name = "Old" }.build()), + ) runCurrent() assertTrue(nodeRestartTracker.restartExpected.value) @@ -1205,11 +1398,11 @@ class RadioConfigViewModelTest { @Test fun `setOwner calls useCase`() = runTest { - val node = Node(num = 123, user = User(id = "!123")) + val node = Node(num = 123, user = User.Builder().also { wb -> wb.id = "!123" }.build()) nodeRepository.setNodes(listOf(node)) viewModel = createViewModel() - val user = User(long_name = "Test User") + val user = User.Builder().also { wb -> wb.long_name = "Test User" }.build() everySuspend { radioConfigUseCase.setOwner(any(), any(), any()) } returns 42 viewModel.setOwner(user) @@ -1219,31 +1412,19 @@ class RadioConfigViewModelTest { @Test fun `saveUserConfig sends setHamMode for licensed local node`() = runTest { - val node = Node(num = 123, user = User(id = "!123")) + val node = Node(num = 123, user = User.Builder().also { wb -> wb.id = "!123" }.build()) nodeRepository.setNodes(listOf(node)) nodeRepository.setMyNodeInfo(myNodeInfo(myNodeNum = 123)) viewModel = createViewModel() - val user = User(long_name = "KK7ABC", short_name = "KK7A", is_licensed = true) - everySuspend { radioConfigUseCase.setHamMode(any(), any(), any()) } returns 42 - - viewModel.saveUserConfig(user) - - verifySuspend { - radioConfigUseCase.setHamMode(123, HamParameters(call_sign = "KK7ABC", short_name = "KK7A"), any()) - } - verifySuspend(exactly(0)) { radioConfigUseCase.setOwner(any(), any(), any()) } - } - - @Test - fun `saveUserConfig splits a composed ham name into call sign and long name`() = runTest { - val node = Node(num = 123, user = User(id = "!123")) - nodeRepository.setNodes(listOf(node)) - nodeRepository.setMyNodeInfo(myNodeInfo(myNodeNum = 123)) - viewModel = createViewModel() - - // The User form carries the composed name firmware builds; set_ham_mode wants the halves back. - val user = User(long_name = "KK7ABC//Attic Heltec", short_name = "KK7A", is_licensed = true) + val user = + User.Builder() + .also { wb -> + wb.long_name = "KK7ABC" + wb.short_name = "KK7A" + wb.is_licensed = true + } + .build() everySuspend { radioConfigUseCase.setHamMode(any(), any(), any()) } returns 42 viewModel.saveUserConfig(user) @@ -1251,7 +1432,48 @@ class RadioConfigViewModelTest { verifySuspend { radioConfigUseCase.setHamMode( 123, - HamParameters(call_sign = "KK7ABC", short_name = "KK7A", long_name = "Attic Heltec"), + HamParameters.Builder() + .also { wb -> + wb.call_sign = "KK7ABC" + wb.short_name = "KK7A" + } + .build(), + any(), + ) + } + verifySuspend(exactly(0)) { radioConfigUseCase.setOwner(any(), any(), any()) } + } + + @Test + fun `saveUserConfig splits a composed ham name into call sign and long name`() = runTest { + val node = Node(num = 123, user = User.Builder().also { wb -> wb.id = "!123" }.build()) + nodeRepository.setNodes(listOf(node)) + nodeRepository.setMyNodeInfo(myNodeInfo(myNodeNum = 123)) + viewModel = createViewModel() + + // The User form carries the composed name firmware builds; set_ham_mode wants the halves back. + val user = + User.Builder() + .also { wb -> + wb.long_name = "KK7ABC//Attic Heltec" + wb.short_name = "KK7A" + wb.is_licensed = true + } + .build() + everySuspend { radioConfigUseCase.setHamMode(any(), any(), any()) } returns 42 + + viewModel.saveUserConfig(user) + + verifySuspend { + radioConfigUseCase.setHamMode( + 123, + HamParameters.Builder() + .also { wb -> + wb.call_sign = "KK7ABC" + wb.short_name = "KK7A" + wb.long_name = "Attic Heltec" + } + .build(), any(), ) } @@ -1259,12 +1481,18 @@ class RadioConfigViewModelTest { @Test fun `saveUserConfig sends setOwner for unlicensed user`() = runTest { - val node = Node(num = 123, user = User(id = "!123")) + val node = Node(num = 123, user = User.Builder().also { wb -> wb.id = "!123" }.build()) nodeRepository.setNodes(listOf(node)) nodeRepository.setMyNodeInfo(myNodeInfo(myNodeNum = 123)) viewModel = createViewModel() - val user = User(long_name = "Test User", short_name = "TU") + val user = + User.Builder() + .also { wb -> + wb.long_name = "Test User" + wb.short_name = "TU" + } + .build() everySuspend { radioConfigUseCase.setOwner(any(), any(), any()) } returns 42 viewModel.saveUserConfig(user) @@ -1275,13 +1503,20 @@ class RadioConfigViewModelTest { @Test fun `saveUserConfig never sends setHamMode to a remote node`() = runTest { - val localNode = Node(num = 100, user = User(id = "!100")) - val remoteNode = Node(num = 456, user = User(id = "!456")) + val localNode = Node(num = 100, user = User.Builder().also { wb -> wb.id = "!100" }.build()) + val remoteNode = Node(num = 456, user = User.Builder().also { wb -> wb.id = "!456" }.build()) nodeRepository.setNodes(listOf(localNode, remoteNode)) nodeRepository.setMyNodeInfo(myNodeInfo(myNodeNum = 100)) viewModel = createViewModel(destNum = 456) - val user = User(long_name = "KK7ABC", short_name = "KK7A", is_licensed = true) + val user = + User.Builder() + .also { wb -> + wb.long_name = "KK7ABC" + wb.short_name = "KK7A" + wb.is_licensed = true + } + .build() everySuspend { radioConfigUseCase.setOwner(any(), any(), any()) } returns 42 viewModel.saveUserConfig(user) @@ -1292,19 +1527,26 @@ class RadioConfigViewModelTest { @Test fun `saveUserConfig routes subsequent licensed saves to setOwner`() = runTest { - val node = Node(num = 123, user = User(id = "!123")) + val node = Node(num = 123, user = User.Builder().also { wb -> wb.id = "!123" }.build()) nodeRepository.setNodes(listOf(node)) nodeRepository.setMyNodeInfo(myNodeInfo(myNodeNum = 123)) viewModel = createViewModel() - val user = User(long_name = "KK7ABC", short_name = "KK7A", is_licensed = true) + val user = + User.Builder() + .also { wb -> + wb.long_name = "KK7ABC" + wb.short_name = "KK7A" + wb.is_licensed = true + } + .build() everySuspend { radioConfigUseCase.setHamMode(any(), any(), any()) } returns 42 everySuspend { radioConfigUseCase.setOwner(any(), any(), any()) } returns 43 // First save transitions OFF→ON and onboards via set_ham_mode. viewModel.saveUserConfig(user) // A later save while already licensed must use set_owner so other owner fields propagate. - val edited = user.copy(short_name = "KK7B") + val edited = user.newBuilder().also { wb -> wb.short_name = "KK7B" }.build() viewModel.saveUserConfig(edited) verifySuspend(exactly(1)) { radioConfigUseCase.setHamMode(any(), any(), any()) } @@ -1313,11 +1555,18 @@ class RadioConfigViewModelTest { @Test fun `saveUserConfig routes licensed save to setOwner when myNodeInfo is absent`() = runTest { - val node = Node(num = 123, user = User(id = "!123")) + val node = Node(num = 123, user = User.Builder().also { wb -> wb.id = "!123" }.build()) nodeRepository.setNodes(listOf(node)) viewModel = createViewModel() - val user = User(long_name = "KK7ABC", short_name = "KK7A", is_licensed = true) + val user = + User.Builder() + .also { wb -> + wb.long_name = "KK7ABC" + wb.short_name = "KK7A" + wb.is_licensed = true + } + .build() everySuspend { radioConfigUseCase.setOwner(any(), any(), any()) } returns 42 viewModel.saveUserConfig(user) @@ -1328,7 +1577,7 @@ class RadioConfigViewModelTest { @Test fun `setRingtone calls useCase`() = runTest { - val node = Node(num = 123, user = User(id = "!123")) + val node = Node(num = 123, user = User.Builder().also { wb -> wb.id = "!123" }.build()) nodeRepository.setNodes(listOf(node)) viewModel = createViewModel() @@ -1342,7 +1591,7 @@ class RadioConfigViewModelTest { @Test fun `setCannedMessages calls useCase`() = runTest { - val node = Node(num = 123, user = User(id = "!123")) + val node = Node(num = 123, user = User.Builder().also { wb -> wb.id = "!123" }.build()) nodeRepository.setNodes(listOf(node)) viewModel = createViewModel() @@ -1356,8 +1605,8 @@ class RadioConfigViewModelTest { @Test fun `setRingtone retires the pending route read without stranding loading`() = runTest { - val localNode = Node(num = 100, user = User(id = "!100")) - val remoteNode = Node(num = 456, user = User(id = "!456")) + val localNode = Node(num = 100, user = User.Builder().also { wb -> wb.id = "!100" }.build()) + val remoteNode = Node(num = 456, user = User.Builder().also { wb -> wb.id = "!456" }.build()) everySuspend { radioConfigUseCase.getRingtone(any(), any()) } calls { it.args.onRequestIdArg()(41) @@ -1383,8 +1632,8 @@ class RadioConfigViewModelTest { @Test fun `setCannedMessages retires the pending route read without stranding loading`() = runTest { - val localNode = Node(num = 100, user = User(id = "!100")) - val remoteNode = Node(num = 456, user = User(id = "!456")) + val localNode = Node(num = 100, user = User.Builder().also { wb -> wb.id = "!100" }.build()) + val remoteNode = Node(num = 456, user = User.Builder().also { wb -> wb.id = "!456" }.build()) everySuspend { radioConfigUseCase.getCannedMessages(any(), any()) } calls { it.args.onRequestIdArg()(43) @@ -1410,7 +1659,7 @@ class RadioConfigViewModelTest { @Test fun `destNum from SavedStateHandle resolves destNode`() = runTest { - val node = Node(num = 456, user = User(id = "!456")) + val node = Node(num = 456, user = User.Builder().also { wb -> wb.id = "!456" }.build()) nodeRepository.setNodes(listOf(node)) viewModel = createViewModel(destNum = 456) assertEquals(456, viewModel.destNode.value?.num) @@ -1418,12 +1667,17 @@ class RadioConfigViewModelTest { @Test fun `setModuleConfig calls useCase`() = runTest { - val node = Node(num = 123, user = User(id = "!123")) + val node = Node(num = 123, user = User.Builder().also { wb -> wb.id = "!123" }.build()) nodeRepository.setNodes(listOf(node)) viewModel = createViewModel() val config = - org.meshtastic.proto.ModuleConfig(mqtt = org.meshtastic.proto.ModuleConfig.MQTTConfig(enabled = true)) + org.meshtastic.proto.ModuleConfig.Builder() + .also { wb -> + wb.mqtt = + org.meshtastic.proto.ModuleConfig.MQTTConfig.Builder().also { wb -> wb.enabled = true }.build() + } + .build() everySuspend { radioConfigUseCase.setModuleConfig(any(), any(), any()) } returns 42 viewModel.setModuleConfig(config) @@ -1434,13 +1688,13 @@ class RadioConfigViewModelTest { @Test fun `setModuleConfig merges mesh_beacon into state before the radio answers`() = runTest { - val node = Node(num = 123, user = User(id = "!123")) + val node = Node(num = 123, user = User.Builder().also { wb -> wb.id = "!123" }.build()) nodeRepository.setNodes(listOf(node)) viewModel = createViewModel() - val beacon = MeshBeaconConfig(broadcast_message = "join us") + val beacon = MeshBeaconConfig.Builder().also { wb -> wb.broadcast_message = "join us" }.build() everySuspend { radioConfigUseCase.setModuleConfig(any(), any(), any()) } returns 42 - viewModel.setModuleConfig(ModuleConfig(mesh_beacon = beacon)) + viewModel.setModuleConfig(ModuleConfig.Builder().also { wb -> wb.mesh_beacon = beacon }.build()) // Pins the optimistic mesh_beacon merge: a second save in the same session reads this, not the radio's reply. assertEquals(beacon, viewModel.radioConfigState.value.moduleConfig.mesh_beacon) @@ -1448,7 +1702,7 @@ class RadioConfigViewModelTest { @Test fun `setFixedPosition calls useCase`() = runTest { - val node = Node(num = 123, user = User(id = "!123")) + val node = Node(num = 123, user = User.Builder().also { wb -> wb.id = "!123" }.build()) nodeRepository.setNodes(listOf(node)) viewModel = createViewModel() @@ -1462,7 +1716,7 @@ class RadioConfigViewModelTest { @Test fun `removeFixedPosition calls useCase`() = runTest { - val node = Node(num = 123, user = User(id = "!123")) + val node = Node(num = 123, user = User.Builder().also { wb -> wb.id = "!123" }.build()) nodeRepository.setNodes(listOf(node)) viewModel = createViewModel() @@ -1475,11 +1729,11 @@ class RadioConfigViewModelTest { @Test fun `installProfile calls useCase`() = runTest { - val node = Node(num = 123, user = User(id = "!123")) + val node = Node(num = 123, user = User.Builder().also { wb -> wb.id = "!123" }.build()) nodeRepository.setNodes(listOf(node)) viewModel = createViewModel() - val profile = DeviceProfile() + val profile = DeviceProfile.Builder().build() everySuspend { installProfileUseCase(any(), any(), any(), any(), any()) } returns Unit viewModel.installProfile(profile) @@ -1489,10 +1743,10 @@ class RadioConfigViewModelTest { @Test fun `installProfile surfaces malformed channel URL in snackbar`() = runTest { - val node = Node(num = 123, user = User(id = "!123")) + val node = Node(num = 123, user = User.Builder().also { wb -> wb.id = "!123" }.build()) nodeRepository.setNodes(listOf(node)) viewModel = createViewModel() - val profile = DeviceProfile(channel_url = "not-a-channel-url") + val profile = DeviceProfile.Builder().also { wb -> wb.channel_url = "not-a-channel-url" }.build() everySuspend { installProfileUseCase(any(), any(), any(), any(), any()) } calls { throw MalformedMeshtasticUrlException("bad profile") @@ -1510,7 +1764,7 @@ class RadioConfigViewModelTest { @Test fun `processPacketResponse updates state on various results`() = runTest { - val node = Node(num = 123, user = User(id = "!123")) + val node = Node(num = 123, user = User.Builder().also { wb -> wb.id = "!123" }.build()) nodeRepository.setNodes(listOf(node)) val packetFlow = MutableSharedFlow() every { serviceRepository.meshPacketFlow } returns packetFlow @@ -1518,43 +1772,51 @@ class RadioConfigViewModelTest { viewModel = createViewModel() // ConfigResponse - val configResponse = Config(lora = Config.LoRaConfig(hop_limit = 5)) + val configResponse = + Config.Builder() + .also { wb -> wb.lora = Config.LoRaConfig.Builder().also { wb -> wb.hop_limit = 5 }.build() } + .build() every { processRadioResponseUseCase(any(), 123, any()) } returns RadioResponseResult.ConfigResponse(configResponse) - packetFlow.emit(MeshPacket()) + packetFlow.emit(MeshPacket.Builder().build()) assertEquals(5, viewModel.radioConfigState.value.radioConfig.lora?.hop_limit) // ModuleConfigResponse val moduleResponse = - org.meshtastic.proto.ModuleConfig( - telemetry = org.meshtastic.proto.ModuleConfig.TelemetryConfig(device_update_interval = 300), - ) + org.meshtastic.proto.ModuleConfig.Builder() + .also { wb -> + wb.telemetry = + org.meshtastic.proto.ModuleConfig.TelemetryConfig.Builder() + .also { wb -> wb.device_update_interval = 300 } + .build() + } + .build() every { processRadioResponseUseCase(any(), 123, any()) } returns RadioResponseResult.ModuleConfigResponse(moduleResponse) - packetFlow.emit(MeshPacket()) + packetFlow.emit(MeshPacket.Builder().build()) assertEquals(300, viewModel.radioConfigState.value.moduleConfig.telemetry?.device_update_interval) // Owner - val user = User(long_name = "New Name") + val user = User.Builder().also { wb -> wb.long_name = "New Name" }.build() every { processRadioResponseUseCase(any(), 123, any()) } returns RadioResponseResult.Owner(user) - packetFlow.emit(MeshPacket()) + packetFlow.emit(MeshPacket.Builder().build()) assertEquals("New Name", viewModel.radioConfigState.value.userConfig.long_name) // Ringtone every { processRadioResponseUseCase(any(), 123, any()) } returns RadioResponseResult.Ringtone("bell.mp3") - packetFlow.emit(MeshPacket()) + packetFlow.emit(MeshPacket.Builder().build()) assertEquals("bell.mp3", viewModel.radioConfigState.value.ringtone) // Error every { processRadioResponseUseCase(any(), 123, any()) } returns RadioResponseResult.Error(org.meshtastic.core.resources.UiText.DynamicString("Fail")) - packetFlow.emit(MeshPacket()) + packetFlow.emit(MeshPacket.Builder().build()) assertTrue(viewModel.radioConfigState.value.responseState is ResponseState.Error) } @Test fun `routing error clears request timeout without replacing the specific failure`() = runTest { - val node = Node(num = 123, user = User(id = "!123")) + val node = Node(num = 123, user = User.Builder().also { wb -> wb.id = "!123" }.build()) nodeRepository.setNodes(listOf(node)) val packetFlow = MutableSharedFlow() every { serviceRepository.meshPacketFlow } returns packetFlow @@ -1571,7 +1833,7 @@ class RadioConfigViewModelTest { val failure = org.meshtastic.core.resources.UiText.DynamicString("Max Retransmission Reached") every { processRadioResponseUseCase(any(), 123, any()) } returns RadioResponseResult.Error(failure) - packetFlow.emit(MeshPacket()) + packetFlow.emit(MeshPacket.Builder().build()) runCurrent() assertEquals(ResponseState.Error(failure), viewModel.radioConfigState.value.responseState) @@ -1584,11 +1846,16 @@ class RadioConfigViewModelTest { @Test fun `remote read accepts response after max retransmit before request deadline`() = runTest { - val localNode = Node(num = 100, user = User(id = "!100")) - val remoteNode = Node(num = 456, user = User(id = "!456")) + val localNode = Node(num = 100, user = User.Builder().also { wb -> wb.id = "!100" }.build()) + val remoteNode = Node(num = 456, user = User.Builder().also { wb -> wb.id = "!456" }.build()) val packetFlow = MutableSharedFlow() val maxRetransmit = org.meshtastic.core.resources.UiText.DynamicString("Max Retransmission Reached") - val config = Config(device = Config.DeviceConfig(node_info_broadcast_secs = 900)) + val config = + Config.Builder() + .also { wb -> + wb.device = Config.DeviceConfig.Builder().also { wb -> wb.node_info_broadcast_secs = 900 }.build() + } + .build() var response: RadioResponseResult = RadioResponseResult.Error(maxRetransmit, Routing.Error.MAX_RETRANSMIT) every { serviceRepository.meshPacketFlow } returns packetFlow @@ -1604,13 +1871,21 @@ class RadioConfigViewModelTest { viewModel.loadConfigRoute(ConfigRoute.DEVICE) runCurrent() - packetFlow.emit(MeshPacket(decoded = Data(request_id = 42))) + packetFlow.emit( + MeshPacket.Builder() + .also { wb -> wb.decoded = Data.Builder().also { wb -> wb.request_id = 42 }.build() } + .build(), + ) runCurrent() assertTrue(viewModel.radioConfigState.value.responseState is ResponseState.Loading) response = RadioResponseResult.ConfigResponse(config) - packetFlow.emit(MeshPacket(decoded = Data(request_id = 42))) + packetFlow.emit( + MeshPacket.Builder() + .also { wb -> wb.decoded = Data.Builder().also { wb -> wb.request_id = 42 }.build() } + .build(), + ) runCurrent() assertEquals(config.device, viewModel.radioConfigState.value.radioConfig.device) @@ -1623,8 +1898,8 @@ class RadioConfigViewModelTest { @Test fun `remote read surfaces max retransmit at deadline and accepts later response`() = runTest { - val localNode = Node(num = 100, user = User(id = "!100")) - val remoteNode = Node(num = 456, user = User(id = "!456")) + val localNode = Node(num = 100, user = User.Builder().also { wb -> wb.id = "!100" }.build()) + val remoteNode = Node(num = 456, user = User.Builder().also { wb -> wb.id = "!456" }.build()) val packetFlow = MutableSharedFlow() val maxRetransmit = org.meshtastic.core.resources.UiText.DynamicString("Max Retransmission Reached") var nextRequestId = 42 @@ -1635,7 +1910,12 @@ class RadioConfigViewModelTest { it.args.onRequestIdArg()(nextRequestId) nextRequestId } - val config = Config(device = Config.DeviceConfig(node_info_broadcast_secs = 900)) + val config = + Config.Builder() + .also { wb -> + wb.device = Config.DeviceConfig.Builder().also { wb -> wb.node_info_broadcast_secs = 900 }.build() + } + .build() var response: RadioResponseResult = RadioResponseResult.Error(maxRetransmit, Routing.Error.MAX_RETRANSMIT) every { processRadioResponseUseCase(any(), 456, any()) } calls { @@ -1648,7 +1928,11 @@ class RadioConfigViewModelTest { viewModel.loadConfigRoute(ConfigRoute.DEVICE) runCurrent() - packetFlow.emit(MeshPacket(decoded = Data(request_id = 42))) + packetFlow.emit( + MeshPacket.Builder() + .also { wb -> wb.decoded = Data.Builder().also { wb -> wb.request_id = 42 }.build() } + .build(), + ) runCurrent() assertTrue(viewModel.radioConfigState.value.responseState is ResponseState.Loading) @@ -1665,13 +1949,21 @@ class RadioConfigViewModelTest { assertTrue(viewModel.radioConfigState.value.responseState is ResponseState.Loading) response = RadioResponseResult.Success - packetFlow.emit(MeshPacket(decoded = Data(request_id = 42))) + packetFlow.emit( + MeshPacket.Builder() + .also { wb -> wb.decoded = Data.Builder().also { wb -> wb.request_id = 42 }.build() } + .build(), + ) runCurrent() assertTrue(viewModel.radioConfigState.value.responseState is ResponseState.Loading) response = RadioResponseResult.ConfigResponse(config) advanceTimeBy(5_000) - packetFlow.emit(MeshPacket(decoded = Data(request_id = 42))) + packetFlow.emit( + MeshPacket.Builder() + .also { wb -> wb.decoded = Data.Builder().also { wb -> wb.request_id = 42 }.build() } + .build(), + ) runCurrent() assertEquals(config.device, viewModel.radioConfigState.value.radioConfig.device) @@ -1684,11 +1976,16 @@ class RadioConfigViewModelTest { @Test fun `late remote read clears its deadline error without a retry`() = runTest { - val localNode = Node(num = 100, user = User(id = "!100")) - val remoteNode = Node(num = 456, user = User(id = "!456")) + val localNode = Node(num = 100, user = User.Builder().also { wb -> wb.id = "!100" }.build()) + val remoteNode = Node(num = 456, user = User.Builder().also { wb -> wb.id = "!456" }.build()) val packetFlow = MutableSharedFlow() val maxRetransmit = org.meshtastic.core.resources.UiText.DynamicString("Max Retransmission Reached") - val config = Config(device = Config.DeviceConfig(node_info_broadcast_secs = 900)) + val config = + Config.Builder() + .also { wb -> + wb.device = Config.DeviceConfig.Builder().also { wb -> wb.node_info_broadcast_secs = 900 }.build() + } + .build() var response: RadioResponseResult = RadioResponseResult.Error(maxRetransmit, Routing.Error.MAX_RETRANSMIT) every { serviceRepository.meshPacketFlow } returns packetFlow @@ -1709,14 +2006,22 @@ class RadioConfigViewModelTest { viewModel.loadConfigRoute(ConfigRoute.DEVICE) runCurrent() - packetFlow.emit(MeshPacket(decoded = Data(request_id = 42))) + packetFlow.emit( + MeshPacket.Builder() + .also { wb -> wb.decoded = Data.Builder().also { wb -> wb.request_id = 42 }.build() } + .build(), + ) runCurrent() advanceTimeBy(30_001) runCurrent() assertEquals(ResponseState.Error(maxRetransmit), viewModel.radioConfigState.value.responseState) response = RadioResponseResult.ConfigResponse(config) - packetFlow.emit(MeshPacket(decoded = Data(request_id = 42))) + packetFlow.emit( + MeshPacket.Builder() + .also { wb -> wb.decoded = Data.Builder().also { wb -> wb.request_id = 42 }.build() } + .build(), + ) runCurrent() assertEquals(config.device, viewModel.radioConfigState.value.radioConfig.device) @@ -1725,12 +2030,22 @@ class RadioConfigViewModelTest { @Test fun `late owner read is retained when the user route reads only the owner`() = runTest { - val localNode = Node(num = 100, user = User(id = "!100")) + val localNode = Node(num = 100, user = User.Builder().also { wb -> wb.id = "!100" }.build()) val remoteNode = - Node(num = 456, user = User(id = "!456"), metadata = DeviceMetadata(firmware_version = "2.7.21")) + Node( + num = 456, + user = User.Builder().also { wb -> wb.id = "!456" }.build(), + metadata = DeviceMetadata.Builder().also { wb -> wb.firmware_version = "2.7.21" }.build(), + ) val packetFlow = MutableSharedFlow() val maxRetransmit = org.meshtastic.core.resources.UiText.DynamicString("Max Retransmission Reached") - val owner = User(id = "!456", long_name = "Late") + val owner = + User.Builder() + .also { wb -> + wb.id = "!456" + wb.long_name = "Late" + } + .build() var response: RadioResponseResult = RadioResponseResult.Error(maxRetransmit, Routing.Error.MAX_RETRANSMIT) every { serviceRepository.meshPacketFlow } returns packetFlow @@ -1751,7 +2066,11 @@ class RadioConfigViewModelTest { viewModel.loadConfigRoute(ConfigRoute.USER) runCurrent() - packetFlow.emit(MeshPacket(decoded = Data(request_id = 42))) + packetFlow.emit( + MeshPacket.Builder() + .also { wb -> wb.decoded = Data.Builder().also { wb -> wb.request_id = 42 }.build() } + .build(), + ) runCurrent() advanceTimeBy(30_001) runCurrent() @@ -1760,7 +2079,11 @@ class RadioConfigViewModelTest { // Firmware without the status message module answers one get, so the route is a single-response read // whatever ConfigRoute.USER.hasReadFanOut says, and the late owner still lands. response = RadioResponseResult.Owner(owner) - packetFlow.emit(MeshPacket(decoded = Data(request_id = 42))) + packetFlow.emit( + MeshPacket.Builder() + .also { wb -> wb.decoded = Data.Builder().also { wb -> wb.request_id = 42 }.build() } + .build(), + ) runCurrent() assertEquals(owner, viewModel.radioConfigState.value.userConfig) @@ -1769,10 +2092,15 @@ class RadioConfigViewModelTest { @Test fun `late remote read routing ack cannot complete a newer save`() = runTest { - val localNode = Node(num = 100, user = User(id = "!100")) - val remoteNode = Node(num = 456, user = User(id = "!456")) + val localNode = Node(num = 100, user = User.Builder().also { wb -> wb.id = "!100" }.build()) + val remoteNode = Node(num = 456, user = User.Builder().also { wb -> wb.id = "!456" }.build()) val packetFlow = MutableSharedFlow() - val config = Config(device = Config.DeviceConfig(node_info_broadcast_secs = 900)) + val config = + Config.Builder() + .also { wb -> + wb.device = Config.DeviceConfig.Builder().also { wb -> wb.node_info_broadcast_secs = 900 }.build() + } + .build() every { serviceRepository.meshPacketFlow } returns packetFlow everySuspend { radioConfigUseCase.getConfig(any(), any(), any()) } calls @@ -1806,7 +2134,11 @@ class RadioConfigViewModelTest { runCurrent() assertEquals(ResponseState.Loading(), viewModel.radioConfigState.value.responseState) - packetFlow.emit(MeshPacket(decoded = Data(request_id = 42))) + packetFlow.emit( + MeshPacket.Builder() + .also { wb -> wb.decoded = Data.Builder().also { wb -> wb.request_id = 42 }.build() } + .build(), + ) runCurrent() assertEquals( @@ -1815,19 +2147,33 @@ class RadioConfigViewModelTest { "the retained read ACK must not consume the active save request", ) - packetFlow.emit(MeshPacket(decoded = Data(request_id = 43))) + packetFlow.emit( + MeshPacket.Builder() + .also { wb -> wb.decoded = Data.Builder().also { wb -> wb.request_id = 43 }.build() } + .build(), + ) runCurrent() assertTrue(viewModel.radioConfigState.value.responseState is ResponseState.Success) } @Test fun `newer save supersedes a retained read and its retry`() = runTest { - val localNode = Node(num = 100, user = User(id = "!100")) - val remoteNode = Node(num = 456, user = User(id = "!456")) + val localNode = Node(num = 100, user = User.Builder().also { wb -> wb.id = "!100" }.build()) + val remoteNode = Node(num = 456, user = User.Builder().also { wb -> wb.id = "!456" }.build()) val packetFlow = MutableSharedFlow() val maxRetransmit = org.meshtastic.core.resources.UiText.DynamicString("Max Retransmission Reached") - val staleConfig = Config(device = Config.DeviceConfig(node_info_broadcast_secs = 900)) - val savedConfig = Config(device = Config.DeviceConfig(node_info_broadcast_secs = 300)) + val staleConfig = + Config.Builder() + .also { wb -> + wb.device = Config.DeviceConfig.Builder().also { wb -> wb.node_info_broadcast_secs = 900 }.build() + } + .build() + val savedConfig = + Config.Builder() + .also { wb -> + wb.device = Config.DeviceConfig.Builder().also { wb -> wb.node_info_broadcast_secs = 300 }.build() + } + .build() var nextReadId = 42 every { serviceRepository.meshPacketFlow } returns packetFlow @@ -1869,7 +2215,11 @@ class RadioConfigViewModelTest { runCurrent() assertEquals(ResponseState.Loading(), viewModel.radioConfigState.value.responseState) - packetFlow.emit(MeshPacket(decoded = Data(request_id = 42))) + packetFlow.emit( + MeshPacket.Builder() + .also { wb -> wb.decoded = Data.Builder().also { wb -> wb.request_id = 42 }.build() } + .build(), + ) runCurrent() assertEquals( ResponseState.Loading(), @@ -1878,7 +2228,11 @@ class RadioConfigViewModelTest { ) assertEquals(savedConfig.device, viewModel.radioConfigState.value.radioConfig.device) - packetFlow.emit(MeshPacket(decoded = Data(request_id = 44))) + packetFlow.emit( + MeshPacket.Builder() + .also { wb -> wb.decoded = Data.Builder().also { wb -> wb.request_id = 44 }.build() } + .build(), + ) runCurrent() assertTrue(viewModel.radioConfigState.value.responseState is ResponseState.Success) @@ -1889,11 +2243,16 @@ class RadioConfigViewModelTest { @Test fun `remote write keeps max retransmit terminal`() = runTest { - val localNode = Node(num = 100, user = User(id = "!100")) - val remoteNode = Node(num = 456, user = User(id = "!456")) + val localNode = Node(num = 100, user = User.Builder().also { wb -> wb.id = "!100" }.build()) + val remoteNode = Node(num = 456, user = User.Builder().also { wb -> wb.id = "!456" }.build()) val packetFlow = MutableSharedFlow() val maxRetransmit = org.meshtastic.core.resources.UiText.DynamicString("Max Retransmission Reached") - val config = Config(device = Config.DeviceConfig(node_info_broadcast_secs = 900)) + val config = + Config.Builder() + .also { wb -> + wb.device = Config.DeviceConfig.Builder().also { wb -> wb.node_info_broadcast_secs = 900 }.build() + } + .build() every { serviceRepository.meshPacketFlow } returns packetFlow everySuspend { radioConfigUseCase.setConfig(any(), any(), any()) } calls @@ -1909,7 +2268,11 @@ class RadioConfigViewModelTest { viewModel.setConfig(config) runCurrent() - packetFlow.emit(MeshPacket(decoded = Data(request_id = 42))) + packetFlow.emit( + MeshPacket.Builder() + .also { wb -> wb.decoded = Data.Builder().also { wb -> wb.request_id = 42 }.build() } + .build(), + ) runCurrent() assertEquals(ResponseState.Error(maxRetransmit), viewModel.radioConfigState.value.responseState) @@ -1921,7 +2284,7 @@ class RadioConfigViewModelTest { @Test fun `Admin actions call correct useCases`() = runTest { - val node = Node(num = 123, user = User(id = "!123")) + val node = Node(num = 123, user = User.Builder().also { wb -> wb.id = "!123" }.build()) nodeRepository.setNodes(listOf(node)) val packetFlow = MutableSharedFlow() every { serviceRepository.meshPacketFlow } returns packetFlow @@ -1932,27 +2295,29 @@ class RadioConfigViewModelTest { everySuspend { adminActionsUseCase.shutdown(any(), any()) } returns 42 // Set metadata to allow shutdown every { processRadioResponseUseCase(any(), 123, any()) } returns - RadioResponseResult.Metadata(DeviceMetadata(canShutdown = true)) - packetFlow.emit(MeshPacket()) + RadioResponseResult.Metadata(DeviceMetadata.Builder().also { wb -> wb.canShutdown = true }.build()) + packetFlow.emit(MeshPacket.Builder().build()) viewModel.setResponseStateLoading(AdminRoute.SHUTDOWN) // AdminRoute fires sendAdminRequest after receiving ConfigResponse (session key), // not after a routing ACK (Success). - every { processRadioResponseUseCase(any(), 123, any()) } returns RadioResponseResult.ConfigResponse(Config()) - packetFlow.emit(MeshPacket()) + every { processRadioResponseUseCase(any(), 123, any()) } returns + RadioResponseResult.ConfigResponse(Config.Builder().build()) + packetFlow.emit(MeshPacket.Builder().build()) verifySuspend { adminActionsUseCase.shutdown(123, any()) } // NODEDB_RESET everySuspend { adminActionsUseCase.nodedbReset(any(), any(), any(), any()) } returns 42 viewModel.setResponseStateLoading(AdminRoute.NODEDB_RESET) - every { processRadioResponseUseCase(any(), 123, any()) } returns RadioResponseResult.ConfigResponse(Config()) - packetFlow.emit(MeshPacket()) + every { processRadioResponseUseCase(any(), 123, any()) } returns + RadioResponseResult.ConfigResponse(Config.Builder().build()) + packetFlow.emit(MeshPacket.Builder().build()) verifySuspend { adminActionsUseCase.nodedbReset(123, any(), any(), any()) } } @Test fun `setResponseStateLoading for various routes calls correct useCases`() = runTest { - val node = Node(num = 123, user = User(id = "!123")) + val node = Node(num = 123, user = User.Builder().also { wb -> wb.id = "!123" }.build()) nodeRepository.setNodes(listOf(node)) viewModel = createViewModel() @@ -1982,7 +2347,7 @@ class RadioConfigViewModelTest { @Test fun `loadConfigRoute hides progress overlay for local settings refresh`() = runTest { - val localNode = Node(num = 123, user = User(id = "!123")) + val localNode = Node(num = 123, user = User.Builder().also { wb -> wb.id = "!123" }.build()) nodeRepository.setNodes(listOf(localNode)) nodeRepository.setMyNodeInfo(myNodeInfo(myNodeNum = 123)) viewModel = createViewModel(destNum = null) @@ -2003,8 +2368,8 @@ class RadioConfigViewModelTest { @Test fun `loadConfigRoute shows progress overlay for remote settings refresh`() = runTest { - val localNode = Node(num = 100, user = User(id = "!100")) - val remoteNode = Node(num = 456, user = User(id = "!456")) + val localNode = Node(num = 100, user = User.Builder().also { wb -> wb.id = "!100" }.build()) + val remoteNode = Node(num = 456, user = User.Builder().also { wb -> wb.id = "!456" }.build()) nodeRepository.setNodes(listOf(localNode, remoteNode)) nodeRepository.setMyNodeInfo(myNodeInfo(myNodeNum = 100)) viewModel = createViewModel(destNum = 456) @@ -2025,7 +2390,7 @@ class RadioConfigViewModelTest { @Test fun `registerRequestId timeout clears request and sets error`() = runTest { - val node = Node(num = 123, user = User(id = "!123")) + val node = Node(num = 123, user = User.Builder().also { wb -> wb.id = "!123" }.build()) nodeRepository.setNodes(listOf(node)) viewModel = createViewModel() @@ -2051,8 +2416,8 @@ class RadioConfigViewModelTest { @Test fun `ensureLoadingForRemote sets loading state for remote nodes`() = runTest { - val localNode = Node(num = 100, user = User(id = "!100")) - val remoteNode = Node(num = 456, user = User(id = "!456")) + val localNode = Node(num = 100, user = User.Builder().also { wb -> wb.id = "!100" }.build()) + val remoteNode = Node(num = 456, user = User.Builder().also { wb -> wb.id = "!456" }.build()) nodeRepository.setNodes(listOf(localNode, remoteNode)) nodeRepository.setMyNodeInfo(myNodeInfo(myNodeNum = 100)) @@ -2077,8 +2442,8 @@ class RadioConfigViewModelTest { @Test fun `ensureLoadingForRemote is no-op when already loading`() = runTest { - val localNode = Node(num = 100, user = User(id = "!100")) - val remoteNode = Node(num = 456, user = User(id = "!456")) + val localNode = Node(num = 100, user = User.Builder().also { wb -> wb.id = "!100" }.build()) + val remoteNode = Node(num = 456, user = User.Builder().also { wb -> wb.id = "!456" }.build()) nodeRepository.setNodes(listOf(localNode, remoteNode)) nodeRepository.setMyNodeInfo(myNodeInfo(myNodeNum = 100)) @@ -2095,7 +2460,7 @@ class RadioConfigViewModelTest { @Test fun `local destination exposes its PlatformIO target`() = runTest { - val localNode = Node(num = 100, user = User(id = "!100")) + val localNode = Node(num = 100, user = User.Builder().also { wb -> wb.id = "!100" }.build()) nodeRepository.setNodes(listOf(localNode)) nodeRepository.setMyNodeInfo(myNodeInfo(myNodeNum = 100, pioEnv = "tlora-v2-1-1_8")) @@ -2108,7 +2473,7 @@ class RadioConfigViewModelTest { @Test fun `local destination updates its PlatformIO target when identity is unchanged`() = runTest { - val localNode = Node(num = 100, user = User(id = "!100")) + val localNode = Node(num = 100, user = User.Builder().also { wb -> wb.id = "!100" }.build()) nodeRepository.setNodes(listOf(localNode)) nodeRepository.setMyNodeInfo(myNodeInfo(myNodeNum = 100, pioEnv = "tlora-t3s3-v1")) val localVm = createViewModel(destNum = 100) @@ -2140,8 +2505,8 @@ class RadioConfigViewModelTest { @Test fun `remote destination never inherits gateway PlatformIO target`() = runTest { - val localNode = Node(num = 100, user = User(id = "!100")) - val remoteNode = Node(num = 456, user = User(id = "!456")) + val localNode = Node(num = 100, user = User.Builder().also { wb -> wb.id = "!100" }.build()) + val remoteNode = Node(num = 456, user = User.Builder().also { wb -> wb.id = "!456" }.build()) nodeRepository.setNodes(listOf(localNode, remoteNode)) nodeRepository.setMyNodeInfo(myNodeInfo(myNodeNum = 100, pioEnv = "tlora-v2-1-1_8")) @@ -2154,7 +2519,7 @@ class RadioConfigViewModelTest { @Test fun `loraRegionPresetMapFlow populates state`() = runTest { - val node = Node(num = 123, user = User(id = "!123")) + val node = Node(num = 123, user = User.Builder().also { wb -> wb.id = "!123" }.build()) nodeRepository.setNodes(listOf(node)) val mapFlow = MutableStateFlow(null) every { radioConfigRepository.loraRegionPresetMapFlow } returns mapFlow @@ -2163,7 +2528,12 @@ class RadioConfigViewModelTest { assertEquals(null, viewModel.radioConfigState.value.loraRegionPresetMap) - val map = LoRaRegionPresetMap(groups = listOf(LoRaPresetGroup(licensed_only = true))) + val map = + LoRaRegionPresetMap.Builder() + .also { wb -> + wb.groups = listOf(LoRaPresetGroup.Builder().also { wb -> wb.licensed_only = true }.build()) + } + .build() mapFlow.value = map runCurrent() @@ -2172,7 +2542,17 @@ class RadioConfigViewModelTest { @Test fun `localIsLicensed tracks the destination node's is_licensed flag`() = runTest { - val node = Node(num = 123, user = User(id = "!123", is_licensed = true)) + val node = + Node( + num = 123, + user = + User.Builder() + .also { wb -> + wb.id = "!123" + wb.is_licensed = true + } + .build(), + ) nodeRepository.setNodes(listOf(node)) viewModel = createViewModel() runCurrent() @@ -2185,26 +2565,46 @@ class RadioConfigViewModelTest { // A firmware region swap (e.g. EU sibling) is applied live; the form must reflect the device's actual // value, which is re-read on next LoRa-screen entry — NOT applied optimistically here, and the save ACK // must not trigger an in-place re-read (that would suppress the normal save-success UX). - val node = Node(num = 123, user = User(id = "!123")) + val node = Node(num = 123, user = User.Builder().also { wb -> wb.id = "!123" }.build()) nodeRepository.setNodes(listOf(node)) nodeRepository.setMyNodeInfo(myNodeInfo(myNodeNum = 123)) val packetFlow = MutableSharedFlow() every { serviceRepository.meshPacketFlow } returns packetFlow every { radioConfigRepository.localConfigFlow } returns - MutableStateFlow(LocalConfig(lora = Config.LoRaConfig(region = Config.LoRaConfig.RegionCode.EU_868))) + MutableStateFlow( + LocalConfig.Builder() + .also { wb -> + wb.lora = + Config.LoRaConfig.Builder() + .also { wb -> wb.region = Config.LoRaConfig.RegionCode.EU_868 } + .build() + } + .build(), + ) viewModel = createViewModel(destNum = 123) runCurrent() everySuspend { radioConfigUseCase.setConfig(any(), any(), any()) } returns 42 - viewModel.setConfig(Config(lora = Config.LoRaConfig(region = Config.LoRaConfig.RegionCode.US))) + viewModel.setConfig( + Config.Builder() + .also { wb -> + wb.lora = + Config.LoRaConfig.Builder().also { wb -> wb.region = Config.LoRaConfig.RegionCode.US }.build() + } + .build(), + ) runCurrent() // The optimistic state still reflects the device's value, not the requested region. assertEquals(Config.LoRaConfig.RegionCode.EU_868, viewModel.radioConfigState.value.radioConfig.lora?.region) every { processRadioResponseUseCase(any(), 123, any()) } returns RadioResponseResult.Success - packetFlow.emit(MeshPacket(decoded = Data(request_id = 42))) + packetFlow.emit( + MeshPacket.Builder() + .also { wb -> wb.decoded = Data.Builder().also { wb -> wb.request_id = 42 }.build() } + .build(), + ) runCurrent() verifySuspend(exactly(0)) { radioConfigUseCase.getConfig(any(), any(), any()) } @@ -2212,24 +2612,30 @@ class RadioConfigViewModelTest { @Test fun `backupSecurityKeys refuses to persist empty keys`() = runTest { - val node = Node(num = 123, user = User(id = "!123")) + val node = Node(num = 123, user = User.Builder().also { wb -> wb.id = "!123" }.build()) nodeRepository.setNodes(listOf(node)) viewModel = createViewModel() // Empty SecurityConfig is the fallback before the device's config response arrives — backing it up and later // restoring it would wipe the device's real keys with blanks. - viewModel.backupSecurityKeys(Config.SecurityConfig()) + viewModel.backupSecurityKeys(Config.SecurityConfig.Builder().build()) verify(exactly(0)) { securityKeyBackupStore.save(any(), any(), any(), any()) } } @Test fun `backupSecurityKeys persists real keys`() = runTest { - val node = Node(num = 123, user = User(id = "!123")) + val node = Node(num = 123, user = User.Builder().also { wb -> wb.id = "!123" }.build()) nodeRepository.setNodes(listOf(node)) viewModel = createViewModel() - val config = Config.SecurityConfig(public_key = "pub".encodeUtf8(), private_key = "priv".encodeUtf8()) + val config = + Config.SecurityConfig.Builder() + .also { wb -> + wb.public_key = "pub".encodeUtf8() + wb.private_key = "priv".encodeUtf8() + } + .build() viewModel.backupSecurityKeys(config) // Pin the exact base64 so a public/private swap or encoding change is caught. @@ -2238,7 +2644,7 @@ class RadioConfigViewModelTest { @Test fun `restoreSecurityKeys is a no-op when no backup exists`() = runTest { - val node = Node(num = 123, user = User(id = "!123")) + val node = Node(num = 123, user = User.Builder().also { wb -> wb.id = "!123" }.build()) nodeRepository.setNodes(listOf(node)) viewModel = createViewModel() every { securityKeyBackupStore.get(123) } returns null @@ -2250,26 +2656,34 @@ class RadioConfigViewModelTest { @Test fun `restoreSecurityKeys pushes decoded config to the device on success`() = runTest { - val node = Node(num = 123, user = User(id = "!123")) + val node = Node(num = 123, user = User.Builder().also { wb -> wb.id = "!123" }.build()) nodeRepository.setNodes(listOf(node)) viewModel = createViewModel() val stored = StoredSecurityKeys(publicKeyBase64 = "cHVi", privateKeyBase64 = "cHJpdg==", timestamp = 1L) - val decoded = Config.SecurityConfig(public_key = "pub".encodeUtf8(), private_key = "priv".encodeUtf8()) + val decoded = + Config.SecurityConfig.Builder() + .also { wb -> + wb.public_key = "pub".encodeUtf8() + wb.private_key = "priv".encodeUtf8() + } + .build() every { securityKeyBackupStore.get(123) } returns stored every { importSecurityConfigUseCase(stored) } returns Result.success(decoded) everySuspend { radioConfigUseCase.setConfig(any(), any(), any()) } returns 42 viewModel.restoreSecurityKeys() - verifySuspend { radioConfigUseCase.setConfig(123, Config(security = decoded), any()) } + verifySuspend { + radioConfigUseCase.setConfig(123, Config.Builder().also { wb -> wb.security = decoded }.build(), any()) + } } private fun fourChannelFixture() = listOf( - ChannelSettings(name = "A"), - ChannelSettings(name = "B"), - ChannelSettings(name = "C"), - ChannelSettings(name = "D"), + ChannelSettings.Builder().also { wb -> wb.name = "A" }.build(), + ChannelSettings.Builder().also { wb -> wb.name = "B" }.build(), + ChannelSettings.Builder().also { wb -> wb.name = "C" }.build(), + ChannelSettings.Builder().also { wb -> wb.name = "D" }.build(), ) private fun myNodeInfo(myNodeNum: Int, pioEnv: String? = null) = MyNodeInfo( @@ -2292,7 +2706,7 @@ class RadioConfigViewModelTest { @Test fun `local reboot-applying config save opens the restart window`() = runTest { - val node = Node(num = 123, user = User(id = "!123")) + val node = Node(num = 123, user = User.Builder().also { wb -> wb.id = "!123" }.build()) nodeRepository.setNodes(listOf(node)) nodeRepository.setMyNodeInfo(myNodeInfo(myNodeNum = 123)) viewModel = createViewModel() @@ -2300,7 +2714,13 @@ class RadioConfigViewModelTest { everySuspend { radioConfigUseCase.setConfig(any(), any(), any()) } returns 42 nodeRestartTracker.onConnected() - viewModel.setConfig(Config(network = Config.NetworkConfig(wifi_enabled = true))) + viewModel.setConfig( + Config.Builder() + .also { wb -> + wb.network = Config.NetworkConfig.Builder().also { wb -> wb.wifi_enabled = true }.build() + } + .build(), + ) runCurrent() assertTrue(nodeRestartTracker.restartExpected.value) @@ -2310,7 +2730,7 @@ class RadioConfigViewModelTest { fun `reboot-applying save resolves to restarting-success when the node drops`() = runTest { // The reboot the save triggers eats the routing ACK; the transport-drop during the restart window is the // real confirmation, so the save dialog must show success ("restarting") rather than a 30s timeout error. - val node = Node(num = 123, user = User(id = "!123")) + val node = Node(num = 123, user = User.Builder().also { wb -> wb.id = "!123" }.build()) nodeRepository.setNodes(listOf(node)) nodeRepository.setMyNodeInfo(myNodeInfo(myNodeNum = 123)) val connFlow = MutableStateFlow(ConnectionState.Connected) @@ -2324,7 +2744,13 @@ class RadioConfigViewModelTest { } nodeRestartTracker.onConnected() - viewModel.setConfig(Config(network = Config.NetworkConfig(wifi_enabled = true))) + viewModel.setConfig( + Config.Builder() + .also { wb -> + wb.network = Config.NetworkConfig.Builder().also { wb -> wb.wifi_enabled = true }.build() + } + .build(), + ) runCurrent() assertTrue(viewModel.radioConfigState.value.responseState is ResponseState.Loading) @@ -2337,8 +2763,8 @@ class RadioConfigViewModelTest { @Test fun `remote config save does not open the restart window`() = runTest { - val localNode = Node(num = 100, user = User(id = "!100")) - val remoteNode = Node(num = 456, user = User(id = "!456")) + val localNode = Node(num = 100, user = User.Builder().also { wb -> wb.id = "!100" }.build()) + val remoteNode = Node(num = 456, user = User.Builder().also { wb -> wb.id = "!456" }.build()) nodeRepository.setNodes(listOf(localNode, remoteNode)) nodeRepository.setMyNodeInfo(myNodeInfo(myNodeNum = 100)) viewModel = createViewModel(destNum = 456) @@ -2346,7 +2772,13 @@ class RadioConfigViewModelTest { everySuspend { radioConfigUseCase.setConfig(any(), any(), any()) } returns 42 nodeRestartTracker.onConnected() - viewModel.setConfig(Config(network = Config.NetworkConfig(wifi_enabled = true))) + viewModel.setConfig( + Config.Builder() + .also { wb -> + wb.network = Config.NetworkConfig.Builder().also { wb -> wb.wifi_enabled = true }.build() + } + .build(), + ) runCurrent() assertFalse(nodeRestartTracker.restartExpected.value) @@ -2358,12 +2790,19 @@ class RadioConfigViewModelTest { private fun List.onRequestIdArg(): (Int) -> Unit = last() as (Int) -> Unit /** A real ROUTING_APP ack for [requestId] as the radio would deliver it, sent by node [from]. */ -private fun routingAck(requestId: Int, from: Int) = MeshPacket( - from = from, - decoded = - Data( - portnum = PortNum.ROUTING_APP, - request_id = requestId, - payload = ByteString.of(*Routing(error_reason = Routing.Error.NONE).encode()), - ), -) +private fun routingAck(requestId: Int, from: Int) = MeshPacket.Builder() + .also { wb -> + wb.from = from + wb.decoded = + Data.Builder() + .also { wb -> + wb.portnum = PortNum.ROUTING_APP + wb.request_id = requestId + wb.payload = + ByteString.of( + *Routing.Builder().also { wb -> wb.error_reason = Routing.Error.NONE }.build().encode(), + ) + } + .build() + } + .build() diff --git a/feature/settings/src/commonTest/kotlin/org/meshtastic/feature/settings/radio/channel/ChannelScreenTest.kt b/feature/settings/src/commonTest/kotlin/org/meshtastic/feature/settings/radio/channel/ChannelScreenTest.kt index 6425fe9c21..98b43bb43a 100644 --- a/feature/settings/src/commonTest/kotlin/org/meshtastic/feature/settings/radio/channel/ChannelScreenTest.kt +++ b/feature/settings/src/commonTest/kotlin/org/meshtastic/feature/settings/radio/channel/ChannelScreenTest.kt @@ -28,7 +28,10 @@ import kotlin.test.assertFalse class ChannelScreenTest { @Test fun `channel share state defaults to a replace URL`() { - val channelSet = ChannelSet(lora_config = LoRaConfig(region = RegionCode.US)) + val channelSet = + ChannelSet.Builder() + .also { wb -> wb.lora_config = LoRaConfig.Builder().also { wb -> wb.region = RegionCode.US }.build() } + .build() val url = ChannelShareState().uriString(channelSet) diff --git a/feature/settings/src/commonTest/kotlin/org/meshtastic/feature/settings/radio/channel/component/EditChannelDialogTest.kt b/feature/settings/src/commonTest/kotlin/org/meshtastic/feature/settings/radio/channel/component/EditChannelDialogTest.kt index 3e16911d53..3cb33240ec 100644 --- a/feature/settings/src/commonTest/kotlin/org/meshtastic/feature/settings/radio/channel/component/EditChannelDialogTest.kt +++ b/feature/settings/src/commonTest/kotlin/org/meshtastic/feature/settings/radio/channel/component/EditChannelDialogTest.kt @@ -67,7 +67,13 @@ class EditChannelDialogTest { @Test fun `generated name psk is restored to default when name is cleared after reopen`() { - val generatedSettings = ChannelSettings(name = "custom", psk = Channel.getRandomKey()) + val generatedSettings = + ChannelSettings.Builder() + .also { wb -> + wb.name = "custom" + wb.psk = Channel.getRandomKey() + } + .build() val update = generatedSettings.applyChannelNameEdit( diff --git a/feature/settings/src/commonTest/kotlin/org/meshtastic/feature/settings/radio/component/EditDeviceProfileDialogTest.kt b/feature/settings/src/commonTest/kotlin/org/meshtastic/feature/settings/radio/component/EditDeviceProfileDialogTest.kt index fc9bedfac5..6fb0d28e19 100644 --- a/feature/settings/src/commonTest/kotlin/org/meshtastic/feature/settings/radio/component/EditDeviceProfileDialogTest.kt +++ b/feature/settings/src/commonTest/kotlin/org/meshtastic/feature/settings/radio/component/EditDeviceProfileDialogTest.kt @@ -36,12 +36,21 @@ class EditDeviceProfileDialogTest { private val title = "Export configuration" private val deviceProfile = - DeviceProfile( - long_name = "Long name", - short_name = "Short name", - channel_url = "https://meshtastic.org/e/#CgMSAQESBggBQANIAQ", - fixed_position = Position(latitude_i = 327766650, longitude_i = -967969890, altitude = 138), - ) + DeviceProfile.Builder() + .also { wb -> + wb.long_name = "Long name" + wb.short_name = "Short name" + wb.channel_url = "https://meshtastic.org/e/#CgMSAQESBggBQANIAQ" + wb.fixed_position = + Position.Builder() + .also { wb -> + wb.latitude_i = 327766650 + wb.longitude_i = -967969890 + wb.altitude = 138 + } + .build() + } + .build() @Test fun testEditDeviceProfileDialog_showsDialogTitle() = runComposeUiTest { diff --git a/feature/settings/src/commonTest/kotlin/org/meshtastic/feature/settings/radio/component/LoRaBandwidthUiTest.kt b/feature/settings/src/commonTest/kotlin/org/meshtastic/feature/settings/radio/component/LoRaBandwidthUiTest.kt index 559263ab30..4d94d19177 100644 --- a/feature/settings/src/commonTest/kotlin/org/meshtastic/feature/settings/radio/component/LoRaBandwidthUiTest.kt +++ b/feature/settings/src/commonTest/kotlin/org/meshtastic/feature/settings/radio/component/LoRaBandwidthUiTest.kt @@ -43,7 +43,14 @@ class LoRaBandwidthUiTest { @Test fun invalidPersistedValues_explainWhyAndCannotSendUntilReplaced() = runComposeUiTest { val initialConfig = - Config.LoRaConfig(use_preset = false, region = RegionCode.LORA_24, bandwidth = 125, hop_limit = 1) + Config.LoRaConfig.Builder() + .also { wb -> + wb.use_preset = false + wb.region = RegionCode.LORA_24 + wb.bandwidth = 125 + wb.hop_limit = 1 + } + .build() lateinit var configState: ConfigState var savedConfig: Config.LoRaConfig? = null @@ -80,14 +87,14 @@ class LoRaBandwidthUiTest { } } - runOnIdle { configState.value = configState.value.copy(hop_limit = 2) } + runOnIdle { configState.value = configState.value.newBuilder().also { wb -> wb.hop_limit = 2 }.build() } onNodeWithText("Unsupported (125 kHz)").assertIsDisplayed() onNodeWithText(getString(Res.string.bandwidth_unsupported_summary)).assertIsDisplayed() onNodeWithText(getString(Res.string.save_changes)).assertIsNotEnabled().performClick() runOnIdle { assertNull(savedConfig) } - runOnIdle { configState.value = configState.value.copy(bandwidth = 0) } + runOnIdle { configState.value = configState.value.newBuilder().also { wb -> wb.bandwidth = 0 }.build() } onNodeWithText("Default (812.5 kHz)").assertIsDisplayed() onNodeWithText(getString(Res.string.save_changes)).assertIsEnabled().performClick() diff --git a/feature/settings/src/commonTest/kotlin/org/meshtastic/feature/settings/radio/component/MeshBeaconConfigPolicyTest.kt b/feature/settings/src/commonTest/kotlin/org/meshtastic/feature/settings/radio/component/MeshBeaconConfigPolicyTest.kt index 71070fca3e..102dff34e4 100644 --- a/feature/settings/src/commonTest/kotlin/org/meshtastic/feature/settings/radio/component/MeshBeaconConfigPolicyTest.kt +++ b/feature/settings/src/commonTest/kotlin/org/meshtastic/feature/settings/radio/component/MeshBeaconConfigPolicyTest.kt @@ -46,17 +46,29 @@ class MeshBeaconConfigPolicyTest { @Test fun beaconPresetConstraint_mapPresent_usesFirmwareGroup() { val map = - LoRaRegionPresetMap( - groups = - listOf( - LoRaPresetGroup( - presets = listOf(ModemPreset.SHORT_FAST, ModemPreset.SHORT_TURBO), - default_preset = ModemPreset.SHORT_FAST, - licensed_only = true, - ), - ), - region_groups = listOf(LoRaRegionPresets(region = RegionCode.US, group_index = 0)), - ) + LoRaRegionPresetMap.Builder() + .also { wb -> + wb.groups = + listOf( + LoRaPresetGroup.Builder() + .also { wb -> + wb.presets = listOf(ModemPreset.SHORT_FAST, ModemPreset.SHORT_TURBO) + wb.default_preset = ModemPreset.SHORT_FAST + wb.licensed_only = true + } + .build(), + ) + wb.region_groups = + listOf( + LoRaRegionPresets.Builder() + .also { wb -> + wb.region = RegionCode.US + wb.group_index = 0 + } + .build(), + ) + } + .build() val constraint = beaconPresetConstraint(map, RegionCode.US) @@ -67,7 +79,13 @@ class MeshBeaconConfigPolicyTest { @Test fun beaconPresetConstraint_mapPresentButRegionMissing_fallsBackToConservative() { - val map = LoRaRegionPresetMap(groups = emptyList(), region_groups = emptyList()) + val map = + LoRaRegionPresetMap.Builder() + .also { wb -> + wb.groups = emptyList() + wb.region_groups = emptyList() + } + .build() val constraint = beaconPresetConstraint(map, RegionCode.JP) @@ -103,7 +121,7 @@ class MeshBeaconConfigPolicyTest { @Test fun beaconOfferChannelIndex_nullChannel_returnsNull() { - val channelList = listOf(ChannelSettings(name = "Primary")) + val channelList = listOf(ChannelSettings.Builder().also { wb -> wb.name = "Primary" }.build()) assertNull(beaconOfferChannelIndex(null, selectableBeaconChannels(channelList))) } @@ -112,18 +130,48 @@ class MeshBeaconConfigPolicyTest { fun beaconOfferChannelIndex_matchByNameAndPsk_returnsIndex() { val channelList = listOf( - ChannelSettings(name = "Primary", psk = "a".encodeUtf8()), - ChannelSettings(name = "Secondary", psk = "b".encodeUtf8()), + ChannelSettings.Builder() + .also { wb -> + wb.name = "Primary" + wb.psk = "a".encodeUtf8() + } + .build(), + ChannelSettings.Builder() + .also { wb -> + wb.name = "Secondary" + wb.psk = "b".encodeUtf8() + } + .build(), ) - val offer = ChannelSettings(name = "Secondary", psk = "b".encodeUtf8()) + val offer = + ChannelSettings.Builder() + .also { wb -> + wb.name = "Secondary" + wb.psk = "b".encodeUtf8() + } + .build() assertEquals(1, beaconOfferChannelIndex(offer, selectableBeaconChannels(channelList))) } @Test fun beaconOfferChannelIndex_noRadioChannelMatches_returnsNull() { - val channelList = listOf(ChannelSettings(name = "Primary", psk = "a".encodeUtf8())) - val offer = ChannelSettings(name = "Stale", psk = "z".encodeUtf8()) + val channelList = + listOf( + ChannelSettings.Builder() + .also { wb -> + wb.name = "Primary" + wb.psk = "a".encodeUtf8() + } + .build(), + ) + val offer = + ChannelSettings.Builder() + .also { wb -> + wb.name = "Stale" + wb.psk = "z".encodeUtf8() + } + .build() assertNull(beaconOfferChannelIndex(offer, selectableBeaconChannels(channelList))) } @@ -133,8 +181,17 @@ class MeshBeaconConfigPolicyTest { // A blank/empty offer's identity coincidentally matches the placeholder secondary in the RAW list; once // filtered through selectableBeaconChannels that slot is gone, so the match must not go through (design#140 // Q2) -- otherwise the offer picker would silently select an excluded, never-rendered item. - val channelList = listOf(ChannelSettings(name = "Primary", psk = "a".encodeUtf8()), ChannelSettings()) - val offer = ChannelSettings() + val channelList = + listOf( + ChannelSettings.Builder() + .also { wb -> + wb.name = "Primary" + wb.psk = "a".encodeUtf8() + } + .build(), + ChannelSettings.Builder().build(), + ) + val offer = ChannelSettings.Builder().build() assertNull(beaconOfferChannelIndex(offer, selectableBeaconChannels(channelList))) } @@ -143,9 +200,19 @@ class MeshBeaconConfigPolicyTest { fun selectableBeaconChannels_placeholderSecondaryExcluded_followingRealSlotKeepsTrueIndex() { val channelList = listOf( - ChannelSettings(name = "Primary", psk = "a".encodeUtf8()), - ChannelSettings(), // placeholder secondary: blank name, empty psk - ChannelSettings(name = "Real", psk = "b".encodeUtf8()), + ChannelSettings.Builder() + .also { wb -> + wb.name = "Primary" + wb.psk = "a".encodeUtf8() + } + .build(), + ChannelSettings.Builder().build(), // placeholder secondary: blank name, empty psk + ChannelSettings.Builder() + .also { wb -> + wb.name = "Real" + wb.psk = "b".encodeUtf8() + } + .build(), ) val selectable = selectableBeaconChannels(channelList) @@ -155,7 +222,7 @@ class MeshBeaconConfigPolicyTest { @Test fun selectableBeaconChannels_blankPrimaryKept() { - val channelList = listOf(ChannelSettings()) + val channelList = listOf(ChannelSettings.Builder().build()) val selectable = selectableBeaconChannels(channelList) @@ -164,7 +231,11 @@ class MeshBeaconConfigPolicyTest { @Test fun selectableBeaconChannels_nameOnlySecondaryKept() { - val channelList = listOf(ChannelSettings(name = "Primary"), ChannelSettings(name = "Named")) + val channelList = + listOf( + ChannelSettings.Builder().also { wb -> wb.name = "Primary" }.build(), + ChannelSettings.Builder().also { wb -> wb.name = "Named" }.build(), + ) val selectable = selectableBeaconChannels(channelList) @@ -173,7 +244,11 @@ class MeshBeaconConfigPolicyTest { @Test fun selectableBeaconChannels_pskOnlySecondaryKept() { - val channelList = listOf(ChannelSettings(name = "Primary"), ChannelSettings(psk = "b".encodeUtf8())) + val channelList = + listOf( + ChannelSettings.Builder().also { wb -> wb.name = "Primary" }.build(), + ChannelSettings.Builder().also { wb -> wb.psk = "b".encodeUtf8() }.build(), + ) val selectable = selectableBeaconChannels(channelList) @@ -184,7 +259,11 @@ class MeshBeaconConfigPolicyTest { fun selectableBeaconChannels_oneBytePskCleartextSentinelSecondaryKept() { // A raw ChannelSettings.psk of size 1 (firmware's cleartext sentinel is a single 0x00 byte) is not padding: // isChannelPlaceholder only treats size == 0 as placeholder. - val channelList = listOf(ChannelSettings(name = "Primary"), ChannelSettings(psk = "\u0000".encodeUtf8())) + val channelList = + listOf( + ChannelSettings.Builder().also { wb -> wb.name = "Primary" }.build(), + ChannelSettings.Builder().also { wb -> wb.psk = "\u0000".encodeUtf8() }.build(), + ) val selectable = selectableBeaconChannels(channelList) @@ -194,17 +273,30 @@ class MeshBeaconConfigPolicyTest { @Test fun stampBeaconConfigForSave_stampsRegionAndPresetOnConfigAndTargets() { val radioLora = - Config.LoRaConfig(region = RegionCode.EU_868, modem_preset = ModemPreset.MEDIUM_FAST, use_preset = true) + Config.LoRaConfig.Builder() + .also { wb -> + wb.region = RegionCode.EU_868 + wb.modem_preset = ModemPreset.MEDIUM_FAST + wb.use_preset = true + } + .build() val config = - MeshBeaconConfig( - broadcast_offer_region = RegionCode.US, - broadcast_offer_preset = ModemPreset.LONG_FAST, - broadcast_targets = - listOf( - MeshBeaconConfig.BroadcastTarget(region = RegionCode.JP), - MeshBeaconConfig.BroadcastTarget(region = RegionCode.CN, preset = ModemPreset.SHORT_FAST), - ), - ) + MeshBeaconConfig.Builder() + .also { wb -> + wb.broadcast_offer_region = RegionCode.US + wb.broadcast_offer_preset = ModemPreset.LONG_FAST + wb.broadcast_targets = + listOf( + MeshBeaconConfig.BroadcastTarget.Builder().also { wb -> wb.region = RegionCode.JP }.build(), + MeshBeaconConfig.BroadcastTarget.Builder() + .also { wb -> + wb.region = RegionCode.CN + wb.preset = ModemPreset.SHORT_FAST + } + .build(), + ) + } + .build() val stamped = stampBeaconConfigForSave(config, config, radioLora, channelList = emptyList()) @@ -222,8 +314,17 @@ class MeshBeaconConfigPolicyTest { // running config" semantics (module_config.proto). Region is the one field every target always gets, per // save-time stamping (behavior 1), regardless of the row's own null fields. val radioLora = - Config.LoRaConfig(region = RegionCode.EU_868, modem_preset = ModemPreset.MEDIUM_FAST, use_preset = true) - val config = MeshBeaconConfig(broadcast_targets = listOf(MeshBeaconConfig.BroadcastTarget())) + Config.LoRaConfig.Builder() + .also { wb -> + wb.region = RegionCode.EU_868 + wb.modem_preset = ModemPreset.MEDIUM_FAST + wb.use_preset = true + } + .build() + val config = + MeshBeaconConfig.Builder() + .also { wb -> wb.broadcast_targets = listOf(MeshBeaconConfig.BroadcastTarget.Builder().build()) } + .build() val stamped = stampBeaconConfigForSave(config, config, radioLora, channelList = emptyList()) @@ -236,9 +337,21 @@ class MeshBeaconConfigPolicyTest { @Test fun stampBeaconConfigForSave_untouchedOfferChannel_defaultsToPrimary() { val radioLora = - Config.LoRaConfig(region = RegionCode.US, modem_preset = ModemPreset.LONG_FAST, use_preset = true) - val primary = ChannelSettings(name = "Primary", psk = "a".encodeUtf8()) - val config = MeshBeaconConfig(broadcast_offer_channel = null) + Config.LoRaConfig.Builder() + .also { wb -> + wb.region = RegionCode.US + wb.modem_preset = ModemPreset.LONG_FAST + wb.use_preset = true + } + .build() + val primary = + ChannelSettings.Builder() + .also { wb -> + wb.name = "Primary" + wb.psk = "a".encodeUtf8() + } + .build() + val config = MeshBeaconConfig.Builder().also { wb -> wb.broadcast_offer_channel = null }.build() val stamped = stampBeaconConfigForSave(config, config, radioLora, channelList = listOf(primary)) @@ -249,12 +362,29 @@ class MeshBeaconConfigPolicyTest { @Test fun stampBeaconConfigForSave_alreadySetOfferChannel_isKeptEvenIfStale() { val radioLora = - Config.LoRaConfig(region = RegionCode.US, modem_preset = ModemPreset.LONG_FAST, use_preset = true) - val stale = ChannelSettings(name = "Stale", psk = "z".encodeUtf8()) - val config = MeshBeaconConfig(broadcast_offer_channel = stale) + Config.LoRaConfig.Builder() + .also { wb -> + wb.region = RegionCode.US + wb.modem_preset = ModemPreset.LONG_FAST + wb.use_preset = true + } + .build() + val stale = + ChannelSettings.Builder() + .also { wb -> + wb.name = "Stale" + wb.psk = "z".encodeUtf8() + } + .build() + val config = MeshBeaconConfig.Builder().also { wb -> wb.broadcast_offer_channel = stale }.build() val stamped = - stampBeaconConfigForSave(config, config, radioLora, channelList = listOf(ChannelSettings(name = "Primary"))) + stampBeaconConfigForSave( + config, + config, + radioLora, + channelList = listOf(ChannelSettings.Builder().also { wb -> wb.name = "Primary" }.build()), + ) assertEquals("Stale", stamped.broadcast_offer_channel?.name) assertEquals("z".encodeUtf8(), stamped.broadcast_offer_channel?.psk) @@ -265,55 +395,100 @@ class MeshBeaconConfigPolicyTest { // A beacon invites a stranger's radio to join this channel: the radio's own index/id/uplink/downlink/module // flags have no meaning there and must never leak onto someone else's node. val radioLora = - Config.LoRaConfig(region = RegionCode.US, modem_preset = ModemPreset.LONG_FAST, use_preset = true) + Config.LoRaConfig.Builder() + .also { wb -> + wb.region = RegionCode.US + wb.modem_preset = ModemPreset.LONG_FAST + wb.use_preset = true + } + .build() val fullChannel = - ChannelSettings( - name = "Primary", - psk = "a".encodeUtf8(), - channel_num = 5, - id = 42, - uplink_enabled = true, - downlink_enabled = true, - ) - val config = MeshBeaconConfig(broadcast_offer_channel = null) + ChannelSettings.Builder() + .also { wb -> + wb.name = "Primary" + wb.psk = "a".encodeUtf8() + wb.channel_num = 5 + wb.id = 42 + wb.uplink_enabled = true + wb.downlink_enabled = true + } + .build() + val config = MeshBeaconConfig.Builder().also { wb -> wb.broadcast_offer_channel = null }.build() val stamped = stampBeaconConfigForSave(config, config, radioLora, channelList = listOf(fullChannel)) - assertEquals(ChannelSettings(name = "Primary", psk = "a".encodeUtf8()), stamped.broadcast_offer_channel) + assertEquals( + ChannelSettings.Builder() + .also { wb -> + wb.name = "Primary" + wb.psk = "a".encodeUtf8() + } + .build(), + stamped.broadcast_offer_channel, + ) } @Test fun stampBeaconConfigForSave_customParams_preservesStoredBroadcastFieldsVerbatim() { val radioLora = - Config.LoRaConfig(region = RegionCode.US, modem_preset = ModemPreset.LONG_FAST, use_preset = false) + Config.LoRaConfig.Builder() + .also { wb -> + wb.region = RegionCode.US + wb.modem_preset = ModemPreset.LONG_FAST + wb.use_preset = false + } + .build() val listenFlag = MeshBeaconConfig.Flags.FLAG_LISTEN_ENABLED.value val broadcastFlag = MeshBeaconConfig.Flags.FLAG_BROADCAST_ENABLED.value val stored = - MeshBeaconConfig( - flags = listenFlag or broadcastFlag, - broadcast_message = "Stored message", - broadcast_interval_secs = 3600, - broadcast_offer_region = RegionCode.EU_868, - broadcast_offer_preset = ModemPreset.MEDIUM_FAST, - broadcast_offer_channel = ChannelSettings(name = "Stored", psk = "s".encodeUtf8()), - broadcast_targets = - listOf(MeshBeaconConfig.BroadcastTarget(region = RegionCode.EU_868, channel_index = 1)), - ) + MeshBeaconConfig.Builder() + .also { wb -> + wb.flags = listenFlag or broadcastFlag + wb.broadcast_message = "Stored message" + wb.broadcast_interval_secs = 3600 + wb.broadcast_offer_region = RegionCode.EU_868 + wb.broadcast_offer_preset = ModemPreset.MEDIUM_FAST + wb.broadcast_offer_channel = + ChannelSettings.Builder() + .also { wb -> + wb.name = "Stored" + wb.psk = "s".encodeUtf8() + } + .build() + wb.broadcast_targets = + listOf( + MeshBeaconConfig.BroadcastTarget.Builder() + .also { wb -> + wb.region = RegionCode.EU_868 + wb.channel_index = 1 + } + .build(), + ) + } + .build() // The form differs on every broadcast field, plus clears the BROADCAST flag (the only edit the gated UI // actually allows) -- none of the broadcast field edits should survive the save. val form = - stored.copy( - flags = listenFlag, - broadcast_message = "Edited but discarded", - broadcast_interval_secs = 7200, - broadcast_offer_region = RegionCode.JP, - broadcast_offer_preset = ModemPreset.SHORT_FAST, - broadcast_offer_channel = ChannelSettings(name = "Different"), - broadcast_targets = emptyList(), - ) + stored + .newBuilder() + .also { wb -> + wb.flags = listenFlag + wb.broadcast_message = "Edited but discarded" + wb.broadcast_interval_secs = 7200 + wb.broadcast_offer_region = RegionCode.JP + wb.broadcast_offer_preset = ModemPreset.SHORT_FAST + wb.broadcast_offer_channel = ChannelSettings.Builder().also { wb -> wb.name = "Different" }.build() + wb.broadcast_targets = emptyList() + } + .build() val stamped = - stampBeaconConfigForSave(form, stored, radioLora, channelList = listOf(ChannelSettings(name = "Primary"))) + stampBeaconConfigForSave( + form, + stored, + radioLora, + channelList = listOf(ChannelSettings.Builder().also { wb -> wb.name = "Primary" }.build()), + ) assertEquals(stored.broadcast_message, stamped.broadcast_message) assertEquals(stored.broadcast_interval_secs, stamped.broadcast_interval_secs) @@ -327,7 +502,7 @@ class MeshBeaconConfigPolicyTest { @Test fun selectBeaconTargetChannel_noPresetYet_preselectsCurrentPreset() { - val target = MeshBeaconConfig.BroadcastTarget(preset = null) + val target = MeshBeaconConfig.BroadcastTarget.Builder().also { wb -> wb.preset = null }.build() val updated = selectBeaconTargetChannel(target, channelIndex = 2, currentPreset = ModemPreset.SHORT_FAST) @@ -337,7 +512,8 @@ class MeshBeaconConfigPolicyTest { @Test fun selectBeaconTargetChannel_presetAlreadyChosen_isNotOverwritten() { - val target = MeshBeaconConfig.BroadcastTarget(preset = ModemPreset.LONG_MODERATE) + val target = + MeshBeaconConfig.BroadcastTarget.Builder().also { wb -> wb.preset = ModemPreset.LONG_MODERATE }.build() val updated = selectBeaconTargetChannel(target, channelIndex = 3, currentPreset = ModemPreset.SHORT_FAST) @@ -347,7 +523,13 @@ class MeshBeaconConfigPolicyTest { @Test fun selectBeaconTargetChannel_defaultSentinel_clearsChannelAndLeavesPresetUntouched() { - val target = MeshBeaconConfig.BroadcastTarget(channel_index = 2, preset = null) + val target = + MeshBeaconConfig.BroadcastTarget.Builder() + .also { wb -> + wb.channel_index = 2 + wb.preset = null + } + .build() val updated = selectBeaconTargetChannel(target, channelIndex = null, currentPreset = ModemPreset.SHORT_FAST) @@ -360,7 +542,13 @@ class MeshBeaconConfigPolicyTest { fun selectBeaconTargetChannel_defaultSentinelWithConcretePreset_presetUnchanged() { // Same as the null-preset case above, but with a preset the user has already deliberately chosen -- the // regression this guards is "Default" silently resetting a concrete preset, not just leaving null alone. - val target = MeshBeaconConfig.BroadcastTarget(channel_index = 2, preset = ModemPreset.SHORT_FAST) + val target = + MeshBeaconConfig.BroadcastTarget.Builder() + .also { wb -> + wb.channel_index = 2 + wb.preset = ModemPreset.SHORT_FAST + } + .build() val updated = selectBeaconTargetChannel(target, channelIndex = null, currentPreset = ModemPreset.LONG_FAST) @@ -372,12 +560,20 @@ class MeshBeaconConfigPolicyTest { fun seedBeaconTargets_emptyStoredList_seedsOneDefaultRow() { val seeded = seedBeaconTargets(emptyList()) - assertEquals(listOf(MeshBeaconConfig.BroadcastTarget()), seeded) + assertEquals(listOf(MeshBeaconConfig.BroadcastTarget.Builder().build()), seeded) } @Test fun seedBeaconTargets_nonEmptyStoredList_isUnchanged() { - val stored = listOf(MeshBeaconConfig.BroadcastTarget(channel_index = 1, preset = ModemPreset.LONG_FAST)) + val stored = + listOf( + MeshBeaconConfig.BroadcastTarget.Builder() + .also { wb -> + wb.channel_index = 1 + wb.preset = ModemPreset.LONG_FAST + } + .build(), + ) val seeded = seedBeaconTargets(stored) @@ -388,18 +584,24 @@ class MeshBeaconConfigPolicyTest { fun initialBeaconFormState_emptyStoredConfig_seedsFormTargetsThroughTheProductionPath() { // Calls the exact function MeshBeaconConfigScreen calls to build formState's initial value -- not // seedBeaconTargets directly -- so this breaks if the screen's wiring to it ever comes apart. - val loaded = MeshBeaconConfig(broadcast_message = "hi", broadcast_targets = emptyList()) + val loaded = + MeshBeaconConfig.Builder() + .also { wb -> + wb.broadcast_message = "hi" + wb.broadcast_targets = emptyList() + } + .build() val initial = initialBeaconFormState(loaded) - assertEquals(listOf(MeshBeaconConfig.BroadcastTarget()), initial.broadcast_targets) + assertEquals(listOf(MeshBeaconConfig.BroadcastTarget.Builder().build()), initial.broadcast_targets) assertEquals("hi", initial.broadcast_message) } @Test fun initialBeaconFormState_nonEmptyStoredConfig_isUnchanged() { - val stored = listOf(MeshBeaconConfig.BroadcastTarget(channel_index = 3)) - val loaded = MeshBeaconConfig(broadcast_targets = stored) + val stored = listOf(MeshBeaconConfig.BroadcastTarget.Builder().also { wb -> wb.channel_index = 3 }.build()) + val loaded = MeshBeaconConfig.Builder().also { wb -> wb.broadcast_targets = stored }.build() val initial = initialBeaconFormState(loaded) @@ -410,21 +612,32 @@ class MeshBeaconConfigPolicyTest { fun removeBeaconTarget_removingOneOfSeveral_dropsOnlyThatRow() { val targets = listOf( - MeshBeaconConfig.BroadcastTarget(channel_index = 0), - MeshBeaconConfig.BroadcastTarget(channel_index = 1), + MeshBeaconConfig.BroadcastTarget.Builder().also { wb -> wb.channel_index = 0 }.build(), + MeshBeaconConfig.BroadcastTarget.Builder().also { wb -> wb.channel_index = 1 }.build(), ) val updated = removeBeaconTarget(targets, index = 0) - assertEquals(listOf(MeshBeaconConfig.BroadcastTarget(channel_index = 1)), updated) + assertEquals( + listOf(MeshBeaconConfig.BroadcastTarget.Builder().also { wb -> wb.channel_index = 1 }.build()), + updated, + ) } @Test fun removeBeaconTarget_removingTheOnlyRow_reseedsADefaultRowInstead() { - val targets = listOf(MeshBeaconConfig.BroadcastTarget(channel_index = 4, preset = ModemPreset.SHORT_FAST)) + val targets = + listOf( + MeshBeaconConfig.BroadcastTarget.Builder() + .also { wb -> + wb.channel_index = 4 + wb.preset = ModemPreset.SHORT_FAST + } + .build(), + ) val updated = removeBeaconTarget(targets, index = 0) - assertEquals(listOf(MeshBeaconConfig.BroadcastTarget()), updated) + assertEquals(listOf(MeshBeaconConfig.BroadcastTarget.Builder().build()), updated) } } diff --git a/feature/settings/src/commonTest/kotlin/org/meshtastic/feature/settings/radio/component/MeshBeaconConfigUiTest.kt b/feature/settings/src/commonTest/kotlin/org/meshtastic/feature/settings/radio/component/MeshBeaconConfigUiTest.kt index 22b02a4fcb..8f25e587d5 100644 --- a/feature/settings/src/commonTest/kotlin/org/meshtastic/feature/settings/radio/component/MeshBeaconConfigUiTest.kt +++ b/feature/settings/src/commonTest/kotlin/org/meshtastic/feature/settings/radio/component/MeshBeaconConfigUiTest.kt @@ -74,7 +74,7 @@ class MeshBeaconConfigUiTest { onBack = {}, responseState = ResponseState.Empty, onDismissPacketResponse = {}, - configState = rememberConfigState(MeshBeaconConfig()), + configState = rememberConfigState(MeshBeaconConfig.Builder().build()), enabled = false, onSave = {}, ) { @@ -91,11 +91,23 @@ class MeshBeaconConfigUiTest { @Test fun requiredOfferedChannel_defaultsToPrimaryAndStampsRegionAndPresetOnSave() = runComposeUiTest { val radioLora = - Config.LoRaConfig(region = RegionCode.US, modem_preset = ModemPreset.MEDIUM_FAST, use_preset = true) + Config.LoRaConfig.Builder() + .also { wb -> + wb.region = RegionCode.US + wb.modem_preset = ModemPreset.MEDIUM_FAST + wb.use_preset = true + } + .build() // channel_num deliberately set: proves the save path narrows to name+psk, not the whole ChannelSettings. - val primary = ChannelSettings(name = "Primary", channel_num = 7) + val primary = + ChannelSettings.Builder() + .also { wb -> + wb.name = "Primary" + wb.channel_num = 7 + } + .build() val channelList = listOf(primary) - val initialConfig = MeshBeaconConfig() + val initialConfig = MeshBeaconConfig.Builder().build() lateinit var configState: ConfigState var savedConfig: MeshBeaconConfig? = null @@ -121,7 +133,11 @@ class MeshBeaconConfigUiTest { channelItems = channelList.mapIndexed { index, s -> DropDownItem(index, s.name) }, enabled = true, onChannelSelect = { - configState.value = configState.value.copy(broadcast_offer_channel = it) + configState.value = + configState.value + .newBuilder() + .also { wb -> wb.broadcast_offer_channel = it } + .build() }, ) } @@ -130,12 +146,17 @@ class MeshBeaconConfigUiTest { } // Never touched the picker: mark the form dirty another way, then save with the untouched offer channel. - runOnIdle { configState.value = configState.value.copy(broadcast_message = "hi") } + runOnIdle { + configState.value = configState.value.newBuilder().also { wb -> wb.broadcast_message = "hi" }.build() + } onNodeWithText(getString(Res.string.save_changes)).assertIsEnabled().performClick() runOnIdle { - assertEquals(ChannelSettings(name = "Primary"), savedConfig?.broadcast_offer_channel) + assertEquals( + ChannelSettings.Builder().also { wb -> wb.name = "Primary" }.build(), + savedConfig?.broadcast_offer_channel, + ) assertEquals(RegionCode.US, savedConfig?.broadcast_offer_region) assertEquals(ModemPreset.MEDIUM_FAST, savedConfig?.broadcast_offer_preset) } @@ -143,9 +164,18 @@ class MeshBeaconConfigUiTest { @Test fun customParamsRadio_storedBroadcastOff_toggleDisabledAndSectionsHidden() = runComposeUiTest { - val radioLora = Config.LoRaConfig(region = RegionCode.US, use_preset = false) + val radioLora = + Config.LoRaConfig.Builder() + .also { wb -> + wb.region = RegionCode.US + wb.use_preset = false + } + .build() val broadcastFlag = MeshBeaconConfig.Flags.FLAG_BROADCAST_ENABLED.value - val storedConfig = MeshBeaconConfig(flags = MeshBeaconConfig.Flags.FLAG_LISTEN_ENABLED.value) + val storedConfig = + MeshBeaconConfig.Builder() + .also { wb -> wb.flags = MeshBeaconConfig.Flags.FLAG_LISTEN_ENABLED.value } + .build() lateinit var configState: ConfigState setContent { @@ -169,14 +199,17 @@ class MeshBeaconConfigUiTest { enabled = gate.toggleEnabled, onCheckedChange = { configState.value = - configState.value.copy( - flags = - if (it) { - configState.value.flags or broadcastFlag - } else { - configState.value.flags and broadcastFlag.inv() - }, - ) + configState.value + .newBuilder() + .also { wb -> + wb.flags = + if (it) { + configState.value.flags or broadcastFlag + } else { + configState.value.flags and broadcastFlag.inv() + } + } + .build() }, ) Text(getString(Res.string.mesh_beacon_broadcast_requires_preset)) @@ -195,10 +228,16 @@ class MeshBeaconConfigUiTest { @Test fun customParamsRadio_storedBroadcastOn_sectionsVisibleButDisabledAndToggleCanSwitchOff() = runComposeUiTest { - val radioLora = Config.LoRaConfig(region = RegionCode.US, use_preset = false) + val radioLora = + Config.LoRaConfig.Builder() + .also { wb -> + wb.region = RegionCode.US + wb.use_preset = false + } + .build() val broadcastFlag = MeshBeaconConfig.Flags.FLAG_BROADCAST_ENABLED.value val listenFlag = MeshBeaconConfig.Flags.FLAG_LISTEN_ENABLED.value - val storedConfig = MeshBeaconConfig(flags = listenFlag or broadcastFlag) + val storedConfig = MeshBeaconConfig.Builder().also { wb -> wb.flags = listenFlag or broadcastFlag }.build() lateinit var configState: ConfigState setContent { @@ -222,14 +261,17 @@ class MeshBeaconConfigUiTest { enabled = gate.toggleEnabled, onCheckedChange = { configState.value = - configState.value.copy( - flags = - if (it) { - configState.value.flags or broadcastFlag - } else { - configState.value.flags and broadcastFlag.inv() - }, - ) + configState.value + .newBuilder() + .also { wb -> + wb.flags = + if (it) { + configState.value.flags or broadcastFlag + } else { + configState.value.flags and broadcastFlag.inv() + } + } + .build() }, ) } @@ -243,7 +285,10 @@ class MeshBeaconConfigUiTest { isError = false, keyboardOptions = KeyboardOptions.Default, keyboardActions = KeyboardActions.Default, - onValueChanged = { configState.value = configState.value.copy(broadcast_message = it) }, + onValueChanged = { + configState.value = + configState.value.newBuilder().also { wb -> wb.broadcast_message = it }.build() + }, ) } } @@ -262,12 +307,18 @@ class MeshBeaconConfigUiTest { @Test fun emptyChannelList_offerPickerShowsDisabledPlaceholderRow() = runComposeUiTest { val radioLora = - Config.LoRaConfig(region = RegionCode.US, modem_preset = ModemPreset.LONG_FAST, use_preset = true) + Config.LoRaConfig.Builder() + .also { wb -> + wb.region = RegionCode.US + wb.modem_preset = ModemPreset.LONG_FAST + wb.use_preset = true + } + .build() lateinit var configState: ConfigState setContent { AppTheme { - configState = rememberConfigState(MeshBeaconConfig()) + configState = rememberConfigState(MeshBeaconConfig.Builder().build()) OfferChannelPreference( offerChannel = configState.value.broadcast_offer_channel, channelList = emptyList(), @@ -275,7 +326,10 @@ class MeshBeaconConfigUiTest { radioLora = radioLora, channelItems = emptyList(), enabled = true, - onChannelSelect = { configState.value = configState.value.copy(broadcast_offer_channel = it) }, + onChannelSelect = { + configState.value = + configState.value.newBuilder().also { wb -> wb.broadcast_offer_channel = it }.build() + }, ) } } @@ -285,9 +339,15 @@ class MeshBeaconConfigUiTest { @Test fun customParamsRadio_emptyChannelListListenOnlyEdit_saveButtonEnabled() = runComposeUiTest { - val radioLora = Config.LoRaConfig(region = RegionCode.US, use_preset = false) + val radioLora = + Config.LoRaConfig.Builder() + .also { wb -> + wb.region = RegionCode.US + wb.use_preset = false + } + .build() val listenFlag = MeshBeaconConfig.Flags.FLAG_LISTEN_ENABLED.value - val initialConfig = MeshBeaconConfig() + val initialConfig = MeshBeaconConfig.Builder().build() val emptyChannelList = emptyList() lateinit var configState: ConfigState var savedConfig: MeshBeaconConfig? = null @@ -319,14 +379,17 @@ class MeshBeaconConfigUiTest { enabled = true, onCheckedChange = { configState.value = - configState.value.copy( - flags = - if (it) { - configState.value.flags or listenFlag - } else { - configState.value.flags and listenFlag.inv() - }, - ) + configState.value + .newBuilder() + .also { wb -> + wb.flags = + if (it) { + configState.value.flags or listenFlag + } else { + configState.value.flags and listenFlag.inv() + } + } + .build() }, ) } @@ -348,7 +411,7 @@ class MeshBeaconConfigUiTest { // rather than just the policy function in isolation. val presetConstraint = RegionPresetConstraint(presets = listOf(ModemPreset.LONG_FAST), ModemPreset.LONG_FAST, false) - val seededTargets = initialBeaconFormState(MeshBeaconConfig()).broadcast_targets + val seededTargets = initialBeaconFormState(MeshBeaconConfig.Builder().build()).broadcast_targets setContent { AppTheme { @@ -395,7 +458,7 @@ class MeshBeaconConfigUiTest { // Not just "one row, one remove button" -- that would also pass for a wrong non-empty result. The replaced // row must be a genuine fresh default: both fields null, not carried over from the removed row. - runOnIdle { assertEquals(listOf(MeshBeaconConfig.BroadcastTarget()), targetsState.value) } + runOnIdle { assertEquals(listOf(MeshBeaconConfig.BroadcastTarget.Builder().build()), targetsState.value) } onNodeWithText(getString(Res.string.mesh_beacon_target, 1)).assertIsDisplayed() onAllNodesWithText(getString(Res.string.mesh_beacon_target_remove)).assertCountEquals(1) } @@ -410,7 +473,14 @@ class MeshBeaconConfigUiTest { AppTheme { targetsState = remember { mutableStateOf( - listOf(MeshBeaconConfig.BroadcastTarget(channel_index = 0, preset = ModemPreset.LONG_FAST)), + listOf( + MeshBeaconConfig.BroadcastTarget.Builder() + .also { wb -> + wb.channel_index = 0 + wb.preset = ModemPreset.LONG_FAST + } + .build(), + ), ) } BroadcastTargetsCard( @@ -447,7 +517,14 @@ class MeshBeaconConfigUiTest { AppTheme { targetsState = remember { mutableStateOf( - listOf(MeshBeaconConfig.BroadcastTarget(channel_index = 0, preset = ModemPreset.LONG_FAST)), + listOf( + MeshBeaconConfig.BroadcastTarget.Builder() + .also { wb -> + wb.channel_index = 0 + wb.preset = ModemPreset.LONG_FAST + } + .build(), + ), ) } BroadcastTargetsCard( diff --git a/feature/settings/src/commonTest/kotlin/org/meshtastic/feature/settings/radio/component/PacketAuthenticitySettingTest.kt b/feature/settings/src/commonTest/kotlin/org/meshtastic/feature/settings/radio/component/PacketAuthenticitySettingTest.kt index d16f9e3d48..900a0d1022 100644 --- a/feature/settings/src/commonTest/kotlin/org/meshtastic/feature/settings/radio/component/PacketAuthenticitySettingTest.kt +++ b/feature/settings/src/commonTest/kotlin/org/meshtastic/feature/settings/radio/component/PacketAuthenticitySettingTest.kt @@ -48,20 +48,23 @@ class PacketAuthenticitySettingTest { fun `protobuf default is compatible`() { assertEquals( PacketSignaturePolicy.PACKET_SIGNATURE_POLICY_COMPATIBLE, - Config.SecurityConfig().packet_signature_policy, + Config.SecurityConfig.Builder().build().packet_signature_policy, ) } @Test fun `balanced selection updates the device config field`() = runComposeUiTest { - var updatedConfig = Config.SecurityConfig() + var updatedConfig = Config.SecurityConfig.Builder().build() setContent { AppTheme { PacketAuthenticitySetting( selectedPolicy = updatedConfig.packet_signature_policy, connected = true, supported = true, - onPolicyChange = { policy -> updatedConfig = updatedConfig.copy(packet_signature_policy = policy) }, + onPolicyChange = { policy -> + updatedConfig = + updatedConfig.newBuilder().also { wb -> wb.packet_signature_policy = policy }.build() + }, ) } } diff --git a/feature/settings/src/commonTest/kotlin/org/meshtastic/feature/settings/radio/component/SecurityConfigScreenTest.kt b/feature/settings/src/commonTest/kotlin/org/meshtastic/feature/settings/radio/component/SecurityConfigScreenTest.kt index 775dbca405..d8dc87c772 100644 --- a/feature/settings/src/commonTest/kotlin/org/meshtastic/feature/settings/radio/component/SecurityConfigScreenTest.kt +++ b/feature/settings/src/commonTest/kotlin/org/meshtastic/feature/settings/radio/component/SecurityConfigScreenTest.kt @@ -42,7 +42,7 @@ import kotlin.test.assertTrue class SecurityConfigScreenTest { @Test fun `delayed remote public key updates display and copy without recreating screen`() = runComposeUiTest { - var securityConfig by mutableStateOf(Config.SecurityConfig()) + var securityConfig by mutableStateOf(Config.SecurityConfig.Builder().build()) var renderedFormState: ConfigState? = null var copiedPublicKey: ByteString? = null @@ -69,7 +69,7 @@ class SecurityConfigScreenTest { onNodeWithText(encodedPublicKey).assertDoesNotExist() // Firmware 2.8 redacts the remote private key, so only the public key changes when the delayed response lands. - securityConfig = Config.SecurityConfig(public_key = publicKey) + securityConfig = Config.SecurityConfig.Builder().also { wb -> wb.public_key = publicKey }.build() waitForIdle() onNodeWithText(encodedPublicKey).assertIsDisplayed() @@ -80,7 +80,8 @@ class SecurityConfigScreenTest { // A local private-key edit invalidates the device-derived public key for both render and copy. runOnIdle { val formState = checkNotNull(renderedFormState) - formState.value = formState.value.copy(private_key = ByteArray(32) { 7 }.toByteString()) + formState.value = + formState.value.newBuilder().also { wb -> wb.private_key = ByteArray(32) { 7 }.toByteString() }.build() } waitForIdle() @@ -92,11 +93,12 @@ class SecurityConfigScreenTest { @Test fun `private key is redacted for a remote node until a new one is entered`() { - val remote = Config.SecurityConfig(public_key = ByteArray(32) { 1 }.toByteString()) + val remote = + Config.SecurityConfig.Builder().also { wb -> wb.public_key = ByteArray(32) { 1 }.toByteString() }.build() assertTrue(isPrivateKeyRedacted(remote, isLocal = false)) // The local node always reports its own key, so nothing is withheld there. - val local = remote.copy(private_key = ByteArray(32) { 2 }.toByteString()) + val local = remote.newBuilder().also { wb -> wb.private_key = ByteArray(32) { 2 }.toByteString() }.build() assertFalse(isPrivateKeyRedacted(local, isLocal = true)) assertFalse(isPrivateKeyRedacted(remote, isLocal = true)) diff --git a/feature/settings/src/commonTest/kotlin/org/meshtastic/feature/settings/radio/component/UserNameFieldsTest.kt b/feature/settings/src/commonTest/kotlin/org/meshtastic/feature/settings/radio/component/UserNameFieldsTest.kt index 52558d1f02..0b710bdec6 100644 --- a/feature/settings/src/commonTest/kotlin/org/meshtastic/feature/settings/radio/component/UserNameFieldsTest.kt +++ b/feature/settings/src/commonTest/kotlin/org/meshtastic/feature/settings/radio/component/UserNameFieldsTest.kt @@ -68,7 +68,15 @@ class UserNameFieldsTest { @Test fun `the ham long name field only exists while licensed`() = runComposeUiTest { - showNameFields(User(long_name = "Attic Heltec", short_name = "ATTC"), hamMode = false) + showNameFields( + User.Builder() + .also { wb -> + wb.long_name = "Attic Heltec" + wb.short_name = "ATTC" + } + .build(), + hamMode = false, + ) onNodeWithTag(HAM_LONG_NAME_TEST_TAG).assertDoesNotExist() assertEquals("Attic Heltec", fieldText(USER_LONG_NAME_TEST_TAG)) @@ -76,7 +84,15 @@ class UserNameFieldsTest { @Test fun `the call sign field shows only the call sign half of a composed name`() = runComposeUiTest { - showNameFields(User(long_name = "KD2ABC//Attic Heltec", short_name = "ABC"), hamMode = true) + showNameFields( + User.Builder() + .also { wb -> + wb.long_name = "KD2ABC//Attic Heltec" + wb.short_name = "ABC" + } + .build(), + hamMode = true, + ) assertEquals("KD2ABC", fieldText(USER_LONG_NAME_TEST_TAG)) assertEquals("Attic Heltec", fieldText(HAM_LONG_NAME_TEST_TAG)) @@ -84,7 +100,16 @@ class UserNameFieldsTest { @Test fun `editing the call sign keeps the long name half`() = runComposeUiTest { - val user = showNameFields(User(long_name = "KD2ABC//Attic Heltec", short_name = "ABC"), hamMode = true) + val user = + showNameFields( + User.Builder() + .also { wb -> + wb.long_name = "KD2ABC//Attic Heltec" + wb.short_name = "ABC" + } + .build(), + hamMode = true, + ) field(USER_LONG_NAME_TEST_TAG).performTextClearance() field(USER_LONG_NAME_TEST_TAG).performTextInput("N0CALL") @@ -94,7 +119,16 @@ class UserNameFieldsTest { @Test fun `editing the long name keeps the call sign`() = runComposeUiTest { - val user = showNameFields(User(long_name = "KD2ABC", short_name = "ABC"), hamMode = true) + val user = + showNameFields( + User.Builder() + .also { wb -> + wb.long_name = "KD2ABC" + wb.short_name = "ABC" + } + .build(), + hamMode = true, + ) field(HAM_LONG_NAME_TEST_TAG).performTextInput("Garage") @@ -103,7 +137,16 @@ class UserNameFieldsTest { @Test fun `clearing the long name leaves the node named after the call sign alone`() = runComposeUiTest { - val user = showNameFields(User(long_name = "KD2ABC//Attic Heltec", short_name = "ABC"), hamMode = true) + val user = + showNameFields( + User.Builder() + .also { wb -> + wb.long_name = "KD2ABC//Attic Heltec" + wb.short_name = "ABC" + } + .build(), + hamMode = true, + ) field(HAM_LONG_NAME_TEST_TAG).performTextClearance() @@ -112,7 +155,16 @@ class UserNameFieldsTest { @Test fun `the widest pair the proto can carry composes whole`() = runComposeUiTest { - val user = showNameFields(User(long_name = "KD2ABCD", short_name = "ABC"), hamMode = true) + val user = + showNameFields( + User.Builder() + .also { wb -> + wb.long_name = "KD2ABCD" + wb.short_name = "ABC" + } + .build(), + hamMode = true, + ) field(HAM_LONG_NAME_TEST_TAG).performTextInput("Attic Heltec 3") diff --git a/feature/settings/src/commonTest/kotlin/org/meshtastic/feature/settings/util/DeviceProfileExportFileNameTest.kt b/feature/settings/src/commonTest/kotlin/org/meshtastic/feature/settings/util/DeviceProfileExportFileNameTest.kt new file mode 100644 index 0000000000..084e2cc32a --- /dev/null +++ b/feature/settings/src/commonTest/kotlin/org/meshtastic/feature/settings/util/DeviceProfileExportFileNameTest.kt @@ -0,0 +1,112 @@ +/* + * 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 . + */ +package org.meshtastic.feature.settings.util + +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertFalse +import kotlin.test.assertTrue + +class DeviceProfileExportFileNameTest { + + private val date = "20260919" + + @Test + fun `prefers the long name over the short name`() { + assertEquals( + "Meshtastic_BaseStation_20260919_nodeConfig.cfg", + deviceProfileExportFileName(longName = "BaseStation", shortName = "BASE", dateStamp = date), + ) + } + + @Test + fun `falls back to the short name when the long name is missing or blank`() { + val expected = "Meshtastic_BASE_20260919_nodeConfig.cfg" + assertEquals(expected, deviceProfileExportFileName(longName = null, shortName = "BASE", dateStamp = date)) + assertEquals(expected, deviceProfileExportFileName(longName = " ", shortName = "BASE", dateStamp = date)) + } + + @Test + fun `falls back to a placeholder when neither name is usable`() { + assertEquals( + "Meshtastic_node_20260919_nodeConfig.cfg", + deviceProfileExportFileName(longName = null, shortName = null, dateStamp = date), + ) + assertEquals( + "Meshtastic_node_20260919_nodeConfig.cfg", + deviceProfileExportFileName(longName = "", shortName = "", dateStamp = date), + ) + } + + @Test + fun `replaces characters a storage provider would mangle`() { + assertEquals( + "Meshtastic_James_s_Roof_Node_20260919_nodeConfig.cfg", + deviceProfileExportFileName(longName = "James's Roof/Node", shortName = "ROOF", dateStamp = date), + ) + } + + @Test + fun `keeps letters and digits of any script`() { + assertEquals( + "Meshtastic_Küche_東京_2_20260919_nodeConfig.cfg", + deviceProfileExportFileName(longName = "Küche 東京 2", shortName = "KU", dateStamp = date), + ) + } + + @Test + fun `keeps a supplementary-plane letter`() { + // U+10400 DESERET CAPITAL LETTER LONG I is one code point across two UTF-16 units. + assertEquals( + "Meshtastic_\uD801\uDC00_20260919_nodeConfig.cfg", + deviceProfileExportFileName(longName = "\uD801\uDC00", shortName = "DS", dateStamp = date), + ) + } + + @Test + fun `drops a supplementary-plane emoji`() { + // U+1F4CD ROUND PUSHPIN is also two units, but it is a symbol rather than a letter. + assertEquals( + "Meshtastic_PIN_20260919_nodeConfig.cfg", + deviceProfileExportFileName(longName = "\uD83D\uDCCD", shortName = "PIN", dateStamp = date), + ) + } + + @Test + fun `collapses a run of unsafe characters into one separator`() { + assertEquals( + "Meshtastic_Roof_Node_20260919_nodeConfig.cfg", + deviceProfileExportFileName(longName = " Roof // Node ", shortName = "ROOF", dateStamp = date), + ) + } + + @Test + fun `falls back to the short name when sanitizing empties the long name`() { + assertEquals( + "Meshtastic_ROOF_20260919_nodeConfig.cfg", + deviceProfileExportFileName(longName = "📡🌲", shortName = "ROOF", dateStamp = date), + ) + } + + @Test + fun `caps an over-long name without leaving a trailing separator`() { + val fileName = deviceProfileExportFileName(longName = "a ".repeat(60), shortName = "AAAA", dateStamp = date) + val nodeName = fileName.removePrefix("Meshtastic_").removeSuffix("_${date}_nodeConfig.cfg") + assertTrue(nodeName.length <= 48, "expected the name segment to be capped, was ${nodeName.length}") + assertFalse(nodeName.endsWith("_"), "expected no trailing separator, was '$nodeName'") + } +} diff --git a/feature/settings/src/commonTest/kotlin/org/meshtastic/feature/settings/util/FieldMetadataBoundsTest.kt b/feature/settings/src/commonTest/kotlin/org/meshtastic/feature/settings/util/FieldMetadataBoundsTest.kt new file mode 100644 index 0000000000..c4fa07b1fb --- /dev/null +++ b/feature/settings/src/commonTest/kotlin/org/meshtastic/feature/settings/util/FieldMetadataBoundsTest.kt @@ -0,0 +1,87 @@ +/* + * 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 . + */ +package org.meshtastic.feature.settings.util + +import org.meshtastic.proto.Config +import org.meshtastic.proto.FieldMetadata +import org.meshtastic.proto.FieldMetadataRegistry +import org.meshtastic.proto.ModuleConfig +import org.meshtastic.proto.current +import org.meshtastic.proto.hop_limit +import org.meshtastic.proto.red +import org.meshtastic.proto.spread_factor +import org.meshtastic.proto.tx_power +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertNull + +class FieldMetadataBoundsTest { + + @Test + fun hopLimit_readsItsBoundsFromTheRegistry() { + assertEquals(0..7, Config.LoRaConfig.hop_limit.intRange) + } + + @Test + fun ambientLighting_readsItsBoundsFromTheRegistry() { + assertEquals(0..31, ModuleConfig.AmbientLightingConfig.current.intRange) + assertEquals(0..255, ModuleConfig.AmbientLightingConfig.red.intRange) + } + + @Test + fun aFieldWithoutBounds_isUnbounded() { + assertNull(FieldMetadata.Builder().build().intRange) + } + + @Test + fun spreadFactor_readsItsBoundsFromTheRegistry() { + assertEquals(5..12, Config.LoRaConfig.spread_factor.intRange) + } + + @Test + fun boundsBeyondTheIntDomain_areUnbounded() { + val metadata = FieldMetadata.Builder().min_value(1e18).max_value(2e18).build() + assertNull(metadata.intRange) + } + + @Test + fun aMaxAboveIntMax_clampsInsteadOfSaturatingToAPoint() { + val metadata = FieldMetadata.Builder().min_value(0.0).max_value(4294967295.0).build() + assertEquals(0..Int.MAX_VALUE, metadata.intRange) + } + + @Test + fun fractionalBounds_roundInward() { + val metadata = FieldMetadata.Builder().min_value(1.5).max_value(2.5).build() + assertEquals(2..2, metadata.intRange) + } + + @Test + fun aOneSidedBound_isUnbounded() { + assertNull(FieldMetadata.Builder().max_value(7.0).build().intRange) + } + + @Test + fun unit_isCarriedAlongsideTheBounds() { + assertEquals("dBm", Config.LoRaConfig.tx_power.unit) + } + + @Test + fun companionAccessor_isTheRegistryEntry() { + assertEquals(FieldMetadataRegistry.get("meshtastic.Config.LoRaConfig", 8), Config.LoRaConfig.hop_limit) + } +} diff --git a/feature/wifi-provision/detekt-baseline.xml b/feature/wifi-provision/detekt-baseline.xml index 2449b6221b..e772b78633 100644 --- a/feature/wifi-provision/detekt-baseline.xml +++ b/feature/wifi-provision/detekt-baseline.xml @@ -2,7 +2,7 @@ - ModifierMissing:WifiProvisionScreen.kt:@OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class) @Suppress("LongMethod") @Composable fun WifiProvisionScreen + ModifierMissing:WifiProvisionScreen.kt:@OptIn(ExperimentalMaterial3Api::class) @Suppress("LongMethod") @Composable fun WifiProvisionScreen PreviewPublic:WifiProvisionPreviews.kt:@PreviewLightDark @Composable fun ConnectedFailedPreview PreviewPublic:WifiProvisionPreviews.kt:@PreviewLightDark @Composable fun ConnectedSuccessPreview PreviewPublic:WifiProvisionPreviews.kt:@PreviewLightDark @Composable fun ConnectedWithNetworksPreview diff --git a/feature/wifi-provision/src/commonMain/kotlin/org/meshtastic/feature/wifiprovision/ui/ProvisionStatusCard.kt b/feature/wifi-provision/src/commonMain/kotlin/org/meshtastic/feature/wifiprovision/ui/ProvisionStatusCard.kt index c356c2e3ab..d3d4afe06f 100644 --- a/feature/wifi-provision/src/commonMain/kotlin/org/meshtastic/feature/wifiprovision/ui/ProvisionStatusCard.kt +++ b/feature/wifi-provision/src/commonMain/kotlin/org/meshtastic/feature/wifiprovision/ui/ProvisionStatusCard.kt @@ -44,7 +44,6 @@ import org.meshtastic.core.ui.icon.Success import org.meshtastic.feature.wifiprovision.WifiProvisionUiState.ProvisionStatus /** Inline status card matching the web flasher's colored status feedback. */ -@OptIn(ExperimentalMaterial3ExpressiveApi::class) @Composable internal fun ProvisionStatusCard(provisionStatus: ProvisionStatus, isProvisioning: Boolean) { val colors = statusCardColors(provisionStatus, isProvisioning) diff --git a/feature/wifi-provision/src/commonMain/kotlin/org/meshtastic/feature/wifiprovision/ui/WifiProvisionScreen.kt b/feature/wifi-provision/src/commonMain/kotlin/org/meshtastic/feature/wifiprovision/ui/WifiProvisionScreen.kt index 0e89e4d9a6..e5442f3567 100644 --- a/feature/wifi-provision/src/commonMain/kotlin/org/meshtastic/feature/wifiprovision/ui/WifiProvisionScreen.kt +++ b/feature/wifi-provision/src/commonMain/kotlin/org/meshtastic/feature/wifiprovision/ui/WifiProvisionScreen.kt @@ -159,7 +159,7 @@ import org.meshtastic.feature.wifiprovision.model.WifiNetwork private const val NETWORK_LIST_MAX_HEIGHT_DP = 240 -@OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class) +@OptIn(ExperimentalMaterial3Api::class) @Suppress("LongMethod") @Composable fun WifiProvisionScreen( @@ -278,7 +278,6 @@ private val Phase.isLoading: Boolean // --------------------------------------------------------------------------- /** BLE scanning spinner — shown while searching for a device. */ -@OptIn(ExperimentalMaterial3ExpressiveApi::class) @Composable internal fun ScanningBleContent(error: WifiProvisionError.ConnectFailed? = null, onRetry: () -> Unit = {}) { CenteredStatusContent { @@ -310,7 +309,6 @@ internal fun ScanningBleContent(error: WifiProvisionError.ConnectFailed? = null, * Confirmation step shown after BLE device discovery — the Android analog of the web flasher's native BLE pairing * prompt. Gives the user a clear "device found" moment before proceeding. */ -@OptIn(ExperimentalMaterial3ExpressiveApi::class) @Composable internal fun DeviceFoundContent(deviceName: String?, onProceed: () -> Unit, onCancel: () -> Unit) { CenteredStatusContent { @@ -351,7 +349,6 @@ internal fun DeviceFoundContent(deviceName: String?, onProceed: () -> Unit, onCa } /** Network scanning spinner — shown during the initial scan when no networks are loaded yet. */ -@OptIn(ExperimentalMaterial3ExpressiveApi::class) @Composable internal fun ScanningNetworksContent() { CenteredStatusContent { @@ -365,7 +362,6 @@ internal fun ScanningNetworksContent() { * Main configuration screen shown after BLE connection — mirrors the web flasher's connected state. All controls (scan * button, network list, SSID/password fields, Apply, status) are on one screen. */ -@OptIn(ExperimentalMaterial3ExpressiveApi::class) @Suppress("LongMethod", "LongParameterList") @Composable internal fun ConnectedContent( diff --git a/gradle.properties b/gradle.properties index c54853d66b..0c0d58f7e6 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,6 +1,9 @@ # --- Android build features --- android.defaults.buildfeatures.resvalues=false android.defaults.buildfeatures.shaders=false +# Lint analysis runs in its own worker, so neither org.gradle.jvmargs nor +# kotlin.daemon.jvmargs applies to it. The default heap OOMs on a cold analysis. +android.experimental.lint.heapSize=3G # --- Compose --- android.experimental.enableScreenshotTest=true @@ -10,8 +13,13 @@ enableComposeCompilerReports=false # --- Kotlin --- kotlin.code.style=official -kotlin.daemon.jvm.options=-Xmx4g -XX\:+UseG1GC -XX\:SoftRefLRUPolicyMSPerMB=1 -XX\:ReservedCodeCacheSize=320m -XX\:+HeapDumpOnOutOfMemoryError +# Only kotlin.daemon.jvmargs is read from here; kotlin.daemon.jvm.options is a system +# property. Unset, the daemon inherits org.gradle.jvmargs' 8g and over-commits CI runners. +kotlin.daemon.jvmargs=-Xmx6g -XX\:+UseG1GC -XX\:SoftRefLRUPolicyMSPerMB=1 -XX\:ReservedCodeCacheSize=320m -XX\:+HeapDumpOnOutOfMemoryError kotlin.daemon.useFallback=false +# The Kotlin/Native compiler runs in its own JVM, governed by neither of the heaps +# above. Its 3g default OOMs compiling every module for iOS from a cold cache. +kotlin.native.jvmArgs=-Xmx5g -XX\:+UseG1GC -XX\:+HeapDumpOnOutOfMemoryError # --- KSP --- # (incremental processing is default-on in KSP2; only the non-default flag is declared) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 36b883307a..28e84230fc 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -2,36 +2,40 @@ xmlutil = "1.0.2.1" # Android -agp = "9.4.0" +agp = "9.4.1" appcompat = "1.8.0" appfunctions = "1.0.0-alpha11" # androidx -androidx-sqlite-bundled = "2.7.0" +androidx-sqlite-bundled = "2.7.1" androidx-work = "2.11.2" datastore = "1.2.1" glance = "1.2.0" lifecycle = "2.11.0" jetbrains-lifecycle = "2.11.0" -navigation3 = "1.1.1" +navigation3 = "1.2.0-beta01" +# navigation3-runtime is AndroidX's own KMP artifact, separate from the JetBrains +# navigation3-ui mirror above; the two refs move independently and the mirror does not +# publish every AndroidX version, so they are not always equal. +navigation3-runtime = "1.2.0-rc01" navigationevent = "1.1.0" paging = "3.5.1" -room = "3.0.2" +room = "3.0.3" koin = "4.2.2" -koin-plugin = "1.1.0" +koin-plugin = "1.2.1" # Kotlin -kotlin = "2.4.10" +kotlin = "2.4.20" kotlinx-coroutines-android = "1.11.0" kotlinx-datetime = "0.8.0-0.6.x-compat" kotlinx-serialization = "1.11.0" ktlint = "1.7.1" ktfmt = "0.61" kover = "0.9.9" -mokkery = "3.4.2" +mokkery = "3.5.0" junit5 = "6.1.3" junit-platform = "6.1.3" # aligned with junit5 — JUnit Platform uses 1.x scheme -kotest = "6.2.4" +kotest = "6.2.5" konsist = "0.17.3" turbine = "1.2.1" @@ -42,7 +46,7 @@ compose-screenshot = "0.0.1-alpha16" # `benchmark` drives both the androidx.benchmark macro lib AND the androidx.baselineprofile # Gradle plugin (same coordinates/version). Kept on the alpha track to stay compatible with # AGP 9.x (the stable 1.4.x line predates AGP 9 support). -benchmark = "1.5.0-rc02" +benchmark = "1.5.0" profileinstaller = "1.4.1" androidx-uiautomator = "2.4.0" @@ -57,8 +61,8 @@ compose-multiplatform-material3 = "1.12.0-alpha03" # current `compose-multiplatform` release maps to (see CMP release notes). # AndroidCompose.kt's resolutionStrategy force-aligns these groups to *this* version # at resolution time, so it is the source of truth for the Android target. -androidx-compose-bom-aligned = "1.12.0" -jetbrains-adaptive = "1.3.0-beta02" +androidx-compose-bom-aligned = "1.12.1" +jetbrains-adaptive = "1.3.0-rc01" # Google # The 5.1.1 floor was load-bearing while the app parsed KML through maps-utils: 5.0.0's data.parser @@ -67,7 +71,7 @@ jetbrains-adaptive = "1.3.0-beta02" # 2.8.0–2.8.1). KML is now read by the app's own xmlutil-based converter and that parser is unused, so # the pairing — and the canary test that guarded it — is gone. The floor stays as a plain minimum. android-maps-utils = "5.2.0" -maplibre-compose = "0.15.0" +maplibre-compose = "0.17.0" maps-compose = "8.6.0" # ML Kit @@ -80,32 +84,32 @@ mlkit-translate = "17.0.3" camerax = "1.6.2" # Networking -ktor = "3.5.2" +ktor = "3.6.0" # Other aboutlibraries = "15.2.0" jserialcomm = "2.11.4" -coil = "3.6.2" +coil = "3.6.3" datadog-gradle = "1.31.0" -dd-sdk-android = "3.13.1" +dd-sdk-android = "3.14.0" detekt = "2.0.0-alpha.6" dokka = "2.2.0" -devtools-ksp = "2.3.11" +devtools-ksp = "2.3.12" firebase-crashlytics-gradle = "3.0.8" google-services-gradle = "4.5.0" -markdown = "0.7.12" +markdown = "0.7.14" markdownRenderer = "0.45.0" okio = "3.18.2" uri-kmp = "0.0.21" spotless = "8.10.2" vico = "3.3.1" -kable = "0.44.3" -mqttastic = "0.8.1" +kable = "0.45.0" +mqttastic = "0.8.2" jmdns = "3.6.3" qrcode-kotlin = "4.5.0" -takpacket-sdk = "0.9.1" +takpacket-sdk = "0.9.2-SNAPSHOT" # PoC: DisplayFrame screen mirroring, locally published from protobufs@screen-mirror-poc. -# Revert to main's pin once protobufs#1054 is tagged. main tracks 2.8.0.23-gca2cb1a-SNAPSHOT. +# Revert to main's pin once protobufs#1054 is tagged. main tracks 2.8.0.96-g16fbc6c-SNAPSHOT. # (requires -PuseMavenLocal or the gradle.properties flag on this branch) meshtastic-protobufs = "2.99.0-screen-mirror-poc-SNAPSHOT" @@ -153,6 +157,7 @@ jetbrains-lifecycle-runtime-compose = { module = "org.jetbrains.androidx.lifecyc jetbrains-lifecycle-viewmodel-compose = { module = "org.jetbrains.androidx.lifecycle:lifecycle-viewmodel-compose", version.ref = "jetbrains-lifecycle" } jetbrains-lifecycle-viewmodel-navigation3 = { module = "org.jetbrains.androidx.lifecycle:lifecycle-viewmodel-navigation3", version.ref = "jetbrains-lifecycle" } jetbrains-navigation3-ui = { module = "org.jetbrains.androidx.navigation3:navigation3-ui", version.ref = "navigation3" } +androidx-navigation3-runtime = { module = "androidx.navigation3:navigation3-runtime", version.ref = "navigation3-runtime" } jetbrains-navigationevent-compose = { module = "org.jetbrains.androidx.navigationevent:navigationevent-compose", version.ref = "navigationevent" } androidx-paging-common = { module = "androidx.paging:paging-common", version.ref = "paging" } androidx-paging-compose = { module = "androidx.paging:paging-compose", version.ref = "paging" } @@ -203,7 +208,7 @@ jetbrains-compose-material3-adaptive-navigation-suite = { module = "org.jetbrain firebase-ai = { module = "com.google.firebase:firebase-ai" } firebase-ai-ondevice = { module = "com.google.firebase:firebase-ai-ondevice", version = "16.0.0-beta05" } firebase-analytics = { module = "com.google.firebase:firebase-analytics" } -firebase-bom = { module = "com.google.firebase:firebase-bom", version = "34.18.0" } +firebase-bom = { module = "com.google.firebase:firebase-bom", version = "34.19.0" } firebase-crashlytics = { module = "com.google.firebase:firebase-crashlytics" } location-services = { module = "com.google.android.gms:play-services-location", version = "21.4.0" } koin-android = { module = "io.insert-koin:koin-android", version.ref = "koin" } @@ -279,7 +284,7 @@ junit-platform-launcher = { module = "org.junit.platform:junit-platform-launcher kotest-assertions = { module = "io.kotest:kotest-assertions-core", version.ref = "kotest" } kotest-property = { module = "io.kotest:kotest-property", version.ref = "kotest" } konsist = { module = "com.lemonappdev:konsist", version.ref = "konsist" } -robolectric = { module = "org.robolectric:robolectric", version = "4.16.1" } +robolectric = { module = "org.robolectric:robolectric", version = "4.17" } turbine = { module = "app.cash.turbine:turbine", version.ref = "turbine" } # Other @@ -329,7 +334,7 @@ screenshot-validation-api = { group = "com.android.tools.screenshot", name = "sc # Build Logic android-gradleApiPlugin = { module = "com.android.tools.build:gradle-api", version.ref = "agp" } -android-tools-common = { module = "com.android.tools:common", version = "32.4.0" } +android-tools-common = { module = "com.android.tools:common", version = "32.4.1" } androidx-room-gradlePlugin = { module = "androidx.room3:room3-gradle-plugin", version.ref = "room" } compose-gradlePlugin = { module = "org.jetbrains.kotlin:compose-compiler-gradle-plugin", version.ref = "kotlin" } compose-multiplatform-gradlePlugin = { module = "org.jetbrains.compose:compose-gradle-plugin", version.ref = "compose-multiplatform" } diff --git a/marketing-screenshots/build.gradle.kts b/marketing-screenshots/build.gradle.kts new file mode 100644 index 0000000000..88a23a7135 --- /dev/null +++ b/marketing-screenshots/build.gradle.kts @@ -0,0 +1,131 @@ +/* + * 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 . + */ +import org.jetbrains.kotlin.gradle.dsl.JvmTarget +import org.meshtastic.buildlogic.maplibreDesktopRuntime + +// Marketing (store listing) screenshots — GENERATE-ONLY, intentionally NOT run in CI. +// +// Unlike :screenshot-tests (a visual-regression gate) and :docs-screenshots (doc-framed compositions), this module +// is a plain JVM program that draws the Play Store / F-Droid listing screenshots: the app's own commonMain screens +// rendered offscreen with Compose Desktop's ImageComposeScene over one sample mesh, and the real MapLibre map captured +// through maplibre-compose's MapSnapshotter. Every shot is the raw screen, as Play requires ("do not position the +// screenshots within device frames"), at one size per form factor: phone 1080x1920, 7-inch 1080x1920, 10-inch +// 2560x1440, Chromebook 1920x1080, Android XR 1920x1200, and the desktop app's Flathub listing at 1280x800. The same +// screens adapt themselves - the app's navigation suite, list-detail and two-pane layouts do the work - so a form +// factor is data, not a copy of the loop. It has no tests, so `./gradlew test` / `allTests` never touch it; the one +// command is +// +// ./gradlew :marketing-screenshots:updateMarketingScreenshots +// +// which writes en-US straight into fastlane/metadata/android/en-US/images/Screenshots/ (phone, sevenInch, +// tenInch, chromebook, xr) and desktopApp/packaging/linux/screenshots/. Any other locale in -PmarketingLocales (comma +// list, default "en-US") renders into build/marketing-screenshots// in the same layout, for a later `fastlane +// supply` run: only en-US is committed because everything under fastlane/ is read straight from git by F-Droid and +// IzzyOnDroid. The sample prose lives in src/main/composeResources/values/strings.xml, which Crowdin's first rule +// already picks up. +// -PmarketingFramed=true also writes the framed 1242x2484 phone variants (bezel, caption banner) under +// build/marketing-screenshots/framed// for website and social use. The map needs a Vulkan loader the JVM can +// find: on a stock Ubuntu nothing, on a Nix host, whose shell replaces LD_LIBRARY_PATH with its own, pass +// -PmarketingLibraryPath=/usr/lib/x86_64-linux-gnu. +plugins { + alias(libs.plugins.kotlin.jvm) + alias(libs.plugins.compose.compiler) + alias(libs.plugins.compose.multiplatform) + alias(libs.plugins.meshtastic.detekt) + alias(libs.plugins.meshtastic.spotless) +} + +kotlin { + jvmToolchain { + languageVersion.set(JavaLanguageVersion.of(25)) + vendor.set(JvmVendorSpec.JETBRAINS) + } + compilerOptions { jvmTarget.set(JvmTarget.JVM_25) } +} + +dependencies { + implementation(compose.desktop.currentOs) + implementation(libs.compose.multiplatform.foundation) + implementation(libs.compose.multiplatform.material3) + implementation(libs.compose.multiplatform.runtime) + implementation(libs.compose.multiplatform.ui) + implementation(libs.compose.multiplatform.resources) + implementation(libs.jetbrains.compose.material3.adaptive) + implementation(libs.jetbrains.compose.material3.adaptive.layout) + implementation(libs.jetbrains.compose.material3.adaptive.navigation.suite) + implementation(libs.kotlinx.coroutines.core) + implementation(libs.kotlinx.serialization.json) + implementation(libs.maplibre.compose) + maplibreDesktopRuntime() + + implementation(projects.core.ble) + implementation(projects.core.common) + implementation(projects.core.database) + implementation(projects.core.model) + implementation(projects.core.navigation) + implementation(projects.core.resources) + implementation(projects.core.ui) + implementation(projects.feature.connections) + implementation(projects.feature.map) + implementation(projects.feature.mapMaplibre) + implementation(projects.feature.messaging) + implementation(projects.feature.node) + implementation(projects.feature.settings) + implementation(libs.meshtastic.protobufs) +} + +compose.resources { + publicResClass = false + packageOfResClass = "org.meshtastic.screenshot.marketing.resources" +} + +// Script-level vals are re-bound to locals inside the task block: a lambda that reads them directly captures the +// script object, which the configuration cache refuses to serialize. +val repositoryRoot = isolated.rootProject.projectDirectory +val marketingOutput = layout.buildDirectory.dir("marketing-screenshots") +val marketingLocales = providers.gradleProperty("marketingLocales").orElse("en-US") +val marketingFramed = providers.gradleProperty("marketingFramed").orElse("false") +// The property wins because a Nix dev shell rewrites LD_LIBRARY_PATH on entry, so the exported one never arrives. +val hostLibraryPath = + providers.gradleProperty("marketingLibraryPath").orElse(providers.environmentVariable("LD_LIBRARY_PATH")) + +tasks.register("updateMarketingScreenshots") { + description = + "Renders the store-listing screenshots into fastlane/metadata/android/en-US/images/ and " + + "desktopApp/packaging/linux/screenshots/ (other locales into build/marketing-screenshots/)." + group = "store-screenshots" + val output = repositoryRoot + val buildOutput = marketingOutput + val locales = marketingLocales + val framed = marketingFramed + val libraryPath = hostLibraryPath + mainClass.set("org.meshtastic.screenshot.marketing.MainKt") + classpath = sourceSets.main.get().runtimeClasspath + // maplibre-compose reaches maplibre-native through the FFM API, which refuses to load without this. + jvmArgs("--enable-native-access=ALL-UNNAMED", "-Djava.awt.headless=true", "-Xmx2G") + argumentProviders.add( + CommandLineArgumentProvider { + listOf(output.asFile.absolutePath, buildOutput.get().asFile.absolutePath, locales.get(), framed.get()) + }, + ) + // The generator must provably run without a display, whatever the daemon inherited; the Vulkan loader search + // path, on the other hand, is the caller's to set. + environment.remove("DISPLAY") + environment.remove("WAYLAND_DISPLAY") + libraryPath.orNull?.let { environment("LD_LIBRARY_PATH", it) } + outputs.upToDateWhen { false } +} diff --git a/marketing-screenshots/src/main/composeResources/values-ar/strings.xml b/marketing-screenshots/src/main/composeResources/values-ar/strings.xml new file mode 100644 index 0000000000..68657761d4 --- /dev/null +++ b/marketing-screenshots/src/main/composeResources/values-ar/strings.xml @@ -0,0 +1,7 @@ + + + diff --git a/marketing-screenshots/src/main/composeResources/values-be/strings.xml b/marketing-screenshots/src/main/composeResources/values-be/strings.xml new file mode 100644 index 0000000000..68657761d4 --- /dev/null +++ b/marketing-screenshots/src/main/composeResources/values-be/strings.xml @@ -0,0 +1,7 @@ + + + diff --git a/marketing-screenshots/src/main/composeResources/values-bg/strings.xml b/marketing-screenshots/src/main/composeResources/values-bg/strings.xml new file mode 100644 index 0000000000..68657761d4 --- /dev/null +++ b/marketing-screenshots/src/main/composeResources/values-bg/strings.xml @@ -0,0 +1,7 @@ + + + diff --git a/marketing-screenshots/src/main/composeResources/values-ca/strings.xml b/marketing-screenshots/src/main/composeResources/values-ca/strings.xml new file mode 100644 index 0000000000..68657761d4 --- /dev/null +++ b/marketing-screenshots/src/main/composeResources/values-ca/strings.xml @@ -0,0 +1,7 @@ + + + diff --git a/marketing-screenshots/src/main/composeResources/values-cs/strings.xml b/marketing-screenshots/src/main/composeResources/values-cs/strings.xml new file mode 100644 index 0000000000..68657761d4 --- /dev/null +++ b/marketing-screenshots/src/main/composeResources/values-cs/strings.xml @@ -0,0 +1,7 @@ + + + diff --git a/marketing-screenshots/src/main/composeResources/values-de/strings.xml b/marketing-screenshots/src/main/composeResources/values-de/strings.xml new file mode 100644 index 0000000000..68657761d4 --- /dev/null +++ b/marketing-screenshots/src/main/composeResources/values-de/strings.xml @@ -0,0 +1,7 @@ + + + diff --git a/marketing-screenshots/src/main/composeResources/values-el/strings.xml b/marketing-screenshots/src/main/composeResources/values-el/strings.xml new file mode 100644 index 0000000000..68657761d4 --- /dev/null +++ b/marketing-screenshots/src/main/composeResources/values-el/strings.xml @@ -0,0 +1,7 @@ + + + diff --git a/marketing-screenshots/src/main/composeResources/values-es/strings.xml b/marketing-screenshots/src/main/composeResources/values-es/strings.xml new file mode 100644 index 0000000000..68657761d4 --- /dev/null +++ b/marketing-screenshots/src/main/composeResources/values-es/strings.xml @@ -0,0 +1,7 @@ + + + diff --git a/marketing-screenshots/src/main/composeResources/values-et/strings.xml b/marketing-screenshots/src/main/composeResources/values-et/strings.xml new file mode 100644 index 0000000000..68657761d4 --- /dev/null +++ b/marketing-screenshots/src/main/composeResources/values-et/strings.xml @@ -0,0 +1,7 @@ + + + diff --git a/marketing-screenshots/src/main/composeResources/values-fi/strings.xml b/marketing-screenshots/src/main/composeResources/values-fi/strings.xml new file mode 100644 index 0000000000..68657761d4 --- /dev/null +++ b/marketing-screenshots/src/main/composeResources/values-fi/strings.xml @@ -0,0 +1,7 @@ + + + diff --git a/marketing-screenshots/src/main/composeResources/values-fr/strings.xml b/marketing-screenshots/src/main/composeResources/values-fr/strings.xml new file mode 100644 index 0000000000..68657761d4 --- /dev/null +++ b/marketing-screenshots/src/main/composeResources/values-fr/strings.xml @@ -0,0 +1,7 @@ + + + diff --git a/marketing-screenshots/src/main/composeResources/values-ga/strings.xml b/marketing-screenshots/src/main/composeResources/values-ga/strings.xml new file mode 100644 index 0000000000..68657761d4 --- /dev/null +++ b/marketing-screenshots/src/main/composeResources/values-ga/strings.xml @@ -0,0 +1,7 @@ + + + diff --git a/marketing-screenshots/src/main/composeResources/values-gl/strings.xml b/marketing-screenshots/src/main/composeResources/values-gl/strings.xml new file mode 100644 index 0000000000..68657761d4 --- /dev/null +++ b/marketing-screenshots/src/main/composeResources/values-gl/strings.xml @@ -0,0 +1,7 @@ + + + diff --git a/marketing-screenshots/src/main/composeResources/values-he/strings.xml b/marketing-screenshots/src/main/composeResources/values-he/strings.xml new file mode 100644 index 0000000000..68657761d4 --- /dev/null +++ b/marketing-screenshots/src/main/composeResources/values-he/strings.xml @@ -0,0 +1,7 @@ + + + diff --git a/marketing-screenshots/src/main/composeResources/values-hr/strings.xml b/marketing-screenshots/src/main/composeResources/values-hr/strings.xml new file mode 100644 index 0000000000..68657761d4 --- /dev/null +++ b/marketing-screenshots/src/main/composeResources/values-hr/strings.xml @@ -0,0 +1,7 @@ + + + diff --git a/marketing-screenshots/src/main/composeResources/values-ht/strings.xml b/marketing-screenshots/src/main/composeResources/values-ht/strings.xml new file mode 100644 index 0000000000..68657761d4 --- /dev/null +++ b/marketing-screenshots/src/main/composeResources/values-ht/strings.xml @@ -0,0 +1,7 @@ + + + diff --git a/marketing-screenshots/src/main/composeResources/values-hu/strings.xml b/marketing-screenshots/src/main/composeResources/values-hu/strings.xml new file mode 100644 index 0000000000..68657761d4 --- /dev/null +++ b/marketing-screenshots/src/main/composeResources/values-hu/strings.xml @@ -0,0 +1,7 @@ + + + diff --git a/marketing-screenshots/src/main/composeResources/values-is/strings.xml b/marketing-screenshots/src/main/composeResources/values-is/strings.xml new file mode 100644 index 0000000000..68657761d4 --- /dev/null +++ b/marketing-screenshots/src/main/composeResources/values-is/strings.xml @@ -0,0 +1,7 @@ + + + diff --git a/marketing-screenshots/src/main/composeResources/values-it/strings.xml b/marketing-screenshots/src/main/composeResources/values-it/strings.xml new file mode 100644 index 0000000000..68657761d4 --- /dev/null +++ b/marketing-screenshots/src/main/composeResources/values-it/strings.xml @@ -0,0 +1,7 @@ + + + diff --git a/marketing-screenshots/src/main/composeResources/values-ja/strings.xml b/marketing-screenshots/src/main/composeResources/values-ja/strings.xml new file mode 100644 index 0000000000..68657761d4 --- /dev/null +++ b/marketing-screenshots/src/main/composeResources/values-ja/strings.xml @@ -0,0 +1,7 @@ + + + diff --git a/marketing-screenshots/src/main/composeResources/values-ko/strings.xml b/marketing-screenshots/src/main/composeResources/values-ko/strings.xml new file mode 100644 index 0000000000..68657761d4 --- /dev/null +++ b/marketing-screenshots/src/main/composeResources/values-ko/strings.xml @@ -0,0 +1,7 @@ + + + diff --git a/marketing-screenshots/src/main/composeResources/values-lt/strings.xml b/marketing-screenshots/src/main/composeResources/values-lt/strings.xml new file mode 100644 index 0000000000..68657761d4 --- /dev/null +++ b/marketing-screenshots/src/main/composeResources/values-lt/strings.xml @@ -0,0 +1,7 @@ + + + diff --git a/marketing-screenshots/src/main/composeResources/values-nl/strings.xml b/marketing-screenshots/src/main/composeResources/values-nl/strings.xml new file mode 100644 index 0000000000..68657761d4 --- /dev/null +++ b/marketing-screenshots/src/main/composeResources/values-nl/strings.xml @@ -0,0 +1,7 @@ + + + diff --git a/marketing-screenshots/src/main/composeResources/values-no/strings.xml b/marketing-screenshots/src/main/composeResources/values-no/strings.xml new file mode 100644 index 0000000000..68657761d4 --- /dev/null +++ b/marketing-screenshots/src/main/composeResources/values-no/strings.xml @@ -0,0 +1,7 @@ + + + diff --git a/marketing-screenshots/src/main/composeResources/values-pl/strings.xml b/marketing-screenshots/src/main/composeResources/values-pl/strings.xml new file mode 100644 index 0000000000..68657761d4 --- /dev/null +++ b/marketing-screenshots/src/main/composeResources/values-pl/strings.xml @@ -0,0 +1,7 @@ + + + diff --git a/marketing-screenshots/src/main/composeResources/values-pt-rBR/strings.xml b/marketing-screenshots/src/main/composeResources/values-pt-rBR/strings.xml new file mode 100644 index 0000000000..68657761d4 --- /dev/null +++ b/marketing-screenshots/src/main/composeResources/values-pt-rBR/strings.xml @@ -0,0 +1,7 @@ + + + diff --git a/marketing-screenshots/src/main/composeResources/values-pt/strings.xml b/marketing-screenshots/src/main/composeResources/values-pt/strings.xml new file mode 100644 index 0000000000..68657761d4 --- /dev/null +++ b/marketing-screenshots/src/main/composeResources/values-pt/strings.xml @@ -0,0 +1,7 @@ + + + diff --git a/marketing-screenshots/src/main/composeResources/values-ro/strings.xml b/marketing-screenshots/src/main/composeResources/values-ro/strings.xml new file mode 100644 index 0000000000..68657761d4 --- /dev/null +++ b/marketing-screenshots/src/main/composeResources/values-ro/strings.xml @@ -0,0 +1,7 @@ + + + diff --git a/marketing-screenshots/src/main/composeResources/values-ru/strings.xml b/marketing-screenshots/src/main/composeResources/values-ru/strings.xml new file mode 100644 index 0000000000..68657761d4 --- /dev/null +++ b/marketing-screenshots/src/main/composeResources/values-ru/strings.xml @@ -0,0 +1,7 @@ + + + diff --git a/marketing-screenshots/src/main/composeResources/values-sk/strings.xml b/marketing-screenshots/src/main/composeResources/values-sk/strings.xml new file mode 100644 index 0000000000..68657761d4 --- /dev/null +++ b/marketing-screenshots/src/main/composeResources/values-sk/strings.xml @@ -0,0 +1,7 @@ + + + diff --git a/marketing-screenshots/src/main/composeResources/values-sl/strings.xml b/marketing-screenshots/src/main/composeResources/values-sl/strings.xml new file mode 100644 index 0000000000..68657761d4 --- /dev/null +++ b/marketing-screenshots/src/main/composeResources/values-sl/strings.xml @@ -0,0 +1,7 @@ + + + diff --git a/marketing-screenshots/src/main/composeResources/values-sq/strings.xml b/marketing-screenshots/src/main/composeResources/values-sq/strings.xml new file mode 100644 index 0000000000..68657761d4 --- /dev/null +++ b/marketing-screenshots/src/main/composeResources/values-sq/strings.xml @@ -0,0 +1,7 @@ + + + diff --git a/marketing-screenshots/src/main/composeResources/values-sr/strings.xml b/marketing-screenshots/src/main/composeResources/values-sr/strings.xml new file mode 100644 index 0000000000..68657761d4 --- /dev/null +++ b/marketing-screenshots/src/main/composeResources/values-sr/strings.xml @@ -0,0 +1,7 @@ + + + diff --git a/marketing-screenshots/src/main/composeResources/values-srp/strings.xml b/marketing-screenshots/src/main/composeResources/values-srp/strings.xml new file mode 100644 index 0000000000..68657761d4 --- /dev/null +++ b/marketing-screenshots/src/main/composeResources/values-srp/strings.xml @@ -0,0 +1,7 @@ + + + diff --git a/marketing-screenshots/src/main/composeResources/values-sv/strings.xml b/marketing-screenshots/src/main/composeResources/values-sv/strings.xml new file mode 100644 index 0000000000..68657761d4 --- /dev/null +++ b/marketing-screenshots/src/main/composeResources/values-sv/strings.xml @@ -0,0 +1,7 @@ + + + diff --git a/marketing-screenshots/src/main/composeResources/values-tr/strings.xml b/marketing-screenshots/src/main/composeResources/values-tr/strings.xml new file mode 100644 index 0000000000..68657761d4 --- /dev/null +++ b/marketing-screenshots/src/main/composeResources/values-tr/strings.xml @@ -0,0 +1,7 @@ + + + diff --git a/marketing-screenshots/src/main/composeResources/values-uk/strings.xml b/marketing-screenshots/src/main/composeResources/values-uk/strings.xml new file mode 100644 index 0000000000..68657761d4 --- /dev/null +++ b/marketing-screenshots/src/main/composeResources/values-uk/strings.xml @@ -0,0 +1,7 @@ + + + diff --git a/marketing-screenshots/src/main/composeResources/values-zh-rCN/strings.xml b/marketing-screenshots/src/main/composeResources/values-zh-rCN/strings.xml new file mode 100644 index 0000000000..68657761d4 --- /dev/null +++ b/marketing-screenshots/src/main/composeResources/values-zh-rCN/strings.xml @@ -0,0 +1,7 @@ + + + diff --git a/marketing-screenshots/src/main/composeResources/values-zh-rTW/strings.xml b/marketing-screenshots/src/main/composeResources/values-zh-rTW/strings.xml new file mode 100644 index 0000000000..68657761d4 --- /dev/null +++ b/marketing-screenshots/src/main/composeResources/values-zh-rTW/strings.xml @@ -0,0 +1,7 @@ + + + diff --git a/marketing-screenshots/src/main/composeResources/values/strings.xml b/marketing-screenshots/src/main/composeResources/values/strings.xml new file mode 100644 index 0000000000..990845c8ae --- /dev/null +++ b/marketing-screenshots/src/main/composeResources/values/strings.xml @@ -0,0 +1,33 @@ + + + + Set up a private encrypted mesh with your group in seconds. + Share a channel with a QR code + Positions shared over the mesh, with no internet required. + Every node on the map + Messages hop node to node over LoRa. No towers, no SIM, no subscription. + Text without cell service + Signal, uptime, hops and security, one tap from the list. + Every node in detail + Battery, signal, distance and hops for every node you can hear. + See everyone on your mesh + OpenFreeMap © OpenMapTiles Data from OpenStreetMap + Water at the crossing is low, safe to ford + Heading up from the trailhead now, 4 of us + Lunch at the lookout, back on the air in 30 + Made the ridge, good signal back to base + Parked at the overflow lot, radio on + Great, we'll take the crossing route back + Bringing the truck around to the lower lot at 3 + Copy. Weather window closes around 2, keep moving + Can you grab the first aid kit from the truck? + Can you see me on the map yet? + Got the spare battery in the truck if anyone needs it + Trail crew meets at the north gate at 8 + Thanks for the weather heads-up + %1$s (%2$d) + diff --git a/marketing-screenshots/src/main/kotlin/org/meshtastic/screenshot/marketing/ConnectionsScreen.kt b/marketing-screenshots/src/main/kotlin/org/meshtastic/screenshot/marketing/ConnectionsScreen.kt new file mode 100644 index 0000000000..e83ed4eea2 --- /dev/null +++ b/marketing-screenshots/src/main/kotlin/org/meshtastic/screenshot/marketing/ConnectionsScreen.kt @@ -0,0 +1,169 @@ +/* + * 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 . + */ +@file:Suppress("MagicNumber") + +package org.meshtastic.screenshot.marketing + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.heightIn +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Card +import androidx.compose.material3.Scaffold +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import org.jetbrains.compose.resources.stringResource +import org.meshtastic.core.ble.BleConnectionState +import org.meshtastic.core.ble.BleDevice +import org.meshtastic.core.model.ConnectionState +import org.meshtastic.core.model.DeviceType +import org.meshtastic.core.model.Node +import org.meshtastic.core.navigation.TopLevelDestination +import org.meshtastic.core.resources.Res +import org.meshtastic.core.resources.connections +import org.meshtastic.core.resources.disconnect +import org.meshtastic.core.resources.firmware_version +import org.meshtastic.core.resources.rssi +import org.meshtastic.core.resources.unknown +import org.meshtastic.core.ui.component.AdaptiveTwoPane +import org.meshtastic.core.ui.component.MainAppBar +import org.meshtastic.feature.connections.model.DeviceListEntry +import org.meshtastic.feature.connections.ui.components.CurrentlyConnectedInfo +import org.meshtastic.feature.connections.ui.components.CurrentlyConnectedText +import org.meshtastic.feature.connections.ui.components.DeviceList +import org.meshtastic.feature.connections.ui.components.TransportSelector + +/** + * The connections tab as the app lays it out: the connected radio's card and the transport selector first, the + * discovery list second, side by side in the same [AdaptiveTwoPane] on an expanded window. Base Camp is connected over + * Bluetooth, with two more of the group's radios in range; the list shows one transport at a time, the app's rule. + */ +@Composable +internal fun ConnectionsScreen(mesh: SampleMesh) { + val radios = mesh.bleRadios() + MarketingTheme { + AppShell(TopLevelDestination.Connect) { + Scaffold( + topBar = { + MainAppBar( + title = stringResource(Res.string.connections), + ourNode = mesh.baseCamp, + showNodeChip = true, + canNavigateUp = false, + onNavigateUp = {}, + onClickChip = {}, + actions = {}, + ) + }, + ) { padding -> + Column( + modifier = Modifier.fillMaxSize().padding(padding).padding(horizontal = 16.dp), + verticalArrangement = Arrangement.spacedBy(16.dp), + ) { + Spacer(modifier = Modifier.height(4.dp)) + AdaptiveTwoPane( + first = { + ConnectedCard(mesh.baseCamp) + TransportSelector(activeTransport = DeviceType.BLE, onSelectTransport = {}) + }, + second = { + Box(modifier = Modifier.weight(1f).fillMaxWidth()) { + DeviceList( + connectionState = ConnectionState.Connected, + selectedDevice = radios.first().fullAddress, + bleDevices = radios, + usbDevices = emptyList(), + discoveredTcpDevices = emptyList(), + recentTcpDevices = emptyList(), + isBleScanning = false, + isNetworkScanning = false, + activeTransport = DeviceType.BLE, + onSelectDevice = {}, + onToggleBleScan = {}, + onToggleNetworkScan = {}, + onAddManualAddress = { _, _ -> }, + onRemoveRecentAddress = {}, + ) + } + }, + ) + } + } + } + } +} + +/** The connected-device card in the app's connected state: the same card shell and the feature's own contents. */ +@Composable +private fun ConnectedCard(node: Node) { + Card(modifier = Modifier.fillMaxWidth()) { + Box(modifier = Modifier.fillMaxWidth().heightIn(min = 100.dp), contentAlignment = Alignment.Center) { + CurrentlyConnectedInfo( + node = node, + text = + CurrentlyConnectedText( + unknownLabel = stringResource(Res.string.unknown), + rssiLabel = stringResource(Res.string.rssi), + disconnectLabel = stringResource(Res.string.disconnect), + firmwareVersion = stringResource(Res.string.firmware_version, SampleMesh.FIRMWARE_VERSION), + ), + onNavigateToNodeDetails = {}, + onClickDisconnect = {}, + ) + } + } +} + +/** + * The Bluetooth pane's radios: Base Camp, bonded and connected, so its row carries the node chip the app shows for a + * radio it has met before; then two of the group's radios advertising nearby under their factory names, which are the + * node number's last four hex digits. + */ +private fun SampleMesh.bleRadios(): List = listOf( + DeviceListEntry.Ble(SampleBleDevice(baseCamp, rssi = -58, connected = true), node = baseCamp), + DeviceListEntry.Ble(SampleBleDevice(trailhead, rssi = -71, connected = false)), + DeviceListEntry.Ble(SampleBleDevice(sarahsTruck, rssi = -84, connected = false)), +) + +/** A radio as the scanner would report it: advertised name and address derived from the node it belongs to. */ +private class SampleBleDevice(node: Node, override val rssi: Int, connected: Boolean) : BleDevice { + private val hex = node.num.toUInt().toString(16).padStart(8, '0') + + override val name: String = "Meshtastic_${hex.takeLast(4)}" + + override val address: String = hex.takeLast(6).uppercase().chunked(2).joinToString(":", prefix = "F0:9E:9E:") + + override val state: StateFlow = + MutableStateFlow(if (connected) BleConnectionState.Connected else BleConnectionState.Disconnected()) + + override val isBonded: Boolean = true + + override val isConnected: Boolean = connected + + override suspend fun readRssi(): Int = rssi + + override suspend fun bond() = Unit +} diff --git a/marketing-screenshots/src/main/kotlin/org/meshtastic/screenshot/marketing/FormFactor.kt b/marketing-screenshots/src/main/kotlin/org/meshtastic/screenshot/marketing/FormFactor.kt new file mode 100644 index 0000000000..76f187da59 --- /dev/null +++ b/marketing-screenshots/src/main/kotlin/org/meshtastic/screenshot/marketing/FormFactor.kt @@ -0,0 +1,123 @@ +/* + * 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 . + */ +package org.meshtastic.screenshot.marketing + +/** + * One listing surface: the pixel size the store wants, the density that puts the app's window class where that device's + * users see it, which shots it lists and where they go. The screens are surface-agnostic; only this changes between + * them. + * + * @property name the log label, and the fastlane folder for the five Play surfaces. + * @property shots the shots in listing order; [naming] turns a position in it into a file name. + * @property committedDir where the `en-US` set lands, relative to the repository root - the one tracked in git. + * @property localeDir where every other locale lands, relative to `//`. + * @property mapZoom the map shot's zoom: the 1x layouts have far more room in both directions, so they sit half a level + * closer without any of the nine chips leaving the frame. + */ +internal data class FormFactor( + val name: String, + val widthPx: Int, + val heightPx: Int, + val density: Float, + val mapZoom: Double, + val shots: List, + val naming: FileNaming, + val committedDir: String, + val localeDir: String, +) { + fun fileName(shot: Shot): String = naming.fileName(shots.indexOf(shot), shot) +} + +/** How a form factor names its files: each surface's consumer reads a different pattern off the disk. */ +internal enum class FileNaming { + /** `1_messages`: Play and F-Droid sort the listing by file name. */ + Fastlane { + override fun fileName(index: Int, shot: Shot): String = "${index + 1}_${shot.slug}" + }, + + /** `meshtastic-desktop-01-nodes`: the names the Flathub metainfo's `` URLs point at. */ + Desktop { + override fun fileName(index: Int, shot: Shot): String = + "meshtastic-desktop-${(index + 1).toString().padStart(2, '0')}-${shot.slug}" + }, ; + + abstract fun fileName(index: Int, shot: Shot): String +} + +internal object FormFactors { + /** The Play listing, in listing order. */ + private val playShots = listOf(Shot.Messages, Shot.Nodes, Shot.Map, Shot.NodeDetail, Shot.Channels) + + /** + * A Play surface: `fastlane supply` and the Play API know phone, sevenInch and tenInch; chromebook and xr have no + * upload slot anywhere and are uploaded by hand in Play Console. Only `en-US` is committed, because everything + * under `fastlane/` is read straight from git by F-Droid and IzzyOnDroid. + */ + private fun play(folder: String, widthPx: Int, heightPx: Int, density: Float, mapZoom: Double) = FormFactor( + name = folder, + widthPx = widthPx, + heightPx = heightPx, + density = density, + mapZoom = mapZoom, + shots = playShots, + naming = FileNaming.Fastlane, + committedDir = "fastlane/metadata/android/en-US/images/$folder", + localeDir = "images/$folder", + ) + + /** 1080x1920 at 2.5x is 432x768 dp: a compact window, so the app lays out its bottom navigation bar. */ + val phone = play("phoneScreenshots", 1080, 1920, 2.5f, 11.5) + + /** + * 7-inch portrait. 1080x1920 at 1.8x is 600x1067 dp: the medium width class, where the app switches to a navigation + * rail but keeps one pane. Play's tablet rules want 9:16; a Nexus 7's own 1200x1920 is 5:8. + */ + val sevenInch = play("sevenInchScreenshots", 1080, 1920, 1.8f, 11.5) + + /** + * 10-inch landscape. 2560x1440 at 2x is 1280x720 dp: expanded, so list-detail and two-pane layouts split. Its map + * area is the shortest of the Play five, so it keeps the phone's zoom; one level closer puts the river node off the + * bottom edge. + */ + val tenInch = play("tenInchScreenshots", 2560, 1440, 2f, 11.5) + + /** A Chromebook at 1x: 1920x1080 dp, expanded. No supply or API slot; hand-uploaded in Play Console. */ + val chromebook = play("chromebookScreenshots", 1920, 1080, 1f, 12.0) + + /** Android XR's 8:5 panel at 1x: 1920x1200 dp, expanded, the same shell as the Chromebook. Hand-uploaded. */ + val xr = play("xrScreenshots", 1920, 1200, 1f, 12.0) + + /** + * The desktop app's Flathub listing: 1280x800 at 1x, AppStream's recommended 16:10, expanded, the same shell as the + * Chromebook. Its map area is the 10-inch's plus 80 dp, too little for the closer zoom. The metainfo lists the + * files by commit-pinned URL, so a regenerated set is two commits: the files, then the URLs. + */ + val desktop = + FormFactor( + name = "desktop", + widthPx = 1280, + heightPx = 800, + density = 1f, + mapZoom = 11.5, + shots = listOf(Shot.Nodes, Shot.Messages, Shot.Map, Shot.Connections, Shot.Settings), + naming = FileNaming.Desktop, + committedDir = "desktopApp/packaging/linux/screenshots", + localeDir = "desktop", + ) + + val all = listOf(phone, sevenInch, tenInch, chromebook, xr, desktop) +} diff --git a/marketing-screenshots/src/main/kotlin/org/meshtastic/screenshot/marketing/Frame.kt b/marketing-screenshots/src/main/kotlin/org/meshtastic/screenshot/marketing/Frame.kt new file mode 100644 index 0000000000..ac552bfb75 --- /dev/null +++ b/marketing-screenshots/src/main/kotlin/org/meshtastic/screenshot/marketing/Frame.kt @@ -0,0 +1,230 @@ +/* + * 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 . + */ +@file:Suppress("MagicNumber") + +package org.meshtastic.screenshot.marketing + +import androidx.compose.foundation.Canvas +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.aspectRatio +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.geometry.CornerRadius +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.geometry.Size +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.ImageBitmap +import androidx.compose.ui.graphics.Path +import androidx.compose.ui.graphics.drawscope.Stroke +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import org.jetbrains.compose.resources.StringResource +import org.jetbrains.compose.resources.stringResource +import org.meshtastic.core.ui.theme.AppTheme + +/** The framed phone canvas: 1242x2484 is Play's 2:1 long-side limit; drawn at 3x so the layout is written in dp. */ +internal object Frame { + const val WIDTH_PX = 1242 + const val HEIGHT_PX = 2484 + const val DENSITY = 3f +} + +/** + * The opt-in framed variant, for the website and social posts rather than the store: a phone [screen] inside a + * captioned bezel on the 1242x2484 canvas. Play itself forbids device frames in listing screenshots. Only the phone's + * shots come through here, and every one of them carries a caption. + */ +internal fun framePhone(screen: ImageBitmap, shot: Shot): ImageBitmap { + val title = requireNotNull(shot.captionTitle) { "$shot has no caption to frame" } + val description = requireNotNull(shot.captionDescription) { "$shot has no caption to frame" } + return renderScreen(Frame.WIDTH_PX, Frame.HEIGHT_PX, Frame.DENSITY) { StoreFrame(title, description, screen) } +} + +private val Background = Color(0xFF1F2937) +private val BezelColor = Color(0xFF0B0F14) +private val StatusBarColor = Color(0xFF111418) + +// The bezel's proportions, in reference dp: a 9:16 screen with a status bar above it and an even inset around both. +private const val SCREEN_W = 315f +private const val SCREEN_H = 560f +private const val STATUS_H = 24f +private const val INSET = 7f +private const val BEZEL_ASPECT = (SCREEN_W + 2 * INSET) / (STATUS_H + SCREEN_H + 2 * INSET) +private val BezelCorner = 34.dp + +// Tall enough for a two-line title plus a three-line description at the sizes above. +private val CAPTION_BLOCK_HEIGHT = 232.dp + +/** + * A captioned store frame: the headline and body copy across the top in the app's typography, then a phone bezel with a + * drawn status bar and [screen] scaled inside it, sized so the whole phone fits above the bottom edge. + */ +@Composable +private fun StoreFrame(title: StringResource, description: StringResource, screen: ImageBitmap) { + AppTheme(darkTheme = true, dynamicColor = false) { + Column( + modifier = Modifier.fillMaxSize().background(Background).padding(horizontal = 28.dp), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + // The caption block has a fixed height whatever the text wraps to, so the bezel below it is the same + // size at the same place in all five shots; a shorter caption centres within the block. + Box(modifier = Modifier.fillMaxWidth().height(CAPTION_BLOCK_HEIGHT), contentAlignment = Alignment.Center) { + Column(horizontalAlignment = Alignment.CenterHorizontally) { + Text( + text = stringResource(title), + style = + MaterialTheme.typography.displaySmall.copy( + fontWeight = FontWeight.Bold, + lineHeight = 44.sp, + ), + color = Color.White, + textAlign = TextAlign.Center, + ) + Spacer(Modifier.height(14.dp)) + Text( + text = stringResource(description), + style = MaterialTheme.typography.titleLarge.copy(fontSize = 20.sp, lineHeight = 27.sp), + color = Color.White.copy(alpha = 0.82f), + textAlign = TextAlign.Center, + modifier = Modifier.padding(horizontal = 8.dp), + ) + } + } + Box(modifier = Modifier.weight(1f).fillMaxWidth(), contentAlignment = Alignment.TopCenter) { + PhoneBezel(screen) + } + Spacer(Modifier.height(24.dp)) + } + } +} + +@Composable +private fun PhoneBezel(screen: ImageBitmap) { + Column( + modifier = + Modifier.fillMaxHeight() + .aspectRatio(BEZEL_ASPECT, matchHeightConstraintsFirst = true) + .clip(RoundedCornerShape(BezelCorner)) + .background(BezelColor) + .padding(INSET.dp), + ) { + Column(modifier = Modifier.fillMaxSize().clip(RoundedCornerShape(BezelCorner - INSET.dp))) { + StatusBar(modifier = Modifier.fillMaxWidth().weight(STATUS_H)) + Image( + bitmap = screen, + contentDescription = null, + modifier = Modifier.fillMaxWidth().weight(SCREEN_H), + contentScale = ContentScale.FillBounds, + ) + } + } +} + +/** A fixed 9:41 status bar with signal, Wi-Fi and a full battery, drawn rather than captured so it never varies. */ +@Composable +private fun StatusBar(modifier: Modifier = Modifier) { + Row( + modifier = modifier.background(StatusBarColor).padding(horizontal = 22.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween, + ) { + Text( + text = "9:41", + style = MaterialTheme.typography.labelLarge.copy(fontWeight = FontWeight.SemiBold), + color = Color.White, + ) + Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(6.dp)) { + SignalGlyph() + WifiGlyph() + BatteryGlyph() + } + } +} + +@Composable +private fun SignalGlyph() { + Canvas(modifier = Modifier.size(width = 14.dp, height = 12.dp)) { + val path = + Path().apply { + moveTo(0f, size.height) + lineTo(size.width, 0f) + lineTo(size.width, size.height) + close() + } + drawPath(path, Color.White) + } +} + +@Composable +private fun WifiGlyph() { + Canvas(modifier = Modifier.size(width = 16.dp, height = 12.dp)) { + val stroke = Stroke(width = 2.dp.toPx()) + for (i in 0 until 3) { + val radius = size.width * (0.5f - i * 0.16f) + drawArc( + color = Color.White, + startAngle = 225f, + sweepAngle = 90f, + useCenter = false, + topLeft = Offset(size.width / 2 - radius, size.height - radius), + size = Size(radius * 2, radius * 2), + style = stroke, + ) + } + drawCircle(Color.White, radius = 1.5.dp.toPx(), center = Offset(size.width / 2, size.height - 1.dp.toPx())) + } +} + +@Composable +private fun BatteryGlyph() { + Canvas(modifier = Modifier.size(width = 24.dp, height = 12.dp)) { + val body = Size(size.width - 3.dp.toPx(), size.height) + drawRoundRect(Color.White, size = body, cornerRadius = CornerRadius(3.dp.toPx()), style = Stroke(1.5.dp.toPx())) + drawRoundRect( + Color.White, + topLeft = Offset(2.dp.toPx(), 2.dp.toPx()), + size = Size(body.width - 4.dp.toPx(), body.height - 4.dp.toPx()), + cornerRadius = CornerRadius(1.5.dp.toPx()), + ) + drawRoundRect( + Color.White, + topLeft = Offset(body.width + 1.dp.toPx(), size.height * 0.3f), + size = Size(2.dp.toPx(), size.height * 0.4f), + cornerRadius = CornerRadius(1.dp.toPx()), + ) + } +} diff --git a/marketing-screenshots/src/main/kotlin/org/meshtastic/screenshot/marketing/Main.kt b/marketing-screenshots/src/main/kotlin/org/meshtastic/screenshot/marketing/Main.kt new file mode 100644 index 0000000000..2ebcf1783a --- /dev/null +++ b/marketing-screenshots/src/main/kotlin/org/meshtastic/screenshot/marketing/Main.kt @@ -0,0 +1,151 @@ +/* + * 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 . + */ +package org.meshtastic.screenshot.marketing + +import androidx.compose.runtime.Composable +import androidx.compose.ui.graphics.ImageBitmap +import androidx.compose.ui.unit.IntSize +import java.io.File +import java.util.Locale +import kotlin.math.ceil + +private const val NANOS_PER_MILLI = 1_000_000L +private const val COMMITTED_LOCALE = "en-US" +private const val ARG_REPO_DIR = 0 +private const val ARG_BUILD_DIR = 1 +private const val ARG_LOCALES = 2 +private const val ARG_FRAMED = 3 +private const val ARG_COUNT = 4 + +/** A BCP 47 tag as Crowdin and Play spell it: a language, then optional script, region or variant subtags. */ +private val LANGUAGE_TAG = Regex("^[A-Za-z]{2,3}(-[A-Za-z0-9]{2,8})*$") + +/** + * Renders the listing screenshots: every [FormFactors.all] entry's shots, for each requested locale. + * + * Arguments: the repository root, the build output directory, a comma-separated locale list, and whether to also write + * the framed phone variants. `en-US` is written to each form factor's [FormFactor.committedDir] under the root; every + * other locale to `//` + its [FormFactor.localeDir], the same layout, for a later `fastlane supply`. + * Framed variants go to `/framed//`. + * + * The map is captured first, once per form factor, at the size the map screen lays its map area out at, and reused + * across locales: neither the basemap nor the chips are localized. Each locale then sets the JVM default locale, which + * is what the app's own strings, numbers and dates follow, and builds its own [SampleMesh] so the "last heard" labels + * are measured from a clock a few seconds old, before any of its screens are composed. + */ +fun main(args: Array) { + require(args.size == ARG_COUNT) { "usage: " } + val repoDir = File(args[ARG_REPO_DIR]) + val buildDir = File(args[ARG_BUILD_DIR]) + val locales = args[ARG_LOCALES].split(',').map { it.trim() }.filter { it.isNotEmpty() } + require(locales.isNotEmpty()) { "at least one locale is required, got '${args[ARG_LOCALES]}'" } + // Locales name output directories under the build dir, so only a language tag is accepted - nothing that could + // carry a path separator or a parent reference. + locales.forEach { require(LANGUAGE_TAG.matches(it)) { "'$it' is not a language tag such as en-US or pt-BR" } } + val framed = args[ARG_FRAMED].toBooleanStrict() + + val t0 = System.nanoTime() + val mapAreas = FormFactors.all.associateWith { it.mapArea() } + val mapSnapshots = MapSnapshot.capture(mapAreas.values.distinct(), SampleMesh()) + log("${mapSnapshots.size} map captures in ${ms(t0)} ms") + + for (locale in locales) { + Locale.setDefault(Locale.forLanguageTag(locale)) + val mesh = SampleMesh() + for (formFactor in FormFactors.all) { + val outDir = + if (locale == COMMITTED_LOCALE) { + File(repoDir, formFactor.committedDir) + } else { + File(buildDir, "$locale/${formFactor.localeDir}") + } + outDir.mkdirs() + val phoneShots = mutableMapOf() + for (shot in formFactor.shots) { + val t = System.nanoTime() + val snapshot = mapSnapshots.getValue(mapAreas.getValue(formFactor)) + val screen = formFactor.render(screen(shot, mesh, snapshot)) + val file = File(outDir, "${formFactor.fileName(shot)}.png") + screen.writePng(file) + log("$locale/${formFactor.name}/${file.name}: ${screen.width}x${screen.height} in ${ms(t)} ms") + if (framed && formFactor == FormFactors.phone) phoneShots[shot] = screen + } + if (phoneShots.isNotEmpty()) { + val framedDir = File(buildDir, "framed/$locale").apply { mkdirs() } + for ((shot, screen) in phoneShots) { + val t = System.nanoTime() + val frame = framePhone(screen, shot) + val file = File(framedDir, "${formFactor.fileName(shot)}.png") + frame.writePng(file) + log("framed/$locale/${file.name}: ${frame.width}x${frame.height} in ${ms(t)} ms") + } + } + } + } + log("done in ${ms(t0)} ms") +} + +private fun screen(shot: Shot, mesh: SampleMesh, mapSnapshot: ImageBitmap): @Composable () -> Unit = when (shot) { + Shot.Messages -> { + { MessagesScreen(mesh) } + } + + Shot.Nodes -> { + { NodesScreen(mesh) } + } + + Shot.Map -> { + { MapScreen(mesh, mapSnapshot) } + } + + Shot.NodeDetail -> { + { NodeDetailScreen(mesh) } + } + + Shot.Channels -> { + { ChannelsScreen(mesh) } + } + + Shot.Connections -> { + { ConnectionsScreen(mesh) } + } + + Shot.Settings -> { + { SettingsScreen() } + } +} + +private fun FormFactor.render(content: @Composable () -> Unit): ImageBitmap = + renderScreen(widthPx, heightPx, density, content) + +/** Lays the map screen out once with no image and reads back the map area, rounded up to whole dp. */ +private fun FormFactor.mapArea(): MapArea { + var area = IntSize.Zero + val mesh = SampleMesh() + render { MapScreen(mesh, snapshot = null, onMapArea = { area = it }) } + check(area != IntSize.Zero) { "$name: the map screen laid out no map area" } + return MapArea( + widthDp = ceil(area.width / density).toInt(), + heightDp = ceil(area.height / density).toInt(), + density = density, + zoom = mapZoom, + ) +} + +private fun ms(since: Long): Long = (System.nanoTime() - since) / NANOS_PER_MILLI + +private fun log(line: String) = System.err.println("[marketing-screenshots] $line") diff --git a/marketing-screenshots/src/main/kotlin/org/meshtastic/screenshot/marketing/MapSnapshot.kt b/marketing-screenshots/src/main/kotlin/org/meshtastic/screenshot/marketing/MapSnapshot.kt new file mode 100644 index 0000000000..b6b03f2889 --- /dev/null +++ b/marketing-screenshots/src/main/kotlin/org/meshtastic/screenshot/marketing/MapSnapshot.kt @@ -0,0 +1,147 @@ +/* + * 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 . + */ +@file:Suppress("MagicNumber") + +package org.meshtastic.screenshot.marketing + +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.ui.graphics.ImageBitmap +import androidx.compose.ui.graphics.toPixelMap +import androidx.compose.ui.platform.LocalFontFamilyResolver +import androidx.compose.ui.text.font.createFontFamilyResolver +import kotlinx.coroutines.NonCancellable +import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.withContext +import kotlinx.coroutines.withTimeout +import kotlinx.io.files.Path +import org.maplibre.compose.camera.CameraPosition +import org.maplibre.compose.map.LocalMapState +import org.maplibre.compose.map.MapRuntime +import org.maplibre.compose.map.MapRuntimeOptions +import org.maplibre.compose.map.MapSnapshotRequest +import org.maplibre.compose.map.createMapRuntime +import org.maplibre.compose.style.BaseStyle +import org.maplibre.spatialk.geojson.Position +import org.meshtastic.feature.map.maplibre.layers.NodeLayers +import org.meshtastic.feature.map.maplibre.style.Basemaps +import java.nio.file.Files + +/** One capture: the map area in dp at the screen density, and the zoom the form factor asks for. */ +internal data class MapArea(val widthDp: Int, val heightDp: Int, val density: Float, val zoom: Double) + +/** + * Captures the real mesh map — the app's default basemap with its own [NodeLayers] chips over [SampleMesh] — through + * maplibre-compose's snapshotter, with no window and no display. Each [MapArea] is the map area between a form factor's + * app bar and its navigation at that screen's density, so the screen composable places it without rescaling. + */ +internal object MapSnapshot { + private const val CAPTURE_TIMEOUT_MS = 180_000L + private const val MAX_SETTLE_RUNTIMES = 4 + + fun capture(areas: List, mesh: SampleMesh): Map = + areas.associateWith { area -> captureUntilStable(area, mesh) } + + /** + * Label and glyph placement depends on the order tiles arrive, which the network decides on the first load, so a + * capture can differ from the next run's by a few pixels; a second capture from the same runtime always matches the + * first, because that runtime already holds the placement. So each capture comes from a fresh runtime over one + * shared tile cache: the first fills the cache from the network, the ones after it read the cache in a stable + * order, and two consecutive runtimes agreeing means the run is reproducible. A warm runtime costs about a second. + */ + private fun captureUntilStable(area: MapArea, mesh: SampleMesh): ImageBitmap = runBlocking { + val cacheDir = Files.createTempDirectory("marketing-maplibre") + val cacheFile = Path(cacheDir.resolve("cache.db").toString()) + try { + var previous = captureOnce(cacheFile, area, mesh) + repeat(MAX_SETTLE_RUNTIMES - 1) { + val next = captureOnce(cacheFile, area, mesh) + if (next.toPixelMap().buffer.contentEquals(previous.toPixelMap().buffer)) return@runBlocking next + previous = next + } + System.err.println( + "[marketing-screenshots] warning: map ${area.widthDp}x${area.heightDp}@${area.density} never settled", + ) + previous + } finally { + cacheDir.toFile().deleteRecursively() + } + } + + private suspend fun captureOnce(cacheFile: Path, area: MapArea, mesh: SampleMesh): ImageBitmap { + val runtime = createMapRuntime(MapRuntimeOptions(cacheFile = cacheFile)) + return try { + withTimeout(CAPTURE_TIMEOUT_MS) { runtime.capture(area, mesh) } + } finally { + withContext(NonCancellable) { + runtime.close() + runtime.awaitClosed() + } + } + } + + /** + * The app's default basemap, labels included: a street map with no names is not the in-app experience Play asks + * for. MapLibre packs glyphs into an atlas in the order tiles arrive, so on the wide layouts a label's antialiased + * edge can land one level off between generations - a dozen pixels, invisible - which is why [captureUntilStable] + * warns rather than fails when two runtimes disagree, and why a regenerated wide map may not `cmp` the last one. + */ + private val baseStyle: BaseStyle = BaseStyle.Uri(Basemaps.Liberty.styleUri) + + private suspend fun MapRuntime.capture(area: MapArea, mesh: SampleMesh): ImageBitmap { + // NodeLayers reads LocalMapState (for cluster clicks) and rasterizes chips with a TextMeasurer; the + // snapshotter's own composition provides neither, so both are supplied here. + val mapState = createMapState(baseStyle) + val fontResolver = createFontFamilyResolver() + val snapshotter = + createSnapshotter(baseStyle) { + CompositionLocalProvider( + LocalMapState provides mapState, + LocalFontFamilyResolver provides fontResolver, + ) { + NodeLayers( + nodes = mesh.nodes, + myNodeNum = mesh.baseCamp.num, + showPrecisionCircles = false, + onNodeClick = {}, + onClusterZoom = { _, _ -> }, + onClusterMembers = {}, + visibleBounds = null, + zoom = area.zoom.toInt(), + ) + } + } + return try { + val request = + MapSnapshotRequest( + width = area.widthDp, + height = area.heightDp, + cameraPosition = + CameraPosition( + target = Position(longitude = SampleMesh.CENTER_LON, latitude = SampleMesh.CENTER_LAT), + zoom = area.zoom, + ), + density = area.density, + ) + snapshotter.capture(request) + } finally { + withContext(NonCancellable) { + snapshotter.close() + snapshotter.awaitClosed() + } + } + } +} diff --git a/marketing-screenshots/src/main/kotlin/org/meshtastic/screenshot/marketing/MessagesScreen.kt b/marketing-screenshots/src/main/kotlin/org/meshtastic/screenshot/marketing/MessagesScreen.kt new file mode 100644 index 0000000000..758018c9b0 --- /dev/null +++ b/marketing-screenshots/src/main/kotlin/org/meshtastic/screenshot/marketing/MessagesScreen.kt @@ -0,0 +1,197 @@ +/* + * 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 . + */ +@file:Suppress("MagicNumber") + +package org.meshtastic.screenshot.marketing + +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.lazy.itemsIndexed +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import org.jetbrains.compose.resources.stringResource +import org.meshtastic.core.model.Channel +import org.meshtastic.core.model.Message +import org.meshtastic.core.navigation.TopLevelDestination +import org.meshtastic.core.resources.Res +import org.meshtastic.core.resources.channels +import org.meshtastic.core.resources.conversations +import org.meshtastic.core.resources.direct_messages +import org.meshtastic.core.resources.map +import org.meshtastic.core.resources.send +import org.meshtastic.core.resources.type_a_message +import org.meshtastic.core.ui.component.MainAppBar +import org.meshtastic.core.ui.icon.MeshtasticIcons +import org.meshtastic.core.ui.icon.Send +import org.meshtastic.feature.messaging.component.MessageItem +import org.meshtastic.feature.messaging.component.MessageTopBar +import org.meshtastic.feature.messaging.ui.contact.ContactItem + +/** + * The messages tab: the LongTurbo thread, and beside it on an expanded window the conversation list the app shows in + * the list pane. A compact window shows the thread alone, as the app does once a conversation is open. + */ +@Composable +internal fun MessagesScreen(mesh: SampleMesh) { + MarketingTheme { + AppShell(TopLevelDestination.Messages) { + ListDetail(compactPane = Pane.Detail, list = { ConversationsPane(mesh) }, detail = { ThreadPane(mesh) }) + } + } +} + +/** The conversation list from the messaging feature's own [ContactItem] rows under its own app bar. */ +@Composable +private fun ConversationsPane(mesh: SampleMesh) { + val (channels, direct) = mesh.contacts.partition { it.contactKey.endsWith("^all") } + val openThread = mesh.contacts.first().contactKey + Scaffold( + topBar = { + MainAppBar( + title = stringResource(Res.string.conversations), + ourNode = mesh.baseCamp, + showNodeChip = true, + canNavigateUp = false, + onNavigateUp = {}, + onClickChip = {}, + actions = {}, + ) + }, + ) { padding -> + LazyColumn(modifier = Modifier.fillMaxSize().padding(padding)) { + item { SectionHeader(stringResource(Res.string.channels), channels.size) } + items(channels, key = { it.contactKey }) { contact -> + ContactItem( + contact = contact.copy(lastMessageText = previewLine(mesh, contact.contactKey)), + selected = false, + isActive = contact.contactKey == openThread, + channels = mesh.channelSet, + ) + } + item { SectionHeader(stringResource(Res.string.direct_messages), direct.size) } + items(direct, key = { it.contactKey }) { contact -> + ContactItem( + contact = contact.copy(lastMessageText = previewLine(mesh, contact.contactKey)), + selected = false, + channels = mesh.channelSet, + ) + } + } + } +} + +/** The app prefixes a received preview with the sender's short name and leaves our own bare. */ +@Composable +private fun previewLine(mesh: SampleMesh, contactKey: String): String { + val (sender, line) = mesh.contactPreview.getValue(contactKey) + val text = stringResource(line) + return if (sender == null) text else "${sender.user.short_name}: $text" +} + +/** The LongTurbo channel thread, built from the messaging feature's own bubbles, reactions and top bar. */ +@Composable +private fun ThreadPane(mesh: SampleMesh) { + val channelName = Channel(mesh.channelSet.settings.first(), mesh.channelSet.lora_config!!).name + val messages = mesh.messages.map { it.copy(text = stringResource(mesh.messageText.getValue(it.uuid))) } + Scaffold( + topBar = { + MessageTopBar( + title = channelName, + channelIndex = 0, + mismatchKey = false, + onNavigateBack = {}, + channels = mesh.channelSet, + channelIndexParam = 0, + showQuickChat = false, + onToggleQuickChat = {}, + ) + }, + bottomBar = { Composer() }, + ) { padding -> + // The app's list is reversed and anchored at the newest message, so a short window shows the end of the + // thread; the grouping rule still reads in thread order. + val newestFirst = messages.asReversed() + LazyColumn( + modifier = Modifier.fillMaxSize().padding(padding), + contentPadding = PaddingValues(vertical = 8.dp), + reverseLayout = true, + ) { + itemsIndexed(newestFirst, key = { _, message -> message.uuid }) { reversedIndex, message -> + val index = messages.lastIndex - reversedIndex + val prevSame = index > 0 && sameGroup(messages[index - 1], message) + val nextSame = index < messages.lastIndex && sameGroup(message, messages[index + 1]) + MessageItem( + message = message, + node = message.node, + ourNode = mesh.baseCamp, + selected = false, + showUserName = !prevSame, + hasSamePrev = prevSame, + hasSameNext = nextSame, + emojis = mesh.reactions[message.uuid].orEmpty(), + ) + } + } + } +} + +/** The list's own grouping rule: one sender, one direction, within ten minutes. The feature's copy is internal. */ +private fun sameGroup(older: Message, newer: Message): Boolean = older.fromLocal == newer.fromLocal && + older.node.num == newer.node.num && + newer.receivedTime - older.receivedTime < 10 * 60_000L + +/** + * The composer row. The feature's `MessageInput` is internal to `feature:messaging`, so this is the same shape drawn + * from Material 3 primitives: an outlined field with the real placeholder string and the real send icon. + */ +@Composable +private fun Composer() { + Row( + modifier = Modifier.fillMaxWidth().padding(horizontal = 8.dp, vertical = 8.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + OutlinedTextField( + value = "", + onValueChange = {}, + modifier = Modifier.weight(1f), + placeholder = { Text(stringResource(Res.string.type_a_message)) }, + shape = MaterialTheme.shapes.extraLarge, + singleLine = true, + ) + IconButton(onClick = {}) { + Icon( + imageVector = MeshtasticIcons.Send, + contentDescription = stringResource(Res.string.send), + tint = MaterialTheme.colorScheme.primary, + ) + } + } +} diff --git a/marketing-screenshots/src/main/kotlin/org/meshtastic/screenshot/marketing/NodesScreen.kt b/marketing-screenshots/src/main/kotlin/org/meshtastic/screenshot/marketing/NodesScreen.kt new file mode 100644 index 0000000000..dd1b687248 --- /dev/null +++ b/marketing-screenshots/src/main/kotlin/org/meshtastic/screenshot/marketing/NodesScreen.kt @@ -0,0 +1,143 @@ +/* + * 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 . + */ +@file:Suppress("MagicNumber") + +package org.meshtastic.screenshot.marketing + +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.material3.Scaffold +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import org.jetbrains.compose.resources.stringResource +import org.meshtastic.core.common.util.MeasurementSystem +import org.meshtastic.core.model.ConnectionState +import org.meshtastic.core.model.Node +import org.meshtastic.core.navigation.TopLevelDestination +import org.meshtastic.core.resources.Res +import org.meshtastic.core.resources.nodes +import org.meshtastic.core.ui.component.MainAppBar +import org.meshtastic.core.ui.component.NodeItem +import org.meshtastic.feature.node.detail.NodeDetailContent +import org.meshtastic.feature.node.detail.NodeDetailUiState +import org.meshtastic.feature.node.model.LogsType +import org.meshtastic.feature.node.model.MetricsState + +/** + * The nodes tab: the list, and beside it on an expanded window a node's detail page. Summit Solar sits in the detail + * pane here so this shot and [NodeDetailScreen], which opens Ridge Top, are two different pages on a wide window. + */ +@Composable +internal fun NodesScreen(mesh: SampleMesh) { + MarketingTheme { + AppShell(TopLevelDestination.Nodes) { + ListDetail( + compactPane = Pane.List, + list = { NodesPane(mesh) }, + detail = { NodeDetailPane(mesh, mesh.summitSolar) }, + ) + } + } +} + +/** Ridge Top's detail page, with the list beside it on an expanded window. */ +@Composable +internal fun NodeDetailScreen(mesh: SampleMesh) { + MarketingTheme { + AppShell(TopLevelDestination.Nodes) { + ListDetail( + compactPane = Pane.Detail, + list = { NodesPane(mesh) }, + detail = { NodeDetailPane(mesh, mesh.ridgeTop) }, + ) + } + } +} + +/** The node list as the app shows it: our node's chip in the bar, one [NodeItem] card per node. */ +@Composable +private fun NodesPane(mesh: SampleMesh) { + val ourNode = mesh.baseCamp + Scaffold( + topBar = { + MainAppBar( + title = stringResource(Res.string.nodes), + ourNode = ourNode, + showNodeChip = true, + canNavigateUp = false, + onNavigateUp = {}, + onClickChip = {}, + actions = {}, + ) + }, + ) { padding -> + LazyColumn(modifier = Modifier.fillMaxSize().padding(padding).padding(horizontal = 8.dp)) { + items(mesh.nodes, key = { it.num }) { node -> + NodeItem( + thisNode = ourNode, + thatNode = node, + distanceUnits = MeasurementSystem.METRIC, + tempInFahrenheit = false, + connectionState = ConnectionState.Connected, + isActive = node.num == ourNode.num, + ) + } + } + } +} + +/** A remote, unmanaged node's page from the node feature's own [NodeDetailContent]. */ +@Composable +private fun NodeDetailPane(mesh: SampleMesh, node: Node) { + Scaffold( + topBar = { + MainAppBar( + title = node.user.long_name, + ourNode = mesh.baseCamp, + showNodeChip = false, + canNavigateUp = true, + onNavigateUp = {}, + onClickChip = {}, + actions = {}, + ) + }, + ) { padding -> + NodeDetailContent( + uiState = + NodeDetailUiState( + node = node, + ourNode = mesh.baseCamp, + metricsState = MetricsState(isLocal = false, isManaged = false), + availableLogs = + setOf( + LogsType.DEVICE, + LogsType.POSITIONS, + LogsType.ENVIRONMENT, + LogsType.SIGNAL, + LogsType.TRACEROUTE, + ), + ), + onAction = {}, + onFirmwareSelect = {}, + onSaveNotes = { _, _ -> }, + modifier = Modifier.padding(padding), + ) + } +} diff --git a/marketing-screenshots/src/main/kotlin/org/meshtastic/screenshot/marketing/Render.kt b/marketing-screenshots/src/main/kotlin/org/meshtastic/screenshot/marketing/Render.kt new file mode 100644 index 0000000000..fc53a80dd5 --- /dev/null +++ b/marketing-screenshots/src/main/kotlin/org/meshtastic/screenshot/marketing/Render.kt @@ -0,0 +1,61 @@ +/* + * 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 . + */ +package org.meshtastic.screenshot.marketing + +import androidx.compose.runtime.Composable +import androidx.compose.ui.ImageComposeScene +import androidx.compose.ui.graphics.ImageBitmap +import androidx.compose.ui.graphics.asSkiaBitmap +import androidx.compose.ui.graphics.toComposeImageBitmap +import androidx.compose.ui.unit.Density +import androidx.compose.ui.use +import org.jetbrains.skia.EncodedImageFormat +import org.jetbrains.skia.Image +import java.io.File + +/** Bounded settle: frames rendered until the scene reports no pending invalidation, or this many at most. */ +private const val MAX_FRAMES = 30 +private const val FRAME_NANOS = 16_000_000L + +/** + * Step one of the pipeline, form-factor agnostic: composes [content] offscreen at the given pixel size and density and + * returns the settled frame. A phone screen, a desktop window and a store frame all come through here. + */ +internal fun renderScreen(width: Int, height: Int, density: Float, content: @Composable () -> Unit): ImageBitmap = + ImageComposeScene(width = width, height = height, density = Density(density), content = content).use { scene -> + var nanos = 0L + var frames = 1 + var image = scene.render(nanos) + while (scene.hasInvalidations() && frames < MAX_FRAMES) { + nanos += FRAME_NANOS + val next = scene.render(nanos) + // Each superseded frame is a native Skia image; the last one is owned by the returned bitmap. + image.close() + image = next + frames++ + } + if (scene.hasInvalidations()) { + System.err.println("[marketing-screenshots] warning: scene still invalidating after $frames frames") + } + image.toComposeImageBitmap() + } + +internal fun ImageBitmap.writePng(file: File) { + Image.makeFromBitmap(asSkiaBitmap()).use { image -> + checkNotNull(image.encodeToData(EncodedImageFormat.PNG)).use { data -> file.writeBytes(data.bytes) } + } +} diff --git a/marketing-screenshots/src/main/kotlin/org/meshtastic/screenshot/marketing/SampleMesh.kt b/marketing-screenshots/src/main/kotlin/org/meshtastic/screenshot/marketing/SampleMesh.kt new file mode 100644 index 0000000000..0401e31bfc --- /dev/null +++ b/marketing-screenshots/src/main/kotlin/org/meshtastic/screenshot/marketing/SampleMesh.kt @@ -0,0 +1,556 @@ +/* + * 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 . + */ +@file:Suppress("MagicNumber") + +package org.meshtastic.screenshot.marketing + +import okio.ByteString.Companion.toByteString +import org.jetbrains.compose.resources.StringResource +import org.meshtastic.core.common.util.nowMillis +import org.meshtastic.core.model.Contact +import org.meshtastic.core.model.ContactKey +import org.meshtastic.core.model.Message +import org.meshtastic.core.model.MessageStatus +import org.meshtastic.core.model.Node +import org.meshtastic.core.model.Reaction +import org.meshtastic.proto.ChannelSet +import org.meshtastic.proto.ChannelSettings +import org.meshtastic.proto.Config +import org.meshtastic.proto.DeviceMetadata +import org.meshtastic.proto.DeviceMetrics +import org.meshtastic.proto.EnvironmentMetrics +import org.meshtastic.proto.HardwareModel +import org.meshtastic.proto.Position +import org.meshtastic.proto.PowerMetrics +import org.meshtastic.proto.User +import org.meshtastic.screenshot.marketing.resources.Res +import org.meshtastic.screenshot.marketing.resources.marketing_msg_crossing_low +import org.meshtastic.screenshot.marketing.resources.marketing_msg_heading_up +import org.meshtastic.screenshot.marketing.resources.marketing_msg_lunch_lookout +import org.meshtastic.screenshot.marketing.resources.marketing_msg_made_ridge +import org.meshtastic.screenshot.marketing.resources.marketing_msg_parked +import org.meshtastic.screenshot.marketing.resources.marketing_msg_take_crossing +import org.meshtastic.screenshot.marketing.resources.marketing_msg_truck_around +import org.meshtastic.screenshot.marketing.resources.marketing_msg_weather_window +import org.meshtastic.screenshot.marketing.resources.marketing_preview_first_aid +import org.meshtastic.screenshot.marketing.resources.marketing_preview_see_me_on_map +import org.meshtastic.screenshot.marketing.resources.marketing_preview_spare_battery +import org.meshtastic.screenshot.marketing.resources.marketing_preview_trail_crew +import org.meshtastic.screenshot.marketing.resources.marketing_preview_weather_thanks +import java.time.LocalDate +import java.time.ZoneId + +/** + * One plausible mesh shared by every store screenshot, so the five shots tell one story: a base camp, a ridge-top + * router, and a handful of hikers, boats and trucks spread across a valley outside Dallas. Nine nodes, so the map never + * clusters them. "Last heard" and message timestamps hang off the wall clock because the row composables format them + * relative to now. + * + * Node and channel names are literals: they are what a real group typed into their radios, and the channel names feed + * the QR payload. The prose - the thread, the direct-message previews - is a string resource per line, resolved at + * composition so each locale renders its own translation once Crowdin has filled it in. + * + * Built once per render pass: "last heard" hangs off [now], the wall clock when the mesh was built, and the row + * composables format it against the wall clock at composition. [heardAgo] places every offset mid-minute, so a label + * can only change once composition falls more than 30 s behind construction; one locale's pass takes a third of that. + */ +internal class SampleMesh(private val now: Int = (nowMillis / 1_000L).toInt()) { + companion object { + const val CENTER_LAT = 32.7767 + const val CENTER_LON = -96.797 + + /** The firmware every radio in the group runs; the routers report it, and the connections card shows it. */ + const val FIRMWARE_VERSION = "2.8.1.f3c2a9" + private const val HALF_MINUTE_SECONDS = 30 + private const val JUST_NOW_SECONDS = 5 + } + + /** 0 reads "Now"; anything else lands 30 s into that minute so a delayed composition cannot move the label. */ + private fun heardAgo(minutes: Int): Int = if (minutes == 0) JUST_NOW_SECONDS else minutes * 60 + HALF_MINUTE_SECONDS + + /** A fixed, obviously synthetic 256-bit key so the detail page shows a keyed node rather than a key warning. */ + private val ridgeKey = ByteArray(32) { i -> (i * 13 + 29).toByte() }.toByteString() + private val summitKey = ByteArray(32) { i -> (i * 17 + 41).toByte() }.toByteString() + + val baseCamp = + node( + 0x2b3c4d5e, + "Base Camp", + "BASE", + HardwareModel.HELTEC_V3, + Role.CLIENT, + 95, + 0, + 0, + 0.0, + 0.0, + 0f, + 0, + favorite = true, + ) + val ridgeTop = + node( + 0x1a2b3c4d, + "Ridge Top", + "RDGE", + HardwareModel.RAK4631, + Role.ROUTER, + 88, + 0, + 2, + 0.04, + 0.03, + 11.5f, + -68, + favorite = true, + ) + .let { base -> + base.copy(user = base.user.newBuilder().also { it.public_key = ridgeKey }.build(), publicKey = ridgeKey) + } + .copy( + metadata = + DeviceMetadata.Builder() + .also { + it.firmware_version = FIRMWARE_VERSION + it.hw_model = HardwareModel.RAK4631 + it.role = Role.ROUTER + it.hasBluetooth = true + it.hasWifi = false + it.canShutdown = true + } + .build(), + environmentMetrics = + EnvironmentMetrics.Builder() + .also { + it.temperature = 17.4f + it.relative_humidity = 41f + it.barometric_pressure = 862.7f + } + .build(), + deviceMetrics = + DeviceMetrics.Builder() + .also { + it.battery_level = 88 + it.voltage = 4.02f + it.channel_utilization = 6.8f + it.air_util_tx = 2.3f + it.uptime_seconds = 19 * 86_400 + 7 * 3_600 + } + .build(), + ) + val trailhead = + node(0x3c4d5e6f, "Trailhead", "TRLH", HardwareModel.TBEAM, Role.CLIENT, 72, 1, 3, -0.03, 0.045, 8.25f, -92) + val riverCrossing = + node( + 0x4d5e6f70, + "River Crossing", + "RIVR", + HardwareModel.T_ECHO, + Role.CLIENT_MUTE, + 64, + 1, + 7, + -0.045, + -0.02, + 6.0f, + -101, + ) + + /** A solar router two hops out: the second node with a full detail page, so the wide layouts show two of them. */ + val summitSolar = + node( + 0x5e6f7081, + "Summit Solar", + "SMMT", + HardwareModel.RAK4631, + Role.ROUTER, + 91, + 2, + 15, + 0.05, + -0.04, + 3.5f, + -110, + favorite = true, + ) + .let { base -> + base.copy( + user = base.user.newBuilder().also { it.public_key = summitKey }.build(), + publicKey = summitKey, + ) + } + .copy( + metadata = + DeviceMetadata.Builder() + .also { + it.firmware_version = FIRMWARE_VERSION + it.hw_model = HardwareModel.RAK4631 + it.role = Role.ROUTER + it.hasBluetooth = true + it.hasWifi = false + it.canShutdown = true + } + .build(), + environmentMetrics = + EnvironmentMetrics.Builder() + .also { + it.temperature = 9.6f + it.relative_humidity = 58f + it.barometric_pressure = 791.3f + } + .build(), + powerMetrics = + PowerMetrics.Builder() + .also { + it.ch1_voltage = 13.8f + it.ch1_current = 412f + } + .build(), + deviceMetrics = + DeviceMetrics.Builder() + .also { + it.battery_level = 91 + it.voltage = 4.09f + it.channel_utilization = 5.1f + it.air_util_tx = 1.9f + it.uptime_seconds = 63 * 86_400 + 2 * 3_600 + } + .build(), + ) + val kayakDan = + node( + 0x6f708192, + "Kayak Dan", + "KDAN", + HardwareModel.HELTEC_WIRELESS_TRACKER, + Role.TRACKER, + 58, + 2, + 25, + 0.012, + -0.042, + 2.75f, + -113, + ) + val fireLookout = + node( + 0x708192a3, + "Old Fire Lookout", + "LOOK", + HardwareModel.TBEAM, + Role.CLIENT, + 47, + 3, + 60, + -0.01, + 0.02, + -1.5f, + -118, + ) + val sarahsTruck = + node( + 0x8192a3b4.toInt(), + "Sarah's Truck", + "SRAH", + HardwareModel.T_DECK, + Role.CLIENT, + 83, + 1, + 4, + 0.025, + 0.01, + 9.0f, + -85, + ) + val hamShack = + node( + 0xa3b4c5d6.toInt(), + "Ham Shack", + "SHCK", + HardwareModel.STATION_G2, + Role.CLIENT, + 77, + 2, + 11, + -0.02, + -0.035, + 4.5f, + -104, + ) + + /** The node list, in the order the real list would show it: us first, then favorites, then by last heard. */ + val nodes: List = + listOf(baseCamp, ridgeTop, summitSolar, trailhead, sarahsTruck, riverCrossing, hamShack, kayakDan, fireLookout) + + /** LongTurbo (index 0, default PSK), a private group channel with a full 256-bit key, and a second private one. */ + val channelSet: ChannelSet = + ChannelSet.Builder() + .also { set -> + set.settings = + listOf( + ChannelSettings.Builder().also { it.psk = byteArrayOf(1).toByteString() }.build(), + ChannelSettings.Builder() + .also { + it.name = "Basecamp" + it.psk = ByteArray(32) { i -> (i * 7 + 3).toByte() }.toByteString() + } + .build(), + ChannelSettings.Builder() + .also { + it.name = "TrailCrew" + it.psk = ByteArray(32) { i -> (i * 11 + 5).toByte() }.toByteString() + } + .build(), + ) + set.lora_config = + Config.LoRaConfig.Builder() + .also { + it.use_preset = true + it.modem_preset = Config.LoRaConfig.ModemPreset.LONG_TURBO + it.region = Config.LoRaConfig.RegionCode.US + it.hop_limit = 3 + it.tx_enabled = true + } + .build() + } + .build() + + private val messageTextByUuid = mutableMapOf() + + /** + * The LongTurbo thread as seen from Base Camp: a day trip checking in from the ridge and the river. Each + * [Message.text] is empty here; [messageText] carries the line as a resource, and the screen fills it in at + * composition. + */ + val messages: List = + listOf( + received(trailhead, Res.string.marketing_msg_heading_up, "09:02", 39, 5.5f, -92, hops = 1), + received(sarahsTruck, Res.string.marketing_msg_parked, "09:05", 36, 9.0f, -85, hops = 1), + sent(Res.string.marketing_msg_weather_window, "09:07", 34), + received(trailhead, Res.string.marketing_msg_made_ridge, "09:31", 10, 11.25f, -70, hops = 1) + .withReactions(reaction(baseCamp, "👍", 9), reaction(sarahsTruck, "🔥", 8)), + received(kayakDan, Res.string.marketing_msg_crossing_low, "09:34", 7, 2.75f, -113, hops = 2), + sent(Res.string.marketing_msg_take_crossing, "09:35", 6), + received(sarahsTruck, Res.string.marketing_msg_truck_around, "09:38", 3, 8.5f, -88, hops = 1) + .withReactions(reaction(trailhead, "❤️", 2)), + received(trailhead, Res.string.marketing_msg_lunch_lookout, "09:41", 0, 10.0f, -74, hops = 1), + ) + + /** The prose of each message in [messages], by uuid. */ + val messageText: Map + get() = messageTextByUuid + + /** + * The conversation list beside the thread on wide layouts: the three channels, then the hikers Base Camp has + * messaged directly. [Contact.lastMessageText] is empty here for the same reason as [Message.text]; the preview + * line is in [contactPreview]. + */ + val contacts: List = + listOf( + channelContact(0, "LongTurbo", at = "09:41", unread = 0), + channelContact(1, "Basecamp", at = "09:23", unread = 0), + channelContact(2, "TrailCrew", at = "08:55", unread = 2), + directContact(trailhead, at = "09:29"), + directContact(sarahsTruck, at = "09:14", unread = 1), + directContact(kayakDan, at = "08:46"), + ) + + /** + * The preview line of each contact in [contacts], by contact key: the sender whose short name the app prefixes the + * line with (none when Base Camp sent it), and the line itself. + */ + val contactPreview: Map> = + mapOf( + contacts[0].contactKey to (trailhead to Res.string.marketing_msg_lunch_lookout), + contacts[1].contactKey to (sarahsTruck to Res.string.marketing_preview_spare_battery), + contacts[2].contactKey to (trailhead to Res.string.marketing_preview_trail_crew), + contacts[3].contactKey to (trailhead to Res.string.marketing_preview_weather_thanks), + contacts[4].contactKey to (null to Res.string.marketing_preview_first_aid), + contacts[5].contactKey to (kayakDan to Res.string.marketing_preview_see_me_on_map), + ) + + /** + * The row shows a message from today as its clock time, so these are fixed times of day rather than offsets from + * now, in step with the thread's own "09:02" stamps; an offset would print the wall clock. + */ + private fun todayAt(time: String): Long { + val (hour, minute) = time.split(':').map(String::toInt) + return LocalDate.now().atTime(hour, minute).atZone(ZoneId.systemDefault()).toInstant().toEpochMilli() + } + + private fun channelContact(index: Int, name: String, at: String, unread: Int): Contact = Contact( + contactKey = ContactKey.broadcast(index).value, + shortName = index.toString(), + longName = name, + lastMessageTime = todayAt(at), + lastMessageText = "", + unreadCount = unread, + messageCount = messages.size, + isMuted = false, + isUnmessageable = false, + ) + + private fun directContact(from: Node, at: String, unread: Int = 0): Contact = Contact( + contactKey = "0${from.user.id}", + shortName = from.user.short_name, + longName = from.user.long_name, + lastMessageTime = todayAt(at), + lastMessageText = "", + unreadCount = unread, + messageCount = 4, + isMuted = false, + isUnmessageable = false, + nodeColors = from.colors, + ) + + /** Reactions render from `MessageItem`'s own list, not [Message.emojis], so they are carried per message here. */ + val reactions: Map> = messages.associate { it.uuid to it.emojis } + + private fun Message.withReactions(vararg reactions: Reaction): Message = copy(emojis = reactions.toList()) + + private fun reaction(from: Node, emoji: String, minutesAgo: Int): Reaction = Reaction( + replyId = 0, + user = from.user, + emoji = emoji, + timestamp = nowMillis - minutesAgo * 60_000L, + snr = 6.0f, + rssi = -90, + hopsAway = from.hopsAway, + ) + + private var nextMessageId = 0L + + private fun received( + from: Node, + text: StringResource, + time: String, + minutesAgo: Int, + snr: Float, + rssi: Int, + hops: Int, + ): Message = message( + from, + text, + time, + minutesAgo, + fromLocal = false, + status = MessageStatus.RECEIVED, + snr = snr, + rssi = rssi, + hops = hops, + ) + + private fun sent(text: StringResource, time: String, minutesAgo: Int): Message = message( + baseCamp, + text, + time, + minutesAgo, + fromLocal = true, + status = MessageStatus.DELIVERED, + snr = null, + rssi = null, + hops = 0, + ) + + private fun message( + from: Node, + text: StringResource, + time: String, + minutesAgo: Int, + fromLocal: Boolean, + status: MessageStatus, + snr: Float?, + rssi: Int?, + hops: Int, + ): Message { + val id = ++nextMessageId + messageTextByUuid[id] = text + return Message( + uuid = id, + receivedTime = nowMillis - minutesAgo * 60_000L, + node = from, + text = "", + fromLocal = fromLocal, + time = time, + read = true, + status = status, + routingError = 0, + packetId = 0x5a00 + id.toInt(), + emojis = emptyList(), + snr = snr, + rssi = rssi, + hopsAway = hops, + replyId = null, + ) + } + + private fun node( + num: Int, + longName: String, + shortName: String, + hw: HardwareModel, + role: Role, + battery: Int, + hops: Int, + heardMinutesAgo: Int, + dLat: Double, + dLon: Double, + snr: Float, + rssi: Int, + favorite: Boolean = false, + ): Node = Node( + num = num, + user = + User.Builder() + .also { + it.id = "!${num.toUInt().toString(16).padStart(8, '0')}" + it.long_name = longName + it.short_name = shortName + it.hw_model = hw + it.role = role + } + .build(), + position = + Position.Builder() + .also { + it.latitude_i = ((CENTER_LAT + dLat) * 1e7).toInt() + it.longitude_i = ((CENTER_LON + dLon) * 1e7).toInt() + it.altitude = 120 + (num and 0xff) + it.sats_in_view = 8 + it.time = now - heardAgo(heardMinutesAgo) + } + .build(), + lastHeard = now - heardAgo(heardMinutesAgo), + channel = 0, + snr = snr, + rssi = rssi, + deviceMetrics = + DeviceMetrics.Builder() + .also { + it.battery_level = battery + it.voltage = 3.6f + battery / 250f + it.channel_utilization = 4.2f + it.air_util_tx = 1.1f + it.uptime_seconds = 86_400 + } + .build(), + hopsAway = hops, + isFavorite = favorite, + ) +} + +private typealias Role = Config.DeviceConfig.Role diff --git a/marketing-screenshots/src/main/kotlin/org/meshtastic/screenshot/marketing/Screens.kt b/marketing-screenshots/src/main/kotlin/org/meshtastic/screenshot/marketing/Screens.kt new file mode 100644 index 0000000000..ab9d717588 --- /dev/null +++ b/marketing-screenshots/src/main/kotlin/org/meshtastic/screenshot/marketing/Screens.kt @@ -0,0 +1,228 @@ +/* + * 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 . + */ +@file:Suppress("MagicNumber") + +package org.meshtastic.screenshot.marketing + +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.material3.Button +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedButton +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.ImageBitmap +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.layout.onSizeChanged +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.IntSize +import androidx.compose.ui.unit.dp +import org.jetbrains.compose.resources.stringResource +import org.meshtastic.core.model.Channel +import org.meshtastic.core.model.util.getChannelUrl +import org.meshtastic.core.navigation.TopLevelDestination +import org.meshtastic.core.resources.Res +import org.meshtastic.core.resources.channels +import org.meshtastic.core.resources.edit +import org.meshtastic.core.resources.generate_qr_code +import org.meshtastic.core.resources.map +import org.meshtastic.core.resources.qr_code +import org.meshtastic.core.resources.share_qr_subtext +import org.meshtastic.core.ui.component.AdaptiveTwoPane +import org.meshtastic.core.ui.component.ChannelSelection +import org.meshtastic.core.ui.component.MainAppBar +import org.meshtastic.core.ui.icon.MeshtasticIcons +import org.meshtastic.core.ui.icon.QrCode +import org.meshtastic.core.ui.theme.AppTheme +import org.meshtastic.core.ui.util.rememberQrCodePainter +import org.meshtastic.feature.map.component.MapControlsOverlay +import org.meshtastic.feature.map.component.MapZoomControls +import org.meshtastic.proto.Config +import org.meshtastic.screenshot.marketing.resources.marketing_map_attribution +import org.meshtastic.screenshot.marketing.resources.Res as MarketingRes + +/** Every store screenshot is the dark theme, matching the live listing; dynamic color would follow the host. */ +@Composable +internal fun MarketingTheme(content: @Composable () -> Unit) { + AppTheme(darkTheme = true, dynamicColor = false) { Surface(modifier = Modifier.fillMaxSize(), content = content) } +} + +/** + * The map screen: the real MapLibre snapshot (basemap plus the app's own node chips, captured by [MapSnapshot]) under + * the map screen's real app bar, toolbar and zoom controls. The library draws its attribution as a MapView overlay, + * which the snapshotter has no equivalent of, so the same credit line is written here. + * + * Composed once per form factor with no [snapshot] to measure the map area - [onMapArea] reports it in pixels - and + * again with the capture made at exactly that size, so the image is placed without rescaling. + */ +@Composable +internal fun MapScreen(mesh: SampleMesh, snapshot: ImageBitmap?, onMapArea: (IntSize) -> Unit = {}) { + MarketingTheme { + AppShell(TopLevelDestination.Map) { + Scaffold( + topBar = { + MainAppBar( + title = stringResource(Res.string.map), + ourNode = mesh.baseCamp, + showNodeChip = true, + canNavigateUp = false, + onNavigateUp = {}, + onClickChip = {}, + actions = {}, + ) + }, + ) { padding -> + Box(modifier = Modifier.fillMaxSize().padding(padding).onSizeChanged(onMapArea)) { + if (snapshot != null) { + Image( + bitmap = snapshot, + contentDescription = null, + modifier = Modifier.fillMaxSize(), + contentScale = ContentScale.Crop, + ) + } + MapControlsOverlay( + modifier = Modifier.align(Alignment.TopCenter).padding(top = 8.dp), + onToggleFilterMenu = {}, + onSitePlannerClick = {}, + onToggleLocationTracking = {}, + ) + MapZoomControls( + onZoomIn = {}, + onZoomOut = {}, + modifier = Modifier.align(Alignment.BottomEnd).padding(16.dp), + ) + Surface( + modifier = Modifier.align(Alignment.BottomStart).padding(8.dp), + color = Color.White.copy(alpha = 0.8f), + shape = MaterialTheme.shapes.small, + ) { + Text( + text = stringResource(MarketingRes.string.marketing_map_attribution), + modifier = Modifier.padding(horizontal = 8.dp, vertical = 4.dp), + style = MaterialTheme.typography.labelSmall, + color = Color.Black, + ) + } + } + } + } + } +} + +/** + * The channel list with the share affordance, in the same [AdaptiveTwoPane] the app's channel screen uses: the rows and + * the edit button first, the QR second, stacked on a phone and side by side on an expanded window. The real screen + * shows the QR in a dialog window, which an offscreen scene does not compose, so the same QR painter is laid out + * inline. + */ +@Composable +internal fun ChannelsScreen(mesh: SampleMesh) { + val channelSet = mesh.channelSet + val loraConfig = channelSet.lora_config ?: Config.LoRaConfig.Builder().build() + val presetName = Channel(loraConfig = loraConfig).name + MarketingTheme { + AppShell(TopLevelDestination.Settings) { + Scaffold( + topBar = { + MainAppBar( + title = stringResource(Res.string.channels), + ourNode = mesh.baseCamp, + showNodeChip = false, + canNavigateUp = true, + onNavigateUp = {}, + onClickChip = {}, + actions = {}, + ) + }, + ) { padding -> + AdaptiveTwoPane( + modifier = Modifier.fillMaxSize().padding(padding).padding(horizontal = 24.dp, vertical = 16.dp), + first = { + channelSet.settings.forEachIndexed { index, settings -> + ChannelSelection( + index = index, + title = settings.name.ifEmpty { presetName }, + enabled = true, + isSelected = true, + onSelected = {}, + channel = Channel(settings, loraConfig), + ) + } + OutlinedButton( + onClick = {}, + modifier = Modifier.fillMaxWidth(), + colors = + ButtonDefaults.outlinedButtonColors(contentColor = MaterialTheme.colorScheme.onSurface), + ) { + Text(text = stringResource(Res.string.edit)) + } + }, + second = { ShareCard(channelSet.getChannelUrl().toString()) }, + ) + } + } + } +} + +@Composable +private fun ShareCard(url: String) { + val qrSizePx = with(LocalDensity.current) { 200.dp.roundToPx() } + val qrPainter = rememberQrCodePainter(remember(url) { url }, qrSizePx) + Column(modifier = Modifier.fillMaxWidth(), horizontalAlignment = Alignment.CenterHorizontally) { + Button(onClick = {}, modifier = Modifier.padding(16.dp)) { + Icon(imageVector = MeshtasticIcons.QrCode, contentDescription = null) + Spacer(modifier = Modifier.width(8.dp)) + Text(text = stringResource(Res.string.generate_qr_code)) + } + Surface(shape = MaterialTheme.shapes.large, color = MaterialTheme.colorScheme.surfaceContainerHigh) { + Column(modifier = Modifier.padding(16.dp), horizontalAlignment = Alignment.CenterHorizontally) { + Image( + painter = qrPainter, + contentDescription = stringResource(Res.string.qr_code), + modifier = Modifier.size(200.dp), + contentScale = ContentScale.Fit, + ) + Text( + text = stringResource(Res.string.share_qr_subtext), + modifier = Modifier.fillMaxWidth().padding(top = 12.dp), + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + textAlign = TextAlign.Center, + ) + } + } + Spacer(modifier = Modifier.height(8.dp)) + } +} diff --git a/marketing-screenshots/src/main/kotlin/org/meshtastic/screenshot/marketing/SettingsScreen.kt b/marketing-screenshots/src/main/kotlin/org/meshtastic/screenshot/marketing/SettingsScreen.kt new file mode 100644 index 0000000000..4fa410b557 --- /dev/null +++ b/marketing-screenshots/src/main/kotlin/org/meshtastic/screenshot/marketing/SettingsScreen.kt @@ -0,0 +1,72 @@ +/* + * 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 . + */ +package org.meshtastic.screenshot.marketing + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.Scaffold +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import org.jetbrains.compose.resources.stringResource +import org.meshtastic.core.navigation.TopLevelDestination +import org.meshtastic.core.resources.Res +import org.meshtastic.core.resources.bottom_nav_settings +import org.meshtastic.core.ui.component.MainAppBar +import org.meshtastic.feature.settings.radio.RadioConfigItemList +import org.meshtastic.feature.settings.radio.RadioConfigState + +/** + * The settings tab as the desktop app lays it out: `DesktopSettingsScreen`'s app bar and scrolling column, which needs + * two ViewModels, over the settings feature's own [RadioConfigItemList] - the configuration, backup and advanced + * sections a connected local radio gets. The app-level sections that follow them fall below an 800 px window. + */ +@Composable +internal fun SettingsScreen() { + MarketingTheme { + AppShell(TopLevelDestination.Settings) { + Scaffold( + topBar = { + MainAppBar( + title = stringResource(Res.string.bottom_nav_settings), + ourNode = null, + showNodeChip = false, + canNavigateUp = false, + onNavigateUp = {}, + onClickChip = {}, + actions = {}, + ) + }, + ) { padding -> + Column( + modifier = Modifier.padding(padding).verticalScroll(rememberScrollState()).padding(16.dp), + verticalArrangement = Arrangement.spacedBy(16.dp), + ) { + RadioConfigItemList( + state = RadioConfigState(isLocal = true, connected = true), + isManaged = false, + isOtaCapable = false, + onNavigate = {}, + ) + } + } + } + } +} diff --git a/marketing-screenshots/src/main/kotlin/org/meshtastic/screenshot/marketing/Shell.kt b/marketing-screenshots/src/main/kotlin/org/meshtastic/screenshot/marketing/Shell.kt new file mode 100644 index 0000000000..43668d60fb --- /dev/null +++ b/marketing-screenshots/src/main/kotlin/org/meshtastic/screenshot/marketing/Shell.kt @@ -0,0 +1,180 @@ +/* + * 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 . + */ +package org.meshtastic.screenshot.marketing + +import androidx.compose.foundation.background +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Icon +import androidx.compose.material3.LocalMinimumInteractiveComponentSize +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.material3.VerticalDragHandle +import androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi +import androidx.compose.material3.adaptive.currentWindowAdaptiveInfoV2 +import androidx.compose.material3.adaptive.layout.AnimatedPane +import androidx.compose.material3.adaptive.layout.ListDetailPaneScaffold +import androidx.compose.material3.adaptive.layout.PaneAdaptedValue +import androidx.compose.material3.adaptive.layout.PaneExpansionState +import androidx.compose.material3.adaptive.layout.ThreePaneScaffoldScope +import androidx.compose.material3.adaptive.layout.ThreePaneScaffoldValue +import androidx.compose.material3.adaptive.layout.calculatePaneScaffoldDirective +import androidx.compose.material3.adaptive.layout.rememberPaneExpansionState +import androidx.compose.material3.adaptive.navigationsuite.NavigationSuiteScaffold +import androidx.compose.material3.adaptive.navigationsuite.NavigationSuiteScaffoldDefaults +import androidx.compose.material3.adaptive.navigationsuite.NavigationSuiteType +import androidx.compose.runtime.Composable +import androidx.compose.runtime.movableContentOf +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import kotlinx.coroutines.flow.emptyFlow +import org.jetbrains.compose.resources.stringResource +import org.jetbrains.compose.resources.vectorResource +import org.meshtastic.core.model.ConnectionState +import org.meshtastic.core.model.DeviceType +import org.meshtastic.core.navigation.TopLevelDestination +import org.meshtastic.core.ui.component.AnimatedConnectionsNavIcon +import org.meshtastic.core.ui.navigation.icon +import org.meshtastic.screenshot.marketing.resources.Res +import org.meshtastic.screenshot.marketing.resources.marketing_section_header + +/** + * The app's navigation shell, from the same [NavigationSuiteScaffold], destinations and icons as + * `MeshtasticNavigationSuite`, which itself needs the live ViewModels this generator does not have. The scaffold reads + * the window class: a compact phone gets the bottom bar, anything wider the rail with labels, and the drawer it would + * promote to at expanded widths is capped to the rail exactly as the app caps it. + */ +@Composable +internal fun AppShell(selected: TopLevelDestination, content: @Composable () -> Unit) { + val layoutType = + NavigationSuiteScaffoldDefaults.calculateFromAdaptiveInfo(currentWindowAdaptiveInfoV2()).let { + if (it == NavigationSuiteType.NavigationDrawer) NavigationSuiteType.NavigationRail else it + } + val showLabels = layoutType == NavigationSuiteType.NavigationRail + NavigationSuiteScaffold( + layoutType = layoutType, + navigationSuiteItems = { + TopLevelDestination.entries.forEach { destination -> + item( + selected = destination == selected, + onClick = {}, + icon = { NavigationIcon(destination) }, + label = + if (showLabels) { + { Text(stringResource(destination.label)) } + } else { + null + }, + ) + } + }, + ) { + Row { content() } + } +} + +@Composable +private fun NavigationIcon(destination: TopLevelDestination) { + if (destination == TopLevelDestination.Connect) { + AnimatedConnectionsNavIcon( + connectionState = ConnectionState.Connected, + deviceType = DeviceType.fromAddress("x00:11:22:33:44:55"), + meshActivityFlow = emptyFlow(), + ) + } else { + Icon(imageVector = vectorResource(destination.icon), contentDescription = stringResource(destination.label)) + } +} + +/** Which pane a list-detail screen shows when the window has room for only one. */ +internal enum class Pane { + List, + Detail, +} + +/** + * The app's list-detail layout: `MeshtasticNavDisplay` hands the nodes and messages routes to a + * `ListDetailSceneStrategy`, which splits them into a [ListDetailPaneScaffold] with a draggable divider once + * [calculatePaneScaffoldDirective] grants a second partition - the expanded width class - and shows the one route on + * top otherwise. This is the same scaffold, directive and drag handle, with the panes given directly. + */ +@OptIn(ExperimentalMaterial3AdaptiveApi::class) +@Composable +internal fun ListDetail(compactPane: Pane, list: @Composable () -> Unit, detail: @Composable () -> Unit) { + val directive = calculatePaneScaffoldDirective(currentWindowAdaptiveInfoV2()) + // Movable content, as AdaptiveTwoPane does, so neither slot is emitted directly from two branches. + val listPane = remember { movableContentOf(list) } + val detailPane = remember { movableContentOf(detail) } + if (directive.maxHorizontalPartitions > 1) { + ListDetailPaneScaffold( + directive = directive, + value = + ThreePaneScaffoldValue( + primary = PaneAdaptedValue.Expanded, + secondary = PaneAdaptedValue.Expanded, + tertiary = PaneAdaptedValue.Hidden, + ), + listPane = { AnimatedPane { listPane() } }, + detailPane = { AnimatedPane { detailPane() } }, + paneExpansionState = rememberPaneExpansionState(), + paneExpansionDragHandle = { state -> PaneExpansionDragHandle(state) }, + ) + } else { + when (compactPane) { + Pane.List -> listPane() + Pane.Detail -> detailPane() + } + } +} + +@OptIn(ExperimentalMaterial3AdaptiveApi::class) +@Composable +private fun ThreePaneScaffoldScope.PaneExpansionDragHandle(state: PaneExpansionState) { + val interactionSource = remember { MutableInteractionSource() } + VerticalDragHandle( + modifier = + Modifier.paneExpansionDraggable( + state = state, + minTouchTargetSize = LocalMinimumInteractiveComponentSize.current, + interactionSource = interactionSource, + ), + interactionSource = interactionSource, + ) +} + +/** The conversation list's section header; the app's own is private to its feature module. */ +@Composable +internal fun SectionHeader(title: String, count: Int) { + Row( + modifier = + Modifier.fillMaxWidth() + .background(MaterialTheme.colorScheme.surface) + .padding(horizontal = 16.dp, vertical = 8.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + Text( + text = stringResource(Res.string.marketing_section_header, title, count), + style = MaterialTheme.typography.titleSmall, + color = MaterialTheme.colorScheme.primary, + modifier = Modifier.weight(1f), + ) + } +} diff --git a/marketing-screenshots/src/main/kotlin/org/meshtastic/screenshot/marketing/Shot.kt b/marketing-screenshots/src/main/kotlin/org/meshtastic/screenshot/marketing/Shot.kt new file mode 100644 index 0000000000..a2ea193a28 --- /dev/null +++ b/marketing-screenshots/src/main/kotlin/org/meshtastic/screenshot/marketing/Shot.kt @@ -0,0 +1,62 @@ +/* + * 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 . + */ +package org.meshtastic.screenshot.marketing + +import org.jetbrains.compose.resources.StringResource +import org.meshtastic.screenshot.marketing.resources.Res +import org.meshtastic.screenshot.marketing.resources.marketing_caption_channels_description +import org.meshtastic.screenshot.marketing.resources.marketing_caption_channels_title +import org.meshtastic.screenshot.marketing.resources.marketing_caption_map_description +import org.meshtastic.screenshot.marketing.resources.marketing_caption_map_title +import org.meshtastic.screenshot.marketing.resources.marketing_caption_messages_description +import org.meshtastic.screenshot.marketing.resources.marketing_caption_messages_title +import org.meshtastic.screenshot.marketing.resources.marketing_caption_node_detail_description +import org.meshtastic.screenshot.marketing.resources.marketing_caption_node_detail_title +import org.meshtastic.screenshot.marketing.resources.marketing_caption_nodes_description +import org.meshtastic.screenshot.marketing.resources.marketing_caption_nodes_title + +/** + * Every screen the generator can draw. Which of them a surface lists, in what order and under what file name is the + * form factor's ([FormFactor.shots], [FileNaming]); [slug] is the name's stable part. The caption is the headline and + * body copy the framed variant draws above the phone, as string resources so Crowdin carries them; the two shots only + * the desktop lists have none, because no framed variant ever shows them. + */ +internal enum class Shot( + val slug: String, + val captionTitle: StringResource? = null, + val captionDescription: StringResource? = null, +) { + Messages( + "messages", + Res.string.marketing_caption_messages_title, + Res.string.marketing_caption_messages_description, + ), + Nodes("nodes", Res.string.marketing_caption_nodes_title, Res.string.marketing_caption_nodes_description), + Map("map", Res.string.marketing_caption_map_title, Res.string.marketing_caption_map_description), + NodeDetail( + "node_detail", + Res.string.marketing_caption_node_detail_title, + Res.string.marketing_caption_node_detail_description, + ), + Channels( + "channels", + Res.string.marketing_caption_channels_title, + Res.string.marketing_caption_channels_description, + ), + Connections("connections"), + Settings("settings"), +} diff --git a/screenshot-tests/src/screenshotTest/kotlin/org/meshtastic/screenshots/core/AlertScreenshotTests.kt b/screenshot-tests/src/screenshotTest/kotlin/org/meshtastic/screenshots/core/AlertScreenshotTests.kt index c695855d70..12b05630f9 100644 --- a/screenshot-tests/src/screenshotTest/kotlin/org/meshtastic/screenshots/core/AlertScreenshotTests.kt +++ b/screenshot-tests/src/screenshotTest/kotlin/org/meshtastic/screenshots/core/AlertScreenshotTests.kt @@ -26,6 +26,8 @@ import org.meshtastic.core.ui.util.PreviewIconAlert import org.meshtastic.core.ui.util.PreviewMultipleChoiceAlert import org.meshtastic.core.ui.util.PreviewNfcWriteFailedAlert import org.meshtastic.core.ui.util.PreviewNfcWriteSuccessAlert +import org.meshtastic.core.ui.util.PreviewShareDialog +import org.meshtastic.core.ui.util.PreviewShareDialogWithNfc import org.meshtastic.core.ui.util.PreviewTextAlert @PreviewTest @@ -83,3 +85,17 @@ fun ScreenshotNfcWriteFailedAlert() { fun ScreenshotSharedContactImportAlert() { PreviewSharedContactImportAlert() } + +@PreviewTest +@PreviewLightDark +@Composable +fun ScreenshotShareDialog() { + PreviewShareDialog() +} + +@PreviewTest +@PreviewLightDark +@Composable +fun ScreenshotShareDialogWithNfc() { + PreviewShareDialogWithNfc() +} diff --git a/screenshot-tests/src/screenshotTest/kotlin/org/meshtastic/screenshots/core/NodeSecurityScreenshotTests.kt b/screenshot-tests/src/screenshotTest/kotlin/org/meshtastic/screenshots/core/NodeSecurityScreenshotTests.kt new file mode 100644 index 0000000000..8f993f58c3 --- /dev/null +++ b/screenshot-tests/src/screenshotTest/kotlin/org/meshtastic/screenshots/core/NodeSecurityScreenshotTests.kt @@ -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 . + */ +package org.meshtastic.screenshots.core + +import androidx.compose.runtime.Composable +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.PreviewLightDark +import com.android.tools.screenshot.PreviewTest +import org.meshtastic.core.ui.component.NodeSecurityGlyphsPreview +import org.meshtastic.core.ui.component.SecurityLegendPreview + +/** + * The node security indicators (design#149). + * + * The two verification glyphs are composites drawn at render time rather than single assets, so a golden is the only + * thing that catches the badge losing legibility at the sizes the node rows use. + */ +@PreviewTest +@PreviewLightDark +@Composable +fun ScreenshotNodeSecurityGlyphs() { + NodeSecurityGlyphsPreview() +} + +@PreviewTest +@Preview +@Composable +fun ScreenshotNodeSecurityLegend() { + SecurityLegendPreview() +} diff --git a/screenshot-tests/src/screenshotTest/kotlin/org/meshtastic/screenshots/feature/MessagingScreenshotTests.kt b/screenshot-tests/src/screenshotTest/kotlin/org/meshtastic/screenshots/feature/MessagingScreenshotTests.kt index 000f3cc6e3..2c12ad3e3c 100644 --- a/screenshot-tests/src/screenshotTest/kotlin/org/meshtastic/screenshots/feature/MessagingScreenshotTests.kt +++ b/screenshot-tests/src/screenshotTest/kotlin/org/meshtastic/screenshots/feature/MessagingScreenshotTests.kt @@ -30,6 +30,7 @@ import org.meshtastic.feature.messaging.DeliveryInfo import org.meshtastic.feature.messaging.EditQuickChatDialogPreview import org.meshtastic.feature.messaging.MessageInputPreview import org.meshtastic.feature.messaging.QuickChatItemPreview +import org.meshtastic.feature.messaging.RetiredChannelNoticePreview import org.meshtastic.feature.messaging.component.MessageActionsContentPreview import org.meshtastic.feature.messaging.component.MessageItemGroupedRunPreview import org.meshtastic.feature.messaging.component.MessageItemMarkdownPreview @@ -37,6 +38,7 @@ import org.meshtastic.feature.messaging.component.MessageItemSignedPreview import org.meshtastic.feature.messaging.component.MessageItemStatusStatesPreview import org.meshtastic.feature.messaging.component.MessageSearchBarPreview import org.meshtastic.feature.messaging.component.ReactionItemPreview +import org.meshtastic.feature.messaging.ui.contact.ContactItemArchivedPreview @PreviewTest @PreviewLightDark @@ -59,6 +61,20 @@ fun ScreenshotMessageInput() { MessageInputPreview() } +@PreviewTest +@PreviewLightDark +@Composable +fun ScreenshotRetiredChannelNotice() { + RetiredChannelNoticePreview() +} + +@PreviewTest +@PreviewLightDark +@Composable +fun ScreenshotContactItemArchived() { + ContactItemArchivedPreview() +} + @PreviewTest @PreviewLightDark @Composable diff --git a/screenshot-tests/src/screenshotTest/kotlin/org/meshtastic/screenshots/feature/NodeScreenshotTests.kt b/screenshot-tests/src/screenshotTest/kotlin/org/meshtastic/screenshots/feature/NodeScreenshotTests.kt index c5e6efee2c..4677aa641a 100644 --- a/screenshot-tests/src/screenshotTest/kotlin/org/meshtastic/screenshots/feature/NodeScreenshotTests.kt +++ b/screenshot-tests/src/screenshotTest/kotlin/org/meshtastic/screenshots/feature/NodeScreenshotTests.kt @@ -20,8 +20,10 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.tooling.preview.PreviewLightDark import com.android.tools.screenshot.PreviewTest import org.meshtastic.feature.node.component.AirQualityInfoCardsPreview +import org.meshtastic.feature.node.component.AirQualityInfoCardsStatusPreview import org.meshtastic.feature.node.component.DeviceActionsLocalPreview import org.meshtastic.feature.node.component.DeviceActionsRemotePreview +import org.meshtastic.feature.node.component.EnvironmentMetricsLightningPreview import org.meshtastic.feature.node.component.EnvironmentMetricsPreview import org.meshtastic.feature.node.component.HopHistogramContentPreview import org.meshtastic.feature.node.component.HopHistogramEmptyPreview @@ -43,6 +45,7 @@ import org.meshtastic.feature.node.component.PositionInlineContentPreview import org.meshtastic.feature.node.component.PowerMetricsNoCurrentPreview import org.meshtastic.feature.node.component.PowerMetricsPartialPreview import org.meshtastic.feature.node.component.PowerMetricsPreview +import org.meshtastic.feature.node.component.SoilWaterMetricsPreview import org.meshtastic.feature.node.component.TelemetricActionsSectionEmptyPreview import org.meshtastic.feature.node.component.TelemetricActionsSectionLocalPreview import org.meshtastic.feature.node.component.TelemetricActionsSectionPreview @@ -53,7 +56,9 @@ import org.meshtastic.feature.node.detail.NodeDetailContentRemotePreview import org.meshtastic.feature.node.metrics.DeviceMetricsCardPreview import org.meshtastic.feature.node.metrics.LegendPreview import org.meshtastic.feature.node.metrics.PreviewAirQualityCards +import org.meshtastic.feature.node.metrics.PreviewAirQualityCardsStatus import org.meshtastic.feature.node.metrics.PreviewEnvironmentMetricsContent +import org.meshtastic.feature.node.metrics.PreviewEnvironmentMetricsContentLightning @PreviewTest @PreviewLightDark @@ -307,3 +312,42 @@ fun ScreenshotNodeItemUnheard() { fun ScreenshotNodeItemCompactUnheard() { NodeItemCompactUnheardPreview() } + +// --------------------------------------------------------------------------- +// Soil & water chemistry, lightning and PM sensor status (PR #7237) +// --------------------------------------------------------------------------- + +@PreviewTest +@PreviewLightDark +@Composable +fun ScreenshotSoilWaterMetrics() { + SoilWaterMetricsPreview() +} + +@PreviewTest +@PreviewLightDark +@Composable +fun ScreenshotEnvironmentMetricsLightning() { + EnvironmentMetricsLightningPreview() +} + +@PreviewTest +@PreviewLightDark +@Composable +fun ScreenshotAirQualityInfoCardsStatus() { + AirQualityInfoCardsStatusPreview() +} + +@PreviewTest +@PreviewLightDark +@Composable +fun ScreenshotEnvironmentMetricsContentLightning() { + PreviewEnvironmentMetricsContentLightning() +} + +@PreviewTest +@PreviewLightDark +@Composable +fun ScreenshotAirQualityCardsStatus() { + PreviewAirQualityCardsStatus() +} diff --git a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/core/AlertScreenshotTestsKt/ScreenshotShareDialogWithNfc_Dark_d19fbf1f_0.png b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/core/AlertScreenshotTestsKt/ScreenshotShareDialogWithNfc_Dark_d19fbf1f_0.png new file mode 100644 index 0000000000..162c8c97e4 Binary files /dev/null and b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/core/AlertScreenshotTestsKt/ScreenshotShareDialogWithNfc_Dark_d19fbf1f_0.png differ diff --git a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/core/AlertScreenshotTestsKt/ScreenshotShareDialogWithNfc_Light_b29dc7a7_0.png b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/core/AlertScreenshotTestsKt/ScreenshotShareDialogWithNfc_Light_b29dc7a7_0.png new file mode 100644 index 0000000000..46494b0528 Binary files /dev/null and b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/core/AlertScreenshotTestsKt/ScreenshotShareDialogWithNfc_Light_b29dc7a7_0.png differ diff --git a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/core/AlertScreenshotTestsKt/ScreenshotShareDialog_Dark_d19fbf1f_0.png b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/core/AlertScreenshotTestsKt/ScreenshotShareDialog_Dark_d19fbf1f_0.png new file mode 100644 index 0000000000..3d24ace552 Binary files /dev/null and b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/core/AlertScreenshotTestsKt/ScreenshotShareDialog_Dark_d19fbf1f_0.png differ diff --git a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/core/AlertScreenshotTestsKt/ScreenshotShareDialog_Light_b29dc7a7_0.png b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/core/AlertScreenshotTestsKt/ScreenshotShareDialog_Light_b29dc7a7_0.png new file mode 100644 index 0000000000..efaa43ee3b Binary files /dev/null and b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/core/AlertScreenshotTestsKt/ScreenshotShareDialog_Light_b29dc7a7_0.png differ diff --git a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/core/NodeSecurityScreenshotTestsKt/ScreenshotNodeSecurityGlyphs_Dark_d19fbf1f_0.png b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/core/NodeSecurityScreenshotTestsKt/ScreenshotNodeSecurityGlyphs_Dark_d19fbf1f_0.png new file mode 100644 index 0000000000..0937527e6d Binary files /dev/null and b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/core/NodeSecurityScreenshotTestsKt/ScreenshotNodeSecurityGlyphs_Dark_d19fbf1f_0.png differ diff --git a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/core/NodeSecurityScreenshotTestsKt/ScreenshotNodeSecurityGlyphs_Light_b29dc7a7_0.png b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/core/NodeSecurityScreenshotTestsKt/ScreenshotNodeSecurityGlyphs_Light_b29dc7a7_0.png new file mode 100644 index 0000000000..0937527e6d Binary files /dev/null and b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/core/NodeSecurityScreenshotTestsKt/ScreenshotNodeSecurityGlyphs_Light_b29dc7a7_0.png differ diff --git a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/core/NodeSecurityScreenshotTestsKt/ScreenshotNodeSecurityLegend_0.png b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/core/NodeSecurityScreenshotTestsKt/ScreenshotNodeSecurityLegend_0.png new file mode 100644 index 0000000000..fc7931476b Binary files /dev/null and b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/core/NodeSecurityScreenshotTestsKt/ScreenshotNodeSecurityLegend_0.png differ diff --git a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/DocsScreenshotTestsKt/ScreenshotDocsBrowserEmpty_Dark_d19fbf1f_0.png b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/DocsScreenshotTestsKt/ScreenshotDocsBrowserEmpty_Dark_d19fbf1f_0.png index a073c81f95..661e00e666 100644 Binary files a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/DocsScreenshotTestsKt/ScreenshotDocsBrowserEmpty_Dark_d19fbf1f_0.png and b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/DocsScreenshotTestsKt/ScreenshotDocsBrowserEmpty_Dark_d19fbf1f_0.png differ diff --git a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/DocsScreenshotTestsKt/ScreenshotDocsBrowserEmpty_Light_b29dc7a7_0.png b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/DocsScreenshotTestsKt/ScreenshotDocsBrowserEmpty_Light_b29dc7a7_0.png index eeac8e08b0..636e089ad2 100644 Binary files a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/DocsScreenshotTestsKt/ScreenshotDocsBrowserEmpty_Light_b29dc7a7_0.png and b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/DocsScreenshotTestsKt/ScreenshotDocsBrowserEmpty_Light_b29dc7a7_0.png differ diff --git a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/DocsScreenshotTestsKt/ScreenshotDocsBrowser_Dark_d19fbf1f_0.png b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/DocsScreenshotTestsKt/ScreenshotDocsBrowser_Dark_d19fbf1f_0.png index 90911e10b1..d3d472efbe 100644 Binary files a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/DocsScreenshotTestsKt/ScreenshotDocsBrowser_Dark_d19fbf1f_0.png and b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/DocsScreenshotTestsKt/ScreenshotDocsBrowser_Dark_d19fbf1f_0.png differ diff --git a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/DocsScreenshotTestsKt/ScreenshotDocsBrowser_Light_b29dc7a7_0.png b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/DocsScreenshotTestsKt/ScreenshotDocsBrowser_Light_b29dc7a7_0.png index 857fc0fcf3..2cd30d802a 100644 Binary files a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/DocsScreenshotTestsKt/ScreenshotDocsBrowser_Light_b29dc7a7_0.png and b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/DocsScreenshotTestsKt/ScreenshotDocsBrowser_Light_b29dc7a7_0.png differ diff --git a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/DocsScreenshotTestsKt/ScreenshotDocsSearchBarEmpty_Dark_d19fbf1f_0.png b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/DocsScreenshotTestsKt/ScreenshotDocsSearchBarEmpty_Dark_d19fbf1f_0.png index ccdb1a515f..2b1c87d807 100644 Binary files a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/DocsScreenshotTestsKt/ScreenshotDocsSearchBarEmpty_Dark_d19fbf1f_0.png and b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/DocsScreenshotTestsKt/ScreenshotDocsSearchBarEmpty_Dark_d19fbf1f_0.png differ diff --git a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/DocsScreenshotTestsKt/ScreenshotDocsSearchBarEmpty_Light_b29dc7a7_0.png b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/DocsScreenshotTestsKt/ScreenshotDocsSearchBarEmpty_Light_b29dc7a7_0.png index 91dc4e2c4d..0708349058 100644 Binary files a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/DocsScreenshotTestsKt/ScreenshotDocsSearchBarEmpty_Light_b29dc7a7_0.png and b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/DocsScreenshotTestsKt/ScreenshotDocsSearchBarEmpty_Light_b29dc7a7_0.png differ diff --git a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/DocsScreenshotTestsKt/ScreenshotDocsSearchBarWithQuery_Dark_d19fbf1f_0.png b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/DocsScreenshotTestsKt/ScreenshotDocsSearchBarWithQuery_Dark_d19fbf1f_0.png index 607267e08f..c5fa9d2082 100644 Binary files a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/DocsScreenshotTestsKt/ScreenshotDocsSearchBarWithQuery_Dark_d19fbf1f_0.png and b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/DocsScreenshotTestsKt/ScreenshotDocsSearchBarWithQuery_Dark_d19fbf1f_0.png differ diff --git a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/DocsScreenshotTestsKt/ScreenshotDocsSearchBarWithQuery_Light_b29dc7a7_0.png b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/DocsScreenshotTestsKt/ScreenshotDocsSearchBarWithQuery_Light_b29dc7a7_0.png index 77b7323357..e6e5c22c8e 100644 Binary files a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/DocsScreenshotTestsKt/ScreenshotDocsSearchBarWithQuery_Light_b29dc7a7_0.png and b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/DocsScreenshotTestsKt/ScreenshotDocsSearchBarWithQuery_Light_b29dc7a7_0.png differ diff --git a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/MessagingScreenshotTestsKt/ScreenshotContactItemArchived_Dark_d19fbf1f_0.png b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/MessagingScreenshotTestsKt/ScreenshotContactItemArchived_Dark_d19fbf1f_0.png new file mode 100644 index 0000000000..d44b1bc5ec Binary files /dev/null and b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/MessagingScreenshotTestsKt/ScreenshotContactItemArchived_Dark_d19fbf1f_0.png differ diff --git a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/MessagingScreenshotTestsKt/ScreenshotContactItemArchived_Light_b29dc7a7_0.png b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/MessagingScreenshotTestsKt/ScreenshotContactItemArchived_Light_b29dc7a7_0.png new file mode 100644 index 0000000000..1ddbdaecf5 Binary files /dev/null and b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/MessagingScreenshotTestsKt/ScreenshotContactItemArchived_Light_b29dc7a7_0.png differ diff --git a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/MessagingScreenshotTestsKt/ScreenshotRetiredChannelNotice_Dark_d19fbf1f_0.png b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/MessagingScreenshotTestsKt/ScreenshotRetiredChannelNotice_Dark_d19fbf1f_0.png new file mode 100644 index 0000000000..5b465d9af4 Binary files /dev/null and b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/MessagingScreenshotTestsKt/ScreenshotRetiredChannelNotice_Dark_d19fbf1f_0.png differ diff --git a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/MessagingScreenshotTestsKt/ScreenshotRetiredChannelNotice_Light_b29dc7a7_0.png b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/MessagingScreenshotTestsKt/ScreenshotRetiredChannelNotice_Light_b29dc7a7_0.png new file mode 100644 index 0000000000..e1cca3c69e Binary files /dev/null and b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/MessagingScreenshotTestsKt/ScreenshotRetiredChannelNotice_Light_b29dc7a7_0.png differ diff --git a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/NodeScreenshotTestsKt/ScreenshotAirQualityCardsStatus_Dark_d19fbf1f_0.png b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/NodeScreenshotTestsKt/ScreenshotAirQualityCardsStatus_Dark_d19fbf1f_0.png new file mode 100644 index 0000000000..a3bd7db68d Binary files /dev/null and b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/NodeScreenshotTestsKt/ScreenshotAirQualityCardsStatus_Dark_d19fbf1f_0.png differ diff --git a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/NodeScreenshotTestsKt/ScreenshotAirQualityCardsStatus_Light_b29dc7a7_0.png b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/NodeScreenshotTestsKt/ScreenshotAirQualityCardsStatus_Light_b29dc7a7_0.png new file mode 100644 index 0000000000..892df0cd5e Binary files /dev/null and b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/NodeScreenshotTestsKt/ScreenshotAirQualityCardsStatus_Light_b29dc7a7_0.png differ diff --git a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/NodeScreenshotTestsKt/ScreenshotAirQualityInfoCardsStatus_Dark_d19fbf1f_0.png b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/NodeScreenshotTestsKt/ScreenshotAirQualityInfoCardsStatus_Dark_d19fbf1f_0.png new file mode 100644 index 0000000000..019337d9d8 Binary files /dev/null and b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/NodeScreenshotTestsKt/ScreenshotAirQualityInfoCardsStatus_Dark_d19fbf1f_0.png differ diff --git a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/NodeScreenshotTestsKt/ScreenshotAirQualityInfoCardsStatus_Light_b29dc7a7_0.png b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/NodeScreenshotTestsKt/ScreenshotAirQualityInfoCardsStatus_Light_b29dc7a7_0.png new file mode 100644 index 0000000000..c2f3d0d91f Binary files /dev/null and b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/NodeScreenshotTestsKt/ScreenshotAirQualityInfoCardsStatus_Light_b29dc7a7_0.png differ diff --git a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/NodeScreenshotTestsKt/ScreenshotEnvironmentMetricsContentLightning_Dark_d19fbf1f_0.png b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/NodeScreenshotTestsKt/ScreenshotEnvironmentMetricsContentLightning_Dark_d19fbf1f_0.png new file mode 100644 index 0000000000..9b358113e3 Binary files /dev/null and b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/NodeScreenshotTestsKt/ScreenshotEnvironmentMetricsContentLightning_Dark_d19fbf1f_0.png differ diff --git a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/NodeScreenshotTestsKt/ScreenshotEnvironmentMetricsContentLightning_Light_b29dc7a7_0.png b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/NodeScreenshotTestsKt/ScreenshotEnvironmentMetricsContentLightning_Light_b29dc7a7_0.png new file mode 100644 index 0000000000..0e7958fe34 Binary files /dev/null and b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/NodeScreenshotTestsKt/ScreenshotEnvironmentMetricsContentLightning_Light_b29dc7a7_0.png differ diff --git a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/NodeScreenshotTestsKt/ScreenshotEnvironmentMetricsLightning_Dark_d19fbf1f_0.png b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/NodeScreenshotTestsKt/ScreenshotEnvironmentMetricsLightning_Dark_d19fbf1f_0.png new file mode 100644 index 0000000000..1831f076a6 Binary files /dev/null and b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/NodeScreenshotTestsKt/ScreenshotEnvironmentMetricsLightning_Dark_d19fbf1f_0.png differ diff --git a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/NodeScreenshotTestsKt/ScreenshotEnvironmentMetricsLightning_Light_b29dc7a7_0.png b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/NodeScreenshotTestsKt/ScreenshotEnvironmentMetricsLightning_Light_b29dc7a7_0.png new file mode 100644 index 0000000000..2e6609b91c Binary files /dev/null and b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/NodeScreenshotTestsKt/ScreenshotEnvironmentMetricsLightning_Light_b29dc7a7_0.png differ diff --git a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/NodeScreenshotTestsKt/ScreenshotNodeDetailContentLocal_Dark_d19fbf1f_0.png b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/NodeScreenshotTestsKt/ScreenshotNodeDetailContentLocal_Dark_d19fbf1f_0.png index bbc964604d..73c86c5913 100644 Binary files a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/NodeScreenshotTestsKt/ScreenshotNodeDetailContentLocal_Dark_d19fbf1f_0.png and b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/NodeScreenshotTestsKt/ScreenshotNodeDetailContentLocal_Dark_d19fbf1f_0.png differ diff --git a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/NodeScreenshotTestsKt/ScreenshotNodeDetailContentLocal_Light_b29dc7a7_0.png b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/NodeScreenshotTestsKt/ScreenshotNodeDetailContentLocal_Light_b29dc7a7_0.png index f60dfaa04d..aa98d11c44 100644 Binary files a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/NodeScreenshotTestsKt/ScreenshotNodeDetailContentLocal_Light_b29dc7a7_0.png and b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/NodeScreenshotTestsKt/ScreenshotNodeDetailContentLocal_Light_b29dc7a7_0.png differ diff --git a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/NodeScreenshotTestsKt/ScreenshotNodeDetailContentMinimal_Dark_d19fbf1f_0.png b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/NodeScreenshotTestsKt/ScreenshotNodeDetailContentMinimal_Dark_d19fbf1f_0.png index a4478d494d..9d5c7c68c6 100644 Binary files a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/NodeScreenshotTestsKt/ScreenshotNodeDetailContentMinimal_Dark_d19fbf1f_0.png and b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/NodeScreenshotTestsKt/ScreenshotNodeDetailContentMinimal_Dark_d19fbf1f_0.png differ diff --git a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/NodeScreenshotTestsKt/ScreenshotNodeDetailContentMinimal_Light_b29dc7a7_0.png b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/NodeScreenshotTestsKt/ScreenshotNodeDetailContentMinimal_Light_b29dc7a7_0.png index 686532c0c7..3cff49ae37 100644 Binary files a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/NodeScreenshotTestsKt/ScreenshotNodeDetailContentMinimal_Light_b29dc7a7_0.png and b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/NodeScreenshotTestsKt/ScreenshotNodeDetailContentMinimal_Light_b29dc7a7_0.png differ diff --git a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/NodeScreenshotTestsKt/ScreenshotNodeDetailContentRemote_Dark_d19fbf1f_0.png b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/NodeScreenshotTestsKt/ScreenshotNodeDetailContentRemote_Dark_d19fbf1f_0.png index 953f5e25fe..506326cad4 100644 Binary files a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/NodeScreenshotTestsKt/ScreenshotNodeDetailContentRemote_Dark_d19fbf1f_0.png and b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/NodeScreenshotTestsKt/ScreenshotNodeDetailContentRemote_Dark_d19fbf1f_0.png differ diff --git a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/NodeScreenshotTestsKt/ScreenshotNodeDetailContentRemote_Light_b29dc7a7_0.png b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/NodeScreenshotTestsKt/ScreenshotNodeDetailContentRemote_Light_b29dc7a7_0.png index 3b08f18bbf..3f0c137129 100644 Binary files a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/NodeScreenshotTestsKt/ScreenshotNodeDetailContentRemote_Light_b29dc7a7_0.png and b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/NodeScreenshotTestsKt/ScreenshotNodeDetailContentRemote_Light_b29dc7a7_0.png differ diff --git a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/NodeScreenshotTestsKt/ScreenshotNodeDetailsSectionSigned_Dark_d19fbf1f_0.png b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/NodeScreenshotTestsKt/ScreenshotNodeDetailsSectionSigned_Dark_d19fbf1f_0.png index 0fe7a06dcb..8262a55997 100644 Binary files a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/NodeScreenshotTestsKt/ScreenshotNodeDetailsSectionSigned_Dark_d19fbf1f_0.png and b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/NodeScreenshotTestsKt/ScreenshotNodeDetailsSectionSigned_Dark_d19fbf1f_0.png differ diff --git a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/NodeScreenshotTestsKt/ScreenshotNodeDetailsSectionSigned_Light_b29dc7a7_0.png b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/NodeScreenshotTestsKt/ScreenshotNodeDetailsSectionSigned_Light_b29dc7a7_0.png index e6d277eaa7..ae2a0a2cbb 100644 Binary files a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/NodeScreenshotTestsKt/ScreenshotNodeDetailsSectionSigned_Light_b29dc7a7_0.png and b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/NodeScreenshotTestsKt/ScreenshotNodeDetailsSectionSigned_Light_b29dc7a7_0.png differ diff --git a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/NodeScreenshotTestsKt/ScreenshotNodeDetailsSection_Dark_d19fbf1f_0.png b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/NodeScreenshotTestsKt/ScreenshotNodeDetailsSection_Dark_d19fbf1f_0.png index 4c886c80c9..0d83499779 100644 Binary files a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/NodeScreenshotTestsKt/ScreenshotNodeDetailsSection_Dark_d19fbf1f_0.png and b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/NodeScreenshotTestsKt/ScreenshotNodeDetailsSection_Dark_d19fbf1f_0.png differ diff --git a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/NodeScreenshotTestsKt/ScreenshotNodeDetailsSection_Light_b29dc7a7_0.png b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/NodeScreenshotTestsKt/ScreenshotNodeDetailsSection_Light_b29dc7a7_0.png index 500e0ff32f..3c9eaeb7b7 100644 Binary files a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/NodeScreenshotTestsKt/ScreenshotNodeDetailsSection_Light_b29dc7a7_0.png and b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/NodeScreenshotTestsKt/ScreenshotNodeDetailsSection_Light_b29dc7a7_0.png differ diff --git a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/NodeScreenshotTestsKt/ScreenshotNodeItemCompactActive_Dark_d19fbf1f_0.png b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/NodeScreenshotTestsKt/ScreenshotNodeItemCompactActive_Dark_d19fbf1f_0.png index b9f3f32e00..727cedd1aa 100644 Binary files a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/NodeScreenshotTestsKt/ScreenshotNodeItemCompactActive_Dark_d19fbf1f_0.png and b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/NodeScreenshotTestsKt/ScreenshotNodeItemCompactActive_Dark_d19fbf1f_0.png differ diff --git a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/NodeScreenshotTestsKt/ScreenshotNodeItemCompactActive_Light_b29dc7a7_0.png b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/NodeScreenshotTestsKt/ScreenshotNodeItemCompactActive_Light_b29dc7a7_0.png index 17f8e7fffd..b780129a95 100644 Binary files a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/NodeScreenshotTestsKt/ScreenshotNodeItemCompactActive_Light_b29dc7a7_0.png and b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/NodeScreenshotTestsKt/ScreenshotNodeItemCompactActive_Light_b29dc7a7_0.png differ diff --git a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/NodeScreenshotTestsKt/ScreenshotNodeItemCompactAllFields_Dark_d19fbf1f_0.png b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/NodeScreenshotTestsKt/ScreenshotNodeItemCompactAllFields_Dark_d19fbf1f_0.png index a93e62f7a8..c80c6f1806 100644 Binary files a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/NodeScreenshotTestsKt/ScreenshotNodeItemCompactAllFields_Dark_d19fbf1f_0.png and b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/NodeScreenshotTestsKt/ScreenshotNodeItemCompactAllFields_Dark_d19fbf1f_0.png differ diff --git a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/NodeScreenshotTestsKt/ScreenshotNodeItemCompactAllFields_Light_b29dc7a7_0.png b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/NodeScreenshotTestsKt/ScreenshotNodeItemCompactAllFields_Light_b29dc7a7_0.png index e7c46b3cec..efc8848c0c 100644 Binary files a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/NodeScreenshotTestsKt/ScreenshotNodeItemCompactAllFields_Light_b29dc7a7_0.png and b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/NodeScreenshotTestsKt/ScreenshotNodeItemCompactAllFields_Light_b29dc7a7_0.png differ diff --git a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/NodeScreenshotTestsKt/ScreenshotNodeItemCompactMinimal_Dark_d19fbf1f_0.png b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/NodeScreenshotTestsKt/ScreenshotNodeItemCompactMinimal_Dark_d19fbf1f_0.png index 45a212d021..6590d63a82 100644 Binary files a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/NodeScreenshotTestsKt/ScreenshotNodeItemCompactMinimal_Dark_d19fbf1f_0.png and b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/NodeScreenshotTestsKt/ScreenshotNodeItemCompactMinimal_Dark_d19fbf1f_0.png differ diff --git a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/NodeScreenshotTestsKt/ScreenshotNodeItemCompactMinimal_Light_b29dc7a7_0.png b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/NodeScreenshotTestsKt/ScreenshotNodeItemCompactMinimal_Light_b29dc7a7_0.png index 5de0030719..d77f03181d 100644 Binary files a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/NodeScreenshotTestsKt/ScreenshotNodeItemCompactMinimal_Light_b29dc7a7_0.png and b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/NodeScreenshotTestsKt/ScreenshotNodeItemCompactMinimal_Light_b29dc7a7_0.png differ diff --git a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/NodeScreenshotTestsKt/ScreenshotNodeItemCompactOnlineRemote_Dark_d19fbf1f_0.png b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/NodeScreenshotTestsKt/ScreenshotNodeItemCompactOnlineRemote_Dark_d19fbf1f_0.png index d014f3ffc8..c108979f01 100644 Binary files a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/NodeScreenshotTestsKt/ScreenshotNodeItemCompactOnlineRemote_Dark_d19fbf1f_0.png and b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/NodeScreenshotTestsKt/ScreenshotNodeItemCompactOnlineRemote_Dark_d19fbf1f_0.png differ diff --git a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/NodeScreenshotTestsKt/ScreenshotNodeItemCompactOnlineRemote_Light_b29dc7a7_0.png b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/NodeScreenshotTestsKt/ScreenshotNodeItemCompactOnlineRemote_Light_b29dc7a7_0.png index 151b304649..fe8a83f458 100644 Binary files a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/NodeScreenshotTestsKt/ScreenshotNodeItemCompactOnlineRemote_Light_b29dc7a7_0.png and b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/NodeScreenshotTestsKt/ScreenshotNodeItemCompactOnlineRemote_Light_b29dc7a7_0.png differ diff --git a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/NodeScreenshotTestsKt/ScreenshotNodeItemCompactUnheard_Dark_d19fbf1f_0.png b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/NodeScreenshotTestsKt/ScreenshotNodeItemCompactUnheard_Dark_d19fbf1f_0.png index c08ad8844a..eab291012c 100644 Binary files a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/NodeScreenshotTestsKt/ScreenshotNodeItemCompactUnheard_Dark_d19fbf1f_0.png and b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/NodeScreenshotTestsKt/ScreenshotNodeItemCompactUnheard_Dark_d19fbf1f_0.png differ diff --git a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/NodeScreenshotTestsKt/ScreenshotNodeItemCompactUnheard_Light_b29dc7a7_0.png b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/NodeScreenshotTestsKt/ScreenshotNodeItemCompactUnheard_Light_b29dc7a7_0.png index 89026ca1e9..ecff0c5d64 100644 Binary files a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/NodeScreenshotTestsKt/ScreenshotNodeItemCompactUnheard_Light_b29dc7a7_0.png and b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/NodeScreenshotTestsKt/ScreenshotNodeItemCompactUnheard_Light_b29dc7a7_0.png differ diff --git a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/NodeScreenshotTestsKt/ScreenshotNodeItemCompactUnknown_Dark_d19fbf1f_0.png b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/NodeScreenshotTestsKt/ScreenshotNodeItemCompactUnknown_Dark_d19fbf1f_0.png index b6be7ccb97..bbac657918 100644 Binary files a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/NodeScreenshotTestsKt/ScreenshotNodeItemCompactUnknown_Dark_d19fbf1f_0.png and b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/NodeScreenshotTestsKt/ScreenshotNodeItemCompactUnknown_Dark_d19fbf1f_0.png differ diff --git a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/NodeScreenshotTestsKt/ScreenshotNodeItemCompactUnknown_Light_b29dc7a7_0.png b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/NodeScreenshotTestsKt/ScreenshotNodeItemCompactUnknown_Light_b29dc7a7_0.png index e90ed4397f..51ec9b4dd0 100644 Binary files a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/NodeScreenshotTestsKt/ScreenshotNodeItemCompactUnknown_Light_b29dc7a7_0.png and b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/NodeScreenshotTestsKt/ScreenshotNodeItemCompactUnknown_Light_b29dc7a7_0.png differ diff --git a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/NodeScreenshotTestsKt/ScreenshotNodeItemCompleteActive_Dark_d19fbf1f_0.png b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/NodeScreenshotTestsKt/ScreenshotNodeItemCompleteActive_Dark_d19fbf1f_0.png index c2f62a4102..b53181eb88 100644 Binary files a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/NodeScreenshotTestsKt/ScreenshotNodeItemCompleteActive_Dark_d19fbf1f_0.png and b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/NodeScreenshotTestsKt/ScreenshotNodeItemCompleteActive_Dark_d19fbf1f_0.png differ diff --git a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/NodeScreenshotTestsKt/ScreenshotNodeItemCompleteActive_Light_b29dc7a7_0.png b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/NodeScreenshotTestsKt/ScreenshotNodeItemCompleteActive_Light_b29dc7a7_0.png index d8f7f61688..5fd28fe073 100644 Binary files a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/NodeScreenshotTestsKt/ScreenshotNodeItemCompleteActive_Light_b29dc7a7_0.png and b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/NodeScreenshotTestsKt/ScreenshotNodeItemCompleteActive_Light_b29dc7a7_0.png differ diff --git a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/NodeScreenshotTestsKt/ScreenshotNodeItemCompleteOnlineRemote_Dark_d19fbf1f_0.png b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/NodeScreenshotTestsKt/ScreenshotNodeItemCompleteOnlineRemote_Dark_d19fbf1f_0.png index b3f3cc897b..00f02377c3 100644 Binary files a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/NodeScreenshotTestsKt/ScreenshotNodeItemCompleteOnlineRemote_Dark_d19fbf1f_0.png and b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/NodeScreenshotTestsKt/ScreenshotNodeItemCompleteOnlineRemote_Dark_d19fbf1f_0.png differ diff --git a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/NodeScreenshotTestsKt/ScreenshotNodeItemCompleteOnlineRemote_Light_b29dc7a7_0.png b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/NodeScreenshotTestsKt/ScreenshotNodeItemCompleteOnlineRemote_Light_b29dc7a7_0.png index 2225617ff7..06ff9594dc 100644 Binary files a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/NodeScreenshotTestsKt/ScreenshotNodeItemCompleteOnlineRemote_Light_b29dc7a7_0.png and b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/NodeScreenshotTestsKt/ScreenshotNodeItemCompleteOnlineRemote_Light_b29dc7a7_0.png differ diff --git a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/NodeScreenshotTestsKt/ScreenshotNodeItemComplete_Dark_d19fbf1f_0.png b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/NodeScreenshotTestsKt/ScreenshotNodeItemComplete_Dark_d19fbf1f_0.png index eedd12718d..f18feb4b5c 100644 Binary files a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/NodeScreenshotTestsKt/ScreenshotNodeItemComplete_Dark_d19fbf1f_0.png and b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/NodeScreenshotTestsKt/ScreenshotNodeItemComplete_Dark_d19fbf1f_0.png differ diff --git a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/NodeScreenshotTestsKt/ScreenshotNodeItemComplete_Light_b29dc7a7_0.png b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/NodeScreenshotTestsKt/ScreenshotNodeItemComplete_Light_b29dc7a7_0.png index df06128fe2..ae0cd9c88d 100644 Binary files a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/NodeScreenshotTestsKt/ScreenshotNodeItemComplete_Light_b29dc7a7_0.png and b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/NodeScreenshotTestsKt/ScreenshotNodeItemComplete_Light_b29dc7a7_0.png differ diff --git a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/NodeScreenshotTestsKt/ScreenshotNodeItemSigned_Dark_d19fbf1f_0.png b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/NodeScreenshotTestsKt/ScreenshotNodeItemSigned_Dark_d19fbf1f_0.png index 106f2b7071..7468ea1730 100644 Binary files a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/NodeScreenshotTestsKt/ScreenshotNodeItemSigned_Dark_d19fbf1f_0.png and b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/NodeScreenshotTestsKt/ScreenshotNodeItemSigned_Dark_d19fbf1f_0.png differ diff --git a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/NodeScreenshotTestsKt/ScreenshotNodeItemSigned_Light_b29dc7a7_0.png b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/NodeScreenshotTestsKt/ScreenshotNodeItemSigned_Light_b29dc7a7_0.png index e8e7a72430..390a7d8e2a 100644 Binary files a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/NodeScreenshotTestsKt/ScreenshotNodeItemSigned_Light_b29dc7a7_0.png and b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/NodeScreenshotTestsKt/ScreenshotNodeItemSigned_Light_b29dc7a7_0.png differ diff --git a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/NodeScreenshotTestsKt/ScreenshotNodeItemUnheard_Dark_d19fbf1f_0.png b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/NodeScreenshotTestsKt/ScreenshotNodeItemUnheard_Dark_d19fbf1f_0.png index a6d27cfc4f..14bcafccb5 100644 Binary files a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/NodeScreenshotTestsKt/ScreenshotNodeItemUnheard_Dark_d19fbf1f_0.png and b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/NodeScreenshotTestsKt/ScreenshotNodeItemUnheard_Dark_d19fbf1f_0.png differ diff --git a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/NodeScreenshotTestsKt/ScreenshotNodeItemUnheard_Light_b29dc7a7_0.png b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/NodeScreenshotTestsKt/ScreenshotNodeItemUnheard_Light_b29dc7a7_0.png index 95aab3b63d..41b2bc7b91 100644 Binary files a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/NodeScreenshotTestsKt/ScreenshotNodeItemUnheard_Light_b29dc7a7_0.png and b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/NodeScreenshotTestsKt/ScreenshotNodeItemUnheard_Light_b29dc7a7_0.png differ diff --git a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/NodeScreenshotTestsKt/ScreenshotNodeItemUnknown_Dark_d19fbf1f_0.png b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/NodeScreenshotTestsKt/ScreenshotNodeItemUnknown_Dark_d19fbf1f_0.png index b37100f859..dd55087de9 100644 Binary files a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/NodeScreenshotTestsKt/ScreenshotNodeItemUnknown_Dark_d19fbf1f_0.png and b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/NodeScreenshotTestsKt/ScreenshotNodeItemUnknown_Dark_d19fbf1f_0.png differ diff --git a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/NodeScreenshotTestsKt/ScreenshotNodeItemUnknown_Light_b29dc7a7_0.png b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/NodeScreenshotTestsKt/ScreenshotNodeItemUnknown_Light_b29dc7a7_0.png index 518007c357..02d5635986 100644 Binary files a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/NodeScreenshotTestsKt/ScreenshotNodeItemUnknown_Light_b29dc7a7_0.png and b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/NodeScreenshotTestsKt/ScreenshotNodeItemUnknown_Light_b29dc7a7_0.png differ diff --git a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/NodeScreenshotTestsKt/ScreenshotSoilWaterMetrics_Dark_d19fbf1f_0.png b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/NodeScreenshotTestsKt/ScreenshotSoilWaterMetrics_Dark_d19fbf1f_0.png new file mode 100644 index 0000000000..054a4175a2 Binary files /dev/null and b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/NodeScreenshotTestsKt/ScreenshotSoilWaterMetrics_Dark_d19fbf1f_0.png differ diff --git a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/NodeScreenshotTestsKt/ScreenshotSoilWaterMetrics_Light_b29dc7a7_0.png b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/NodeScreenshotTestsKt/ScreenshotSoilWaterMetrics_Light_b29dc7a7_0.png new file mode 100644 index 0000000000..ee079f042b Binary files /dev/null and b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/NodeScreenshotTestsKt/ScreenshotSoilWaterMetrics_Light_b29dc7a7_0.png differ diff --git a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/SettingsScreenshotTestsKt/ScreenshotNodeLayoutSettingsCompactMinimal_Dark_d19fbf1f_0.png b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/SettingsScreenshotTestsKt/ScreenshotNodeLayoutSettingsCompactMinimal_Dark_d19fbf1f_0.png index c11a861fff..a9945e86f3 100644 Binary files a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/SettingsScreenshotTestsKt/ScreenshotNodeLayoutSettingsCompactMinimal_Dark_d19fbf1f_0.png and b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/SettingsScreenshotTestsKt/ScreenshotNodeLayoutSettingsCompactMinimal_Dark_d19fbf1f_0.png differ diff --git a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/SettingsScreenshotTestsKt/ScreenshotNodeLayoutSettingsCompactMinimal_Light_b29dc7a7_0.png b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/SettingsScreenshotTestsKt/ScreenshotNodeLayoutSettingsCompactMinimal_Light_b29dc7a7_0.png index ef462afd68..5663ddcba3 100644 Binary files a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/SettingsScreenshotTestsKt/ScreenshotNodeLayoutSettingsCompactMinimal_Light_b29dc7a7_0.png and b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/SettingsScreenshotTestsKt/ScreenshotNodeLayoutSettingsCompactMinimal_Light_b29dc7a7_0.png differ diff --git a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/SettingsScreenshotTestsKt/ScreenshotNodeLayoutSettingsCompact_Dark_d19fbf1f_0.png b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/SettingsScreenshotTestsKt/ScreenshotNodeLayoutSettingsCompact_Dark_d19fbf1f_0.png index 18e50259f3..e50aebcdad 100644 Binary files a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/SettingsScreenshotTestsKt/ScreenshotNodeLayoutSettingsCompact_Dark_d19fbf1f_0.png and b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/SettingsScreenshotTestsKt/ScreenshotNodeLayoutSettingsCompact_Dark_d19fbf1f_0.png differ diff --git a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/SettingsScreenshotTestsKt/ScreenshotNodeLayoutSettingsCompact_Light_b29dc7a7_0.png b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/SettingsScreenshotTestsKt/ScreenshotNodeLayoutSettingsCompact_Light_b29dc7a7_0.png index 2a2e0eb489..11d779171f 100644 Binary files a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/SettingsScreenshotTestsKt/ScreenshotNodeLayoutSettingsCompact_Light_b29dc7a7_0.png and b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/SettingsScreenshotTestsKt/ScreenshotNodeLayoutSettingsCompact_Light_b29dc7a7_0.png differ diff --git a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/SettingsScreenshotTestsKt/ScreenshotNodeLayoutSettingsComplete_Dark_d19fbf1f_0.png b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/SettingsScreenshotTestsKt/ScreenshotNodeLayoutSettingsComplete_Dark_d19fbf1f_0.png index 35634dbfe5..700d732dc5 100644 Binary files a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/SettingsScreenshotTestsKt/ScreenshotNodeLayoutSettingsComplete_Dark_d19fbf1f_0.png and b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/SettingsScreenshotTestsKt/ScreenshotNodeLayoutSettingsComplete_Dark_d19fbf1f_0.png differ diff --git a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/SettingsScreenshotTestsKt/ScreenshotNodeLayoutSettingsComplete_Light_b29dc7a7_0.png b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/SettingsScreenshotTestsKt/ScreenshotNodeLayoutSettingsComplete_Light_b29dc7a7_0.png index 20f4a878e9..f7248f0c06 100644 Binary files a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/SettingsScreenshotTestsKt/ScreenshotNodeLayoutSettingsComplete_Light_b29dc7a7_0.png and b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/SettingsScreenshotTestsKt/ScreenshotNodeLayoutSettingsComplete_Light_b29dc7a7_0.png differ diff --git a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/SettingsScreenshotTestsKt/ScreenshotSampleNodeCompactAllFields_Dark_d19fbf1f_0.png b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/SettingsScreenshotTestsKt/ScreenshotSampleNodeCompactAllFields_Dark_d19fbf1f_0.png index 11dce144a4..99f80776c2 100644 Binary files a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/SettingsScreenshotTestsKt/ScreenshotSampleNodeCompactAllFields_Dark_d19fbf1f_0.png and b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/SettingsScreenshotTestsKt/ScreenshotSampleNodeCompactAllFields_Dark_d19fbf1f_0.png differ diff --git a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/SettingsScreenshotTestsKt/ScreenshotSampleNodeCompactAllFields_Light_b29dc7a7_0.png b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/SettingsScreenshotTestsKt/ScreenshotSampleNodeCompactAllFields_Light_b29dc7a7_0.png index ef86193534..0e8411370c 100644 Binary files a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/SettingsScreenshotTestsKt/ScreenshotSampleNodeCompactAllFields_Light_b29dc7a7_0.png and b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/SettingsScreenshotTestsKt/ScreenshotSampleNodeCompactAllFields_Light_b29dc7a7_0.png differ diff --git a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/SettingsScreenshotTestsKt/ScreenshotSampleNodeCompactNameOnly_Dark_d19fbf1f_0.png b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/SettingsScreenshotTestsKt/ScreenshotSampleNodeCompactNameOnly_Dark_d19fbf1f_0.png index 0886a42d7c..3d64f9ad3c 100644 Binary files a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/SettingsScreenshotTestsKt/ScreenshotSampleNodeCompactNameOnly_Dark_d19fbf1f_0.png and b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/SettingsScreenshotTestsKt/ScreenshotSampleNodeCompactNameOnly_Dark_d19fbf1f_0.png differ diff --git a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/SettingsScreenshotTestsKt/ScreenshotSampleNodeCompactNameOnly_Light_b29dc7a7_0.png b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/SettingsScreenshotTestsKt/ScreenshotSampleNodeCompactNameOnly_Light_b29dc7a7_0.png index 9a2688804b..224957e2ec 100644 Binary files a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/SettingsScreenshotTestsKt/ScreenshotSampleNodeCompactNameOnly_Light_b29dc7a7_0.png and b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/SettingsScreenshotTestsKt/ScreenshotSampleNodeCompactNameOnly_Light_b29dc7a7_0.png differ diff --git a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/SettingsScreenshotTestsKt/ScreenshotSampleNodeCompactSignalOnly_Dark_d19fbf1f_0.png b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/SettingsScreenshotTestsKt/ScreenshotSampleNodeCompactSignalOnly_Dark_d19fbf1f_0.png index fba70dd7f0..537376d072 100644 Binary files a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/SettingsScreenshotTestsKt/ScreenshotSampleNodeCompactSignalOnly_Dark_d19fbf1f_0.png and b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/SettingsScreenshotTestsKt/ScreenshotSampleNodeCompactSignalOnly_Dark_d19fbf1f_0.png differ diff --git a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/SettingsScreenshotTestsKt/ScreenshotSampleNodeCompactSignalOnly_Light_b29dc7a7_0.png b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/SettingsScreenshotTestsKt/ScreenshotSampleNodeCompactSignalOnly_Light_b29dc7a7_0.png index 616a8502a5..314944319b 100644 Binary files a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/SettingsScreenshotTestsKt/ScreenshotSampleNodeCompactSignalOnly_Light_b29dc7a7_0.png and b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/SettingsScreenshotTestsKt/ScreenshotSampleNodeCompactSignalOnly_Light_b29dc7a7_0.png differ diff --git a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/SettingsScreenshotTestsKt/ScreenshotSampleNodeCompactToggleMatrix_Dark_d19fbf1f_0.png b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/SettingsScreenshotTestsKt/ScreenshotSampleNodeCompactToggleMatrix_Dark_d19fbf1f_0.png index 85cea26153..5bdc6d8892 100644 Binary files a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/SettingsScreenshotTestsKt/ScreenshotSampleNodeCompactToggleMatrix_Dark_d19fbf1f_0.png and b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/SettingsScreenshotTestsKt/ScreenshotSampleNodeCompactToggleMatrix_Dark_d19fbf1f_0.png differ diff --git a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/SettingsScreenshotTestsKt/ScreenshotSampleNodeCompactToggleMatrix_Light_b29dc7a7_0.png b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/SettingsScreenshotTestsKt/ScreenshotSampleNodeCompactToggleMatrix_Light_b29dc7a7_0.png index b58a12053a..c91ddf122e 100644 Binary files a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/SettingsScreenshotTestsKt/ScreenshotSampleNodeCompactToggleMatrix_Light_b29dc7a7_0.png and b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/SettingsScreenshotTestsKt/ScreenshotSampleNodeCompactToggleMatrix_Light_b29dc7a7_0.png differ diff --git a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/SettingsScreenshotTestsKt/ScreenshotSampleNodeCompleteDirectSignal_Dark_d19fbf1f_0.png b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/SettingsScreenshotTestsKt/ScreenshotSampleNodeCompleteDirectSignal_Dark_d19fbf1f_0.png index 0dd384f0bf..29994a67a0 100644 Binary files a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/SettingsScreenshotTestsKt/ScreenshotSampleNodeCompleteDirectSignal_Dark_d19fbf1f_0.png and b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/SettingsScreenshotTestsKt/ScreenshotSampleNodeCompleteDirectSignal_Dark_d19fbf1f_0.png differ diff --git a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/SettingsScreenshotTestsKt/ScreenshotSampleNodeCompleteDirectSignal_Light_b29dc7a7_0.png b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/SettingsScreenshotTestsKt/ScreenshotSampleNodeCompleteDirectSignal_Light_b29dc7a7_0.png index 644e44f487..4125347bd2 100644 Binary files a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/SettingsScreenshotTestsKt/ScreenshotSampleNodeCompleteDirectSignal_Light_b29dc7a7_0.png and b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/SettingsScreenshotTestsKt/ScreenshotSampleNodeCompleteDirectSignal_Light_b29dc7a7_0.png differ diff --git a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/SettingsScreenshotTestsKt/ScreenshotSampleNodeCompleteFahrenheit_Dark_d19fbf1f_0.png b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/SettingsScreenshotTestsKt/ScreenshotSampleNodeCompleteFahrenheit_Dark_d19fbf1f_0.png index e3997267c0..44088cddf5 100644 Binary files a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/SettingsScreenshotTestsKt/ScreenshotSampleNodeCompleteFahrenheit_Dark_d19fbf1f_0.png and b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/SettingsScreenshotTestsKt/ScreenshotSampleNodeCompleteFahrenheit_Dark_d19fbf1f_0.png differ diff --git a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/SettingsScreenshotTestsKt/ScreenshotSampleNodeCompleteFahrenheit_Light_b29dc7a7_0.png b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/SettingsScreenshotTestsKt/ScreenshotSampleNodeCompleteFahrenheit_Light_b29dc7a7_0.png index a5a875169a..5512c875fb 100644 Binary files a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/SettingsScreenshotTestsKt/ScreenshotSampleNodeCompleteFahrenheit_Light_b29dc7a7_0.png and b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/SettingsScreenshotTestsKt/ScreenshotSampleNodeCompleteFahrenheit_Light_b29dc7a7_0.png differ diff --git a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/SettingsScreenshotTestsKt/ScreenshotSampleNodeCompleteImperial_Dark_d19fbf1f_0.png b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/SettingsScreenshotTestsKt/ScreenshotSampleNodeCompleteImperial_Dark_d19fbf1f_0.png index 5580138b39..f95487aced 100644 Binary files a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/SettingsScreenshotTestsKt/ScreenshotSampleNodeCompleteImperial_Dark_d19fbf1f_0.png and b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/SettingsScreenshotTestsKt/ScreenshotSampleNodeCompleteImperial_Dark_d19fbf1f_0.png differ diff --git a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/SettingsScreenshotTestsKt/ScreenshotSampleNodeCompleteImperial_Light_b29dc7a7_0.png b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/SettingsScreenshotTestsKt/ScreenshotSampleNodeCompleteImperial_Light_b29dc7a7_0.png index 1276473b28..35ce29e675 100644 Binary files a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/SettingsScreenshotTestsKt/ScreenshotSampleNodeCompleteImperial_Light_b29dc7a7_0.png and b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/SettingsScreenshotTestsKt/ScreenshotSampleNodeCompleteImperial_Light_b29dc7a7_0.png differ diff --git a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/SettingsScreenshotTestsKt/ScreenshotSampleNodeCompleteToggleMatrix_Dark_d19fbf1f_0.png b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/SettingsScreenshotTestsKt/ScreenshotSampleNodeCompleteToggleMatrix_Dark_d19fbf1f_0.png index 2e5626263e..558659a5e1 100644 Binary files a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/SettingsScreenshotTestsKt/ScreenshotSampleNodeCompleteToggleMatrix_Dark_d19fbf1f_0.png and b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/SettingsScreenshotTestsKt/ScreenshotSampleNodeCompleteToggleMatrix_Dark_d19fbf1f_0.png differ diff --git a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/SettingsScreenshotTestsKt/ScreenshotSampleNodeCompleteToggleMatrix_Light_b29dc7a7_0.png b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/SettingsScreenshotTestsKt/ScreenshotSampleNodeCompleteToggleMatrix_Light_b29dc7a7_0.png index 3e1ddd8382..4b6a3b2f57 100644 Binary files a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/SettingsScreenshotTestsKt/ScreenshotSampleNodeCompleteToggleMatrix_Light_b29dc7a7_0.png and b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/SettingsScreenshotTestsKt/ScreenshotSampleNodeCompleteToggleMatrix_Light_b29dc7a7_0.png differ diff --git a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/SettingsScreenshotTestsKt/ScreenshotSampleNodeComplete_Dark_d19fbf1f_0.png b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/SettingsScreenshotTestsKt/ScreenshotSampleNodeComplete_Dark_d19fbf1f_0.png index 812e988975..07fe201748 100644 Binary files a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/SettingsScreenshotTestsKt/ScreenshotSampleNodeComplete_Dark_d19fbf1f_0.png and b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/SettingsScreenshotTestsKt/ScreenshotSampleNodeComplete_Dark_d19fbf1f_0.png differ diff --git a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/SettingsScreenshotTestsKt/ScreenshotSampleNodeComplete_Light_b29dc7a7_0.png b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/SettingsScreenshotTestsKt/ScreenshotSampleNodeComplete_Light_b29dc7a7_0.png index 9af079ff7a..d35a2812fa 100644 Binary files a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/SettingsScreenshotTestsKt/ScreenshotSampleNodeComplete_Light_b29dc7a7_0.png and b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/SettingsScreenshotTestsKt/ScreenshotSampleNodeComplete_Light_b29dc7a7_0.png differ diff --git a/scripts/check-metadata-length.py b/scripts/check-metadata-length.py deleted file mode 100755 index 7b5dbb236f..0000000000 --- a/scripts/check-metadata-length.py +++ /dev/null @@ -1,79 +0,0 @@ -#!/usr/bin/env python3 -"""Validate Fastlane store-listing metadata against store length limits. - -The ``fastlane/metadata/android`` tree is a mirror of Crowdin: translations are -downloaded on a schedule (see ``.github/workflows/scheduled-updates.yml``). -Crowdin's max-length toggle only blocks *new* submissions; translations entered -before enforcement was enabled are grandfathered in and keep syncing down. This -script is the repo-side guard that catches them regardless of Crowdin state. - -Lengths are measured in Unicode code points (what Google Play and F-Droid / -IzzyOnDroid count), not bytes -- a byte count badly over-reports Cyrillic and -CJK strings. - -Exit status is non-zero if any file exceeds its limit, so it can gate CI. -""" - -from __future__ import annotations - -import os -import sys -from pathlib import Path - -# Repo root = parent of this script's directory (scripts/). -REPO_ROOT = Path(__file__).resolve().parent.parent -METADATA_DIR = REPO_ROOT / "fastlane" / "metadata" / "android" - -# Per-file character limits. Keys are file names under each locale directory. -# 80 is the F-Droid summary / Google Play short-description limit; 30 is the -# Play title limit. Add more entries here to extend coverage. -LIMITS = { - "short_description.txt": 80, - "title.txt": 30, -} - -# Running inside GitHub Actions enables ::error:: annotations on the PR. -IN_GITHUB_ACTIONS = os.environ.get("GITHUB_ACTIONS") == "true" - - -def char_count(path: Path) -> int: - """Code-point length of a metadata file, ignoring trailing whitespace.""" - return len(path.read_text(encoding="utf-8").rstrip()) - - -def main() -> int: - if not METADATA_DIR.is_dir(): - print(f"error: metadata directory not found: {METADATA_DIR}", file=sys.stderr) - return 2 - - violations: list[tuple[Path, int, int]] = [] - - for file_name, limit in sorted(LIMITS.items()): - for path in sorted(METADATA_DIR.glob(f"*/{file_name}")): - count = char_count(path) - if count > limit: - violations.append((path, count, limit)) - - if not violations: - print("All store-listing metadata is within length limits.") - return 0 - - print("Store-listing metadata exceeds length limits:\n") - for path, count, limit in violations: - rel = path.relative_to(REPO_ROOT) - message = f"{rel} is {count} chars (limit {limit})" - print(f" - {message}") - if IN_GITHUB_ACTIONS: - # Annotate the offending file directly in the PR diff view. - print(f"::error file={rel}::{message}") - - print( - "\nThese files are mirrored from Crowdin. Fix them at the source " - "(shorten or remove the overlength translation so it is re-translated), " - "then re-sync -- editing the mirror here is overwritten on the next sync." - ) - return 1 - - -if __name__ == "__main__": - raise SystemExit(main()) diff --git a/scripts/check-store-metadata.py b/scripts/check-store-metadata.py new file mode 100755 index 0000000000..511d502653 --- /dev/null +++ b/scripts/check-store-metadata.py @@ -0,0 +1,154 @@ +#!/usr/bin/env python3 +"""Validate ``fastlane/metadata/android`` against the rules of every store that reads it. + +The tree is a Crowdin mirror (see ``.github/workflows/scheduled-updates.yml``) read by +three stores: ``supply`` uploads it to Google Play, and F-Droid and IzzyOnDroid read it +straight from git. Each check below is one way a store rejects it: + +- **Locale directories.** ``supply`` uploads every directory it finds (uploader.rb's + ``all_languages`` is a bare ``Dir.entries``) and the Crowdin CLI silently ignores + mapping keys it does not recognise, so an unmapped code is rejected only by the Play + API, partway through an upload, after earlier locales have already been written. +- **Lengths.** Crowdin's max-length toggle only blocks *new* submissions; translations + entered before it was enabled are grandfathered in and keep syncing down. Measured in + Unicode code points, which is what Play, F-Droid and IzzyOnDroid count -- a byte count + badly over-reports Cyrillic and CJK. +- **HTML.** fdroidserver's lint rejects a fixed list of tags in a description. + +Exit status is non-zero on any violation, so it can gate CI. Under GitHub Actions each +violation is also a ``::error`` annotation on the offending file. +""" + +from __future__ import annotations + +import os +import re +import sys +from collections.abc import Iterator +from dataclasses import dataclass +from pathlib import Path + +# Repo root = parent of this script's directory (scripts/). +REPO_ROOT = Path(__file__).resolve().parent.parent +METADATA_DIR = REPO_ROOT / "fastlane" / "metadata" / "android" + +# Running inside GitHub Actions enables ::error:: annotations on the PR. +IN_GITHUB_ACTIONS = os.environ.get("GITHUB_ACTIONS") == "true" + +# Play's published set (Play Console Help, "Add your own translations"), not fastlane's +# Supply::Languages::ALL_LANGUAGES. They disagree: fastlane omits Albanian, which Play +# lists. Play is the authority here, because Play is what rejects the upload. +PLAY_LANGUAGES = frozenset( + { + "af", "am", "ar", "az-AZ", "be", "bg", "bn-BD", "ca", "cs-CZ", "da-DK", "de-DE", + "el-GR", "en-AU", "en-CA", "en-GB", "en-IN", "en-SG", "en-US", "en-ZA", "es-419", + "es-ES", "es-US", "et", "eu-ES", "fa", "fi-FI", "fil", "fr-CA", "fr-FR", "gl-ES", + "hi-IN", "hr", "hu-HU", "hy-AM", "id", "is-IS", "it-IT", "iw-IL", "ja-JP", "ka-GE", + "km-KH", "kn-IN", "ko-KR", "ky-KG", "lo-LA", "lt", "lv", "mk-MK", "ml-IN", "mn-MN", + "mr-IN", "ms", "ms-MY", "my-MM", "ne-NP", "nl-NL", "no-NO", "pl-PL", "pt-BR", + "pt-PT", "rm", "ro", "ru-RU", "si-LK", "sk", "sl", "sq", "sr", "sv-SE", "sw", + "ta-IN", "te-IN", "th", "tr-TR", "uk", "vi", "zh-CN", "zh-HK", "zh-TW", "zu", + } +) + +# Per-file character limits, as globs under each locale directory. Each value is the +# tighter of Google Play's limit and F-Droid's (fdroidserver/common.py `char_limits`): +# 30 is Play's title limit (F-Droid allows 50); 80, 4000 and 500 are the same on both. +LIMITS = { + "title.txt": 30, + "short_description.txt": 80, + "full_description.txt": 4000, + "changelogs/*.txt": 500, +} + +# The tag list fdroidserver/lint.py rejects as "Forbidden HTML tags". F-Droid and +# IzzyOnDroid read full_description.txt straight from this tree, so a hit here is a +# listing failure there, not a warning. +FORBIDDEN_HTML = re.compile( + r"<(applet|base|body|button|embed|form|head|html|iframe|img|input|link" + r"|object|picture|script|source|style|svg|video)\b", + re.IGNORECASE, +) + +CROWDIN_HINT = ( + "These files are mirrored from Crowdin. Fix them at the source (shorten or remove " + "the offending translation so it is re-translated), then re-sync -- editing the " + "mirror here is overwritten on the next sync." +) +LOCALE_HINT = "Map the locale in crowdin.yml languages_mapping, or exclude it there." + + +@dataclass(frozen=True, slots=True) +class Violation: + path: Path # relative to the repo root, so it doubles as the annotation target + problem: str + hint: str + + @property + def message(self) -> str: + return f"{self.path} {self.problem}" + + +def locale_dirs() -> list[Path]: + return sorted(p for p in METADATA_DIR.iterdir() if p.is_dir() and not p.name.startswith(".")) + + +def check_locales() -> Iterator[Violation]: + for directory in locale_dirs(): + if directory.name not in PLAY_LANGUAGES: + yield Violation( + directory.relative_to(REPO_ROOT), + f"is not a language code Google Play accepts ('{directory.name}')", + LOCALE_HINT, + ) + + +def check_lengths() -> Iterator[Violation]: + for pattern, limit in LIMITS.items(): + for path in sorted(METADATA_DIR.glob(f"*/{pattern}")): + # Trailing whitespace is not content; stores strip it too. + count = len(path.read_text(encoding="utf-8").rstrip()) + if count > limit: + yield Violation( + path.relative_to(REPO_ROOT), f"is {count} chars (limit {limit})", CROWDIN_HINT + ) + + +def check_html() -> Iterator[Violation]: + for path in sorted(METADATA_DIR.glob("*/full_description.txt")): + for match in FORBIDDEN_HTML.finditer(path.read_text(encoding="utf-8")): + yield Violation( + path.relative_to(REPO_ROOT), + f"uses <{match.group(1)}>, which F-Droid forbids", + CROWDIN_HINT, + ) + + +CHECKS = (check_locales, check_lengths, check_html) + + +def main() -> int: + if not METADATA_DIR.is_dir(): + print(f"error: metadata directory not found: {METADATA_DIR}", file=sys.stderr) + return 2 + + violations = [violation for check in CHECKS for violation in check()] + if not violations: + print(f"All {len(locale_dirs())} store-listing locales are within every store's rules.") + return 0 + + print("Store-listing metadata breaks store rules:\n") + for violation in violations: + print(f" - {violation.message}") + if IN_GITHUB_ACTIONS: + # Annotate the offending file directly in the PR diff view. + print(f"::error file={violation.path}::{violation.problem}") + + # One hint per cause, in first-seen order. + for hint in dict.fromkeys(violation.hint for violation in violations): + print(f"\n{hint}") + return 1 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/lib/abi-parity.sh b/scripts/lib/abi-parity.sh index 966411081f..bd21cc6534 100644 --- a/scripts/lib/abi-parity.sh +++ b/scripts/lib/abi-parity.sh @@ -12,13 +12,11 @@ # fails once it turns up, so an entry cannot outlive its reason: the dependency bump that # closes the gap goes red until the line is deleted with it. # -# fdroid armeabi-v7a: no 32-bit ARM build of MapLibre exists yet. Upstream merged it on -# 2026-08-24 (maplibre-native-ffi #658/#659/#660); the release carrying it, and the -# maplibre-compose bump onto it, are what remove these two lines. Until then the app shows -# a message instead of a map on those devices (#7005). +# Empty since maplibre-compose 0.16.0, whose maplibre-native FFI ships armeabi-v7a: that +# closed the only gap this list ever held (fdroid armeabi-v7a libjniMaplibreNativeC.so and +# libmaplibre-native-c.so, #7005), and the check went red until the lines came out with the +# bump, which is what it is for. ABI_PARITY_KNOWN_GAPS=" -fdroid armeabi-v7a libjniMaplibreNativeC.so -fdroid armeabi-v7a libmaplibre-native-c.so " # apk_libs — basenames of the libs under lib// in the APK, one per line. diff --git a/scripts/release-highlights.py b/scripts/release-highlights.py new file mode 100755 index 0000000000..a7d8ed06c1 --- /dev/null +++ b/scripts/release-highlights.py @@ -0,0 +1,65 @@ +#!/usr/bin/env python3 +"""Prepend the AppStream for this version to the generated release notes. + +The metainfo entry is the only hand-written, user-facing prose in the repo and is +already required by pull-request.yml on the VERSION_NAME_BASE bump. Reuse it rather +than asking anyone to write the same sentence a second time. + +Missing or empty prose is written through as a visible placeholder, never silence: +the release is created as a draft, so a human sees the placeholder before publishing. +""" +import re +import sys +import xml.etree.ElementTree as ET +from pathlib import Path + +# Rendered markdown, not an HTML comment: a comment is invisible on the release page, +# which is exactly where it needs to be noticed before someone hits publish. +PLACEHOLDER = ( + "## Highlights\n\n" + "> [!WARNING]\n" + "> No release highlights were found for {v}. Add a `` for this version to\n" + "> `desktopApp/packaging/linux/org.meshtastic.MeshtasticDesktop.metainfo.xml`, or write them\n" + "> here before publishing this draft.\n" +) + + +def highlights(metainfo: Path, version: str) -> str | None: + try: + root = ET.parse(metainfo).getroot() + except (ET.ParseError, OSError) as exc: + print(f"could not read {metainfo}: {exc}", file=sys.stderr) + return None + for rel in root.findall(".//release"): + if rel.get("version") != version: + continue + desc = rel.find("description") + if desc is None: + return None + paras = [" ".join((p.text or "").split()) for p in desc.findall("p")] + paras = [p for p in paras if p] + return "\n\n".join(paras) or None + return None + + +def main() -> int: + metainfo, config, notes = (Path(a) for a in sys.argv[1:4]) + m = re.search(r"^VERSION_NAME_BASE=(.+)$", config.read_text(), re.M) + if not m: + print("VERSION_NAME_BASE not found; leaving notes unchanged", file=sys.stderr) + return 0 + version = m.group(1).strip() + + body = notes.read_text() if notes.exists() else "" + text = highlights(metainfo, version) + if text: + block = f"## Highlights\n\n{text}\n" + else: + print(f"no for {version}; emitting placeholder", file=sys.stderr) + block = PLACEHOLDER.format(v=version) + notes.write_text(f"{block}\n{body}") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/sync-play-changelog.py b/scripts/sync-play-changelog.py new file mode 100755 index 0000000000..650a7b3076 --- /dev/null +++ b/scripts/sync-play-changelog.py @@ -0,0 +1,90 @@ +#!/usr/bin/env python3 +"""Write the Play "what's new" text from the AppStream release description. + +metainfo.xml already carries one hand-written paragraph per version, required by +pull-request.yml on the VERSION_NAME_BASE bump. This renders that same paragraph +into fastlane/metadata/android/en-US/changelogs/default.txt, followed by a link to +the full release notes, so the store listing says what the release did. + +default.txt rather than a .txt: it is the file crowdin.yml already +maps, so translations continue in place instead of starting from zero on a new +source file every internal build. Play falls back to it for any build without a +version-specific file, which is all of them here. + +Run it in the PR that bumps VERSION_NAME_BASE, so Crowdin has the whole internal +cycle to translate before a production promotion uploads anything. + + python3 scripts/sync-play-changelog.py [--check] +""" +import re +import sys +import xml.etree.ElementTree as ET +from pathlib import Path + +REPO_ROOT = Path(__file__).resolve().parent.parent +METAINFO = REPO_ROOT / "desktopApp/packaging/linux/org.meshtastic.MeshtasticDesktop.metainfo.xml" +CONFIG = REPO_ROOT / "config.properties" +TARGET = REPO_ROOT / "fastlane/metadata/android/en-US/changelogs/default.txt" +RELEASES_URL = "https://github.com/meshtastic/Meshtastic-Android/releases" + +# Google Play caps "what's new" at 500 characters per locale. Translations run +# longer than English, so leave room rather than filling it. +PLAY_LIMIT = 500 +BUDGET = 380 + + +def version() -> str: + m = re.search(r"^VERSION_NAME_BASE=(.+)$", CONFIG.read_text(), re.M) + if not m: + sys.exit("VERSION_NAME_BASE not found in config.properties") + return m.group(1).strip() + + +def description(v: str) -> str: + root = ET.parse(METAINFO).getroot() + for rel in root.findall(".//release"): + if rel.get("version") != v: + continue + desc = rel.find("description") + if desc is None: + break + paras = [" ".join((p.text or "").split()) for p in desc.findall("p")] + paras = [p for p in paras if p] + if paras: + return " ".join(paras) + break + sys.exit( + f"no for {v} in {METAINFO.relative_to(REPO_ROOT)} - " + "add one alongside the VERSION_NAME_BASE bump" + ) + + +def render(text: str) -> str: + if len(text) > BUDGET: + cut = text[:BUDGET].rsplit(" ", 1)[0].rstrip(" .,;:") + text = f"{cut}…" + return f"{text}\n\nFull release notes: {RELEASES_URL}\n" + + +def main() -> int: + body = render(description(version())) + if len(body.rstrip()) > PLAY_LIMIT: + sys.exit(f"rendered text is {len(body.rstrip())} chars, over Play's {PLAY_LIMIT}") + if "--check" in sys.argv: + current = TARGET.read_text() if TARGET.exists() else "" + if current != body: + print( + f"::error file={TARGET.relative_to(REPO_ROOT)}::Play what's-new is stale. " + "Run python3 scripts/sync-play-changelog.py and commit the result.", + file=sys.stderr, + ) + return 1 + print("Play what's-new matches metainfo.") + return 0 + TARGET.write_text(body) + print(f"wrote {TARGET.relative_to(REPO_ROOT)} ({len(body.rstrip())} chars)") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/verify-abi-parity-selftest.sh b/scripts/verify-abi-parity-selftest.sh index 23aa22d621..5203a1ce6d 100755 --- a/scripts/verify-abi-parity-selftest.sh +++ b/scripts/verify-abi-parity-selftest.sh @@ -130,12 +130,16 @@ wrapper "matching splits pass, universal ignored" even 0 "google: all splits shi tree gap google debug armeabi-v7a=liba.so arm64-v8a=liba.so,libengine.so wrapper "unrecorded gap fails with the lib named" gap 1 "armeabi-v7a/libengine.so" +# The two cases that used to live here drove the wrapper through the *checked-in* allowlist: +# one asserted the recorded fdroid gap was excused and counted, the other that the gap +# closing failed until the lines went. maplibre-compose 0.16.0 closed that gap and the lines +# are gone, so neither case has anything real to drive and both would now assert the +# opposite of what the script does. Cases 3 and 5 cover excused and stale entries against +# the classifier directly, with allowlists they set themselves, so only the end-to-end +# variants are lost. This keeps the `*/release` glob those cases also happened to exercise. MAPLIBRE="libjniMaplibreNativeC.so,libmaplibre-native-c.so" -tree known fdroid release armeabi-v7a=liba.so "arm64-v8a=liba.so,$MAPLIBRE" -wrapper "the recorded fdroid gap passes and is counted" known 0 "fdroid: splits match apart from 2 recorded known gap(s)" - -tree stale fdroid release "armeabi-v7a=liba.so,$MAPLIBRE" "arm64-v8a=liba.so,$MAPLIBRE" -wrapper "the recorded gap closing fails until the lines go" stale 1 "known-gap entries whose library is now present" +tree rel fdroid release "armeabi-v7a=liba.so,$MAPLIBRE" "arm64-v8a=liba.so,$MAPLIBRE" +wrapper "release splits are scanned, not just debug" rel 0 "fdroid: all splits ship the same native libraries" tree single google debug arm64-v8a=liba.so wrapper "a lone split is nothing to compare, so no APKs were checked" single 1 "no split APKs found" diff --git a/settings.gradle.kts b/settings.gradle.kts index c24dfd1ae5..1f5c995bb8 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -39,7 +39,7 @@ plugins { // 0.1.7 fixed the Isolated Projects incompatibility (shares state via a BuildService instead of // gradle.extensions) that previously required gating this behind an opt-in property. // 0.2.0 resolves platformDependencies transitively — see the collapsed list in build.gradle.kts. - id("org.meshtastic.flatpak.sources.settings") version "0.2.1" + id("org.meshtastic.flatpak.sources.settings") version "0.2.2" } @Suppress("UnstableApiUsage") @@ -125,5 +125,6 @@ include( ":feature:widget", ":screenshot-tests", ":docs-screenshots", + ":marketing-screenshots", ":baselineprofile", )