Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
50 commits
Select commit Hold shift + click to select a range
29a15e4
Add a map provider abstraction and an AMap implementation
SadGare Aug 8, 2026
962ee6a
Update FindMy backend to 0.9.x, stub unicorn, add locales and startup…
SadGare Aug 8, 2026
376d0ab
Export Room schemas and add v1 to v2 migration tests
parawanderer Aug 8, 2026
ebec689
Add offline harness to measure first-fetch key-search cost
parawanderer Aug 8, 2026
f03abdb
Give debug builds their own applicationId
parawanderer Aug 8, 2026
e0096ef
Label debug builds distinctly on the launcher
parawanderer Aug 8, 2026
7314bc4
Run instrumented tests in CI
parawanderer Aug 8, 2026
7509d2c
Make the accessory_json backfill testable and cover it
parawanderer Aug 8, 2026
2e28a45
Probe for alignment before wide key searches, and cover main.py
parawanderer Aug 8, 2026
b9d90c9
Document every test suite and run the missing one in CI
parawanderer Aug 8, 2026
698119e
Fix package assertion for the suffixed debug applicationId, drop stal…
parawanderer Aug 8, 2026
322fd41
Fail fast on unsupported Anisette providers, keep re-login on server …
parawanderer Aug 8, 2026
9c6b2ec
Ship the unicorn stub as source, fix a Windows build hang, add test w…
parawanderer Aug 8, 2026
c7b3b87
AMap: require a user-supplied API key instead of bundling one
parawanderer Aug 8, 2026
25af21e
Link the AMap setup guide from the API key dialog
parawanderer Aug 8, 2026
54506ed
Gate AMap selection on the first-run screen too
parawanderer Aug 8, 2026
5a43e1d
Export KeyAlignmentRecords from the macOS wizard
parawanderer Aug 8, 2026
2157735
Import KeyAlignmentRecords so the first fetch is not a full history s…
parawanderer Aug 8, 2026
c829a68
Fix map buttons jumping into the status bar when there are no tags
parawanderer Aug 8, 2026
d8e9faf
Show Chinese language options in the same style as every other language
parawanderer Aug 8, 2026
055a17c
Stop a wide key search costing double, and serialise calls into Python
parawanderer Aug 8, 2026
86ddc3e
Give debug builds an inverted launcher icon
parawanderer Aug 8, 2026
bab3ce7
Fix the map buttons properly, and actually invert the debug icon
parawanderer Aug 8, 2026
9b75fea
Capitalise the password field label
parawanderer Aug 8, 2026
01d88fd
Fix the tag cards and keep them clear of the navigation bar
parawanderer Aug 9, 2026
b9f5985
Fetch one accessory at a time and stop refreshes queueing up
parawanderer Aug 9, 2026
a268a12
Actually import KeyAlignmentRecords, and test that it happens
parawanderer Aug 9, 2026
4ab6864
Run instrumented tests on a Gradle managed device
parawanderer Aug 9, 2026
34f3937
Credit contributors on the Information page
parawanderer Aug 9, 2026
31efc13
Give the device list an empty state and long-press to remove
parawanderer Aug 9, 2026
8999c82
Add string tooling, a pre-commit hook, and CI checks for both
parawanderer Aug 9, 2026
b2bcd5f
Fix the two things CI found on its first ever run
parawanderer Aug 9, 2026
93d51ef
Recommend installing gh so an agent can read CI instead of guessing
parawanderer Aug 9, 2026
b54f755
Install Pillow before pyright so it checks the icon script
parawanderer Aug 9, 2026
8711d49
Document running the export wizard from a bare macOS install
parawanderer Aug 9, 2026
ac417d9
Add a macOS bootstrap script for the export wizard
parawanderer Aug 9, 2026
e498376
Make the wizard run on the Python macOS actually ships
parawanderer Aug 9, 2026
4224d77
Fall back to the CLI when Tk cannot start, and document it
parawanderer Aug 9, 2026
0330c15
Pick an interpreter whose Tk actually works, and scope the 3.9 claim
parawanderer Aug 9, 2026
ec2efbb
Keep the branch when telling the user to re-run, and report the real one
parawanderer Aug 9, 2026
11daa78
Stop the exporter version drifting from its release tag
parawanderer Aug 9, 2026
463d368
Stop the import cancelling every accessory after the first
parawanderer Aug 9, 2026
eff937d
Pull the map's decision logic out of the Activity and test it
parawanderer Aug 9, 2026
ae48f12
Restore the selected marker being raised, and re-enable the periodic …
parawanderer Aug 9, 2026
5bd3de1
Raise the marker on a marker tap too, and cover the focus rules
parawanderer Aug 9, 2026
92c8da3
Measure the tag cards so the row cannot go ragged again
parawanderer Aug 9, 2026
969fce2
Fetch history for the range that was actually asked for
parawanderer Aug 9, 2026
1d400ee
Stop a timed-out request looking like a day with no reports
parawanderer Aug 9, 2026
ca2eefa
Let a failed fetch reach the Retry button that already exists
parawanderer Aug 9, 2026
c388b8d
Say in the README that the project has tests
parawanderer Aug 9, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
97 changes: 97 additions & 0 deletions .claude/skills/add-strings/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
---
name: add-strings
description: Add or back-fill user-facing Android strings across all locales in OpenTagViewer. Use whenever adding, renaming or translating any string that appears in the UI, or when checking translations are complete before a PR.
---

