SPLIT v0.2.1 — the watch app opens
Pre-releaseThe watch app did not open in v0.1.0 or v0.2.0. It does now. Releases can also finally be installed as updates. Both problems were reproduced and fixed against a Wear OS 6 emulator rather than reasoned about.
Important
Upgrading from v0.1.0 or v0.2.0? Uninstall first.
adb uninstall com.carlb.split
Those builds were each signed with a different throwaway key, so Android rejects this one with INSTALL_FAILED_UPDATE_INCOMPATIBLE. This is a one-time break — from v0.2.1 onward every build shares one signing key and updates install in place, keeping your settings and logged strings.
Fixed
The watch app crashed instantly on launch
RecoilGate registered the accelerometer at SENSOR_DELAY_FASTEST (0 µs). Since Android 12 any sampling rate above 200 Hz requires HIGH_SAMPLING_RATE_SENSORS, which was never declared — so registerListener threw straight out of the foreground service:
SecurityException: To use the sampling rate of 0 microseconds, app needs to
declare the normal permission HIGH_SAMPLING_RATE_SENSORS
at RecoilGate.start(RecoilGate.kt:55)
at TimerEngine.openMic(TimerEngine.kt:95)
at TimerService.onStartCommand(TimerService.kt:82)
START_STICKY then had the system recreate the service into the identical crash, so the app opened and vanished, forever.
Four changes, because the permission alone would only fix today's instance:
HIGH_SAMPLING_RATE_SENSORSis declared.RecoilGate.start()can no longer throw — it falls back to a slower sampling rate and reports itself unavailable rather than dying.- The accelerometer is started only when the recoil gate is enabled. It ships off by default, so a disabled optional feature was taking down the whole timer.
START_NOT_STICKY, andstartForeground()happens before anything that can fail.
Releases could not be installed as updates
Every CI run generated a fresh ~/.android/debug.keystore, so each release carried a different signer and Android refused the upgrade. Builds now use a committed signing key, verified identical across both apps.
That key is deliberately public — its credentials are the Android SDK's standard debug constants and the file is in the repository. It exists so upgrades work, not to prove anything about origin. Verify downloads against SHA256SUMS.txt.
Added
- Emulator smoke test in CI — installs both APKs, launches them, fails if the process dies or logs a fatal exception. Both apps compiled, passed 42 unit tests and passed Android Lint while crash-looping; no static check could see this.
- Signing-key stability check in CI — fails the build if the APK signer drifts, which is what silently broke updates last time.
Changed
- README badge now distinguishes emulator launch verified from live fire tested (still no).
- New troubleshooting entries for
INSTALL_FAILED_UPDATE_INCOMPATIBLEand the app closing on launch.
Removed / Breaking
Nothing removed. No API or wire-format change — a v0.2.1 watch and a v0.2.0 phone still interoperate. The signing key change is not a code break but does require the one-time uninstall above.
Install
Install the watch APK on the watch and the phone APK on the phone. They share an applicationId, so putting both on one device replaces one with the other.
adb -s <watch-serial> install -r split-wear-v0.2.1-debug.apk
adb -s <phone-serial> install -r split-mobile-v0.2.1-debug.apk
adb devices lists serials. Verify your download:
sha256sum -c SHA256SUMS.txt
What was actually verified
| Check | Result |
|---|---|
./gradlew build (debug + release, both modules) |
pass |
| Unit tests | 42 / 42 |
| Android Lint | no issues on :wear and :mobile |
| ktlint | 0 violations |
| Watch app launches on Wear OS 6 (API 36) emulator | pass — process alive, no crash |
| Foreground service holds the mic | pass — isForeground=true, types=0x80 |
| Timer arms, sounds the tone, runs the clock | pass |
| Phone app launches | pass — no crash |
| In-place update over a previous v0.2.1 install | pass |
| Both APKs share one signing key | pass (df77d1c4…) |
| Old-key upgrade correctly rejected | reproduced, hence the uninstall note |
Known limitations
No live fire has ever been recorded through this code, and it has not run on a physical watch. An emulator has no microphone worth the name and no gunshots.
The four detector constants remain reasoned estimates, not measurements:
| Constant | Value | Basis |
|---|---|---|
| Clip-run threshold | 3 samples | estimate |
| Echo blanking | 60 ms | estimate |
| Recoil window | ±40 ms | estimate |
| Recoil threshold | 18 m/s² | estimate |
Also unchanged: no instrumented tests beyond the launch smoke check; the browser prototype cannot truly pin an unprocessed audio source; compileSdk stays pinned at 36 until platform 37 ships.
A detection accuracy report from a real range session is still the most valuable thing anyone can contribute.
Full changelog: v0.2.0...v0.2.1