Skip to content

Commit cc37ac3

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 7843bc0 commit cc37ac3

File tree

4 files changed

+156
-2
lines changed

4 files changed

+156
-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: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,24 +23,45 @@ import io.customer.sdk.core.module.CustomerIOModule
2323
* .build()
2424
*
2525
* CustomerIO.initialize(config)
26+
*
27+
* // Then use the location services
28+
* ModuleLocation.instance().locationServices.setLastKnownLocation(37.7749, -122.4194)
2629
* ```
2730
*/
2831
class ModuleLocation @JvmOverloads constructor(
2932
override val moduleConfig: LocationModuleConfig = LocationModuleConfig.Builder().build()
3033
) : CustomerIOModule<LocationModuleConfig> {
3134
override val moduleName: String = MODULE_NAME
3235

36+
/**
37+
* Access the location services API.
38+
* Available after [initialize] is called by the SDK.
39+
*
40+
* @throws UninitializedPropertyAccessException if accessed before initialization
41+
*/
42+
lateinit var locationServices: LocationServices
43+
private set
44+
3345
override fun initialize() {
34-
// Module initialization will be implemented in future PRs
46+
locationServices = LocationServicesImpl(
47+
config = moduleConfig,
48+
logger = SDKComponent.logger,
49+
eventBus = SDKComponent.eventBus
50+
)
3551
}
3652

3753
companion object {
3854
const val MODULE_NAME: String = "Location"
3955

56+
/**
57+
* Returns the initialized [ModuleLocation] instance.
58+
*
59+
* @throws IllegalStateException if the module hasn't been registered with the SDK
60+
*/
4061
@JvmStatic
4162
fun instance(): ModuleLocation {
4263
return SDKComponent.modules[MODULE_NAME] as? ModuleLocation
43-
?: throw IllegalStateException("ModuleLocation not initialized")
64+
?: throw IllegalStateException("ModuleLocation not initialized. Add ModuleLocation to CustomerIOConfigBuilder before calling CustomerIO.initialize().")
4465
}
4566
}
4667
}

0 commit comments

Comments
 (0)