ReviewOS

craft-native/craft

watchPosition hands out ids that clearWatch ignores, and a second watch leaks the first

#179
Closed glennmichael123 opened this 10 days ago ·
10 days ago

watchPosition mints a distinct id per call and stores its callback in a single field:

private var locationCallback: LocationCallback? = null
private var watchId = 0

@JavascriptInterface
fun watchPosition(optionsJson: String): Int {
    ...
    val currentWatchId = ++watchId
    fusedLocationClient = LocationServices.getFusedLocationProviderClient(activity)
    locationCallback = object : LocationCallback() { ... }
    ...
}

@JavascriptInterface
fun clearWatch(watchId: Int) {
    locationCallback?.let { fusedLocationClient?.removeLocationUpdates(it) }
    locationCallback = null
}

Two things follow, and both are silent.

A second watch leaks the first

locationCallback is overwritten, not appended to. The first callback is still registered with the fused location provider — it keeps waking the app for location updates — but nothing holds a reference to it any more, so removeLocationUpdates can never be called with it. It runs until the process dies.

Two watchPosition calls is not an exotic pattern: a map view and a "distance to destination" widget on the same page is enough.

clearWatch ignores the id it is given

The parameter is never read. clearWatch(1) removes whatever callback the field currently holds, which is the most recent watch — so:

  • clearing watch 1 while watch 2 is running stops watch 2;
  • clearing watch 2 twice does nothing the second time and reports nothing;
  • the id watchPosition returned is, in practice, a number the page can only use to be misled.

craft.d.ts declares watchPosition(): number and clearWatch(id: number), which is the W3C geolocation shape — where ids do identify individual watches.

Suggested fix

A map from id to callback:

private val locationCallbacks = mutableMapOf<Int, LocationCallback>()

fun clearWatch(watchId: Int) {
    locationCallbacks.remove(watchId)?.let { fusedLocationClient?.removeLocationUpdates(it) }
}

That fixes both at once: the first watch is no longer orphaned, and the id means what the SDK says it means.

Not migrated to Zig

Recorded as a deferral. Both methods are the Kotlin's own instance state — locationCallback, fusedLocationClient, watchId — and CraftNative has no reference to the CraftBridge that owns them, the same wall setFlashlight/toggleFlashlight sit behind. Migrating either half would leave the other reading a field that had stopped being maintained.

Related: #162 (state a migration cannot reach), #127.

10 days ago

Fixed and shipped on main in 93923c3 (fix(android): track location watches by id). Android now retains every LocationCallback in an ID-keyed map, and clearWatch(id) removes only the corresponding registered callback. Regression coverage was added; CI, SBOM, Binary Size, and Benchmark all passed.

Sign in to comment on this issue.