The fifth listener shim, and the first with two Java interfaces in one flow: OnSuccessListener and OnFailureListener on the Play Services task, plus a LocationCallback for the fresh-location fallback.
It was not actually entangled
I had passed this over as blocked alongside the deferred watchPosition/clearWatch pair (#179), on the grounds that they share CraftBridge fields. Checking rather than assuming: fusedLocationClient is the only field getCurrentPosition writes, and watchPosition assigns it before every use — so leaving it null costs clearWatch nothing. The comment at the seam says so, because the next reader will wonder the same thing.
Six doubles, and three of them started as floats
Location.accuracy, .speed and .bearing are Float. The shim writes them with put(name, value), and the overload Kotlin picks is put(String, double) — primitive widening beats boxing — so they are Doubles by the time numberToString sees them.
That is not a detail to shrug at, because the same float prints differently in the two places this bridge sends one:
| boxed as | 0.1f prints as | |
|---|---|---|
bridge_android_motion (values go into a Map) | Float | 0.1 |
getCurrentPosition (put(String, double)) | Double | 0.10000000149011612 |
Neither is wrong; they are different calls. It is exactly why android_json_number has two functions rather than one widening one — and why the holder widens on the Kotlin side rather than passing floats across.
timestamp needs no JNI call at all: put(String, long) boxes a Long, and numberToString prints one with Long.toString.
bearing is called heading
The one key whose name does not match the field it came from — every other one matches — and so the one a rewrite would get right-looking and wrong. It has its own test.
Two codes, and no third
{code: 1} is the permission refusal, {code: 2} is whatever the client failed with, and a page tells them apart by the number alone — so both are API and both are pinned.
There is no code for "the fresh location request never produced a result". That promise simply never settles, here as there; it is the #157 shape and is left alone rather than quietly improved.
Testing
zig build test:android passes with the ratchet at 51. Two mutations fired: renaming heading back to bearing, and collapsing the two rejection codes. Both libcraft.so targets still build with no NDK.