-
Notifications
You must be signed in to change notification settings - Fork 9
chore: add manual location API (setLastKnownLocation) #655
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,60 @@ | ||
| package io.customer.location | ||
|
|
||
| import android.location.Location | ||
|
|
||
| /** | ||
| * Public API for the Location module. | ||
| * | ||
| * Use [ModuleLocation.locationServices] after initializing the SDK with [ModuleLocation] | ||
| * to get the instance. | ||
| * | ||
| * Example: | ||
| * ``` | ||
| * // Manual location from host app's existing location system | ||
| * ModuleLocation.instance().locationServices.setLastKnownLocation(37.7749, -122.4194) | ||
| * | ||
| * // Or pass an Android Location object | ||
| * ModuleLocation.instance().locationServices.setLastKnownLocation(androidLocation) | ||
| * ``` | ||
| */ | ||
| interface LocationServices { | ||
| /** | ||
| * Sets the last known location from the host app's existing location system. | ||
| * | ||
| * Use this method when your app already manages location and you want to | ||
| * send that data to Customer.io without the SDK managing location permissions | ||
| * or the FusedLocationProviderClient directly. | ||
| * | ||
| * @param latitude the latitude in degrees, must be between -90 and 90 | ||
| * @param longitude the longitude in degrees, must be between -180 and 180 | ||
| */ | ||
| fun setLastKnownLocation(latitude: Double, longitude: Double) | ||
|
|
||
| /** | ||
| * Sets the last known location from an Android [Location] object. | ||
| * | ||
| * Convenience overload for apps that already have a [Location] instance | ||
| * from their own location system. | ||
| * | ||
| * @param location the Android Location object to track | ||
| */ | ||
| fun setLastKnownLocation(location: Location) | ||
|
|
||
| /** | ||
| * Starts a single location update and sends the result to Customer.io. | ||
| * | ||
| * No-ops if location tracking is disabled or permission is not granted. | ||
| * Only one request at a time; calling again cancels any in-flight request | ||
| * and starts a new one. | ||
| * | ||
| * The SDK does not request location permission. The host app must request | ||
| * runtime permissions and only call this when permission is granted. | ||
| */ | ||
| fun requestLocationUpdateOnce() | ||
|
|
||
| /** | ||
| * Cancels any in-flight location request. | ||
| * No-op if nothing is in progress. | ||
| */ | ||
| fun stopLocationUpdates() | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,65 @@ | ||
| package io.customer.location | ||
|
|
||
| import android.location.Location | ||
| import io.customer.sdk.communication.Event | ||
| import io.customer.sdk.communication.EventBus | ||
| import io.customer.sdk.core.util.Logger | ||
|
|
||
| /** | ||
| * Real implementation of [LocationServices]. | ||
| * Handles manual location setting with validation and config checks. | ||
| * | ||
| * SDK-managed location (requestLocationUpdateOnce) will be implemented in a future PR. | ||
| */ | ||
| internal class LocationServicesImpl( | ||
| private val config: LocationModuleConfig, | ||
| private val logger: Logger, | ||
| private val eventBus: EventBus | ||
| ) : LocationServices { | ||
|
|
||
| override fun setLastKnownLocation(latitude: Double, longitude: Double) { | ||
| if (!config.enableLocationTracking) { | ||
| logger.debug("Location tracking is disabled, ignoring setLastKnownLocation.") | ||
| return | ||
| } | ||
|
|
||
| if (!isValidCoordinate(latitude, longitude)) { | ||
| logger.error("Invalid coordinates: lat=$latitude, lng=$longitude. Latitude must be [-90, 90] and longitude [-180, 180].") | ||
| return | ||
| } | ||
|
|
||
| logger.debug("Tracking location: lat=$latitude, lng=$longitude") | ||
|
|
||
| val locationData = Event.LocationData( | ||
| latitude = latitude, | ||
| longitude = longitude | ||
| ) | ||
| eventBus.publish(Event.TrackLocationEvent(location = locationData)) | ||
| } | ||
|
|
||
| override fun setLastKnownLocation(location: Location) { | ||
| setLastKnownLocation(location.latitude, location.longitude) | ||
| } | ||
|
|
||
| override fun requestLocationUpdateOnce() { | ||
| // Will be implemented in the SDK-managed location PR | ||
| logger.debug("requestLocationUpdateOnce is not yet implemented.") | ||
| } | ||
|
|
||
| override fun stopLocationUpdates() { | ||
| // Will be implemented in the SDK-managed location PR | ||
| logger.debug("stopLocationUpdates is not yet implemented.") | ||
| } | ||
|
|
||
| companion object { | ||
| /** | ||
| * Validates that latitude is within [-90, 90] and longitude is within [-180, 180]. | ||
| * Also rejects NaN and Infinity values. | ||
| */ | ||
| internal fun isValidCoordinate(latitude: Double, longitude: Double): Boolean { | ||
| if (latitude.isNaN() || latitude.isInfinite()) return false | ||
| if (longitude.isNaN() || longitude.isInfinite()) return false | ||
| return latitude in -90.0..90.0 && longitude in -180.0..180.0 | ||
| } | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,7 +1,9 @@ | ||
| package io.customer.location | ||
|
|
||
| import android.location.Location | ||
| import io.customer.sdk.core.di.SDKComponent | ||
| import io.customer.sdk.core.module.CustomerIOModule | ||
| import io.customer.sdk.core.util.Logger | ||
|
|
||
| /** | ||
| * Location module for Customer.io SDK. | ||
|
|
@@ -23,24 +25,74 @@ import io.customer.sdk.core.module.CustomerIOModule | |
| * .build() | ||
| * | ||
| * CustomerIO.initialize(config) | ||
| * | ||
| * // Then use the location services | ||
| * ModuleLocation.instance().locationServices.setLastKnownLocation(37.7749, -122.4194) | ||
| * ``` | ||
| */ | ||
| class ModuleLocation @JvmOverloads constructor( | ||
| override val moduleConfig: LocationModuleConfig = LocationModuleConfig.Builder().build() | ||
| ) : CustomerIOModule<LocationModuleConfig> { | ||
| override val moduleName: String = MODULE_NAME | ||
|
|
||
| private var _locationServices: LocationServices? = null | ||
|
|
||
| /** | ||
| * Access the location services API. | ||
| * | ||
| * This property is only usable after [CustomerIO.initialize] has been called with | ||
| * [ModuleLocation] registered. The SDK calls [initialize] on all registered modules | ||
| * during startup, which wires up the real implementation. | ||
| * | ||
| * If accessed before initialization (e.g. calling location APIs before | ||
| * [CustomerIO.initialize]), calls will no-op and log an error instead of crashing. | ||
| * This guards against race conditions during app startup or incorrect call order. | ||
| */ | ||
| val locationServices: LocationServices | ||
| get() = _locationServices ?: UninitializedLocationServices(SDKComponent.logger) | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Cached service stays permanently uninitializedMedium Severity
Additional Locations (1)
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The getter evaluates on every access, it's not a cached field. Normal usage (ModuleLocation.instance().locationServices.setLastKnownLocation(...)) always hits the getter, so it |
||
|
|
||
| override fun initialize() { | ||
| // Module initialization will be implemented in future PRs | ||
| _locationServices = LocationServicesImpl( | ||
| config = moduleConfig, | ||
| logger = SDKComponent.logger, | ||
| eventBus = SDKComponent.eventBus | ||
| ) | ||
| } | ||
|
|
||
| companion object { | ||
| const val MODULE_NAME: String = "Location" | ||
|
|
||
| /** | ||
| * Returns the initialized [ModuleLocation] instance. | ||
| * | ||
| * @throws IllegalStateException if the module hasn't been registered with the SDK | ||
| */ | ||
| @JvmStatic | ||
| fun instance(): ModuleLocation { | ||
| return SDKComponent.modules[MODULE_NAME] as? ModuleLocation | ||
| ?: throw IllegalStateException("ModuleLocation not initialized") | ||
| ?: throw IllegalStateException("ModuleLocation not initialized. Add ModuleLocation to CustomerIOConfigBuilder before calling CustomerIO.initialize().") | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Pre-init access still throws via instanceMedium Severity
Additional Locations (1)
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
| } | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * No-op fallback returned when [ModuleLocation.locationServices] is accessed | ||
| * before the SDK has been initialized. Logs an error for each call to help | ||
| * developers diagnose incorrect call order during development. | ||
| */ | ||
| private class UninitializedLocationServices( | ||
| private val logger: Logger | ||
| ) : LocationServices { | ||
|
|
||
| private fun logNotInitialized() { | ||
| logger.error("Location module is not initialized. Call CustomerIO.initialize() with ModuleLocation before using location APIs.") | ||
| } | ||
|
|
||
| override fun setLastKnownLocation(latitude: Double, longitude: Double) = logNotInitialized() | ||
|
|
||
| override fun setLastKnownLocation(location: Location) = logNotInitialized() | ||
|
|
||
| override fun requestLocationUpdateOnce() = logNotInitialized() | ||
|
|
||
| override fun stopLocationUpdates() = logNotInitialized() | ||
| } | ||


There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Location update APIs are non-functional
Medium Severity
requestLocationUpdateOnce()andstopLocationUpdates()are exposed inLocationServicesbut currently only log messages and perform no location request or cancellation. Calling these APIs appears successful but never emits aTrackLocationEvent, so SDK-managed location tracking is effectively unavailable despite the public contract.Additional Locations (1)
location/src/main/kotlin/io/customer/location/LocationServices.kt#L43-L58