Merge branch 'main' into global_app_bar

# Conflicts:
#	app/src/main/java/com/geeksville/mesh/ui/sharing/Channel.kt
pull/3297/head
Phil Oliver 2025-10-03 11:25:21 -04:00
commit a643579bcb
61 zmienionych plików z 1183 dodań i 313 usunięć

Wyświetl plik

@ -0,0 +1,139 @@
name: Create Internal Release Tag
on:
workflow_dispatch:
inputs:
release_type:
description: "Type of release (auto|patch|minor|major|hotfix)"
required: true
default: auto
type: choice
options: [auto, patch, minor, major, hotfix]
hotfix_base_version:
description: "Base version for hotfix (e.g. 2.5.0) required if release_type=hotfix"
required: false
dry_run:
description: "If true, calculate but do not push tag"
required: false
default: "false"
permissions:
contents: write
jobs:
create-internal-tag:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v5
with:
fetch-depth: 0
- name: Determine Latest Base Version
id: latest
run: |
set -euo pipefail
# List base tags (exclude track/hotfix suffixes)
BASE_TAGS=$(git tag --list 'v[0-9]*.[0-9]*.[0-9]*' --sort=-version:refname | head -n1 || true)
echo "Found latest base tag: $BASE_TAGS"
echo "latest_base_tag=$BASE_TAGS" >> $GITHUB_OUTPUT
- name: Compute Next Version (auto/patch/minor/major)
id: compute
if: ${{ inputs.release_type != 'hotfix' }}
run: |
set -euo pipefail
RTYPE='${{ inputs.release_type }}'
LAST='${{ steps.latest.outputs.latest_base_tag }}'
if [ -z "$LAST" ]; then
BASE_MAJOR=0; BASE_MINOR=0; BASE_PATCH=0
else
V=${LAST#v}
IFS='.' read -r BASE_MAJOR BASE_MINOR BASE_PATCH <<< "$V"
fi
if [ "$RTYPE" = 'auto' ]; then
echo "Determining bump type from commits since $LAST..."
RANGE="$LAST..HEAD"
[ -z "$LAST" ] && RANGE="HEAD" # first release
LOG=$(git log --format=%s $RANGE || true)
BUMP="patch"
if echo "$LOG" | grep -Eiq 'BREAKING CHANGE'; then BUMP=major; fi
if echo "$LOG" | grep -Eiq '^[a-zA-Z]+!:'; then BUMP=major; fi
if [ "$BUMP" != major ] && echo "$LOG" | grep -Eiq '^feat(\(|:)' ; then BUMP=minor; fi
RTYPE=$BUMP
echo "Auto-detected bump: $RTYPE"
fi
case "$RTYPE" in
major)
NEW_MAJOR=$((BASE_MAJOR+1)); NEW_MINOR=0; NEW_PATCH=0;;
minor)
NEW_MAJOR=$BASE_MAJOR; NEW_MINOR=$((BASE_MINOR+1)); NEW_PATCH=0;;
patch)
NEW_MAJOR=$BASE_MAJOR; NEW_MINOR=$BASE_MINOR; NEW_PATCH=$((BASE_PATCH+1));;
*) echo "Unsupported release_type for this step: $RTYPE"; exit 1;;
esac
NEW_VERSION="${NEW_MAJOR}.${NEW_MINOR}.${NEW_PATCH}"
echo "base_version=$NEW_VERSION" >> $GITHUB_OUTPUT
- name: Compute Hotfix Version
id: hotfix
if: ${{ inputs.release_type == 'hotfix' }}
run: |
set -euo pipefail
BASE='${{ inputs.hotfix_base_version }}'
if [ -z "$BASE" ]; then
echo "hotfix_base_version required for hotfix release_type" >&2
exit 1
fi
if ! git tag --list | grep -q "^v$BASE$"; then
echo "Base version tag v$BASE not found (production tag required)." >&2
exit 1
fi
EXISTING=$(git tag --list "v${BASE}-hotfix*" | sed -E 's/^v[0-9]+\.[0-9]+\.[0-9]+-hotfix([0-9]+).*$/\1/' | sort -n | tail -1 || true)
if [ -z "$EXISTING" ]; then NEXT=1; else NEXT=$((EXISTING+1)); fi
HOTFIX_VERSION="${BASE}-hotfix${NEXT}"
echo "hotfix_version=$HOTFIX_VERSION" >> $GITHUB_OUTPUT
- name: Decide Internal Tag
id: tag
run: |
set -euo pipefail
if [ '${{ inputs.release_type }}' = 'hotfix' ]; then
BASE='${{ steps.hotfix.outputs.hotfix_version }}'
else
BASE='${{ steps.compute.outputs.base_version }}'
fi
INTERNAL_TAG="v${BASE}-internal.1"
if git tag --list | grep -q "^${INTERNAL_TAG}$"; then
echo "Tag ${INTERNAL_TAG} already exists." >&2
exit 1
fi
echo "internal_tag=$INTERNAL_TAG" >> $GITHUB_OUTPUT
- name: Dry Run Preview
if: ${{ inputs.dry_run == 'true' }}
run: |
echo "DRY RUN: Would create tag ${{ steps.tag.outputs.internal_tag }} pointing to $(git rev-parse HEAD)"
git log -5 --oneline
- name: Create and Push Tag
if: ${{ inputs.dry_run != 'true' }}
run: |
TAG='${{ steps.tag.outputs.internal_tag }}'
MSG="Initial internal build for ${TAG}"
git tag -a "$TAG" -m "$MSG"
git push origin "$TAG"
echo "Created and pushed $TAG"
- name: Output Summary
run: |
echo "### Internal Tag Created" >> $GITHUB_STEP_SUMMARY
echo "Tag: ${{ steps.tag.outputs.internal_tag }}" >> $GITHUB_STEP_SUMMARY
echo "Release Type: ${{ inputs.release_type }}" >> $GITHUB_STEP_SUMMARY
if [ '${{ inputs.release_type }}' = 'hotfix' ]; then
echo "Base Hotfix Series: ${{ steps.hotfix.outputs.hotfix_version }}" >> $GITHUB_STEP_SUMMARY
else
echo "Base Version: ${{ steps.compute.outputs.base_version }}" >> $GITHUB_STEP_SUMMARY
fi
echo "Dry Run: ${{ inputs.dry_run }}" >> $GITHUB_STEP_SUMMARY

Wyświetl plik

@ -0,0 +1,191 @@
name: Promote Release
on:
workflow_dispatch:
inputs:
target_stage:
description: "Stage to promote to (auto|closed|open|production)"
required: true
default: auto
type: choice
options: [auto, closed, open, production]
base_version:
description: "Explicit base version (e.g. 2.5.0 or 2.5.0-hotfix1). If omitted, latest internal tag base is used."
required: false
allow_skip:
description: "Allow skipping intermediate stages (e.g. internal->production)"
required: false
default: "false"
dry_run:
description: "If true, only compute next tag; don't push"
required: false
default: "false"
permissions:
contents: write
jobs:
promote:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v5
with:
fetch-depth: 0
- name: Determine Base Version
id: base
run: |
set -euo pipefail
INPUT_BASE='${{ inputs.base_version }}'
if [ -n "$INPUT_BASE" ]; then
# Validate an internal tag exists for provided base
if ! git tag --list | grep -q "^v${INPUT_BASE}-internal\."; then
echo "No internal tag found for base version v${INPUT_BASE}." >&2
exit 1
fi
BASE_VERSION="$INPUT_BASE"
else
LATEST_INTERNAL_TAG=$(git tag --list 'v*-internal.*' --sort=-taggerdate | head -n1 || true)
if [ -z "$LATEST_INTERNAL_TAG" ]; then
echo "No internal tags found; nothing to promote." >&2
exit 1
fi
# Strip leading v and suffix -internal.N
BASE_VERSION=$(echo "$LATEST_INTERNAL_TAG" | sed -E 's/^v(.*)-internal\.[0-9]+$/\1/')
fi
echo "Base version: $BASE_VERSION"
echo "base_version=$BASE_VERSION" >> $GITHUB_OUTPUT
- name: Gather Existing Stage Tags
id: scan
run: |
set -euo pipefail
BASE='${{ steps.base.outputs.base_version }}'
INTERNAL_TAGS=$(git tag --list "v${BASE}-internal.*" | sort -V || true)
CLOSED_TAGS=$(git tag --list "v${BASE}-closed.*" | sort -V || true)
OPEN_TAGS=$(git tag --list "v${BASE}-open.*" | sort -V || true)
PROD_TAG=$(git tag --list "v${BASE}" || true)
echo "internal_tags<<EOF" >> $GITHUB_OUTPUT
echo "$INTERNAL_TAGS" >> $GITHUB_OUTPUT
echo EOF >> $GITHUB_OUTPUT
echo "closed_tags<<EOF" >> $GITHUB_OUTPUT
echo "$CLOSED_TAGS" >> $GITHUB_OUTPUT
echo EOF >> $GITHUB_OUTPUT
echo "open_tags<<EOF" >> $GITHUB_OUTPUT
echo "$OPEN_TAGS" >> $GITHUB_OUTPUT
echo EOF >> $GITHUB_OUTPUT
if [ -n "$PROD_TAG" ]; then echo "production_present=true" >> $GITHUB_OUTPUT; else echo "production_present=false" >> $GITHUB_OUTPUT; fi
if [ -z "$INTERNAL_TAGS" ]; then
echo "No internal tags found for base version $BASE." >&2
exit 1
fi
- name: Determine Current Stage
id: current
run: |
set -euo pipefail
PROD='${{ steps.scan.outputs.production_present }}'
CLOSED='${{ steps.scan.outputs.closed_tags }}'
OPEN='${{ steps.scan.outputs.open_tags }}'
if [ "$PROD" = 'true' ]; then CUR=production
elif [ -n "$OPEN" ]; then CUR=open
elif [ -n "$CLOSED" ]; then CUR=closed
else CUR=internal; fi
echo "Current highest stage: $CUR"
echo "current_stage=$CUR" >> $GITHUB_OUTPUT
- name: Decide Target Stage
id: decide
run: |
set -euo pipefail
REQ='${{ inputs.target_stage }}'
CUR='${{ steps.current.outputs.current_stage }}'
ALLOW_SKIP='${{ inputs.allow_skip }}'
order=(internal closed open production)
# helper to get index
idx() { local i=0; for s in "${order[@]}"; do [ "$s" = "$1" ] && echo $i && return; i=$((i+1)); done; echo -1; }
if [ "$REQ" = auto ]; then
CUR_IDX=$(idx "$CUR")
TARGET_IDX=$((CUR_IDX+1))
TARGET_STAGE=${order[$TARGET_IDX]:-}
if [ -z "$TARGET_STAGE" ]; then
echo "Already at production; nothing to promote." >&2
exit 1
fi
else
TARGET_STAGE=$REQ
CUR_IDX=$(idx "$CUR")
REQ_IDX=$(idx "$TARGET_STAGE")
if [ $REQ_IDX -le $CUR_IDX ]; then
echo "Requested stage $TARGET_STAGE is not ahead of current stage $CUR." >&2
exit 1
fi
if [ "$ALLOW_SKIP" != 'true' ] && [ $((CUR_IDX+1)) -ne $REQ_IDX ]; then
echo "Skipping stages not allowed (current=$CUR, requested=$TARGET_STAGE). Enable allow_skip to override." >&2
exit 1
fi
fi
echo "Target stage: $TARGET_STAGE"
echo "target_stage=$TARGET_STAGE" >> $GITHUB_OUTPUT
- name: Compute New Tag
id: tag
run: |
set -euo pipefail
BASE='${{ steps.base.outputs.base_version }}'
TARGET='${{ steps.decide.outputs.target_stage }}'
if [ "$TARGET" = production ]; then
NEW_TAG="v${BASE}"
if git tag --list | grep -q "^${NEW_TAG}$"; then
echo "Production tag ${NEW_TAG} already exists." >&2
exit 1
fi
else
EXISTING=$(git tag --list "v${BASE}-${TARGET}.*" | sed -E "s/^v.*-${TARGET}\.([0-9]+)$/\1/" | sort -n | tail -1 || true)
if [ -z "$EXISTING" ]; then NEXT=1; else NEXT=$((EXISTING+1)); fi
NEW_TAG="v${BASE}-${TARGET}.${NEXT}"
fi
echo "new_tag=$NEW_TAG" >> $GITHUB_OUTPUT
echo "Will create tag: $NEW_TAG"
- name: Resolve Commit to Tag (latest internal for base)
id: commit
run: |
set -euo pipefail
BASE='${{ steps.base.outputs.base_version }}'
LATEST_INTERNAL=$(git tag --list "v${BASE}-internal.*" --sort=-version:refname | head -n1)
if [ -z "$LATEST_INTERNAL" ]; then
echo "No internal tag found for base $BASE (unexpected)." >&2
exit 1
fi
COMMIT=$(git rev-list -n1 "$LATEST_INTERNAL")
echo "commit_sha=$COMMIT" >> $GITHUB_OUTPUT
echo "Using commit $COMMIT from $LATEST_INTERNAL"
- name: Dry Run Summary
if: ${{ inputs.dry_run == 'true' }}
run: |
echo "DRY RUN: Would tag commit ${{ steps.commit.outputs.commit_sha }} with ${{ steps.tag.outputs.new_tag }}"
echo "Current stage: ${{ steps.current.outputs.current_stage }} -> Target: ${{ steps.decide.outputs.target_stage }}"
git log -1 --oneline ${{ steps.commit.outputs.commit_sha }}
- name: Create & Push Tag
if: ${{ inputs.dry_run != 'true' }}
run: |
TAG='${{ steps.tag.outputs.new_tag }}'
COMMIT='${{ steps.commit.outputs.commit_sha }}'
MSG="Promote ${TAG} from ${{ steps.current.outputs.current_stage }} to ${{ steps.decide.outputs.target_stage }}"
git tag -a "$TAG" "$COMMIT" -m "$MSG"
git push origin "$TAG"
echo "Created and pushed $TAG"
- name: Promotion Summary
run: |
echo "### Promotion Tag Created" >> $GITHUB_STEP_SUMMARY
echo "Base Version: ${{ steps.base.outputs.base_version }}" >> $GITHUB_STEP_SUMMARY
echo "Current Stage: ${{ steps.current.outputs.current_stage }}" >> $GITHUB_STEP_SUMMARY
echo "Target Stage: ${{ steps.decide.outputs.target_stage }}" >> $GITHUB_STEP_SUMMARY
echo "New Tag: ${{ steps.tag.outputs.new_tag }}" >> $GITHUB_STEP_SUMMARY
echo "Dry Run: ${{ inputs.dry_run }}" >> $GITHUB_STEP_SUMMARY

