6.0 KiB
BAL Reader (Android)
A minimal Android app that reads a Bitcoin will exported by the BAL Electrum plugin directly from your screen, then lets you view, copy, share, or save the recovered data. Reader only — it never signs or broadcasts.
It decodes all four transfer formats the plugin can export, auto-detecting the format from the first frame:
- BAL QR (the plugin's default), single- and multi-frame, plain and
zlib-compressed, compact
BAL1header (legacyBALQR1frames are still accepted on import); - BC-UR v1 (single-part and
NofMmultipart); - BC-UR v2 (single-part and XOR-fountain multipart — it tolerates dropped, repeated, and out-of-order frames);
- BBQR (
Z/H/2encodings).
Both payload kinds are handled: the whole-will JSON and the plain transaction-hex list.
How decoding works
The app does not reimplement the QR formats. It bundles the plugin's own
codec modules — bal/core/__init__.py, bal/core/animated_qr.py,
bal/core/qrtransfer.py — and runs them verbatim through Chaquopy (CPython
on Android). Kotlin is only camera glue and UI:
Camera (CameraX) → ML Kit QR detection (on-device, no API key)
→ BalDecoder.add(text) → bal.core.animated_qr.AnimatedQrSession
→ when done → BalDecoder.finish()
= session.resolve() → qrtransfer.decode_transfer()
→ balreader.payload.decode_will_payload()
→ ResultActivity: view / copy / share / save
The decode tail mirrors the plugin's import dialog function-for-function, and
android/test_chain/verify_chain.py proves the bundled code decodes every
format the way the desktop import does (including scrambled, duplicated, and
missing frames).
Repository layout
android/
├── app/src/main/
│ ├── AndroidManifest.xml
│ ├── java/life/after/bitcoin/
│ │ ├── BalDecoder.kt Chaquopy bridge over the bundled codecs
│ │ ├── MainActivity.kt camera + ML Kit scan loop + progress
│ │ └── ResultActivity.kt viewer (copy / share / save)
│ └── python/ bundled Python (regenerate, do not hand-edit)
│ ├── bal/core/ SYNCED COPY of the plugin codecs
│ └── balreader/payload.py verbatim copy of dialogs.decode_will_payload
├── scripts/
│ ├── sync_codecs.py re-copy + verify the bundled codecs
│ └── build_apk.py resync codecs, run Gradle, print APK + sha256
└── test_chain/verify_chain.py decode-chain simulation for all formats
Build
You need Android Studio (Jellyfish or newer), JDK 17, an Android SDK with platform 35, and a network connection for the first Gradle sync.
- Open this
android/folder in Android Studio and let it sync (it will fetch the Gradle wrapper 8.14, AGP 8.10.0, Kotlin 2.0.21, Chaquopy 17.0.0, CameraX 1.3.4, and ML Kit). - Connect a phone (API 24+) or start an emulator and press Run.
- Grant the camera permission when asked.
Alternatively, from the command line (from the repository root):
python3 android/scripts/build_apk.py # debug APK + sha256
python3 android/scripts/build_apk.py --release # (unsigned) release APK
The script re-synchronises the bundled codec modules first (so the APK always
carries the current bal/core sources), runs ./gradlew, and prints the APK
path, size and sha256. Flags: --no-sync (skip the re-sync), --offline
(Gradle without downloads), --clean, --verbose.
Equivalent raw Gradle call:
cd android
./gradlew assembleDebug # APK: android/app/build/outputs/apk/debug/app-debug.apk
If the Gradle wrapper jar is missing
gradle/wrapper/gradle-wrapper.jar is committed so ./gradlew works out of
the box. If it is ever absent, Android Studio regenerates it on the first
sync; no manual steps needed.
Use
- In Electrum + BAL, open the will's export dialog.
- Pick a format — start with the default BAL QR, then try BC-UR v1, BC-UR v2, and BBQR.
- Make sure the wording toggle shows a payload (business logic), then display the animated QR and keep it on screen.
- Point the phone at the screen. The header shows the detected format and
received / total; scanning stops automatically when the transfer is complete. - On the result screen: Copy the raw transfer, Share it, Save it
as
will.json(whole will) orwill_tx.txt(transaction list), or press Scan another.
Notes:
- Keep the phone still and the whole QR inside the frame (the codec dedups repeated frames, so a slow capture is fine).
- If the camera glares off the screen, reduce brightness or tilt slightly.
- If scanning jumps between exports, the app detects the format switch, resets, and asks you to let it re-scan.
Keeping the bundled code in sync with the plugin
The codecs under app/src/main/python/bal/core/ are committed copies for
deterministic builds, but they must stay identical to the plugin. Re-run this
after changing bal/core/animated_qr.py or bal/core/qrtransfer.py (and
after any change to decode_will_payload in bal/gui/qt/dialogs.py, which
mirrors balreader/payload.py):
python3 android/scripts/sync_codecs.py # copy
python3 android/scripts/sync_codecs.py --check # verify only (CI-friendly)
python3 android/test_chain/verify_chain.py # full decode-chain regression
verify_chain.py fails if the app's balreader/payload.py ever drifts from
the plugin's decode_will_payload (AST identity + result parity).
Version pins (see PLAN_ANDROID_READER.md)
| Item | Version |
|---|---|
| AGP | 8.10.0 |
| Gradle | 8.14 (wrapper) |
| Kotlin | 2.0.21 |
| Chaquopy | 17.0.0 (Python 3.12) |
| compile / target / min SDK | 35 / 35 / 24 |
| CameraX | 1.3.4 |
| ML Kit barcode-scanning | 17.3.0 |
| JDK | 17 |
License
MIT. The bundled Python codec files inherit the plugin's MIT license
(bal/LICENSE); see app/src/main/python/bal/ for attribution.