SPA2 (riddle-hunters' "Puzzlehunt Assistant") is a self-hosted collection of cipher and code tools for Android (substitution ciphers, Morse code, Braille, and more), plus a checkpoint logbook for field puzzle hunts. Native UI in Java with a C/C++ core (PCRE-based search). The UI is localized in English and Czech.
The main menu (plus the app icon) opens one tool per screen; each cipher has a bottom tab bar switching between Encode, Decode and a Reference. The sample message "Secret Message" is used below so the encoded output is visible. All shots are from the v4.0.0 build on a Pixel 9 Pro XL (SDK 37); they are maintained as on-device captures attached to issue #17.
Dictionary Search searches exactly one dictionary at a time, chosen under Settings → Dictionary Search. Settings → Data sources has an on/off tickbox for every bundled dictionary; unticked sources are skipped even when still selected, and an unselected dictionary that becomes disabled falls back to the first ticked source.
| Source | Asset | Origin / licence |
|---|---|---|
| Czech | cs.canon |
aspell-cs v0.60 word list |
| English | en.canon |
aspell-en v7.1 / SCOWL 7.0 |
| Periodic table | periodic.canon |
GoodmanSciences gist, 118 elements by name and symbol, e.g. ^actinium: or ^he: |
| Pokémon | pokemon.canon |
cristobalmitchell/pokedex (MIT): names/types, e.g. ^pikachu:, ^mrmime:, ^flabebe: |
| Wordle | wordle.canon |
steve-kasica/wordle-words: valid answers/guesses, e.g. ^crane: |
Attribution and licence texts live in app → About → Licence, and in
spa2/src/main/res/raw/lic_sources.txt. The sources are reprocessed into
canon form (lowercase [a-z] key, :Display) and gzipped; the build
unpacks the .gz assets (see tools/canon/gen_canon.py).
Requirements:
- JDK 26 (primary; build also validated on JDK 17 and 24)
- Android SDK with: platform android-36, build-tools 36.1.0, CMake 3.22.1, NDK 27.0.12077973
Point Gradle at your SDK one of these ways:
-
Set
ANDROID_HOME(recommended, no repo changes needed):export ANDROID_HOME=$HOME/Android/Sdk ./gradlew :spa2:assembleDebug
-
Or create
local.propertiesin the repo root (seelocal.properties.example):sdk.dir=/path/to/Android/Sdk
local.properties is machine-specific and intentionally not tracked.
The output APK is spa2/build/outputs/apk/debug/spa2-debug.apk.
The migration relies on a 3-tier test suite (the "migration contract", issue #7). Tiers 1 and 2 run on any JVM anywhere; Tier 3 needs a device.
| Tier | Where it runs | Command |
|---|---|---|
| 1. Pure JVM unit tests | CI + local | ./gradlew :spa2:testDebugUnitTest |
| 2. Robolectric (resources/prefs/XML) | CI + local | ./gradlew :spa2:testDebugUnitTest |
3. Instrumented (native libregrep/PCRE, app smoke) |
physical Pixel, local only | ./gradlew :spa2:connectedDebugAndroidTest |
Tier 3 — the NDK/PCRE safety net (RegExpNativeTest + AppSmokeTest) —
intentionally stays on the attached Pixel (adb -s 52141FDAS001QF):
GitHub Actions runners cannot host physical devices, and Managed/Firebase
Test Lab is deferred (issue #8). When a step touches native code or JNI,
run Tier 3 locally before closing the issue.
.github/workflows/build.yml runs Tier 1+2 and assembleDebug on every
push/PR (JDK 26, platform android-36, build-tools 36.1.0, CMake 3.22.1,
NDK 27.0.12077973, Gradle 9.7.1 from the wrapper). Robolectric is pinned to
4.17-beta-4 — an AGP/JDK-sensitive choice that must advance with the
toolchain (unit tests were validated on JDK 17 and 26).
versionCode/versionName live in spa2/src/main/AndroidManifest.xml.
Codes use the scheme major*10000 + minor*1000 + patch*10 + sub, so
4.0.0 = 40000. Codes only ever increase (Android requires it for sideloaded
upgrades).

