Wyświetl plik

@ -2,6 +2,16 @@ name: Make Release
on:
workflow_dispatch:
inputs:
dry_run:
description: "If true, simulate the release without building, uploading, promoting, or creating a GitHub release"
required: false
default: "false"
type: choice
options: ["false", "true"]
pr_number:
description: "Optional PR number to comment on with dry-run readiness summary"
required: false
push:
tags:
- 'v*'
@ -21,7 +31,10 @@ jobs:
runs-on: ubuntu-latest
outputs:
APP_VERSION_NAME: ${{ steps.get_version_name.outputs.APP_VERSION_NAME }}
APP_VERSION_CODE: ${{ steps.calculate_version_code.outputs.versionCode }}
FINAL_VERSION_CODE: ${{ steps.final_version_code.outputs.FINAL_VERSION_CODE }}
HOTFIX_PATCH: ${{ steps.is_hotfix_patch.outputs.hotfix_patch }}
BASE_TAG: ${{ steps.get_base_tag.outputs.BASE_TAG }}
FULL_TAG: ${{ steps.get_full_tag.outputs.FULL_TAG }}
steps:
- name: Checkout code
uses: actions/checkout@v5
@ -45,6 +58,17 @@ jobs:
id: get_version_name
run: echo "APP_VERSION_NAME=$(echo ${GITHUB_REF_NAME#v} | sed 's/-.*//')" >> $GITHUB_OUTPUT
- name: Get Full Tag
id: get_full_tag
run: echo "FULL_TAG=${GITHUB_REF_NAME}" >> $GITHUB_OUTPUT
- name: Get Base Tag (for release/artifact naming)
id: get_base_tag
run: |
# Remove track/iteration suffix (e.g., -internal.1, -closed.1, -open.1)
BASE_TAG=$(echo ${GITHUB_REF_NAME} | sed 's/\(-internal\.[0-9]\+\|-closed\.[0-9]\+\|-open\.[0-9]\+\)$//')
echo "BASE_TAG=$BASE_TAG" >> $GITHUB_OUTPUT
- name: Extract VERSION_CODE_OFFSET from config.properties
id: get_version_code_offset
run: |
@ -61,9 +85,89 @@ jobs:
shell: bash
# This matches the reproducible versionCode strategy: versionCode = git commit count + offset
release-google:
- name: Check if Hotfix or Patch
id: is_hotfix_patch
run: |
TAG_LOWER=$(echo "${GITHUB_REF_NAME}" | tr '[:upper:]' '[:lower:]')
if [[ "$TAG_LOWER" == *"-hotfix"* || "$TAG_LOWER" == *"-patch"* ]]; then
echo "hotfix_patch=true" >> $GITHUB_OUTPUT
else
echo "hotfix_patch=false" >> $GITHUB_OUTPUT
fi
- name: Download Version Code Artifact (if exists)
id: try_download_version_code
continue-on-error: true
uses: actions/download-artifact@v5
with:
name: version-code
path: .
- name: Generate and Store Version Code (first build for regular release)
if: steps.is_hotfix_patch.outputs.hotfix_patch == 'false' && steps.try_download_version_code.outcome != 'success'
id: generate_and_store_version_code
run: |
VERSION_CODE=${{ steps.calculate_version_code.outputs.versionCode }}
echo "versionCode=$VERSION_CODE" >> $GITHUB_OUTPUT
echo "$VERSION_CODE" > version_code.txt
- name: Upload Version Code Artifact (if generated)
if: |
(steps.is_hotfix_patch.outputs.hotfix_patch == 'true') ||
(steps.is_hotfix_patch.outputs.hotfix_patch == 'false' && steps.generate_and_store_version_code.conclusion == 'success')
uses: actions/upload-artifact@v4
with:
name: version-code
path: version_code.txt
- name: Set Version Code from Artifact (if exists)
if: steps.try_download_version_code.outcome == 'success'
id: set_version_code_from_artifact
run: |
VERSION_CODE=$(cat version_code.txt)
echo "versionCode=$VERSION_CODE" >> $GITHUB_OUTPUT
- name: Set Final Version Code Output
id: final_version_code
run: |
if [ -f version_code.txt ]; then
FV=$(cat version_code.txt)
else
FV=${{ steps.calculate_version_code.outputs.versionCode }}
fi
echo "FINAL_VERSION_CODE=$FV" >> $GITHUB_OUTPUT
check-internal-release:
runs-on: ubuntu-latest
needs: prepare-build-info
outputs:
exists: ${{ steps.check_release.outputs.exists }}
steps:
- name: Checkout code
uses: actions/checkout@v5
with:
fetch-depth: 0
- name: Check for existing GitHub release
id: check_release
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
BASE_TAG=${{ needs.prepare-build-info.outputs.BASE_TAG }}
COMMIT_SHA=$(git rev-parse HEAD)
EXISTING_RELEASE=$(gh release list --limit 100 --json tagName,targetCommitish | jq -r --arg BASE_TAG "$BASE_TAG" --arg COMMIT_SHA "$COMMIT_SHA" '.[] | select(.tagName == $BASE_TAG and .targetCommitish.oid == $COMMIT_SHA)')
if [ -n "$EXISTING_RELEASE" ]; then
echo "An existing release with tag '${BASE_TAG}' was found for this commit."
echo "exists=true" >> $GITHUB_OUTPUT
else
echo "No existing release found for this commit."
echo "exists=false" >> $GITHUB_OUTPUT
fi
release-google:
runs-on: ubuntu-latest
needs: [prepare-build-info, check-internal-release]
outputs:
INTERNAL_VERSION_CODE: ${{ steps.resolve_internal_version_code.outputs.INTERNAL_VERSION_CODE }}
steps:
- name: Checkout code
uses: actions/checkout@v5
@ -76,7 +180,6 @@ jobs:
java-version: '21'
distribution: 'jetbrains'
- name: Setup Gradle
if: contains(github.ref_name, '-internal')
uses: gradle/actions/setup-gradle@v5
with:
cache-encryption-key: ${{ secrets.GRADLE_ENCRYPTION_KEY }}
@ -110,121 +213,258 @@ jobs:
ruby-version: '3.2'
bundler-cache: true
- name: Determine Fastlane Lane
id: fastlane_lane
- name: Dry Run Sanity Version Code Check
if: github.event.inputs.dry_run == 'true'
id: dry_run_sanity
run: |
TAG_NAME="${{ github.ref_name }}"
if [[ "$TAG_NAME" == *"-internal"* ]]; then
echo "lane=internal" >> $GITHUB_OUTPUT
elif [[ "$TAG_NAME" == *"-closed"* ]]; then
echo "lane=closed" >> $GITHUB_OUTPUT
elif [[ "$TAG_NAME" == *"-open"* ]]; then
echo "lane=open" >> $GITHUB_OUTPUT
set -euo pipefail
echo "Performing version code sanity check (dry run)..."
# Query highest existing version code across tracks
bundle exec fastlane get_highest_version_code || true
HIGHEST=$(cat highest_version_code.txt 2>/dev/null || echo 0)
HOTFIX='${{ needs.prepare-build-info.outputs.HOTFIX_PATCH }}'
if [ "$HOTFIX" = "true" ]; then
PLANNED=$((HIGHEST + 1))
echo "Hotfix planned versionCode: $PLANNED (highest existing: $HIGHEST)";
STATUS=ok
else
echo "lane=production" >> $GITHUB_OUTPUT
# Regular base release planned version code (commit count + offset or reused artifact)
PLANNED='${{ needs.prepare-build-info.outputs.FINAL_VERSION_CODE }}'
if [ -z "$PLANNED" ]; then PLANNED=0; fi
if [ "$PLANNED" -le "$HIGHEST" ]; then
echo "ERROR: Planned versionCode $PLANNED is not greater than highest existing $HIGHEST. Adjust VERSION_CODE_OFFSET or convert to hotfix." >&2
STATUS=fail
else
echo "Planned versionCode $PLANNED is greater than existing $HIGHEST: OK";
STATUS=ok
fi
fi
echo "sanity_status=$STATUS" >> $GITHUB_OUTPUT
echo "sanity_highest=$HIGHEST" >> $GITHUB_OUTPUT
echo "sanity_planned=$PLANNED" >> $GITHUB_OUTPUT
# Promotion policy validation (if this is a promotion tag in dry run)
TAG='${{ github.ref_name }}'
if echo "$TAG" | grep -Eq '-(closed|open)$' || [[ "$TAG" != *"-internal"* && "$TAG" != *"-closed"* && "$TAG" != *"-open"* && "$TAG" == v* ]]; then
echo "Checking promotion policy (dry run)..."
if ! bundle exec fastlane get_internal_track_version_code; then
echo "ERROR: Promotion attempted but no internal artifact present." >&2
echo "promotion_status=fail" >> $GITHUB_OUTPUT
[ "$STATUS" = ok ] || true
STATUS=fail
else
echo "Internal artifact present for promotion.";
echo "promotion_status=ok" >> $GITHUB_OUTPUT
fi
else
echo "Not a promotion tag (internal build).";
echo "promotion_status=na" >> $GITHUB_OUTPUT
fi
if [ "$STATUS" = fail ]; then
echo "Dry run sanity check failed." >&2
exit 1
fi
- name: Build and Deploy Google Play Tracks with Fastlane
env:
VERSION_NAME: ${{ needs.prepare-build-info.outputs.APP_VERSION_NAME }}
VERSION_CODE: ${{ needs.prepare-build-info.outputs.APP_VERSION_CODE }}
run: bundle exec fastlane ${{ steps.fastlane_lane.outputs.lane }}
- name: Upload Google AAB artifact
if: contains(github.ref_name, '-internal')
uses: actions/upload-artifact@v4
with:
name: google-aab
path: app/build/outputs/bundle/googleRelease/app-google-release.aab
retention-days: 1
- name: Upload Google APK artifact
if: contains(github.ref_name, '-internal')
uses: actions/upload-artifact@v4
with:
name: google-apk
path: app/build/outputs/apk/google/release/app-google-release.apk
retention-days: 1
- name: Attest Google artifacts provenance
if: contains(github.ref_name, '-internal')
uses: actions/attest-build-provenance@v3
with:
subject-path: |
app/build/outputs/bundle/googleRelease/app-google-release.aab
app/build/outputs/apk/google/release/app-google-release.apk
release-fdroid:
if: contains(github.ref_name, '-internal')
runs-on: ubuntu-latest
needs: prepare-build-info
steps:
- name: Checkout code
uses: actions/checkout@v5
with:
fetch-depth: 0
submodules: 'recursive'
- name: Set up JDK 21
uses: actions/setup-java@v5
with:
java-version: '21'
distribution: 'jetbrains'
- name: Setup Gradle
uses: gradle/actions/setup-gradle@v5
with:
cache-encryption-key: ${{ secrets.GRADLE_ENCRYPTION_KEY }}
build-scan-publish: true
build-scan-terms-of-use-url: 'https://gradle.com/terms-of-service'
build-scan-terms-of-use-agree: 'yes'
- name: Load secrets
env:
KEYSTORE: ${{ secrets.KEYSTORE }}
KEYSTORE_FILENAME: ${{ secrets.KEYSTORE_FILENAME }}
KEYSTORE_PROPERTIES: ${{ secrets.KEYSTORE_PROPERTIES }}
- name: Resolve Version Code For Internal Build
if: needs.check-internal-release.outputs.exists == 'false'
id: resolve_internal_version_code
run: |
echo $KEYSTORE | base64 -di > ./app/$KEYSTORE_FILENAME
echo "$KEYSTORE_PROPERTIES" > ./keystore.properties
if [ "${{ needs.prepare-build-info.outputs.HOTFIX_PATCH }}" = "true" ]; then
echo "Hotfix/Patch detected; querying Google Play for highest version code..."
bundle exec fastlane get_highest_version_code
CODE=$(cat highest_version_code.txt || echo 0)
NEXT_CODE=$((CODE + 1))
# Race mitigation: re-query to ensure no concurrent allocation
bundle exec fastlane get_highest_version_code
NEW_HIGHEST=$(cat highest_version_code.txt || echo 0)
if [ "$NEW_HIGHEST" -ge "$NEXT_CODE" ]; then
echo "Detected race: highest changed from $CODE to $NEW_HIGHEST; bumping again.";
NEXT_CODE=$((NEW_HIGHEST + 1))
fi
echo "Using hotfix version code: $NEXT_CODE (previous highest final: $NEW_HIGHEST)"
echo "INTERNAL_VERSION_CODE=$NEXT_CODE" >> $GITHUB_OUTPUT
echo "VERSION_CODE=$NEXT_CODE" >> $GITHUB_ENV
else
BASE_CODE=${{ needs.prepare-build-info.outputs.FINAL_VERSION_CODE }}
echo "Using base/internal version code: $BASE_CODE"
echo "INTERNAL_VERSION_CODE=$BASE_CODE" >> $GITHUB_OUTPUT
echo "VERSION_CODE=$BASE_CODE" >> $GITHUB_ENV
fi
- name: Setup Fastlane
uses: ruby/setup-ruby@v1
with:
ruby-version: '3.2'
bundler-cache: true
- name: Build F-Droid with Fastlane
- name: Build and Deploy to Internal Track
if: needs.check-internal-release.outputs.exists == 'false' && github.event.inputs.dry_run != 'true'
env:
VERSION_NAME: ${{ needs.prepare-build-info.outputs.APP_VERSION_NAME }}
VERSION_CODE: ${{ needs.prepare-build-info.outputs.APP_VERSION_CODE }}
VERSION_CODE: ${{ env.VERSION_CODE }}
run: bundle exec fastlane internal
- name: Build F-Droid (same version code)
if: needs.check-internal-release.outputs.exists == 'false' && github.event.inputs.dry_run != 'true'
env:
VERSION_NAME: ${{ needs.prepare-build-info.outputs.APP_VERSION_NAME }}
VERSION_CODE: ${{ env.VERSION_CODE }}
run: bundle exec fastlane fdroid_build
- name: Upload F-Droid APK artifact
- name: Generate Build Metadata & Checksums
if: needs.check-internal-release.outputs.exists == 'false' && github.event.inputs.dry_run != 'true'
id: gen_metadata
run: |
set -euo pipefail
AAB=app/build/outputs/bundle/googleRelease/app-google-release.aab
APK_GOOGLE=app/build/outputs/apk/google/release/app-google-release.apk
APK_FDROID=app/build/outputs/apk/fdroid/release/app-fdroid-release.apk
SHA_AAB=$(sha256sum "$AAB" | cut -d' ' -f1)
SHA_APK_GOOGLE=$(sha256sum "$APK_GOOGLE" | cut -d' ' -f1)
SHA_APK_FDROID=$(sha256sum "$APK_FDROID" | cut -d' ' -f1)
GIT_SHA=$(git rev-parse HEAD)
BUILD_TIME=$(date -u +%Y-%m-%dT%H:%M:%SZ)
cat > build-metadata.json <<EOF
{
"baseTag": "${{ needs.prepare-build-info.outputs.BASE_TAG }}",
"fullTag": "${{ needs.prepare-build-info.outputs.FULL_TAG }}",
"versionName": "${{ needs.prepare-build-info.outputs.APP_VERSION_NAME }}",
"versionCode": "${{ env.VERSION_CODE }}",
"hotfixOrPatch": "${{ needs.prepare-build-info.outputs.HOTFIX_PATCH }}",
"gitSha": "$GIT_SHA",
"buildTimeUtc": "$BUILD_TIME",
"artifacts": {
"googleAab": { "path": "$AAB", "sha256": "$SHA_AAB" },
"googleApk": { "path": "$APK_GOOGLE", "sha256": "$SHA_APK_GOOGLE" },
"fdroidApk": { "path": "$APK_FDROID", "sha256": "$SHA_APK_FDROID" }
}
}
EOF
echo "Generated build-metadata.json:"; cat build-metadata.json
echo "AAB_SHA256=$SHA_AAB" >> $GITHUB_ENV
echo "APK_GOOGLE_SHA256=$SHA_APK_GOOGLE" >> $GITHUB_ENV
echo "APK_FDROID_SHA256=$SHA_APK_FDROID" >> $GITHUB_ENV
- name: Upload Build Metadata Artifact
if: needs.check-internal-release.outputs.exists == 'false' && github.event.inputs.dry_run != 'true'
uses: actions/upload-artifact@v4
with:
name: fdroid-apk
path: app/build/outputs/apk/fdroid/release/app-fdroid-release.apk
retention-days: 1
name: build-metadata
path: build-metadata.json
retention-days: 7
- name: Attest F-Droid APK provenance
uses: actions/attest-build-provenance@v3
- name: Upload F-Droid APK artifact
if: needs.check-internal-release.outputs.exists == 'false' && github.event.inputs.dry_run != 'true'
uses: actions/upload-artifact@v4
with:
subject-path: app/build/outputs/apk/fdroid/release/app-fdroid-release.apk
name: fdroid-apk
path: app/build/outputs/apk/fdroid/release/app-fdroid-release.apk
retention-days: 1
create-internal-release:
- name: Promotion Guard - Internal Must Exist
if: steps.fastlane_lane.outputs.lane != '' && github.event.inputs.dry_run != 'true'
run: |
set -e
echo "Validating internal track has an artifact before promotion..."
if ! bundle exec fastlane get_internal_track_version_code; then
echo "ERROR: No internal artifact found to promote. Ensure an internal tag was built first." >&2
exit 1
fi
- name: Fetch Internal Track Version Code (for promotion)
if: steps.fastlane_lane.outputs.lane != '' && github.event.inputs.dry_run != 'true'
run: |
bundle exec fastlane get_internal_track_version_code
CODE=$(cat internal_version_code.txt)
echo "INTERNAL_VERSION_CODE=$CODE" >> $GITHUB_ENV
- name: Promote on Google Play
if: steps.fastlane_lane.outputs.lane != '' && github.event.inputs.dry_run != 'true'
env:
VERSION_NAME: ${{ needs.prepare-build-info.outputs.APP_VERSION_NAME }}
VERSION_CODE: ${{ env.INTERNAL_VERSION_CODE }}
run: bundle exec fastlane ${{ steps.fastlane_lane.outputs.lane }}
- name: Build Summary (Internal Build)
if: needs.check-internal-release.outputs.exists == 'false' && github.event.inputs.dry_run != 'true'
run: |
{
echo "### Internal Build Summary"
echo "Version Name: ${{ needs.prepare-build-info.outputs.APP_VERSION_NAME }}"
echo "Version Code: ${{ env.VERSION_CODE }}"
echo "Base Tag: ${{ needs.prepare-build-info.outputs.BASE_TAG }}"
echo "Full Tag: ${{ needs.prepare-build-info.outputs.FULL_TAG }}"
echo "Hotfix/Patch: ${{ needs.prepare-build-info.outputs.HOTFIX_PATCH }}"
echo "Google AAB SHA256: $AAB_SHA256"
echo "Google APK SHA256: $APK_GOOGLE_SHA256"
echo "F-Droid APK SHA256: $APK_FDROID_SHA256"
} >> $GITHUB_STEP_SUMMARY
- name: Dry Run Summary
if: github.event.inputs.dry_run == 'true'
run: |
echo "### Release Dry Run" >> $GITHUB_STEP_SUMMARY
echo "Tag: ${{ github.ref_name }}" >> $GITHUB_STEP_SUMMARY
echo "Base Tag: ${{ needs.prepare-build-info.outputs.BASE_TAG }}" >> $GITHUB_STEP_SUMMARY
echo "Hotfix/Patch: ${{ needs.prepare-build-info.outputs.HOTFIX_PATCH }}" >> $GITHUB_STEP_SUMMARY
echo "Computed Version Name: ${{ needs.prepare-build-info.outputs.APP_VERSION_NAME }}" >> $GITHUB_STEP_SUMMARY
echo "Planned Version Code Strategy: $([[ '${{ needs.prepare-build-info.outputs.HOTFIX_PATCH }}' == 'true' ]] && echo 'highest+1 from Play' || echo 'commit-count+offset (first internal) or reuse')" >> $GITHUB_STEP_SUMMARY
echo "Sanity Highest Existing VersionCode: ${{ steps.dry_run_sanity.outputs.sanity_highest }}" >> $GITHUB_STEP_SUMMARY
echo "Sanity Planned VersionCode: ${{ steps.dry_run_sanity.outputs.sanity_planned }}" >> $GITHUB_STEP_SUMMARY
echo "Sanity Status: ${{ steps.dry_run_sanity.outputs.sanity_status }}" >> $GITHUB_STEP_SUMMARY
echo "Promotion Policy Check: ${{ steps.dry_run_sanity.outputs.promotion_status }}" >> $GITHUB_STEP_SUMMARY
echo "Would build internal artifacts: $([[ '${{ needs.check-internal-release.outputs.exists }}' == 'false' ]] && echo yes || echo 'no (already exists)')" >> $GITHUB_STEP_SUMMARY
echo "Would promote lane: $([[ -n '${{ steps.fastlane_lane.outputs.lane }}' ]] && echo '${{ steps.fastlane_lane.outputs.lane }}' || echo 'n/a')" >> $GITHUB_STEP_SUMMARY
echo "Would create or update draft GitHub release: $([[ '${{ needs.check-internal-release.outputs.exists }}' == 'false' ]] && echo yes || echo no)" >> $GITHUB_STEP_SUMMARY
- name: Post Dry Run PR Comment
if: github.event.inputs.dry_run == 'true' && github.event.inputs.pr_number != ''
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
BODY=$(cat <<'EOT'
Release Dry Run Summary
----------------------
Tag: ${{ github.ref_name }}
Base Tag: ${{ needs.prepare-build-info.outputs.BASE_TAG }}
Version Name: ${{ needs.prepare-build-info.outputs.APP_VERSION_NAME }}
Hotfix/Patch: ${{ needs.prepare-build-info.outputs.HOTFIX_PATCH }}
Planned VersionCode: ${{ steps.dry_run_sanity.outputs.sanity_planned }} (highest existing: ${{ steps.dry_run_sanity.outputs.sanity_highest }})
Sanity Status: ${{ steps.dry_run_sanity.outputs.sanity_status }}
Promotion Policy: ${{ steps.dry_run_sanity.outputs.promotion_status }}
Would Promote Lane: $([[ -n '${{ steps.fastlane_lane.outputs.lane }}' ]] && echo '${{ steps.fastlane_lane.outputs.lane }}' || echo 'n/a')
Draft Release Action: $([[ '${{ needs.check-internal-release.outputs.exists }}' == 'false' ]] && echo 'would create/update' || echo 'none')
EOT
)
gh pr comment ${{ github.event.inputs.pr_number }} --body "$BODY" || echo "Failed to post PR comment (verify pr_number)"
manage-github-release:
if: github.event.inputs.dry_run != 'true'
runs-on: ubuntu-latest
needs: [prepare-build-info, release-google, release-fdroid]
if: contains(github.ref_name, '-internal')
needs: [prepare-build-info, check-internal-release, release-google]
steps:
- name: Download all artifacts
if: needs.check-internal-release.outputs.exists == 'false'
uses: actions/download-artifact@v5
with:
path: ./artifacts
- name: Compute Release Name (Channel Aware)
id: release_name
run: |
BASE='${{ needs.prepare-build-info.outputs.BASE_TAG }}'
REF='${{ github.ref_name }}'
if [[ "$REF" == *"-internal"* ]]; then
NAME="$BASE (internal)"
elif [[ "$REF" == *"-closed"* ]]; then
NAME="$BASE (closed testing)"
elif [[ "$REF" == *"-open"* ]]; then
NAME="$BASE (open beta)"
else
NAME="$BASE"
fi
echo "Computed release name: $NAME"
echo "name=$NAME" >> $GITHUB_OUTPUT
- name: Create GitHub Release
if: needs.check-internal-release.outputs.exists == 'false'
uses: softprops/action-gh-release@v2
with:
tag_name: v${{ needs.prepare-build-info.outputs.APP_VERSION_NAME }}
name: ${{ github.ref_name }}
tag_name: ${{ needs.prepare-build-info.outputs.BASE_TAG }}
name: ${{ steps.release_name.outputs.name }}
generate_release_notes: true
files: ./artifacts/*/*
draft: true
@ -232,12 +472,8 @@ jobs:
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
promote-release:
runs-on: ubuntu-latest
needs: [prepare-build-info, release-google]
if: "!contains(github.ref_name, '-internal')"
steps:
- name: Determine Release Properties
- name: Determine Release Properties for Promotion
if: "!contains(github.ref_name, '-internal')"
id: release_properties
run: |
TAG_NAME="${{ github.ref_name }}"
@ -252,12 +488,24 @@ jobs:
echo "prerelease=false" >> $GITHUB_OUTPUT
fi
- name: Update GitHub Release
- name: Promote GitHub Release
if: "!contains(github.ref_name, '-internal')"
uses: softprops/action-gh-release@v2
with:
tag_name: v${{ needs.prepare-build-info.outputs.APP_VERSION_NAME }}
name: ${{ github.ref_name }}
tag_name: ${{ needs.prepare-build-info.outputs.BASE_TAG }}
name: ${{ steps.release_name.outputs.name }}
draft: ${{ steps.release_properties.outputs.draft }}
prerelease: ${{ steps.release_properties.outputs.prerelease }}
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Append Metadata to Release Notes
if: needs.check-internal-release.outputs.exists == 'false'
run: |
if [ -f artifacts/build-metadata/build-metadata.json ]; then
echo "\n---\nBuild Metadata JSON:\n" > appended_notes.txt
cat artifacts/build-metadata/build-metadata.json >> appended_notes.txt
gh release edit ${{ needs.prepare-build-info.outputs.BASE_TAG }} --notes-file appended_notes.txt
fi
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

Wyświetl plik

@ -39,6 +39,31 @@ git tag v2.3.5-closed.1
git push origin v2.3.5-closed.1
```
## Hotfixes & Patch Releases
If you need to release a hotfix or patch for a previous version (not the latest mainline), follow this process:
- **Tagging:** Use a tag with a suffix, such as `vX.X.X-hotfix1` or `vX.X.X-patch1` (e.g., `v2.3.5-hotfix1`).
- **Uniqueness:** The release workflow uses the full tag (including suffix) for all artifact and release naming, so each hotfix/patch is uniquely identified.
- **Version Code:** The workflow automatically ensures the version code for a hotfix/patch is strictly greater than any previous release, even if the hotfix is created from an older commit. This prevents Play Store upload errors due to version code regressions.
- **Multiple Releases:** You can have multiple releases for the same base version (e.g., `v2.3.5`, `v2.3.5-hotfix1`, `v2.3.5-patch2`). Each will be published and promoted independently.
### Hotfix Tagging Example
```bash
# On a release branch or after checking out the commit to hotfix
# Tag and push a hotfix release
git tag v2.3.5-hotfix1
git push origin v2.3.5-hotfix1
# For additional hotfixes/patches:
git tag v2.3.5-hotfix2
git push origin v2.3.5-hotfix2
```
### Policy
- Always use a unique tag for each hotfix/patch.
- The version code will always increase, regardless of commit history.
- The full tag is used for all release and artifact naming.
## Manual Checklist
- [ ] Verify build in Google Play Console
- [ ] Review and publish GitHub draft release (for production)

Wyświetl plik

@ -68,6 +68,7 @@
<ID>FinalNewline:UsbRepositoryModule.kt$com.geeksville.mesh.repository.usb.UsbRepositoryModule.kt</ID>
<ID>ForbiddenComment:SafeBluetooth.kt$SafeBluetooth$// TODO: display some kind of UI about restarting BLE</ID>
<ID>LambdaParameterEventTrailing:Channel.kt$onConfirm</ID>
<ID>LambdaParameterEventTrailing:ContactSharing.kt$onSharedContactRequested</ID>
<ID>LambdaParameterEventTrailing:Message.kt$onClick</ID>
<ID>LambdaParameterEventTrailing:Message.kt$onSendMessage</ID>
<ID>LambdaParameterEventTrailing:MessageList.kt$onReply</ID>
@ -183,6 +184,7 @@
<ID>ModifierMissing:SecurityConfigItemList.kt$SecurityConfigScreen</ID>
<ID>ModifierMissing:SettingsScreen.kt$SettingsScreen</ID>
<ID>ModifierMissing:Share.kt$ShareScreen</ID>
<ID>ModifierMissing:SharedContactDialog.kt$SharedContactDialog</ID>
<ID>ModifierMissing:SignalMetrics.kt$SignalMetricsScreen</ID>
<ID>ModifierMissing:TopLevelNavIcon.kt$TopLevelNavIcon</ID>
<ID>ModifierNotUsedAtRoot:DeviceMetrics.kt$modifier = modifier.weight(weight = Y_AXIS_WEIGHT)</ID>

Wyświetl plik

@ -42,7 +42,6 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.geeksville.mesh.model.UIViewModel
import com.geeksville.mesh.ui.MainScreen
import com.geeksville.mesh.ui.intro.AppIntroductionScreen
import com.geeksville.mesh.ui.sharing.toSharedContact
import dagger.hilt.android.AndroidEntryPoint
import org.meshtastic.core.datastore.UiPreferencesDataSource
import org.meshtastic.core.navigation.DEEP_LINK_BASE_URI
@ -118,9 +117,8 @@ class MainActivity : AppCompatActivity() {
Timber.d("App link data is a channel set")
model.requestChannelUrl(it)
} else if (it.path?.startsWith("/v/") == true || it.path?.startsWith("/V/") == true) {
val sharedContact = it.toSharedContact()
Timber.d("App link data is a shared contact: ${sharedContact.user.longName}")
model.setSharedContactRequested(sharedContact)
Timber.d("App link data is a shared contact")
model.setSharedContactRequested(it)
} else {
Timber.d("App link data is not a channel set")
}

Wyświetl plik

@ -43,6 +43,7 @@ import com.geeksville.mesh.copy
import com.geeksville.mesh.repository.radio.MeshActivity
import com.geeksville.mesh.repository.radio.RadioInterfaceService
import com.geeksville.mesh.service.MeshServiceNotifications
import com.geeksville.mesh.ui.sharing.toSharedContact
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
@ -297,8 +298,17 @@ constructor(
val sharedContactRequested: StateFlow<AdminProtos.SharedContact?>
get() = _sharedContactRequested.asStateFlow()
fun setSharedContactRequested(sharedContact: AdminProtos.SharedContact?) {
_sharedContactRequested.value = sharedContact
fun setSharedContactRequested(url: Uri) {
runCatching { _sharedContactRequested.value = url.toSharedContact() }
.onFailure { ex ->
Timber.e(ex, "Shared contact error")
showSnackBar(R.string.contact_invalid)
}
}
/** Called immediately after activity observes requestChannelUrl */
fun clearSharedContactRequested() {
_sharedContactRequested.value = null
}
// Connection state to our radio device
@ -347,23 +357,6 @@ constructor(
}
}
fun setChannel(channel: ChannelProtos.Channel) {
try {
meshService?.setChannel(channel.toByteArray())
} catch (ex: RemoteException) {
Timber.e(ex, "Set channel error")
}
}
/** Set the radio config (also updates our saved copy in preferences). */
fun setChannels(channelSet: AppOnlyProtos.ChannelSet) = viewModelScope.launch {
getChannelList(channelSet.settingsList, channels.value.settingsList).forEach(::setChannel)
radioConfigRepository.replaceAllSettings(channelSet.settingsList)
val newConfig = config { lora = channelSet.loraConfig }
if (config.lora != newConfig.lora) setConfig(newConfig)
}
fun addQuickChatAction(action: QuickChatAction) =
viewModelScope.launch(Dispatchers.IO) { quickChatActionRepository.upsert(action) }

