package ai.beans.mapswidget.location import android.annotation.SuppressLint import android.content.Context import android.location.Location import android.os.Looper import com.google.android.gms.location.FusedLocationProviderClient import com.google.android.gms.location.LocationCallback import com.google.android.gms.location.LocationRequest import com.google.android.gms.location.LocationResult import com.google.android.gms.location.LocationServices import com.google.android.gms.location.Priority /** * Optional Play Services location source for [BeansLocationBridge]. * * Drop this file in alongside `BeansLocationBridge.kt` when your app already * depends on Play Services location: * * ``` * implementation("com.google.android.gms:play-services-location:21.0.1") * ``` * * The bridge finds it by name and uses it automatically — no wiring needed. To be * explicit (or to force the platform provider), pass it yourself: * * ``` * BeansLocationBridge.attach(webView, this, BeansLocationBridge.Options( * locationSource = BeansFusedLocationSource(this) // or PlatformLocationSource(this) * )) * ``` * * Why prefer it: fused fixes blend GPS, wifi, cell and sensors, so a resident * walking a property gets smoother positions and usable bearings, at lower battery * cost than raw GPS. Requires `play-services-location` 20.0.0+ for * `LocationRequest.Builder` / `Priority`. * * Permission is already checked by the bridge before [start] is called, hence the * `@SuppressLint("MissingPermission")`. */ class BeansFusedLocationSource(context: Context) : BeansLocationSource { override val name = "FusedLocationProvider" private val client: FusedLocationProviderClient = LocationServices.getFusedLocationProviderClient(context.applicationContext) private var listener: BeansLocationSource.Listener? = null private var callback: LocationCallback? = null /** * Fused has no device-wide "off" signal to test cheaply; it reports failure * through the callback instead. Returning true keeps the bridge from * pre-emptively erroring — a real outage surfaces as `onUnavailable`. */ override fun isUsable(): Boolean = true @SuppressLint("MissingPermission") override fun start(config: BeansLocationConfig, listener: BeansLocationSource.Listener) { this.listener = listener val priority = if (config.highAccuracy) Priority.PRIORITY_HIGH_ACCURACY else Priority.PRIORITY_BALANCED_POWER_ACCURACY val request = LocationRequest.Builder(priority, config.minIntervalMs) .setMinUpdateIntervalMillis(config.minIntervalMs) .setMinUpdateDistanceMeters(config.minDistanceM) // false on purpose: waiting for an accurate first fix leaves the map // without a user marker for seconds. Deliver early and let it refine. .setWaitForAccurateLocation(false) .build() val cb = object : LocationCallback() { override fun onLocationResult(result: LocationResult) { result.lastLocation?.let { this@BeansFusedLocationSource.listener?.onLocation(it) } } override fun onLocationAvailability( availability: com.google.android.gms.location.LocationAvailability ) { if (!availability.isLocationAvailable) { this@BeansFusedLocationSource.listener ?.onUnavailable(2, "Fused location temporarily unavailable") } } } callback = cb client.requestLocationUpdates(request, cb, Looper.getMainLooper()) // Seed the marker from the cached fix. Asynchronous, so it arrives through // the listener rather than through lastKnown(). client.lastLocation.addOnSuccessListener { cached -> if (cached != null && callback === cb) { this.listener?.onLocation(cached) } } } override fun stop() { callback?.let { client.removeLocationUpdates(it) } callback = null listener = null } /** Fused's cache is only reachable asynchronously; [start] seeds it instead. */ override fun lastKnown(): Location? = null }