# Adding user-facing strings

The app ships ten locales. A string missing from one silently falls back to English — nothing
fails, and it looks correct in whichever language you happen to read. So every UI string has
to land in every locale in one go.

## The rule that matters

**Never pass translations through a shell argument.** Write a JSON file with the Write tool,
then point the script at it. Passing non-ASCII text through `bash -c`, `sed` or a here-doc has
corrupted it twice in this repo — a French apostrophe reached the screen as a literal
`\’`. The Write tool does not go through shell quoting; a `for` loop does.

For the same reason, do not hand-edit ten `strings.xml` files with ten Edit calls unless the
script cannot do what you need. It is slower and it is what the script exists to replace.

## Workflow

1. Find out which locales are required — do not assume, they are discovered from the tree:

```bash
python scripts/add_strings.py --locales
```

2. Write the JSON with the **Write** tool, one entry per string, every locale present:

```json
{
"how_do_i_get_the_zip": {
"default": "How do I get the zip?",
"en": "How do I get the zip?",
"de": "Wie erhalte ich die ZIP-Datei?",
"fr": "Comment obtenir l’archive zip ?",
"nl": "Hoe kom ik aan de zip?",
"ru": "Как получить zip-архив?",
"ja": "zip ファイルの入手方法",
"ko": "zip 파일은 어떻게 받나요?",
"zh-rCN": "如何获取 zip 文件?",
"zh-rTW": "如何取得 zip 檔案?"
}
}
```

Put it in the scratchpad or `tmp/`, not in the repo root.

3. Apply, then confirm nothing drifted:

```bash
python scripts/add_strings.py tmp/new_strings.json
python scripts/add_strings.py --check
```

4. Build. `aapt` is the real judge of whether the XML is acceptable:

```bash
JAVA_HOME='C:\Program Files\Android\Android Studio\jbr' ./gradlew.bat :app:assembleDebug
```

## Flags

| Command | What it does |
| --- | --- |
| `add_strings.py` (no args) | Prints full usage and the input format |
| `--locales` | Lists discovered locales and their files |
| `<file.json>` | Adds strings; **errors** if any locale is missing or the name already exists |
| `--fill <file.json>` | Adds only where missing — for back-filling what `--check` reported |
| `--check` | Fails if any locale lacks a string the default locale has. Run before a PR |

## Translating

Translate properly rather than leaving English in place; the existing files are fully
translated and a stray English string is conspicuous. Points that have come up here:

- French uses a space before `:`, `?` and `!`, and reads better with `’` than `'`
- CJK wants the full-width colon `:` and, for a gap, the ideographic space ` `
- Keep positional format specifiers (`%1$d`, `%2$s`) intact and in a natural order for the
language — they may be reordered, which is the point of the positional form
- Product names (`Google Maps`, `AMap`, `Anisette`) generally stay as they are

## What the script already handles

Do not do these by hand:

- Escaping `'`, `"` and `&` as Android requires
- Preserving `<u>`, `<b>`, `<i>` so emphasis survives — styling that belongs to the copy
belongs in the resource, not in a paint flag in code that would only ever apply to English
- Re-parsing each file after writing, so a bad write fails immediately instead of at aapt time
- `translatable="false"` — set `"translatable": false` and supply only `"default"`

## Related

`AGENTS.md` documents this generically for any agent. Keep the two in step if the tool changes.
95 changes: 95 additions & 0 deletions .githooks/pre-commit
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
#!/usr/bin/env bash
#
# Fast checks that catch the mistakes which are silent otherwise.
#
# Deliberately excludes anything slow. Builds, instrumented tests and the emulator belong in
# CI: a pre-commit hook that takes a minute gets bypassed with --no-verify within a day, and
# a hook people routinely bypass is worse than no hook, because it creates the impression of
# a safety net that is not there.
#
# Install once per clone (hooks are not versioned, so this cannot be automatic):
#
# git config core.hooksPath .githooks
#
# Skip deliberately when you need to: git commit --no-verify
#
set -uo pipefail