Wyświetl plik

@ -90,6 +90,7 @@ import com.geeksville.mesh.ui.common.components.ScannedQrCodeDialog
import com.geeksville.mesh.ui.connections.DeviceType
import com.geeksville.mesh.ui.connections.components.TopLevelNavIcon
import com.geeksville.mesh.ui.metrics.annotateTraceroute
import com.geeksville.mesh.ui.sharing.SharedContactDialog
import com.google.accompanist.permissions.ExperimentalPermissionsApi
import com.google.accompanist.permissions.isGranted
import com.google.accompanist.permissions.rememberPermissionState
@ -136,6 +137,7 @@ fun MainScreen(uIViewModel: UIViewModel = hiltViewModel(), scanModel: BTScanMode
val navController = rememberNavController()
val connectionState by uIViewModel.connectionState.collectAsStateWithLifecycle()
val requestChannelSet by uIViewModel.requestChannelSet.collectAsStateWithLifecycle()
val sharedContactRequested by uIViewModel.sharedContactRequested.collectAsStateWithLifecycle()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
val notificationPermissionState = rememberPermissionState(Manifest.permission.POST_NOTIFICATIONS)
@ -147,7 +149,13 @@ fun MainScreen(uIViewModel: UIViewModel = hiltViewModel(), scanModel: BTScanMode
}
if (connectionState == ConnectionState.CONNECTED) {
requestChannelSet?.let { newChannelSet -> ScannedQrCodeDialog(uIViewModel, newChannelSet) }
sharedContactRequested?.let {
SharedContactDialog(sharedContact = it, onDismiss = { uIViewModel.clearSharedContactRequested() })
}
requestChannelSet?.let { newChannelSet ->
ScannedQrCodeDialog(newChannelSet, onDismiss = { uIViewModel.clearRequestChannelUrl() })
}
}
analytics.addNavigationTrackingEffect(navController = navController)

Wyświetl plik

@ -49,24 +49,28 @@ import androidx.compose.ui.tooling.preview.PreviewScreenSizes
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.Dialog
import androidx.compose.ui.window.DialogProperties
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.geeksville.mesh.AppOnlyProtos.ChannelSet
import com.geeksville.mesh.ConfigProtos.Config.LoRaConfig.ModemPreset
import com.geeksville.mesh.channelSet
import com.geeksville.mesh.copy
import com.geeksville.mesh.model.UIViewModel
import com.geeksville.mesh.ui.settings.radio.components.ChannelSelection
import org.meshtastic.core.model.Channel
import org.meshtastic.core.strings.R
@Composable
fun ScannedQrCodeDialog(viewModel: UIViewModel, incoming: ChannelSet) {
fun ScannedQrCodeDialog(
incoming: ChannelSet,
onDismiss: () -> Unit,
viewModel: ScannedQrCodeViewModel = hiltViewModel(),
) {
val channels by viewModel.channels.collectAsStateWithLifecycle()
ScannedQrCodeDialog(
channels = channels,
incoming = incoming,
onDismiss = viewModel::clearRequestChannelUrl,
onDismiss = onDismiss,
onConfirm = viewModel::setChannels,
)
}

Wyświetl plik

@ -0,0 +1,86 @@
/*
* Copyright (c) 2025 Meshtastic LLC
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.geeksville.mesh.ui.common.components
import android.os.RemoteException
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.geeksville.mesh.AppOnlyProtos
import com.geeksville.mesh.ChannelProtos
import com.geeksville.mesh.ConfigProtos.Config
import com.geeksville.mesh.LocalOnlyProtos.LocalConfig
import com.geeksville.mesh.channelSet
import com.geeksville.mesh.config
import com.geeksville.mesh.model.getChannelList
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
import org.meshtastic.core.data.repository.RadioConfigRepository
import org.meshtastic.core.service.ServiceRepository
import timber.log.Timber
import javax.inject.Inject
@HiltViewModel
class ScannedQrCodeViewModel
@Inject
constructor(
private val radioConfigRepository: RadioConfigRepository,
private val serviceRepository: ServiceRepository,
) : ViewModel() {
val channels =
radioConfigRepository.channelSetFlow.stateIn(
viewModelScope,
SharingStarted.WhileSubscribed(5_000L),
channelSet {},
)
private val localConfig =
radioConfigRepository.localConfigFlow.stateIn(
viewModelScope,
SharingStarted.WhileSubscribed(5_000L),
LocalConfig.getDefaultInstance(),
)
/** Set the radio config (also updates our saved copy in preferences). */
fun setChannels(channelSet: AppOnlyProtos.ChannelSet) = viewModelScope.launch {
getChannelList(channelSet.settingsList, channels.value.settingsList).forEach(::setChannel)
radioConfigRepository.replaceAllSettings(channelSet.settingsList)
val newConfig = config { lora = channelSet.loraConfig }
if (localConfig.value.lora != newConfig.lora) setConfig(newConfig)
}
private fun setChannel(channel: ChannelProtos.Channel) {
try {
serviceRepository.meshService?.setChannel(channel.toByteArray())
} catch (ex: RemoteException) {
Timber.e(ex, "Set channel error")
}
}
// Set the radio config (also updates our saved copy in preferences)
private fun setConfig(config: Config) {
try {
serviceRepository.meshService?.setConfig(config.toByteArray())
} catch (ex: RemoteException) {
Timber.e(ex, "Set config error")
}
}
}

Wyświetl plik

@ -25,23 +25,18 @@ import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.IntrinsicSize
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.selection.selectable
import androidx.compose.foundation.selection.selectableGroup
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.Language
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
@ -49,14 +44,12 @@ import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.compose.ui.window.Dialog
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.geeksville.mesh.ConfigProtos
@ -118,7 +111,8 @@ fun ConnectionsScreen(
val regionUnset = config.lora.region == ConfigProtos.Config.LoRaConfig.RegionCode.UNSET
val bluetoothRssi by connectionsViewModel.bluetoothRssi.collectAsStateWithLifecycle()
val bleDevices by scanModel.bleDevicesForUi.collectAsStateWithLifecycle()
val bondedBleDevices by scanModel.bleDevicesForUi.collectAsStateWithLifecycle()
val scannedBleDevices by scanModel.scanResult.observeAsState(emptyMap())
val discoveredTcpDevices by scanModel.discoveredTcpDevicesForUi.collectAsStateWithLifecycle()
val recentTcpDevices by scanModel.recentTcpDevicesForUi.collectAsStateWithLifecycle()
val usbDevices by scanModel.usbDevicesForUi.collectAsStateWithLifecycle()
@ -152,15 +146,6 @@ fun ConnectionsScreen(
}
}
// State for the device scan dialog
var showScanDialog by remember { mutableStateOf(false) }
val scanResults by scanModel.scanResult.observeAsState(emptyMap())
// Observe scan results to show the dialog
if (scanResults.isNotEmpty()) {
showScanDialog = true
}
LaunchedEffect(connectionState, regionUnset) {
when (connectionState) {
ConnectionState.CONNECTED -> {
@ -245,7 +230,11 @@ fun ConnectionsScreen(
DeviceType.BLE -> {
BLEDevices(
connectionState = connectionState,
btDevices = bleDevices,
bondedDevices = bondedBleDevices,
availableDevices =
scannedBleDevices.values.toList().filterNot { available ->
bondedBleDevices.any { it.address == available.address }
},
selectedDevice = selectedDevice,
scanModel = scanModel,
bluetoothEnabled = bluetoothState.enabled,
@ -280,7 +269,7 @@ fun ConnectionsScreen(
val showWarningNotPaired =
!connectionState.isConnected() &&
!hasShownNotPairedWarning &&
bleDevices.none { it is DeviceListEntry.Ble && it.bonded }
bondedBleDevices.none { it is DeviceListEntry.Ble && it.bonded }
if (showWarningNotPaired) {
Text(
text = stringResource(R.string.warning_not_paired),
@ -294,55 +283,6 @@ fun ConnectionsScreen(
}
}
}
// Compose Device Scan Dialog
if (showScanDialog) {
Dialog(
onDismissRequest = {
showScanDialog = false
scanModel.clearScanResults()
},
) {
Surface(shape = MaterialTheme.shapes.medium) {
Column(modifier = Modifier.padding(16.dp)) {
Text(
text = "Select a Bluetooth device",
style = MaterialTheme.typography.titleLarge,
modifier = Modifier.padding(bottom = 16.dp),
)
Column(modifier = Modifier.selectableGroup()) {
scanResults.values.forEach { device ->
Row(
modifier =
Modifier.fillMaxWidth()
.selectable(
selected = false, // No pre-selection in this dialog
onClick = {
scanModel.onSelected(device)
scanModel.clearScanResults()
showScanDialog = false
},
)
.padding(vertical = 8.dp),
verticalAlignment = Alignment.CenterVertically,
) {
Text(text = device.name)
}
}
}
Spacer(modifier = Modifier.height(16.dp))
TextButton(
onClick = {
scanModel.clearScanResults()
showScanDialog = false
},
) {
Text(stringResource(R.string.cancel))
}
}
}
}
}
}
Box(modifier = Modifier.fillMaxWidth().padding(8.dp)) {

Wyświetl plik

@ -69,7 +69,8 @@ import org.meshtastic.core.ui.component.TitledCard
@Composable
fun BLEDevices(
connectionState: ConnectionState,
btDevices: List<DeviceListEntry>,
bondedDevices: List<DeviceListEntry>,
availableDevices: List<DeviceListEntry>,
selectedDevice: String,
scanModel: BTScanModel,
bluetoothEnabled: Boolean,
@ -153,7 +154,7 @@ fun BLEDevices(
}
}
if (btDevices.isEmpty()) {
if (bondedDevices.isEmpty() && availableDevices.isEmpty()) {
EmptyStateContent(
imageVector = Icons.Rounded.BluetoothDisabled,
text =
@ -165,18 +166,19 @@ fun BLEDevices(
actionButton = scanButton,
)
} else {
TitledCard(title = stringResource(R.string.bluetooth_paired_devices)) {
btDevices.forEach { device ->
val connected =
connectionState == ConnectionState.CONNECTED && device.fullAddress == selectedDevice
DeviceListItem(
connected = connected,
device = device,
onSelect = { scanModel.onSelected(device) },
modifier = Modifier,
)
}
}
bondedDevices.Section(
title = stringResource(R.string.bluetooth_paired_devices),
connectionState = connectionState,
selectedDevice = selectedDevice,
onSelect = scanModel::onSelected,
)
availableDevices.Section(
title = stringResource(R.string.bluetooth_available_devices),
connectionState = connectionState,
selectedDevice = selectedDevice,
onSelect = scanModel::onSelected,
)
scanButton()
}
@ -213,3 +215,25 @@ private fun checkPermissionsAndScan(
permissionsState.launchMultiplePermissionRequest()
}
}
@Composable
private fun List<DeviceListEntry>.Section(
title: String,
connectionState: ConnectionState,
selectedDevice: String,
onSelect: (DeviceListEntry) -> Unit,
) {
if (isNotEmpty()) {
TitledCard(title = title) {
forEach { device ->
val connected = connectionState == ConnectionState.CONNECTED && device.fullAddress == selectedDevice
DeviceListItem(
connected = connected,
device = device,
onSelect = { onSelect(device) },
modifier = Modifier,
)
}
}
}
}

Wyświetl plik

@ -109,17 +109,15 @@ fun NodeListScreen(viewModel: NodeListViewModel = hiltViewModel(), navigateToNod
floatingActionButton = {
val firmwareVersion = DeviceVersion(ourNode?.metadata?.firmwareVersion ?: "0.0.0")
val shareCapable = firmwareVersion.supportsQrCodeSharing()
val scannedContact: AdminProtos.SharedContact? by
val sharedContact: AdminProtos.SharedContact? by
viewModel.sharedContactRequested.collectAsStateWithLifecycle(null)
AddContactFAB(
unfilteredNodes = unfilteredNodes,
scannedContact = scannedContact,
sharedContact = sharedContact,
modifier =
Modifier.animateFloatingActionButton(
visible = !isScrollInProgress && connectionState == ConnectionState.CONNECTED && shareCapable,
alignment = Alignment.BottomEnd,
),
onSharedContactImport = { contact -> viewModel.addSharedContact(contact) },
onSharedContactRequested = { contact -> viewModel.setSharedContactRequested(contact) },
)
},

Wyświetl plik

@ -97,6 +97,8 @@ import com.geeksville.mesh.channelSet
import com.geeksville.mesh.copy
import com.geeksville.mesh.navigation.ConfigRoute
import com.geeksville.mesh.navigation.getNavRouteFrom
import com.geeksville.mesh.ui.common.components.ScannedQrCodeDialog
import com.geeksville.mesh.ui.common.components.ScannedQrCodeDialog
import com.geeksville.mesh.ui.settings.radio.RadioConfigViewModel
import com.geeksville.mesh.ui.settings.radio.components.ChannelSelection
import com.geeksville.mesh.ui.settings.radio.components.PacketResponseStateDialog
@ -147,6 +149,8 @@ fun ChannelScreen(
var shouldAddChannelsState by remember { mutableStateOf(true) }
val requestChannelSet by viewModel.requestChannelSet.collectAsStateWithLifecycle()
/* Animate waiting for the configurations */
var isWaiting by remember { mutableStateOf(false) }
if (isWaiting) {
@ -272,6 +276,8 @@ fun ChannelScreen(
)
}
requestChannelSet?.let { ScannedQrCodeDialog(it, onDismiss = { viewModel.clearRequestChannelUrl() }) }
Scaffold(
topBar = {
MainAppBar(

Wyświetl plik

@ -91,6 +91,10 @@ constructor(
onError()
}
fun clearRequestChannelUrl() {
_requestChannelSet.value = null
}
/** Set the radio config (also updates our saved copy in preferences). */
fun setChannels(channelSet: AppOnlyProtos.ChannelSet) = viewModelScope.launch {
getChannelList(channelSet.settingsList, channels.value.settingsList).forEach(::setChannel)

Wyświetl plik

@ -30,9 +30,7 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.twotone.QrCodeScanner
import androidx.compose.material3.FloatingActionButton
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
@ -72,17 +70,14 @@ import java.net.MalformedURLException
* requests using Accompanist Permissions.
*
* @param modifier Modifier for this composable.
* @param onSharedContactImport Callback invoked when a shared contact is successfully imported.
*/
@OptIn(ExperimentalPermissionsApi::class)
@Suppress("LongMethod", "CyclomaticComplexMethod")
@Composable
fun AddContactFAB(
unfilteredNodes: List<Node>,
scannedContact: AdminProtos.SharedContact?,
sharedContact: AdminProtos.SharedContact?,
modifier: Modifier = Modifier,
onSharedContactImport: (AdminProtos.SharedContact) -> Unit = {},
onSharedContactRequested: (AdminProtos.SharedContact?) -> Unit = {},
onSharedContactRequested: (AdminProtos.SharedContact?) -> Unit,
) {
val barcodeLauncher =
rememberLauncherForActivityResult(ScanContract()) { result ->
@ -101,37 +96,7 @@ fun AddContactFAB(
}
}
scannedContact?.let { contactToImport ->
val nodeNum = contactToImport.nodeNum
val node = unfilteredNodes.find { it.num == nodeNum }
SimpleAlertDialog(
title = R.string.import_shared_contact,
text = {
Column {
if (node != null) {
Text(text = stringResource(R.string.import_known_shared_contact_text))
if (node.user.publicKey.size() > 0 && node.user.publicKey != contactToImport.user?.publicKey) {
Text(
text = stringResource(R.string.public_key_changed),
color = MaterialTheme.colorScheme.error,
)
}
HorizontalDivider()
Text(text = compareUsers(node.user, contactToImport.user))
} else {
Text(text = userFieldsToString(contactToImport.user))
}
}
},
dismissText = stringResource(R.string.cancel),
onDismiss = { onSharedContactRequested(null) },
confirmText = stringResource(R.string.import_label),
onConfirm = {
onSharedContactImport(contactToImport)
onSharedContactRequested(null)
},
)
}
sharedContact?.let { SharedContactDialog(sharedContact = it, onDismiss = { onSharedContactRequested(null) }) }
fun zxingScan() {
Timber.d("Starting zxing QR code scanner")

Wyświetl plik

@ -0,0 +1,72 @@
/*
* Copyright (c) 2025 Meshtastic LLC
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.geeksville.mesh.ui.sharing
import androidx.compose.foundation.layout.Column
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.res.stringResource
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.geeksville.mesh.AdminProtos
import org.meshtastic.core.strings.R
import org.meshtastic.core.ui.component.SimpleAlertDialog
/** A dialog for importing a shared contact that was scanned from a QR code. */
@Composable
fun SharedContactDialog(
sharedContact: AdminProtos.SharedContact,
onDismiss: () -> Unit,
viewModel: SharedContactViewModel = hiltViewModel(),
) {
val unfilteredNodes by viewModel.unfilteredNodes.collectAsStateWithLifecycle()
val nodeNum = sharedContact.nodeNum
val node = unfilteredNodes.find { it.num == nodeNum }
SimpleAlertDialog(
title = R.string.import_shared_contact,
text = {
Column {
if (node != null) {
Text(text = stringResource(R.string.import_known_shared_contact_text))
if (node.user.publicKey.size() > 0 && node.user.publicKey != sharedContact.user?.publicKey) {
Text(
text = stringResource(R.string.public_key_changed),
color = MaterialTheme.colorScheme.error,
)
}
HorizontalDivider()
Text(text = compareUsers(node.user, sharedContact.user))
} else {
Text(text = userFieldsToString(sharedContact.user))
}
}
},
dismissText = stringResource(R.string.cancel),
onDismiss = onDismiss,
confirmText = stringResource(R.string.import_label),
onConfirm = {
viewModel.addSharedContact(sharedContact)
onDismiss()
},
)
}

Wyświetl plik

@ -0,0 +1,53 @@
/*
* Copyright (c) 2025 Meshtastic LLC
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.geeksville.mesh.ui.sharing
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.geeksville.mesh.AdminProtos
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
import org.meshtastic.core.data.repository.NodeRepository
import org.meshtastic.core.database.model.Node
import org.meshtastic.core.service.ServiceAction
import org.meshtastic.core.service.ServiceRepository
import javax.inject.Inject
@HiltViewModel
class SharedContactViewModel
@Inject
constructor(
nodeRepository: NodeRepository,
private val serviceRepository: ServiceRepository,
) : ViewModel() {
val unfilteredNodes: StateFlow<List<Node>> =
nodeRepository
.getNodes()
.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5_000),
initialValue = emptyList(),
)
fun addSharedContact(sharedContact: AdminProtos.SharedContact) =
viewModelScope.launch { serviceRepository.onServiceAction(ServiceAction.ImportContact(sharedContact)) }
}

@ -1 +1 @@
Subproject commit 60c3e6600a2f4e6f49e45aeb47aafd8291a0015c
Subproject commit c1e31a9655e9920a8b5b8eccdf7c69ef1ae42a49

Wyświetl plik

@ -187,6 +187,7 @@
<string name="meshtastic_service_notifications">خدمة الإشعارات</string>
<string name="about">حول</string>
<string name="channel_invalid">This Channel URL is invalid and can not be used</string>
<string name="contact_invalid">This contact is invalid and can not be added</string>
<string name="debug_panel">Debug Panel</string>
<string name="debug_decoded_payload">Decoded Payload:</string>
<string name="debug_logs_export">Export Logs</string>
@ -794,7 +795,8 @@
<string name="no_pax_metrics_logs">No PAX metrics logs available.</string>
<string name="wifi_devices">WiFi Devices</string>
<string name="ble_devices">BLE Devices</string>
<string name="bluetooth_paired_devices">Paired Devices</string>
<string name="bluetooth_paired_devices">Paired devices</string>
<string name="bluetooth_available_devices">Available devices</string>
<string name="connected_device">Connected Device</string>
<string name="routing_error_rate_limit_exceeded">Rate Limit Exceeded. Please try again later.</string>
<string name="view_release">View Release</string>

Wyświetl plik

@ -187,6 +187,7 @@
<string name="meshtastic_service_notifications">Servisna obaveštenja</string>
<string name="about">O nama</string>
<string name="channel_invalid">Ovaj URL kanala je nevažeći i ne može se koristiti.</string>
<string name="contact_invalid">This contact is invalid and can not be added</string>
<string name="debug_panel">Panel za otklanjanje grešaka</string>
<string name="debug_decoded_payload">Decoded Payload:</string>
<string name="debug_logs_export">Export Logs</string>
@ -788,7 +789,8 @@
<string name="no_pax_metrics_logs">No PAX metrics logs available.</string>
<string name="wifi_devices">WiFi Devices</string>
<string name="ble_devices">BLE Devices</string>
<string name="bluetooth_paired_devices">Paired Devices</string>
<string name="bluetooth_paired_devices">Paired devices</string>
<string name="bluetooth_available_devices">Available devices</string>
<string name="connected_device">Connected Device</string>
<string name="routing_error_rate_limit_exceeded">Rate Limit Exceeded. Please try again later.</string>
<string name="view_release">View Release</string>

Wyświetl plik

@ -187,6 +187,7 @@
<string name="meshtastic_service_notifications">Сервизни известия</string>
<string name="about">Относно</string>
<string name="channel_invalid">URL адресът на този канал е невалиден и не може да се използва</string>
<string name="contact_invalid">This contact is invalid and can not be added</string>
<string name="debug_panel">Панел за отстраняване на грешки</string>
<string name="debug_decoded_payload">Decoded Payload:</string>
<string name="debug_logs_export">Експортиране на журнали</string>
@ -786,7 +787,8 @@
<string name="no_pax_metrics_logs">No PAX metrics logs available.</string>
<string name="wifi_devices">WiFi устройства</string>
<string name="ble_devices">BLE устройства</string>
<string name="bluetooth_paired_devices">Сдвоени устройства</string>
<string name="bluetooth_paired_devices">Paired devices</string>
<string name="bluetooth_available_devices">Available devices</string>
<string name="connected_device">Свързано устройство</string>
<string name="routing_error_rate_limit_exceeded">Rate Limit Exceeded. Please try again later.</string>
<string name="view_release">Преглед на изданието</string>

Wyświetl plik

@ -187,6 +187,7 @@
<string name="meshtastic_service_notifications">Notificacions de servei</string>
<string name="about">Sobre</string>
<string name="channel_invalid">La URL d\'aquest canal és invàlida i no es pot fer servir</string>
<string name="contact_invalid">This contact is invalid and can not be added</string>
<string name="debug_panel">Panell de depuració</string>
<string name="debug_decoded_payload">Decoded Payload:</string>
<string name="debug_logs_export">Export Logs</string>
@ -786,7 +787,8 @@
<string name="no_pax_metrics_logs">No PAX metrics logs available.</string>
<string name="wifi_devices">WiFi Devices</string>
<string name="ble_devices">BLE Devices</string>
<string name="bluetooth_paired_devices">Paired Devices</string>
<string name="bluetooth_paired_devices">Paired devices</string>
<string name="bluetooth_available_devices">Available devices</string>
<string name="connected_device">Connected Device</string>
<string name="routing_error_rate_limit_exceeded">Rate Limit Exceeded. Please try again later.</string>
<string name="view_release">View Release</string>

Wyświetl plik

@ -187,6 +187,7 @@
<string name="meshtastic_service_notifications">Servisní upozornění</string>
<string name="about">O aplikaci</string>
<string name="channel_invalid">Tato adresa URL kanálu je neplatná a nelze ji použít</string>
<string name="contact_invalid">This contact is invalid and can not be added</string>
<string name="debug_panel">Panel pro ladění</string>
<string name="debug_decoded_payload">Decoded Payload:</string>
<string name="debug_logs_export">Export Logs</string>
@ -790,7 +791,8 @@
<string name="no_pax_metrics_logs">No PAX metrics logs available.</string>
<string name="wifi_devices">WiFi Devices</string>
<string name="ble_devices">BLE Devices</string>
<string name="bluetooth_paired_devices">Spárovaná zařízení</string>
<string name="bluetooth_paired_devices">Paired devices</string>
<string name="bluetooth_available_devices">Available devices</string>
<string name="connected_device">Připojená zařízení</string>
<string name="routing_error_rate_limit_exceeded">Rate Limit Exceeded. Please try again later.</string>
<string name="view_release">View Release</string>

Wyświetl plik

@ -187,6 +187,7 @@
<string name="meshtastic_service_notifications">Dienstbenachrichtigungen</string>
<string name="about">Über</string>
<string name="channel_invalid">Diese Kanal-URL ist ungültig und kann nicht verwendet werden</string>
<string name="contact_invalid">This contact is invalid and can not be added</string>
<string name="debug_panel">Debug-Ausgaben</string>
<string name="debug_decoded_payload">Dekodiertes Payload:</string>
<string name="debug_logs_export">Protokolle exportieren</string>
@ -786,7 +787,8 @@
<string name="no_pax_metrics_logs">Keine Daten für den Besucherzähler verfügbar.</string>
<string name="wifi_devices">WLAN Geräte</string>
<string name="ble_devices">BLE Geräte</string>
<string name="bluetooth_paired_devices">Gekoppelte Geräte</string>
<string name="bluetooth_paired_devices">Paired devices</string>
<string name="bluetooth_available_devices">Available devices</string>
<string name="connected_device">Verbundene Geräte</string>
<string name="routing_error_rate_limit_exceeded">Sendebegrenzung überschritten. Bitte versuchen Sie es später erneut.</string>
<string name="view_release">Version ansehen</string>

Wyświetl plik

@ -187,6 +187,7 @@
<string name="meshtastic_service_notifications">Ειδοποιήσεις Υπηρεσίας</string>
<string name="about">Σχετικά</string>
<string name="channel_invalid">Αυτό το κανάλι URL δεν είναι ορθό και δεν μπορεί να χρησιμοποιηθεί</string>
<string name="contact_invalid">This contact is invalid and can not be added</string>
<string name="debug_panel">Πίνακας αποσφαλμάτωσης</string>
<string name="debug_decoded_payload">Decoded Payload:</string>
<string name="debug_logs_export">Export Logs</string>
@ -786,7 +787,8 @@
<string name="no_pax_metrics_logs">No PAX metrics logs available.</string>
<string name="wifi_devices">WiFi Devices</string>
<string name="ble_devices">BLE Devices</string>
<string name="bluetooth_paired_devices">Paired Devices</string>
<string name="bluetooth_paired_devices">Paired devices</string>
<string name="bluetooth_available_devices">Available devices</string>
<string name="connected_device">Connected Device</string>
<string name="routing_error_rate_limit_exceeded">Rate Limit Exceeded. Please try again later.</string>
<string name="view_release">View Release</string>

Wyświetl plik

@ -187,6 +187,7 @@
<string name="meshtastic_service_notifications">Notificaciones de servicio</string>
<string name="about">Acerca de</string>
<string name="channel_invalid">La URL de este canal no es válida y no puede utilizarse</string>
<string name="contact_invalid">This contact is invalid and can not be added</string>
<string name="debug_panel">Panel de depuración</string>
<string name="debug_decoded_payload">Decoded Payload:</string>
<string name="debug_logs_export">Exportar registros</string>
@ -788,7 +789,8 @@ Estos datos de ubicación pueden ser utilizados para fines como aparecer en un m
<string name="no_pax_metrics_logs">No PAX metrics logs available.</string>
<string name="wifi_devices">WiFi Devices</string>
<string name="ble_devices">BLE Devices</string>
<string name="bluetooth_paired_devices">Paired Devices</string>
<string name="bluetooth_paired_devices">Paired devices</string>
<string name="bluetooth_available_devices">Available devices</string>
<string name="connected_device">Connected Device</string>
<string name="routing_error_rate_limit_exceeded">Rate Limit Exceeded. Please try again later.</string>
<string name="view_release">View Release</string>

Wyświetl plik

@ -187,6 +187,7 @@
<string name="meshtastic_service_notifications">Teenuse teavitused</string>
<string name="about">Teave</string>
<string name="channel_invalid">Kanali URL on kehtetu ja seda ei saa kasutada</string>
<string name="contact_invalid">This contact is invalid and can not be added</string>
<string name="debug_panel">Arendaja paneel</string>
<string name="debug_decoded_payload">Dekodeeritud andmed:</string>
<string name="debug_logs_export">Salvesta logi</string>
@ -787,6 +788,7 @@
<string name="wifi_devices">WiFi seadmed</string>
<string name="ble_devices">Sinihamba seadmed</string>
<string name="bluetooth_paired_devices">Seotud seadmed</string>
<string name="bluetooth_available_devices">Saadaolevad seadmed</string>
<string name="connected_device">Ühendatud seadmed</string>
<string name="routing_error_rate_limit_exceeded">Limiit ületatud. Proovi hiljem uuesti.</string>
<string name="view_release">Näita versioon</string>
@ -887,5 +889,5 @@
<string name="last_heard_filter_label">Filtreeri viimase kuulmise aja järgi: %s</string>
<string name="dbm_value">%1$d dBm</string>
<string name="error_no_app_to_handle_link">Lingi haldamiseks pole rakendust saadaval.</string>
<string name="system_settings">System Settings</string>
<string name="system_settings">Süsteemi sätted</string>
</resources>

Wyświetl plik

@ -187,6 +187,7 @@
<string name="meshtastic_service_notifications">Palveluilmoitukset</string>
<string name="about">Tietoja</string>
<string name="channel_invalid">Kanavan URL-osoite on virheellinen, eikä sitä voida käyttää</string>
<string name="contact_invalid">This contact is invalid and can not be added</string>
<string name="debug_panel">Vianetsintäpaneeli</string>
<string name="debug_decoded_payload">Dekoodattu data:</string>
<string name="debug_logs_export">Vie lokitiedot</string>
@ -787,6 +788,7 @@
<string name="wifi_devices">WiFi-laitteet</string>
<string name="ble_devices">Bluetooth-laitteet</string>
<string name="bluetooth_paired_devices">Paritetut laitteet</string>
<string name="bluetooth_available_devices">Käytettävissä olevat laitteet</string>
<string name="connected_device">Yhdistetty laite</string>
<string name="routing_error_rate_limit_exceeded">Käyttöraja ylitetty. Yritä myöhemmin uudelleen.</string>
<string name="view_release">Näytä versio</string>

Wyświetl plik

@ -187,6 +187,7 @@
<string name="meshtastic_service_notifications">Notifications de service</string>
<string name="about">A propros</string>
<string name="channel_invalid">Cette URL de canal est invalide et ne peut pas être utilisée</string>
<string name="contact_invalid">This contact is invalid and can not be added</string>
<string name="debug_panel">Panneau de débogage</string>
<string name="debug_decoded_payload">Contenu décodé :</string>
<string name="debug_logs_export">Exporter les logs</string>
@ -786,7 +787,8 @@
<string name="no_pax_metrics_logs">Aucun journal de métriques PAX disponible.</string>
<string name="wifi_devices">Périphériques WiFi</string>
<string name="ble_devices">Appareils BLE</string>
<string name="bluetooth_paired_devices">Périphériques appairés</string>
<string name="bluetooth_paired_devices">Paired devices</string>
<string name="bluetooth_available_devices">Available devices</string>
<string name="connected_device">Périphérique connecté</string>
<string name="routing_error_rate_limit_exceeded">Limite de débit dépassée. Veuillez réessayer plus tard.</string>
<string name="view_release">Voir la version</string>

Wyświetl plik

@ -187,6 +187,7 @@
<string name="meshtastic_service_notifications">Fógraí seirbhíse</string>
<string name="about">Maidir le</string>
<string name="channel_invalid">Tá an URL Cainéil seo neamhdhleathach agus ní féidir é a úsáid</string>
<string name="contact_invalid">This contact is invalid and can not be added</string>
<string name="debug_panel">Painéal Laige</string>
<string name="debug_decoded_payload">Decoded Payload:</string>
<string name="debug_logs_export">Export Logs</string>
@ -792,7 +793,8 @@
<string name="no_pax_metrics_logs">No PAX metrics logs available.</string>
<string name="wifi_devices">WiFi Devices</string>
<string name="ble_devices">BLE Devices</string>
<string name="bluetooth_paired_devices">Paired Devices</string>
<string name="bluetooth_paired_devices">Paired devices</string>
<string name="bluetooth_available_devices">Available devices</string>
<string name="connected_device">Connected Device</string>
<string name="routing_error_rate_limit_exceeded">Rate Limit Exceeded. Please try again later.</string>
<string name="view_release">View Release</string>

Wyświetl plik

@ -187,6 +187,7 @@
<string name="meshtastic_service_notifications">Notificacións de servizo</string>
<string name="about">Acerca de</string>
<string name="channel_invalid">A ligazón desta canle non é válida e non pode usarse</string>
<string name="contact_invalid">This contact is invalid and can not be added</string>
<string name="debug_panel">Panel de depuración</string>
<string name="debug_decoded_payload">Decoded Payload:</string>
<string name="debug_logs_export">Export Logs</string>
@ -786,7 +787,8 @@
<string name="no_pax_metrics_logs">No PAX metrics logs available.</string>
<string name="wifi_devices">WiFi Devices</string>
<string name="ble_devices">BLE Devices</string>
<string name="bluetooth_paired_devices">Paired Devices</string>
<string name="bluetooth_paired_devices">Paired devices</string>
<string name="bluetooth_available_devices">Available devices</string>
<string name="connected_device">Connected Device</string>
<string name="routing_error_rate_limit_exceeded">Rate Limit Exceeded. Please try again later.</string>
<string name="view_release">View Release</string>

Wyświetl plik

@ -187,6 +187,7 @@
<string name="meshtastic_service_notifications">Servisne obavijesti</string>
<string name="about">O programu</string>
<string name="channel_invalid">Ovaj URL kanala je nevažeći i ne može se koristiti</string>
<string name="contact_invalid">This contact is invalid and can not be added</string>
<string name="debug_panel">Otklanjanje pogrešaka</string>
<string name="debug_decoded_payload">Decoded Payload:</string>
<string name="debug_logs_export">Export Logs</string>
@ -788,7 +789,8 @@
<string name="no_pax_metrics_logs">No PAX metrics logs available.</string>
<string name="wifi_devices">WiFi Devices</string>
<string name="ble_devices">BLE Devices</string>
<string name="bluetooth_paired_devices">Paired Devices</string>
<string name="bluetooth_paired_devices">Paired devices</string>
<string name="bluetooth_available_devices">Available devices</string>
<string name="connected_device">Connected Device</string>
<string name="routing_error_rate_limit_exceeded">Rate Limit Exceeded. Please try again later.</string>
<string name="view_release">View Release</string>

Wyświetl plik

@ -187,6 +187,7 @@
<string name="meshtastic_service_notifications">Notifikasyon sèvis</string>
<string name="about">Sou</string>
<string name="channel_invalid">Kanal URL sa a pa valab e yo pa kapab itilize li</string>
<string name="contact_invalid">This contact is invalid and can not be added</string>
<string name="debug_panel">Panno Debug</string>
<string name="debug_decoded_payload">Decoded Payload:</string>
<string name="debug_logs_export">Export Logs</string>
@ -786,7 +787,8 @@
<string name="no_pax_metrics_logs">No PAX metrics logs available.</string>
<string name="wifi_devices">WiFi Devices</string>
<string name="ble_devices">BLE Devices</string>
<string name="bluetooth_paired_devices">Paired Devices</string>
<string name="bluetooth_paired_devices">Paired devices</string>
<string name="bluetooth_available_devices">Available devices</string>
<string name="connected_device">Connected Device</string>
<string name="routing_error_rate_limit_exceeded">Rate Limit Exceeded. Please try again later.</string>
<string name="view_release">View Release</string>

Wyświetl plik

@ -187,6 +187,7 @@
<string name="meshtastic_service_notifications">Szolgáltatás értesítések</string>
<string name="about">A programról</string>
<string name="channel_invalid">Ez a csatorna URL érvénytelen, ezért nem használható.</string>
<string name="contact_invalid">This contact is invalid and can not be added</string>
<string name="debug_panel">Hibakereső panel</string>
<string name="debug_decoded_payload">Dekódolt adat:</string>
<string name="debug_logs_export">Naplók exportálása</string>
@ -786,7 +787,8 @@
<string name="no_pax_metrics_logs">Nem érhetők el PAX metrika naplók.</string>
<string name="wifi_devices">WiFi eszközök</string>
<string name="ble_devices">BLE eszközök</string>
<string name="bluetooth_paired_devices">Párosított eszközök</string>
<string name="bluetooth_paired_devices">Paired devices</string>
<string name="bluetooth_available_devices">Available devices</string>
<string name="connected_device">Csatlakoztatott eszköz</string>
<string name="routing_error_rate_limit_exceeded">Túllépted a sebességkorlátot. Próbáld újra később.</string>
<string name="view_release">Kiadás megtekintése</string>

Wyświetl plik

@ -187,6 +187,7 @@
<string name="meshtastic_service_notifications">Tilkynningar um þjónustu</string>
<string name="about">Um smáforrit</string>
<string name="channel_invalid">Þetta rásar URL er ógilt og ónothæft</string>
<string name="contact_invalid">This contact is invalid and can not be added</string>
<string name="debug_panel">Villuleitarborð</string>
<string name="debug_decoded_payload">Decoded Payload:</string>
<string name="debug_logs_export">Export Logs</string>
@ -786,7 +787,8 @@
<string name="no_pax_metrics_logs">No PAX metrics logs available.</string>
<string name="wifi_devices">WiFi Devices</string>
<string name="ble_devices">BLE Devices</string>
<string name="bluetooth_paired_devices">Paired Devices</string>
<string name="bluetooth_paired_devices">Paired devices</string>
<string name="bluetooth_available_devices">Available devices</string>
<string name="connected_device">Connected Device</string>
<string name="routing_error_rate_limit_exceeded">Rate Limit Exceeded. Please try again later.</string>
<string name="view_release">View Release</string>

Wyświetl plik

@ -187,6 +187,7 @@
<string name="meshtastic_service_notifications">Notifiche di servizio</string>
<string name="about">Informazioni</string>
<string name="channel_invalid">L\'URL di questo Canale non è valida e non può essere usata</string>
<string name="contact_invalid">This contact is invalid and can not be added</string>
<string name="debug_panel">Pannello Di Debug</string>
<string name="debug_decoded_payload">Payload decodificato:</string>
<string name="debug_logs_export">Esporta i logs</string>
@ -786,7 +787,8 @@
<string name="no_pax_metrics_logs">Nessun log delle metriche PAX disponibile.</string>
<string name="wifi_devices">Dispositivi WiFi</string>
<string name="ble_devices">Dispositivi BLE</string>
<string name="bluetooth_paired_devices">Dispositivi associati</string>
<string name="bluetooth_paired_devices">Paired devices</string>
<string name="bluetooth_available_devices">Available devices</string>
<string name="connected_device">Dispositivo connesso</string>
<string name="routing_error_rate_limit_exceeded">Limite di trasmissione superato. Riprova più tardi</string>
<string name="view_release">Visualizza Release</string>

Wyświetl plik

@ -187,6 +187,7 @@
<string name="meshtastic_service_notifications">התראות שירות</string>
<string name="about">אודות</string>
<string name="channel_invalid">כתובת ערוץ זה אינו תקין ולא ניתן לעשות בו שימוש</string>
<string name="contact_invalid">This contact is invalid and can not be added</string>
<string name="debug_panel">פאנל דיבאג</string>
<string name="debug_decoded_payload">Decoded Payload:</string>
<string name="debug_logs_export">Export Logs</string>
@ -790,7 +791,8 @@
<string name="no_pax_metrics_logs">No PAX metrics logs available.</string>
<string name="wifi_devices">WiFi Devices</string>
<string name="ble_devices">BLE Devices</string>
<string name="bluetooth_paired_devices">Paired Devices</string>
<string name="bluetooth_paired_devices">Paired devices</string>
<string name="bluetooth_available_devices">Available devices</string>
<string name="connected_device">Connected Device</string>
<string name="routing_error_rate_limit_exceeded">Rate Limit Exceeded. Please try again later.</string>
<string name="view_release">View Release</string>

Wyświetl plik

@ -188,6 +188,7 @@
<string name="meshtastic_service_notifications">通知サービス</string>
<string name="about">概要</string>
<string name="channel_invalid">このチャンネルURLは無効なため使用できません。</string>
<string name="contact_invalid">This contact is invalid and can not be added</string>
<string name="debug_panel">デバッグ</string>
<string name="debug_decoded_payload">Decoded Payload:</string>
<string name="debug_logs_export">Export Logs</string>
@ -785,7 +786,8 @@
<string name="no_pax_metrics_logs">No PAX metrics logs available.</string>
<string name="wifi_devices">WiFi Devices</string>
<string name="ble_devices">BLE Devices</string>
<string name="bluetooth_paired_devices">Paired Devices</string>
<string name="bluetooth_paired_devices">Paired devices</string>
<string name="bluetooth_available_devices">Available devices</string>
<string name="connected_device">Connected Device</string>
<string name="routing_error_rate_limit_exceeded">Rate Limit Exceeded. Please try again later.</string>
<string name="view_release">View Release</string>

Wyświetl plik

@ -187,6 +187,7 @@
<string name="meshtastic_service_notifications">서비스 알림</string>
<string name="about">앱에 대하여</string>
<string name="channel_invalid">이 채널 URL은 유효하지 않으며 사용할 수 없습니다.</string>
<string name="contact_invalid">This contact is invalid and can not be added</string>
<string name="debug_panel">디버그 패널</string>
<string name="debug_decoded_payload">Decoded Payload:</string>
<string name="debug_logs_export">로그 내보내기</string>
@ -784,7 +785,8 @@
<string name="no_pax_metrics_logs">No PAX metrics logs available.</string>
<string name="wifi_devices">WiFi Devices</string>
<string name="ble_devices">BLE Devices</string>
<string name="bluetooth_paired_devices">Paired Devices</string>
<string name="bluetooth_paired_devices">Paired devices</string>
<string name="bluetooth_available_devices">Available devices</string>
<string name="connected_device">Connected Device</string>
<string name="routing_error_rate_limit_exceeded">Rate Limit Exceeded. Please try again later.</string>
<string name="view_release">View Release</string>

Wyświetl plik

@ -187,6 +187,7 @@
<string name="meshtastic_service_notifications">Paslaugos pranešimai</string>
<string name="about">Apie</string>
<string name="channel_invalid">Šio kanalo URL yra neteisingas ir negali būti naudojamas</string>
<string name="contact_invalid">This contact is invalid and can not be added</string>
<string name="debug_panel">Derinimo skydelis</string>
<string name="debug_decoded_payload">Decoded Payload:</string>
<string name="debug_logs_export">Export Logs</string>
@ -790,7 +791,8 @@
<string name="no_pax_metrics_logs">No PAX metrics logs available.</string>
<string name="wifi_devices">WiFi Devices</string>
<string name="ble_devices">BLE Devices</string>
<string name="bluetooth_paired_devices">Paired Devices</string>
<string name="bluetooth_paired_devices">Paired devices</string>
<string name="bluetooth_available_devices">Available devices</string>
<string name="connected_device">Connected Device</string>
<string name="routing_error_rate_limit_exceeded">Rate Limit Exceeded. Please try again later.</string>
<string name="view_release">View Release</string>

Wyświetl plik

@ -187,6 +187,7 @@
<string name="meshtastic_service_notifications">Servicemeldingen</string>
<string name="about">Over</string>
<string name="channel_invalid">Deze Kanaal URL is ongeldig en kan niet worden gebruikt</string>
<string name="contact_invalid">This contact is invalid and can not be added</string>
<string name="debug_panel">Debug-paneel</string>
<string name="debug_decoded_payload">Decoded Payload:</string>
<string name="debug_logs_export">Export Logs</string>
@ -786,7 +787,8 @@
<string name="no_pax_metrics_logs">No PAX metrics logs available.</string>
<string name="wifi_devices">WiFi Devices</string>
<string name="ble_devices">BLE Devices</string>
<string name="bluetooth_paired_devices">Paired Devices</string>
<string name="bluetooth_paired_devices">Paired devices</string>
<string name="bluetooth_available_devices">Available devices</string>
<string name="connected_device">Connected Device</string>
<string name="routing_error_rate_limit_exceeded">Rate Limit Exceeded. Please try again later.</string>
<string name="view_release">View Release</string>

Wyświetl plik

@ -187,6 +187,7 @@
<string name="meshtastic_service_notifications">Tjeneste meldinger</string>
<string name="about">Om</string>
<string name="channel_invalid">Denne kanall URL er ugyldig og kan ikke benyttes</string>
<string name="contact_invalid">This contact is invalid and can not be added</string>
<string name="debug_panel">Feilsøkningspanel</string>
<string name="debug_decoded_payload">Decoded Payload:</string>
<string name="debug_logs_export">Export Logs</string>
@ -786,7 +787,8 @@
<string name="no_pax_metrics_logs">No PAX metrics logs available.</string>
<string name="wifi_devices">WiFi Devices</string>
<string name="ble_devices">BLE Devices</string>
<string name="bluetooth_paired_devices">Paired Devices</string>
<string name="bluetooth_paired_devices">Paired devices</string>
<string name="bluetooth_available_devices">Available devices</string>
<string name="connected_device">Connected Device</string>
<string name="routing_error_rate_limit_exceeded">Rate Limit Exceeded. Please try again later.</string>
<string name="view_release">View Release</string>

Wyświetl plik

@ -187,6 +187,7 @@
<string name="meshtastic_service_notifications">Powiadomienia o usługach</string>
<string name="about">O aplikacji</string>
<string name="channel_invalid">Ten adres URL kanału jest nieprawidłowy i nie można go użyć</string>
<string name="contact_invalid">This contact is invalid and can not be added</string>
<string name="debug_panel">Panel debugowania</string>
<string name="debug_decoded_payload">Decoded Payload:</string>
<string name="debug_logs_export">Export Logs</string>
@ -790,7 +791,8 @@
<string name="no_pax_metrics_logs">No PAX metrics logs available.</string>
<string name="wifi_devices">WiFi Devices</string>
<string name="ble_devices">BLE Devices</string>
<string name="bluetooth_paired_devices">Paired Devices</string>
<string name="bluetooth_paired_devices">Paired devices</string>
<string name="bluetooth_available_devices">Available devices</string>
<string name="connected_device">Connected Device</string>
<string name="routing_error_rate_limit_exceeded">Rate Limit Exceeded. Please try again later.</string>
<string name="view_release">View Release</string>

Wyświetl plik

@ -187,6 +187,7 @@
<string name="meshtastic_service_notifications">Notificações de serviço</string>
<string name="about">Sobre</string>
<string name="channel_invalid">Este link de canal é inválido e não pode ser usado</string>
<string name="contact_invalid">This contact is invalid and can not be added</string>
<string name="debug_panel">Painel de depuração</string>
<string name="debug_decoded_payload">Pacote Decodificado:</string>
<string name="debug_logs_export">Exportar Logs</string>
@ -786,7 +787,8 @@
<string name="no_pax_metrics_logs">Não há logs de métricas de PAX disponíveis.</string>
<string name="wifi_devices">Dispositivos WiFi</string>
<string name="ble_devices">Dispositivos BLE</string>
<string name="bluetooth_paired_devices">Dispositivos Pareados</string>
<string name="bluetooth_paired_devices">Paired devices</string>
<string name="bluetooth_available_devices">Available devices</string>
<string name="connected_device">Dispositivo Conectado</string>
<string name="routing_error_rate_limit_exceeded">Limite excedido. Por favor, tente novamente mais tarde.</string>
<string name="view_release">Ver Lançamento</string>

Wyświetl plik

@ -187,6 +187,7 @@
<string name="meshtastic_service_notifications">Notificações de serviço</string>
<string name="about">Sobre</string>
<string name="channel_invalid">O Link Deste Canal é inválido e não pode ser usado</string>
<string name="contact_invalid">This contact is invalid and can not be added</string>
<string name="debug_panel">Painel de depuração</string>
<string name="debug_decoded_payload">Decoded Payload:</string>
<string name="debug_logs_export">Export Logs</string>
@ -786,7 +787,8 @@
<string name="no_pax_metrics_logs">No PAX metrics logs available.</string>
<string name="wifi_devices">WiFi Devices</string>
<string name="ble_devices">BLE Devices</string>
<string name="bluetooth_paired_devices">Paired Devices</string>
<string name="bluetooth_paired_devices">Paired devices</string>
<string name="bluetooth_available_devices">Available devices</string>
<string name="connected_device">Connected Device</string>
<string name="routing_error_rate_limit_exceeded">Rate Limit Exceeded. Please try again later.</string>
<string name="view_release">View Release</string>

Wyświetl plik

@ -187,6 +187,7 @@
<string name="meshtastic_service_notifications">Notificările serviciului</string>
<string name="about">Despre</string>
<string name="channel_invalid">Acest URL de canal este invalid și nu poate fi folosit</string>
<string name="contact_invalid">This contact is invalid and can not be added</string>
<string name="debug_panel">Panou debug</string>
<string name="debug_decoded_payload">Decoded Payload:</string>
<string name="debug_logs_export">Export Logs</string>
@ -788,7 +789,8 @@
<string name="no_pax_metrics_logs">No PAX metrics logs available.</string>
<string name="wifi_devices">WiFi Devices</string>
<string name="ble_devices">BLE Devices</string>
<string name="bluetooth_paired_devices">Paired Devices</string>
<string name="bluetooth_paired_devices">Paired devices</string>
<string name="bluetooth_available_devices">Available devices</string>
<string name="connected_device">Connected Device</string>
<string name="routing_error_rate_limit_exceeded">Rate Limit Exceeded. Please try again later.</string>
<string name="view_release">View Release</string>

Wyświetl plik

@ -187,6 +187,7 @@
<string name="meshtastic_service_notifications">Служебные уведомления</string>
<string name="about">О приложении</string>
<string name="channel_invalid">Этот URL-адрес канала недействителен и не может быть использован</string>
<string name="contact_invalid">This contact is invalid and can not be added</string>
<string name="debug_panel">Панель отладки</string>
<string name="debug_decoded_payload">Декодированная нагрузка:</string>
<string name="debug_logs_export">Экспортировать логи</string>
@ -790,7 +791,8 @@
<string name="no_pax_metrics_logs">Нет доступных журналов метрики пассажиров.</string>
<string name="wifi_devices">WiFi устройства</string>
<string name="ble_devices">Устройства BLE</string>
<string name="bluetooth_paired_devices">Сопряженные устройства</string>
<string name="bluetooth_paired_devices">Paired devices</string>
<string name="bluetooth_available_devices">Available devices</string>
<string name="connected_device">Подключённые устройства</string>
<string name="routing_error_rate_limit_exceeded">Превышен лимит запросов. Пожалуйста, повторите попытку позже.</string>
<string name="view_release">Просмотреть релиз</string>

Wyświetl plik

@ -187,6 +187,7 @@
<string name="meshtastic_service_notifications">Notifikácie zo služby</string>
<string name="about">O aplikácii</string>
<string name="channel_invalid">URL adresa tohoto kanála nie je platná a nedá sa použiť</string>
<string name="contact_invalid">This contact is invalid and can not be added</string>
<string name="debug_panel">Debug okno</string>
<string name="debug_decoded_payload">Decoded Payload:</string>
<string name="debug_logs_export">Export Logs</string>
@ -790,7 +791,8 @@
<string name="no_pax_metrics_logs">No PAX metrics logs available.</string>
<string name="wifi_devices">WiFi Devices</string>
<string name="ble_devices">BLE Devices</string>
<string name="bluetooth_paired_devices">Paired Devices</string>
<string name="bluetooth_paired_devices">Paired devices</string>
<string name="bluetooth_available_devices">Available devices</string>
<string name="connected_device">Connected Device</string>
<string name="routing_error_rate_limit_exceeded">Rate Limit Exceeded. Please try again later.</string>
<string name="view_release">View Release</string>

Wyświetl plik

@ -187,6 +187,7 @@
<string name="meshtastic_service_notifications">Obvestila storitve</string>
<string name="about">O programu</string>
<string name="channel_invalid">Neveljaven kanal</string>
<string name="contact_invalid">This contact is invalid and can not be added</string>
<string name="debug_panel">Plošča za odpravljanje napak</string>
<string name="debug_decoded_payload">Decoded Payload:</string>
<string name="debug_logs_export">Export Logs</string>
@ -790,7 +791,8 @@
<string name="no_pax_metrics_logs">No PAX metrics logs available.</string>
<string name="wifi_devices">WiFi Devices</string>
<string name="ble_devices">BLE Devices</string>
<string name="bluetooth_paired_devices">Paired Devices</string>
<string name="bluetooth_paired_devices">Paired devices</string>
<string name="bluetooth_available_devices">Available devices</string>
<string name="connected_device">Connected Device</string>
<string name="routing_error_rate_limit_exceeded">Rate Limit Exceeded. Please try again later.</string>
<string name="view_release">View Release</string>

Wyświetl plik

@ -187,6 +187,7 @@
<string name="meshtastic_service_notifications">Njoftime shërbimi</string>
<string name="about">Rreth</string>
<string name="channel_invalid">Ky URL kanal është i pavlefshëm dhe nuk mund të përdoret</string>
<string name="contact_invalid">This contact is invalid and can not be added</string>
<string name="debug_panel">Paneli i debug-ut</string>
<string name="debug_decoded_payload">Decoded Payload:</string>
<string name="debug_logs_export">Export Logs</string>
@ -786,7 +787,8 @@
<string name="no_pax_metrics_logs">No PAX metrics logs available.</string>
<string name="wifi_devices">WiFi Devices</string>
<string name="ble_devices">BLE Devices</string>
<string name="bluetooth_paired_devices">Paired Devices</string>
<string name="bluetooth_paired_devices">Paired devices</string>
<string name="bluetooth_available_devices">Available devices</string>
<string name="connected_device">Connected Device</string>
<string name="routing_error_rate_limit_exceeded">Rate Limit Exceeded. Please try again later.</string>
<string name="view_release">View Release</string>

Wyświetl plik

@ -187,6 +187,7 @@
<string name="meshtastic_service_notifications">Обавештења о услугама</string>
<string name="about">О</string>
<string name="channel_invalid">Ова URL адреса канала је неважећа и не може се користити</string>
<string name="contact_invalid">This contact is invalid and can not be added</string>
<string name="debug_panel">Панел за отклањање грешака</string>
<string name="debug_decoded_payload">Decoded Payload:</string>
<string name="debug_logs_export">Export Logs</string>
@ -788,7 +789,8 @@
<string name="no_pax_metrics_logs">No PAX metrics logs available.</string>
<string name="wifi_devices">WiFi Devices</string>
<string name="ble_devices">BLE Devices</string>
<string name="bluetooth_paired_devices">Paired Devices</string>
<string name="bluetooth_paired_devices">Paired devices</string>
<string name="bluetooth_available_devices">Available devices</string>
<string name="connected_device">Connected Device</string>
<string name="routing_error_rate_limit_exceeded">Rate Limit Exceeded. Please try again later.</string>
<string name="view_release">View Release</string>

Wyświetl plik

@ -187,6 +187,7 @@
<string name="meshtastic_service_notifications">Tjänsteaviseringar</string>
<string name="about">Om</string>
<string name="channel_invalid">Denna kanal-URL är ogiltig och kan inte användas</string>
<string name="contact_invalid">This contact is invalid and can not be added</string>
<string name="debug_panel">Felsökningspanel</string>
<string name="debug_decoded_payload">Decoded Payload:</string>
<string name="debug_logs_export">Export Logs</string>
@ -786,7 +787,8 @@
<string name="no_pax_metrics_logs">No PAX metrics logs available.</string>
<string name="wifi_devices">WiFi Devices</string>
<string name="ble_devices">BLE Devices</string>
<string name="bluetooth_paired_devices">Paired Devices</string>
<string name="bluetooth_paired_devices">Paired devices</string>
<string name="bluetooth_available_devices">Available devices</string>
<string name="connected_device">Connected Device</string>
<string name="routing_error_rate_limit_exceeded">Rate Limit Exceeded. Please try again later.</string>
<string name="view_release">View Release</string>

Wyświetl plik

@ -187,6 +187,7 @@
<string name="meshtastic_service_notifications">Servis bildirimleri</string>
<string name="about">Hakkında</string>
<string name="channel_invalid">Bu Kanal URL\' si geçersiz ve kullanılamaz</string>
<string name="contact_invalid">This contact is invalid and can not be added</string>
<string name="debug_panel">Hata Ayıklama Paneli</string>
<string name="debug_decoded_payload">Decoded Payload:</string>
<string name="debug_logs_export">Export Logs</string>
@ -786,7 +787,8 @@
<string name="no_pax_metrics_logs">No PAX metrics logs available.</string>
<string name="wifi_devices">WiFi Devices</string>
<string name="ble_devices">BLE Devices</string>
<string name="bluetooth_paired_devices">Paired Devices</string>
<string name="bluetooth_paired_devices">Paired devices</string>
<string name="bluetooth_available_devices">Available devices</string>
<string name="connected_device">Connected Device</string>
<string name="routing_error_rate_limit_exceeded">Rate Limit Exceeded. Please try again later.</string>
<string name="view_release">View Release</string>

Wyświetl plik

@ -187,6 +187,7 @@
<string name="meshtastic_service_notifications">Сервісні сповіщення</string>
<string name="about">Про</string>
<string name="channel_invalid">URL-адреса цього каналу недійсна та не може бути використана</string>
<string name="contact_invalid">This contact is invalid and can not be added</string>
<string name="debug_panel">Панель налагодження</string>
<string name="debug_decoded_payload">Decoded Payload:</string>
<string name="debug_logs_export">Експортувати журнали</string>
@ -790,7 +791,8 @@
<string name="no_pax_metrics_logs">No PAX metrics logs available.</string>
<string name="wifi_devices">WiFi Devices</string>
<string name="ble_devices">BLE Devices</string>
<string name="bluetooth_paired_devices">Paired Devices</string>
<string name="bluetooth_paired_devices">Paired devices</string>
<string name="bluetooth_available_devices">Available devices</string>
<string name="connected_device">Connected Device</string>
<string name="routing_error_rate_limit_exceeded">Rate Limit Exceeded. Please try again later.</string>
<string name="view_release">View Release</string>

Wyświetl plik

@ -187,6 +187,7 @@
<string name="meshtastic_service_notifications">服务通知</string>
<string name="about">关于</string>
<string name="channel_invalid">此频道 URL 无效,无法使用</string>
<string name="contact_invalid">This contact is invalid and can not be added</string>
<string name="debug_panel">调试面板</string>
<string name="debug_decoded_payload">解码Payload</string>
<string name="debug_logs_export">导出程序日志</string>
@ -786,7 +787,8 @@
<string name="no_pax_metrics_logs">无可用的 PAX 计量日志。</string>
<string name="wifi_devices">WiFi 设备</string>
<string name="ble_devices">BLE 设备</string>
<string name="bluetooth_paired_devices">Paired Devices</string>
<string name="bluetooth_paired_devices">Paired devices</string>
<string name="bluetooth_available_devices">Available devices</string>
<string name="connected_device">Connected Device</string>
<string name="routing_error_rate_limit_exceeded">超过速率限制。请稍后再试。</string>
<string name="view_release">查看发行版</string>

Wyświetl plik

@ -187,6 +187,7 @@
<string name="meshtastic_service_notifications">服務通知</string>
<string name="about">關於</string>
<string name="channel_invalid">此頻道 URL 無效,無法使用</string>
<string name="contact_invalid">This contact is invalid and can not be added</string>
<string name="debug_panel">除錯面板</string>
<string name="debug_decoded_payload">解析封包:</string>
<string name="debug_logs_export">匯出日誌</string>
@ -784,7 +785,8 @@
<string name="no_pax_metrics_logs">沒有可用的 PAX 指標日誌。</string>
<string name="wifi_devices">WiFi 裝置</string>
<string name="ble_devices">藍牙裝置</string>
<string name="bluetooth_paired_devices">配對裝置</string>
<string name="bluetooth_paired_devices">Paired devices</string>
<string name="bluetooth_available_devices">Available devices</string>
<string name="connected_device">連接裝置</string>
<string name="routing_error_rate_limit_exceeded">超過速率限制,請稍後再嘗試。</string>
<string name="view_release">查看版本資訊</string>

Wyświetl plik

@ -214,6 +214,7 @@
<string name="meshtastic_service_notifications">Service notifications</string>
<string name="about">About</string>
<string name="channel_invalid">This Channel URL is invalid and can not be used</string>
<string name="contact_invalid">This contact is invalid and can not be added</string>
<string name="debug_panel">Debug Panel</string>
<string name="debug_decoded_payload">Decoded Payload:</string>
<string name="debug_logs_export">Export Logs</string>
@ -354,9 +355,9 @@
<string name="air_util_definition">Percent of airtime for transmission used within the last hour.</string>
<string name="iaq">IAQ</string>
<string name="encryption_psk">Shared Key</string>
<string name="encryption_psk_text">Direct messages are using the shared key for the channel.</string>
<string name="encryption_psk_text">Only channel messages can be sent/received. Direct Messages require the Public Key Infrastructure feature in 2.5+ firmware.</string>
<string name="encryption_pkc">Public Key Encryption</string>
<string name="encryption_pkc_text">Direct messages are using the new public key infrastructure for encryption. Requires firmware version 2.5 or greater.</string>
<string name="encryption_pkc_text">Direct messages are using the new public key infrastructure for encryption.</string>
<string name="encryption_error">Public key mismatch</string>
<string name="encryption_error_text">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 serious 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.</string>
<string name="exchange_userinfo">Exchange user info</string>
@ -821,7 +822,8 @@
<string name="no_pax_metrics_logs">No PAX metrics logs available.</string>
<string name="wifi_devices">WiFi Devices</string>
<string name="ble_devices">BLE Devices</string>
<string name="bluetooth_paired_devices">Paired Devices</string>
<string name="bluetooth_paired_devices">Paired devices</string>
<string name="bluetooth_available_devices">Available devices</string>
<string name="connected_device">Connected Device</string>
<string name="routing_error_rate_limit_exceeded">Rate Limit Exceeded. Please try again later.</string>

Wyświetl plik

@ -23,17 +23,25 @@ platform :android do
desc "Deploy a new version to the internal track on Google Play"
lane :internal do
aab_path = build_google_release
upload_to_play_store(
track: 'internal',
aab: aab_path,
release_status: 'completed',
skip_upload_apk: true,
skip_upload_metadata: true,
skip_upload_changelogs: true,
skip_upload_images: true,
skip_upload_screenshots: true,
)
begin
aab_path = build_google_release
upload_to_play_store(
track: 'internal',
aab: aab_path,
release_status: 'completed',
skip_upload_apk: true,
skip_upload_metadata: true,
skip_upload_changelogs: true,
skip_upload_images: true,
skip_upload_screenshots: true,
)
rescue => exception
if exception.message.include?("Google Api Error: forbidden: A release with version code") && exception.message.include?("already exists for this track")
UI.message("This version code is already on the internal track. No action needed.")
else
raise exception
end
end
end
desc "Promote from internal track to the closed track on Google Play"
@ -41,6 +49,7 @@ platform :android do
upload_to_play_store(
track: 'internal',
track_promote_to: 'NewAlpha',
version_codes: [ENV["VERSION_CODE"].to_i],
release_status: 'completed',
skip_upload_apk: true,
skip_upload_metadata: true,
@ -50,11 +59,12 @@ platform :android do
)
end
desc "Promote from closed track to the open track on Google Play"
desc "Promote from internal track to the open track on Google Play"
lane :open do
upload_to_play_store(
track: 'NewAlpha',
track: 'internal',
track_promote_to: 'beta',
version_codes: [ENV["VERSION_CODE"].to_i],
release_status: 'draft',
skip_upload_apk: true,
skip_upload_metadata: true,
@ -64,11 +74,12 @@ platform :android do
)
end
desc "Promote from open track to the production track on Google Play"
desc "Promote from internal track to the production track on Google Play"
lane :production do
upload_to_play_store(
track: 'open',
track: 'internal',
track_promote_to: 'production',
version_codes: [ENV["VERSION_CODE"].to_i],
release_status: 'draft',
skip_upload_apk: true,
skip_upload_metadata: true,
@ -89,6 +100,35 @@ platform :android do
)
end
desc "Get the highest version code from all Google Play tracks"
lane :get_highest_version_code do
require 'set'
all_codes = Set.new
tracks = ['internal', 'closed', 'open', 'production']
tracks.each do |track|
begin
codes = google_play_track_version_codes(track: track, package_name: 'com.geeksville.mesh')
all_codes.merge(codes.map(&:to_i))
rescue => e
UI.message("Could not fetch version codes for track #{track}: #{e.message}")
end
end
highest = all_codes.max || 0
UI.message("Highest version code on Google Play: #{highest}")
File.write('highest_version_code.txt', highest.to_s)
end
desc "Get the version code currently on the internal track (max if multiple)"
lane :get_internal_track_version_code do
codes = google_play_track_version_codes(track: 'internal', package_name: 'com.geeksville.mesh')
if codes.nil? || codes.empty?
UI.user_error!("No version codes found on internal track. Ensure an internal build has been published before promoting.")
end
max_code = codes.map(&:to_i).max
UI.message("Internal track version code: #{max_code}")
File.write('internal_version_code.txt', max_code.to_s)
end
private_lane :build_google_release do
gradle(
task: "clean bundleGoogleRelease assembleGoogleRelease",

Wyświetl plik

@ -14,6 +14,5 @@
<ID>ParameterNaming:NodeFilterTextField.kt$onToggleShowIgnored</ID>
<ID>PreviewPublic:NodeItem.kt$NodeInfoPreview</ID>
<ID>PreviewPublic:NodeItem.kt$NodeInfoSimplePreview</ID>
<ID>TooManyFunctions:NodeListViewModel.kt$NodeListViewModel : ViewModel</ID>
</CurrentIssues>
</SmellBaseline>

Wyświetl plik

@ -161,9 +161,6 @@ constructor(
uiPreferencesDataSource.setNodeSort(sort.ordinal)
}
fun addSharedContact(sharedContact: AdminProtos.SharedContact) =
viewModelScope.launch { serviceRepository.onServiceAction(ServiceAction.ImportContact(sharedContact)) }
fun setSharedContactRequested(sharedContact: AdminProtos.SharedContact?) {
_sharedContactRequested.value = sharedContact
}