Skip to content

Commit b18ac22

Browse files
Shahroz16claude
andcommitted
feat: add manual location API (setLastKnownLocation)
Implement the manual location tracking API allowing host apps to send their own location data to Customer.io: - LocationServices interface: public API with setLastKnownLocation (raw lat/lng and Android Location overloads), requestLocationUpdateOnce, and stopLocationUpdates stubs - LocationServicesImpl: validates coordinates, checks config, posts TrackLocationEvent via EventBus - UninitializedLocationServices: error-logging stub for pre-init calls - ModuleLocation: wires LocationServicesImpl on initialize() Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 572819f commit b18ac22

File tree

4 files changed

+187
-2
lines changed

4 files changed

+187
-2
lines changed

location/api/location.api

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,20 @@ public final class io/customer/location/LocationModuleConfig$Builder : io/custom
1010
public final fun setEnableLocationTracking (Z)Lio/customer/location/LocationModuleConfig$Builder;
1111
}
1212

13+
public abstract interface class io/customer/location/LocationServices {
14+
public abstract fun requestLocationUpdateOnce ()V
15+
public abstract fun setLastKnownLocation (DD)V
16+
public abstract fun setLastKnownLocation (Landroid/location/Location;)V
17+
public abstract fun stopLocationUpdates ()V
18+
}
19+
1320
public final class io/customer/location/ModuleLocation : io/customer/sdk/core/module/CustomerIOModule {
1421
public static final field Companion Lio/customer/location/ModuleLocation$Companion;
1522
public static final field MODULE_NAME Ljava/lang/String;
1623
public fun <init> ()V
1724
public fun <init> (Lio/customer/location/LocationModuleConfig;)V
1825
public synthetic fun <init> (Lio/customer/location/LocationModuleConfig;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
26+
public final fun getLocationServices ()Lio/customer/location/LocationServices;
1927
public fun getModuleConfig ()Lio/customer/location/LocationModuleConfig;
2028
public synthetic fun getModuleConfig ()Lio/customer/sdk/core/module/CustomerIOModuleConfig;
2129
public fun getModuleName ()Ljava/lang/String;
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
package io.customer.location
2+
3+
import android.location.Location
4+
5+
/**
6+
* Public API for the Location module.
7+
*
8+
* Use [ModuleLocation.locationServices] after initializing the SDK with [ModuleLocation]
9+
* to get the instance.
10+
*
11+
* Example:
12+
* ```
13+
* // Manual location from host app's existing location system
14+
* ModuleLocation.instance().locationServices.setLastKnownLocation(37.7749, -122.4194)
15+
*
16+
* // Or pass an Android Location object
17+
* ModuleLocation.instance().locationServices.setLastKnownLocation(androidLocation)
18+
* ```
19+
*/
20+
interface LocationServices {
21+
/**
22+
* Sets the last known location from the host app's existing location system.
23+
*
24+
* Use this method when your app already manages location and you want to
25+
* send that data to Customer.io without the SDK managing location permissions
26+
* or the FusedLocationProviderClient directly.
27+
*
28+
* @param latitude the latitude in degrees, must be between -90 and 90
29+
* @param longitude the longitude in degrees, must be between -180 and 180
30+
*/
31+
fun setLastKnownLocation(latitude: Double, longitude: Double)
32+
33+
/**
34+
* Sets the last known location from an Android [Location] object.
35+
*
36+
* Convenience overload for apps that already have a [Location] instance
37+
* from their own location system.
38+
*
39+
* @param location the Android Location object to track
40+
*/
41+
fun setLastKnownLocation(location: Location)
42+
43+
/**
44+
* Starts a single location update and sends the result to Customer.io.
45+
*
46+
* No-ops if location tracking is disabled or permission is not granted.
47+
* Only one request at a time; calling again cancels any in-flight request
48+
* and starts a new one.
49+
*
50+
* The SDK does not request location permission. The host app must request
51+
* runtime permissions and only call this when permission is granted.
52+
*/
53+
fun requestLocationUpdateOnce()
54+
55+
/**
56+
* Cancels any in-flight location request.
57+
* No-op if nothing is in progress.
58+
*/
59+
fun stopLocationUpdates()
60+
}
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
package io.customer.location
2+
3+
import android.location.Location
4+
import io.customer.sdk.communication.Event
5+
import io.customer.sdk.communication.EventBus
6+
import io.customer.sdk.core.util.Logger
7+
8+
/**
9+
* Real implementation of [LocationServices].
10+
* Handles manual location setting with validation and config checks.
11+
*
12+
* SDK-managed location (requestLocationUpdateOnce) will be implemented in a future PR.
13+
*/
14+
internal class LocationServicesImpl(
15+
private val config: LocationModuleConfig,
16+
private val logger: Logger,
17+
private val eventBus: EventBus
18+
) : LocationServices {
19+
20+
override fun setLastKnownLocation(latitude: Double, longitude: Double) {
21+
if (!config.enableLocationTracking) {
22+
logger.debug("Location tracking is disabled, ignoring setLastKnownLocation.")
23+
return
24+
}
25+
26+
if (!isValidCoordinate(latitude, longitude)) {
27+
logger.error("Invalid coordinates: lat=$latitude, lng=$longitude. Latitude must be [-90, 90] and longitude [-180, 180].")
28+
return
29+
}
30+
31+
logger.debug("Tracking location: lat=$latitude, lng=$longitude")
32+
33+
val locationData = Event.LocationData(
34+
latitude = latitude,
35+
longitude = longitude
36+
)
37+
eventBus.publish(Event.TrackLocationEvent(location = locationData))
38+
}
39+
40+
override fun setLastKnownLocation(location: Location) {
41+
setLastKnownLocation(location.latitude, location.longitude)
42+
}
43+
44+
override fun requestLocationUpdateOnce() {
45+
// Will be implemented in the SDK-managed location PR
46+
logger.debug("requestLocationUpdateOnce is not yet implemented.")
47+
}
48+
49+
override fun stopLocationUpdates() {
50+
// Will be implemented in the SDK-managed location PR
51+
logger.debug("stopLocationUpdates is not yet implemented.")
52+
}
53+
54+
companion object {
55+
/**
56+
* Validates that latitude is within [-90, 90] and longitude is within [-180, 180].
57+
* Also rejects NaN and Infinity values.
58+
*/
59+
internal fun isValidCoordinate(latitude: Double, longitude: Double): Boolean {
60+
if (latitude.isNaN() || latitude.isInfinite()) return false
61+
if (longitude.isNaN() || longitude.isInfinite()) return false
62+
return latitude in -90.0..90.0 && longitude in -180.0..180.0
63+
}
64+
}
65+
}

location/src/main/kotlin/io/customer/location/ModuleLocation.kt

Lines changed: 54 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
package io.customer.location
22

3+
import android.location.Location
34
import io.customer.sdk.core.di.SDKComponent
45
import io.customer.sdk.core.module.CustomerIOModule
6+
import io.customer.sdk.core.util.Logger
57

68
/**
79
* Location module for Customer.io SDK.
@@ -23,24 +25,74 @@ import io.customer.sdk.core.module.CustomerIOModule
2325
* .build()
2426
*
2527
* CustomerIO.initialize(config)
28+
*
29+
* // Then use the location services
30+
* ModuleLocation.instance().locationServices.setLastKnownLocation(37.7749, -122.4194)
2631
* ```
2732
*/
2833
class ModuleLocation @JvmOverloads constructor(
2934
override val moduleConfig: LocationModuleConfig = LocationModuleConfig.Builder().build()
3035
) : CustomerIOModule<LocationModuleConfig> {
3136
override val moduleName: String = MODULE_NAME
3237

38+
private var _locationServices: LocationServices? = null
39+
40+
/**
41+
* Access the location services API.
42+
*
43+
* This property is only usable after [CustomerIO.initialize] has been called with
44+
* [ModuleLocation] registered. The SDK calls [initialize] on all registered modules
45+
* during startup, which wires up the real implementation.
46+
*
47+
* If accessed before initialization (e.g. calling location APIs before
48+
* [CustomerIO.initialize]), calls will no-op and log an error instead of crashing.
49+
* This guards against race conditions during app startup or incorrect call order.
50+
*/
51+
val locationServices: LocationServices
52+
get() = _locationServices ?: UninitializedLocationServices(SDKComponent.logger)
53+
3354
override fun initialize() {
34-
// Module initialization will be implemented in future PRs
55+
_locationServices = LocationServicesImpl(
56+
config = moduleConfig,
57+
logger = SDKComponent.logger,
58+
eventBus = SDKComponent.eventBus
59+
)
3560
}
3661

3762
companion object {
3863
const val MODULE_NAME: String = "Location"
3964

65+
/**
66+
* Returns the initialized [ModuleLocation] instance.
67+
*
68+
* @throws IllegalStateException if the module hasn't been registered with the SDK
69+
*/
4070
@JvmStatic
4171
fun instance(): ModuleLocation {
4272
return SDKComponent.modules[MODULE_NAME] as? ModuleLocation
43-
?: throw IllegalStateException("ModuleLocation not initialized")
73+
?: throw IllegalStateException("ModuleLocation not initialized. Add ModuleLocation to CustomerIOConfigBuilder before calling CustomerIO.initialize().")
4474
}
4575
}
4676
}
77+
78+
/**
79+
* No-op fallback returned when [ModuleLocation.locationServices] is accessed
80+
* before the SDK has been initialized. Logs an error for each call to help
81+
* developers diagnose incorrect call order during development.
82+
*/
83+
private class UninitializedLocationServices(
84+
private val logger: Logger
85+
) : LocationServices {
86+
87+
private fun logNotInitialized() {
88+
logger.error("Location module is not initialized. Call CustomerIO.initialize() with ModuleLocation before using location APIs.")
89+
}
90+
91+
override fun setLastKnownLocation(latitude: Double, longitude: Double) = logNotInitialized()
92+
93+
override fun setLastKnownLocation(location: Location) = logNotInitialized()
94+
95+
override fun requestLocationUpdateOnce() = logNotInitialized()
96+
97+
override fun stopLocationUpdates() = logNotInitialized()
98+
}

0 commit comments

Comments
 (0)