# Repo root, so the hook works from any subdirectory.
cd "$(git rev-parse --show-toplevel)" || exit 1

failed=0

say() { printf '\n[pre-commit] %s\n' "$*"; }

python_bin() {
if command -v python >/dev/null 2>&1; then printf 'python'
elif command -v python3 >/dev/null 2>&1; then printf 'python3'
else printf ''
fi
}

PYTHON="$(python_bin)"
if [[ -z "$PYTHON" ]]; then
say "No python on PATH; skipping checks."
exit 0
fi

staged() {
git diff --cached --name-only --diff-filter=ACMR
}

# ---------------------------------------------------------------------------------------
# Translations.
#
# A string missing from one locale changes nothing observable: the build passes and the app
# reads correctly to anyone who speaks the language it was written in. It surfaces as a user
# reporting English text in an otherwise translated app, which is why it is worth catching
# at the point the string is added rather than weeks later.
# ---------------------------------------------------------------------------------------
if staged | grep -q "app/src/main/res/values.*/strings.xml"; then
say "strings.xml changed - checking every locale has every string"
if ! "$PYTHON" scripts/add_strings.py --check; then
printf '\n Fix with: python scripts/add_strings.py --fill <file.json>\n'
printf ' See .claude/skills/add-strings/SKILL.md or AGENTS.md\n'
failed=1
fi
fi

# ---------------------------------------------------------------------------------------
# Python: lint and type-check only what is being committed, so the hook stays quick.
# ---------------------------------------------------------------------------------------
# Scoped to the same trees CI type-checks. python/ (the macOS wizard) has pre-existing
# pyright errors and is excluded there too; a hook stricter than CI would block commits on
# debt the author did not create, and the first thing anyone learns from that is --no-verify.
changed_python="$(staged | grep -E '^(scripts|app/src/main/python)/.*\.py$' || true)"

if [[ -n "$changed_python" ]]; then
if "$PYTHON" -m flake8 --version >/dev/null 2>&1; then
say "Linting changed Python with flake8"
# shellcheck disable=SC2086
"$PYTHON" -m flake8 $changed_python || failed=1
fi

if "$PYTHON" -m pyright --version >/dev/null 2>&1; then
say "Type-checking changed Python with pyright"
# shellcheck disable=SC2086
"$PYTHON" -m pyright $changed_python || failed=1
fi
fi

# ---------------------------------------------------------------------------------------
# Secrets. secrets.properties holds a real Google Maps key and is gitignored; committing it
# would publish the key, and a published key cannot be unpublished, only rotated.
# ---------------------------------------------------------------------------------------
if staged | grep -qx "secrets.properties"; then
say "REFUSING: secrets.properties is staged. It holds a real API key and is gitignored."
failed=1
fi

if (( failed )); then
printf '\n[pre-commit] Checks failed. Commit with --no-verify to override.\n\n'
exit 1
fi

exit 0
132 changes: 132 additions & 0 deletions .github/workflows/build-debug.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@ on:
- "gradle.bat"
- "local.defaults.properties"
- "settings.gradle.kts"
# The build shells out to these: the unicorn stub wheel is generated by
# build_unicorn_stub_wheel.py, so a change here can break the build without
# touching anything else in this list.
- "scripts/**"
pull_request:
branches: [ "main" ]
paths:
Expand All @@ -29,11 +33,123 @@ on:
- "gradle.bat"
- "local.defaults.properties"
- "settings.gradle.kts"
# The build shells out to these: the unicorn stub wheel is generated by
# build_unicorn_stub_wheel.py, so a change here can break the build without
# touching anything else in this list.
- "scripts/**"

# Allows you to run this workflow manually from the Actions tab
workflow_dispatch:

jobs:
# Cheap and dependency-free, so it runs on its own and reports in seconds rather than
# after an emulator boot. A string missing from a locale fails nothing on its own - the
# build succeeds and the app looks correct to anyone reading the language it was written
# in - so without this it is only ever found by a user seeing English.
static-checks:
name: Static checks
runs-on: ubuntu-latest

steps:
- name: Checkout code
uses: actions/checkout@v4

- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.12'

- name: Check every locale has every string
run: python scripts/add_strings.py --check

# These scripts gate other checks - add_strings.py gates the step above, and
# exporter_version.py gates the exporter release - so a bug in one of them reports
# green while protecting nothing.
- name: Test the tooling in scripts/
run: |
python -m pip install pytest
python -m pytest scripts/test -v

