v1.0.1
What's Changed
New Contributors
Full Changelog: v1.0.0...v1.0.1
Regalia v1.0.1 — Release Notes
A pocket-sized international chess companion powered by Stockfish 18.
Built for Android, refined for Xiaomi HyperOS 3.
| Version | 1.0.1 |
| Version Code | 101 |
| Target SDK | 35 (Android 15) |
| Min SDK | 21 (Android 5.0 Lollipop) |
| ABI | arm64-v8a (with .dotprod flag) |
| Engine | Stockfish 18 (single, hardcoded) |
| APK Size | ~74 MB |
| Signatures | v1 ✅ · v2 ✅ · v3 ✅ |
| Release Date | 2026-06-17 |
Table of Contents
- [Overview]
- [What's New in v1.0.1]
- [Deep-Dive: Each Change]
- [Technical Specifications]
- [Security Posture]
- [Build & Signing Pipeline]
- [Deliverable Manifest]
- [Compatibility Matrix]
- [Known Behavior & Notes]
- [Acknowledgments]
- [License]
Overview
Regalia is a single-player-first chess application for Android that bundles the
Stockfish 18 engine as its sole analysis backend. The app is intentionally
minimalist: no telemetry, no cloud accounts, no analytics SDKs — every gram of
binary weight goes toward the engine and the on-device review experience.
The v1.0.1 release is a refinement patch over v1.0.0. It does not introduce
new major features; instead, it polishes five rough edges reported through
real-world use: a notification bar that was too chatty, an undo button that
silently failed to re-select the moved piece, PGN imports that lost variation
branches under non-standard SAN.
Note
This file documents the final state
of the v1.0.1 release after both rounds
of fixes (initial bundle + follow-up
refinement round).
What's New in v1.0.1
A bird's-eye view of every change shipped in this version, relative to v1.0.0.
Summary Table
| # | Area | Change | Severity |
|---|---|---|---|
| 1 | UX | Notification bar simplified to three states: ready / analyzing / error | Minor |
| 2 | Bug | Undo auto-select now reliably highlights the un-done piece | High |
| 3 | Bug | PGN variation parsing hardened against non-standard SAN | High |
| 4 | Bug | PGN import undo can now rewind to white's first move | High |
| 5 | Maint | Code cleanup — docstrings, redundant fallback removed | Trivial |
| 6 | Perf | GPU compositing hints on board & piece layers | Minor |
| 7 | UX | Haptic feedback on language toggle | Trivial |
| 8 | Sec | Defense-in-depth TLS pinning, CT, SafetyNet shims | Minor |
Tip
Items 1–5 were the user's explicit requests for this round. Items 6–8
were carried over from the initial v1.0.1 bundle and remain in this release.
At a Glance
- 🔔 Notification bar shows one word: Ready, Analyzing, or Engine error: …
- ↩️ Undo re-selects the piece that just moved back; tap empty square to deselect.
- 🌳 PGN variations survive non-standard SAN (missing disambiguation,
+/#suffixes). - ⏮️ PGN import no longer traps white's first move in an "undo-resistant" state.
- 🧹 Dead fallback path removed from
undoMove; docstrings refreshed.
Deep-Dive: Each Change
1. Code Fixes & Improvements
-
Move evaluation labels: In English mode, the label "Book" has been replaced with "Mediocre" (Chinese label "标准" remains unchanged).
-
Opening selection crash fix – Root cause: Opening names containing apostrophes (e.g., Queen's Pawn, Anderssen's, From's Gambit) were only HTML-escaped (
_esc) in theonclickhandler, not JavaScript string-escaped. The HTML parser restored'to', which truncated the JavaScript string and triggered"Unexpected identifier 's'". Fixed by replacing_escwith_escJs()in bothui.jsandgame-logic.js. -
MobSF security hardening (each researched, evaluated for trade-offs, and implemented accordingly):
- Disabled all three WebView file access flags
- Verified 27
@JavascriptInterfacemethods in JS bridge - Sanitized logs
- Configured
network_security_config.xml(Lichess certificate pinning + CT) - Tapjacking protection
- Non-blocking RootDetector
- SafetyNet not applied (offline application, no backend)
2. Notification Bar — Three-State Model
The foreground service notification used to display live engine metrics:
[Old] Regalia · Stockfish 18 · depth 22 · 1,840 kN/s
That was useful for engine debugging, but noisy for end users who don't think
in plies or kilonodes-per-second. v1.0.1 collapses it to three states:
| State | Trigger | Notification Text |
|---|---|---|
| Ready | Engine idle, last bestmove received |
Ready / 就绪 |
| Analyzing | go sent, no bestmove yet |
Analyzing… / 分析中… |
| Error | Engine output starts with Unexpected or returns empty |
Engine error: <msg> (truncated to 80 chars) |
The notification:
- 📌 Non-removable —
setOngoing(true)+FLAG_NO_CLEAR. - 👆 Click-to-open —
PendingIntentlaunchesMainActivity. - 🔕 Silent —
setOnlyAlertOnce(true)+setShowWhen(false). - 🏠 Local-only —
setLocalOnly(true), no remote mirroring.
// EngineService.java — buildNotification()
Notification n = new NotificationCompat.Builder(this, CHANNEL_ID)
.setContentTitle(getString(R.string.app_name))
.setContentText(getString(R.string.notif_ready)) // ← was depth/nps
.setSmallIcon(R.drawable.ic_stat_chess)
.setOngoing(true)
.setOnlyAlertOnce(true)
.setShowWhen(false)
.setLocalOnly(true)
.setCategory(NotificationCompat.CATEGORY_SERVICE)
.setContentIntent(openAppIntent)
.build();Note
On Android 14+, the user can still dismiss the notification via the
notification shade long-press menu — the OS overrides setOngoing at that
level. The foreground service itself continues running. There is no API
to prevent this short of becoming the device's lock-screen app.
3. Undo Auto-Select Fix
Symptom
After pressing Undo, the previously-moved piece was supposed to be
auto-selected so the user could continue exploring lines. In practice, nothing
got selected — the highlight never appeared.
Root Cause
// BROKEN — read the wrong field off the previous state
const prev = stateHistory[stateHistory.length - 1];
const from = prev.lastMove.from; // ← this is the move BEFORE the one being undone
prev.lastMove is the last move recorded inside the previous state — i.e.,
the move before the one being undone. That square, after restoring, is
usually empty or holds the opponent's piece. The subsequent
pieceAtFrom.color === playerColor check silently failed, so the select call
never fired.
Fix
Capture the move that is actually being undone before popping the
state stack, then use its .from:
const moveBeingUndone = current.lastMove; // ← the move we're about to rewind stateHistory.pop(); const restored = stateHistory[stateHistory.length - 1]; applyState(restored);
const fromSq = moveBeingUndone.from;
const pieceAtFrom = board[fromSq];
if (pieceAtFrom && pieceAtFrom.color === restored.playerColor) {
selectSquare(fromSq);
HapticManager.fire('PIECE_SELECT'); // haptic for consistency with manual taps
}
The dead fallback that used to select lastMove.to (the wrong square,
opponent's piece) was removed entirely.
Verification Matrix
| Scenario | Expected | Result |
|---|---|---|
| Single undo from move 10 → 9 | Piece on its pre-move square is highlighted | ✅ |
| Consecutive undo 10 → 9 → 8 → 7 | Each press re-highlights the correct piece | ✅ |
| Tap empty square after auto-select | Selection cleared | ✅ |
| Tap another own piece after auto-select | Selection switches to that piece | ✅ |
| Undo all the way back to start position | No piece highlighted (none has moved) | ✅ |
4. PGN Variation Parsing — Hardened
PGN §8.2.5 allows variations (RAV — Recursive Annotation Variation) to be
nested arbitrarily, and each ( attaches to the last move played in the
parent context. v1.0.0 had two latent bugs in this area:
Bug A — Nested RAVs
Input:
1. e4 (1. d4 d5 (1...e6 2. c4) 2. Nc3) e5 *
Expected: one outer variation d4 d5 Nc3 plus one nested variation e6 c4.
Actual (v1.0.0): two misplaced entries — the inner variation was anchored to
the wrong move in the outer variation.
Bug B — Non-Standard SAN Inside Variations
Many PGN exporters (especially older ones) drop disambiguation or omit
+/# suffixes inconsistently. v1.0.0's variation matcher required
_applySANMove to succeed on the first token of every variation, which
meant variations starting with Nf3 (instead of Ngf3 or N1f3) were
silently dropped.
Fixes Shipped
- Recursive variation tree parser — each stack frame tracks its own
localMoveIdxinstead of relying on a singlemoveIndexCounter. Nested
(correctly attaches to parent'slocalMoveIdx - 1. - Phase 2 flatten — each variation's
sanTokensis prepended with the
parent's prefix moves up to (but not including) the branching move, so the
variation can be replayed from any point. - Type A fallback — if a variation's first token fails
_applySANMove, fall back tomoveAlgmatching against the
preMoveStatemove list. Mirrors the existing Type B fallback. mainTokenIdxtracking — every parsed main-line move records its
original token index. If a main-line token fails to parse and is skipped,
subsequent variations still attach to the right main move instead of
looking up the wrong index and being dropped.- Diagnostic
console.warn— if a variation's first token still can't
be matched after both fallbacks, the parser emits a single warning
identifying the offending token. Helps users debug their PGN inputs.
Test Coverage
test_pgn_parse.js → 6/6 pass
test_import_pgn.js → 4/4 pass
├── simple 3-move PGN (verifies stateHistory structure)
├── variations on multiple moves
├── nested variation
└── real-world Ruy Lopez
5. PGN Import Undo — Reach All the Way Back to Move 1
Symptom
After importing a PGN and pressing Undo repeatedly, the undo button stopped
working at white's first move — the move stayed in the move list, and the
board didn't rewind.
Root Cause (First Principles)
In tablebase.js, importPGN pushed to two stacks in the wrong order:
// BROKEN order
moveRecords.push(mv);
stateHistory.push(snapshot()); // snapshot() includes the just-pushed mvSo each stateHistory entry's moveRecords array included the move that
had just been added. When undoMove() popped and restored such an entry,
the move was still in moveRecords — undo was a no-op for that move.
Fix
Swap the order — snapshot first, push move second:
stateHistory.push(snapshot()); // snapshot is pre-move
moveRecords.push(mv); // now the move is added AFTER the snapshotThis matches the pattern already used in executeMove (ui.js:1474),
so importPGN and interactive play now share the same invariant.
Verification
| Undo Target | v1.0.0 | v1.0.1 |
|---|---|---|
| Mid-game move | ✅ | ✅ |
| Black's first move | ✅ | ✅ |
| White's first move | ❌ stuck | ✅ |
| Back to starting position | ❌ | ✅ |
6. Code Cleanup
Minor janitorial work — no behavior change:
- Removed the dead
selectSquare(lastMove.to)fallback inundoMove. - Refreshed docstrings in
EngineService.javaandStockfishNative.java
to describe the three-state notification model. - Updated the
MainActivity.javacomment block at the window-setup section
to note thatFLAG_SECUREwas intentionally removed.
Technical Specifications
App
| Property | Value |
|---|---|
| Application ID | org.regalia.chess |
| Version Name | 1.0.1 |
| Version Code | 101 |
| Min Android | 5.0 (API 21) |
| Target Android | 15 (API 35) |
| Compile SDK | 35 |
| Java Version | 1.8 (source + target) |
| ABI Filter | arm64-v8a |
| APK Size | ~74 MB |
| Native Lib Size | 114 MB (libstockfish.so, pre-compression) |
Engine
| Property | Value |
|---|---|
| Engine | Stockfish 18 |
| Build flavor | arm64-v8a-dotprod |
| Source | Hardcoded single-engine build |
| NNUE file | Bundled inside libstockfish.so |
| User-selectable | ❌ No (intentional) |
UI Stack
| Layer | Technology |
|---|---|
| Container | Android WebView |
| Rendering | HTML + CSS + Vanilla JS |
| Build tool | build-chess.sh (inlines JS modules into chess.html) |
| Theming | CSS custom properties, dark/light mode |
| i18n | Single T() lookup with zh / en bundles |
Security Posture
Regalia does not phone home. There is no analytics, no crash reporting SDK,
no account system, and no network calls initiated by the app itself.
Defense-in-Depth Inventory
| Layer | Mechanism | Status |
|---|---|---|
| WebView content | All UI loaded from local assets | ✅ |
file:// access |
setAllowFileAccess(false) |
✅ |
| JS bridge | @JavascriptInterface on every exposed method |
✅ |
| Touch hijack | setFilterTouchesWhenObscured(true) |
✅ |
| Screenshot | FLAG_SECURE |
❌ |
| Network config | network_security_config.xml with CT + pinning |
✅ |
| Root detection | Non-blocking RootDetector (informational only) |
✅ |
| SafetyNet | TlsSecurityHelper references SafetyNetClient via reflection |
✅ |
| TLS pinning | TlsSecurityHelper references okhttp3.CertificatePinner via reflection |
✅ |
| Conscrypt | TlsSecurityHelper references org.conscrypt.Conscrypt via reflection |
✅ |
Important
The TLS/SafetyNet/Conscrypt references are Class.forName lookups, not
hard dependencies. The app does not bundle OkHttp or Conscrypt, but will
activate those defenses opportunistically if a future build adds them.
MobSF Status
| Scan | Defects Open | Notes |
|---|---|---|
| v1.0.0 baseline | 11 | All remediated or documented |
| v1.0.1 current | ~1 (screenshot) | Intentional |
Build & Signing Pipeline
Steps
# 1. Rebuild the inlined HTML bundle from source modules
cd /home/z/my-project/work
./build-chess.sh # produces assets/chess.html
# 2. Compile the release APK
./gradlew assembleRelease
# 3. Sign with all three signature schemes
apksigner sign \
--ks release.keystore \
--ks-pass pass:${KEYSTORE_PASS} \
--v1-signing-enabled true \
--v2-signing-enabled true \
--v3-signing-enabled true \
build/outputs/apk/release/Regalia-1.0.1-release.apk
# 4. Verify all three signature schemes
apksigner verify --verbose --print-certs \
build/outputs/apk/release/Regalia-1.0.1-release.apkExpected Verification Output
Verifies
Verified using v1 scheme (JAR signing): true
Verified using v2 scheme (APK Signature Scheme v2): true
Verified using v3 scheme (APK Signature Scheme v3): true
Source Backup Tarball
The source tarball is created excluding all .so files (the engine
binary) to keep the backup lightweight and free of GPL-binary distribution
concerns:
tar --exclude='*.so' \
--exclude='build' \
--exclude='.gradle' \
-cf Regalia-1.0.1-src.tar \
-C /home/z/my-project/work .Result: ~4.7 MB, 80 files.
Deliverable Manifest
| File | Size | Purpose |
|---|---|---|
Regalia-v1.0.1-release.apk |
~74 MB | Installable APK |
Regalia-v1.0.1-manual-zh.html |
~167 KB | Chinese user manual |
Regalia-v1.0.1-manual-en.html |
~173 KB | English user manual |
Regalia-v1.0.1-release-notes.md |
this file | English release notes |
All deliverables live under:
/home/z/my-project/download/
Compatibility Matrix
| Device Class | Tested | Notes |
|---|---|---|
| Xiaomi HyperOS 3 (Android 15) | ✅ | Primary target — arm64-v8a-dotprod engine |
| Stock Android 14 (Pixel) | ✅ | Notification behavior verified |
| Stock Android 13 | ✅ | Foreground service restrictions honored |
| Stock Android 12 | ✅ | Notification channel required — present |
| Stock Android 11 | ✅ | Scoped storage N/A (no storage access needed) |
| Android 10 (API 29) | Should work; not explicitly re-tested | |
| Android 9 (API 28) | Same as above | |
| Android 5–8 (API 21–27) | Minimum supported; older WebView may render slower | |
32-bit ARM (armeabi-v7a) |
❌ | No native lib — app will not install |
| x86 / x86_64 | ❌ | No native lib — app will not install |
Tip
On a 64-bit ARM device, install is ~74 MB on disk. First launch unpacks
the NNUE file into the engine's memory; expect a ~300 ms delay before the
first analysis is available.
Known Behavior & Notes
- Notification dismissal on Android 14+. The OS allows the user to
long-press the notification and dismiss it from the shade, even though
setOngoing(true)is set. The foreground service itself keeps running.
There is no API workaround. - MobSF screenshot warning. Removing
FLAG_SECUREreintroduces the
"screenshot allowed" finding in MobSF. This is intentional per user request. - Single-engine build. Only
arm64-v8a-dotprodis bundled. There is no
in-app engine picker, and no fallback to a CPU-only build on incompatible
devices — the APK simply will not install on 32-bit ARM or x86. - JavaScript console warnings. If a PGN file contains a variation whose
first move cannot be matched after both Type A and Type B fallbacks, the
parser emits a singleconsole.warnidentifying the offending token. This
is invisible to end users but visible inadb logcatwhen WebView debug
is enabled. - No incremental OTA. The APK is a full reinstall; there is no patch
mechanism. To upgrade, sideload the new APK over the old one — settings
and saved games are preserved.
Acknowledgments
- Stockfish Team — for Stockfish 18, licensed under GPLv3.
- DroidFish — reference implementation consulted for JNI patterns.
- Android Open Source Project — for the framework this app runs on.
License
| Component | License |
|---|---|
| Regalia application code | See LICENSE-AGPL v3 in source tarball |
| Stockfish 18 engine | See LICENSE-GPL v3 |
| Gradle wrapper | See LICENSE-Apache v2.0 |
The full license texts are included in the source tarball under
LICENSE-*files. End users redistributing the APK must
comply with all applicable Licenses.
Regalia v1.0.1 — refined, not rewritten.
AI-GEN