# pyright rather than mypy: the editors used on this project run Pylance, which is
# pyright, and mypy's defaults are lenient enough to pass code Pylance flags.
#
# Scoped to the two clean trees. python/ (the macOS wizard) has pre-existing errors and
# needs its own cleanup before it can be added.
#
# main.py imports findmy and NSKeyedUnArchiver, so those have to be installed first or
# every one of them is reported as an unresolved import and the check means nothing.
#
# Run through pip rather than an action so this is the identical command CONTRIBUTING.md
# tells contributors to run locally.
# Pillow is only used by make_debug_launcher_icon.py, and that script degrades
# gracefully without it. Installed rather than silenced so pyright actually checks the
# file instead of skipping it.
- name: Type-check Python with pyright
run: |
python -m pip install pyright==1.1.411 pillow
python -m pip install -r app/src/test/python/requirements.txt
python -m pyright app/src/main/python scripts

# The Room v1 -> v2 migration can only be exercised on a real Android runtime, so it
# lives in src/androidTest and needs an emulator. `./gradlew test` in the build job
# below runs JVM unit tests only and would silently skip it - which matters because a
# broken migration destroys every existing user's imported beacons and location
# history, and only reproduces on upgrade, never on a fresh install.
instrumented-tests:
name: Instrumented tests (emulator)
environment: 'Android Build'
runs-on: ubuntu-latest
timeout-minutes: 45
steps:
- name: Checkout code
uses: actions/checkout@v4

- name: Set Up JDK
uses: actions/setup-java@v4
with:
distribution: 'zulu'
java-version: '17'
cache: 'gradle'

- name: Change wrapper permissions
run: chmod +x ./gradlew

- name: Create local.properties
run: |
echo "sdk.dir=$ANDROID_HOME" > local.properties

- name: Inject secrets.properties
run: |
echo "MAPS_API_KEY=${{ secrets.MAPS_API_KEY || 'maps_key_default_value' }}" > secrets.properties

# KVM is required for a hardware-accelerated emulator; without it the run times out.
- name: Enable KVM
run: |
echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' \
| sudo tee /etc/udev/rules.d/99-kvm4all.rules
sudo udevadm control --reload-rules
sudo udevadm trigger --name-match=kvm

- name: Run instrumented tests
uses: reactivecircus/android-emulator-runner@v2
with:
api-level: 35
target: google_apis
arch: x86_64
profile: pixel_6
ram-size: 4096M
disable-animations: true
emulator-options: -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none
script: ./gradlew connectedDebugAndroidTest --console=plain

- name: Upload test reports
if: always()
uses: actions/upload-artifact@v4
with:
name: instrumented-test-reports
path: app/build/reports/androidTests/
if-no-files-found: ignore

build:
environment: 'Android Build'
runs-on: ubuntu-latest
Expand Down Expand Up @@ -66,10 +182,26 @@ jobs:
run: |
echo "MAPS_API_KEY=${{ secrets.MAPS_API_KEY || 'maps_key_default_value' }}" > secrets.properties

# No stub wheel to validate any more: it is generated from app/stubs/unicorn/ by
# generateUnicornStubWheel during the build, rather than being checked in.

# Run Tests Build
- name: Run gradle tests
run: ./gradlew test

# Covers app/src/main/python (the module Chaquopy packages into the APK).
# Runs on plain CPython - it imports no Android or Java types.
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.12'

- name: Install Chaquopy bridge test dependencies
run: python -m pip install -r app/src/test/python/requirements.txt

- name: Run Chaquopy bridge tests
run: python -m pytest app/src/test/python -v

# Create APK Debug
- name: Build Debug APK - ${{ env.main_project_module }} module
run: ./gradlew testDebugUnitTest assembleDebug
Expand Down
20 changes: 20 additions & 0 deletions .github/workflows/build-release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -80,9 +80,29 @@ jobs:
run: |
echo "${{ secrets.SIGNING_KEY }}" | base64 --decode > ${{ env.main_project_module }}/${{ env.KEYSTORE_FILE }}

# No stub wheel to validate any more: it is generated from app/stubs/unicorn/ by
# generateUnicornStubWheel during the build, rather than being checked in.

# A release is the worst time to discover a locale is missing a string, since the
# fallback is silent: those users simply get English.
- name: Check every locale has every string
run: python3 scripts/add_strings.py --check

- name: Run gradle tests
run: ./gradlew test

# Covers app/src/main/python, the module Chaquopy packages into the APK.
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.12'

- name: Install Chaquopy bridge test dependencies
run: python -m pip install -r app/src/test/python/requirements.txt

- name: Run Chaquopy bridge tests
run: python -m pytest app/src/test/python -v

- name: Build gradle project
run: ./gradlew build -x lint
env:
Expand Down
Loading
Loading