Skip to content

Commit 8d48f3e

Browse files
Shahroz16claude
andcommitted
chore: replace enableLocationTracking boolean with LocationTrackingMode enum
Introduce a 3-state LocationTrackingMode enum (OFF, MANUAL, ON_APP_START) replacing the boolean enableLocationTracking config. OFF disables all location machinery including identify enrichment and track events. MANUAL preserves existing behavior. ON_APP_START auto-captures location on cold start with cacheOnly mode for identify context enrichment. - Add LocationTrackingMode enum with OFF, MANUAL, ON_APP_START - Update LocationModuleConfig to use trackingMode with isEnabled guard - Add cacheOnly parameter to LocationTracker.onLocationReceived() - Guard restore/enrich/subscribe setup behind isEnabled in ModuleLocation - Remove stopLocationUpdates() (only relevant for continuous tracking) - Add request deduplication via job cancellation in LocationServicesImpl - Update public API surface, tests, and sample app Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 1f518e8 commit 8d48f3e

File tree

14 files changed

+220
-99
lines changed

14 files changed

+220
-99
lines changed

location/api/location.api

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,28 @@
11
public final class io/customer/location/LocationModuleConfig : io/customer/sdk/core/module/CustomerIOModuleConfig {
2-
public synthetic fun <init> (ZLkotlin/jvm/internal/DefaultConstructorMarker;)V
3-
public final fun getEnableLocationTracking ()Z
2+
public synthetic fun <init> (Lio/customer/location/LocationTrackingMode;Lkotlin/jvm/internal/DefaultConstructorMarker;)V
3+
public final fun getTrackingMode ()Lio/customer/location/LocationTrackingMode;
44
}
55

66
public final class io/customer/location/LocationModuleConfig$Builder : io/customer/sdk/core/module/CustomerIOModuleConfig$Builder {
77
public fun <init> ()V
88
public fun build ()Lio/customer/location/LocationModuleConfig;
99
public synthetic fun build ()Lio/customer/sdk/core/module/CustomerIOModuleConfig;
10-
public final fun setEnableLocationTracking (Z)Lio/customer/location/LocationModuleConfig$Builder;
10+
public final fun setLocationTrackingMode (Lio/customer/location/LocationTrackingMode;)Lio/customer/location/LocationModuleConfig$Builder;
1111
}
1212

1313
public abstract interface class io/customer/location/LocationServices {
1414
public abstract fun requestLocationUpdate ()V
1515
public abstract fun setLastKnownLocation (DD)V
1616
public abstract fun setLastKnownLocation (Landroid/location/Location;)V
17-
public abstract fun stopLocationUpdates ()V
17+
}
18+
19+
public final class io/customer/location/LocationTrackingMode : java/lang/Enum {
20+
public static final field MANUAL Lio/customer/location/LocationTrackingMode;
21+
public static final field OFF Lio/customer/location/LocationTrackingMode;
22+
public static final field ON_APP_START Lio/customer/location/LocationTrackingMode;
23+
public static fun getEntries ()Lkotlin/enums/EnumEntries;
24+
public static fun valueOf (Ljava/lang/String;)Lio/customer/location/LocationTrackingMode;
25+
public static fun values ()[Lio/customer/location/LocationTrackingMode;
1826
}
1927

2028
public final class io/customer/location/ModuleLocation : io/customer/sdk/core/module/CustomerIOModule {

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

Lines changed: 18 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -8,30 +8,37 @@ import io.customer.sdk.core.module.CustomerIOModuleConfig
88
*/
99
class LocationModuleConfig private constructor(
1010
/**
11-
* Whether location tracking is enabled.
11+
* The tracking mode for the location module.
1212
*
13-
* When false, the location module is effectively disabled and all location
14-
* tracking operations will no-op silently.
13+
* - [LocationTrackingMode.OFF]: Location tracking is disabled; all operations no-op.
14+
* - [LocationTrackingMode.MANUAL]: Host app controls when location is captured (default).
15+
* - [LocationTrackingMode.ON_APP_START]: SDK auto-captures location on cold start,
16+
* caching it for identify context enrichment.
1517
*/
16-
val enableLocationTracking: Boolean
18+
val trackingMode: LocationTrackingMode
1719
) : CustomerIOModuleConfig {
1820

21+
/**
22+
* Whether location tracking is enabled (any mode other than [LocationTrackingMode.OFF]).
23+
*/
24+
internal val isEnabled: Boolean
25+
get() = trackingMode != LocationTrackingMode.OFF
26+
1927
class Builder : CustomerIOModuleConfig.Builder<LocationModuleConfig> {
20-
private var enableLocationTracking: Boolean = true
28+
private var trackingMode: LocationTrackingMode = LocationTrackingMode.MANUAL
2129

2230
/**
23-
* Sets whether location tracking is enabled.
24-
* When disabled, all location operations will no-op silently.
25-
* Default is true.
31+
* Sets the location tracking mode.
32+
* Default is [LocationTrackingMode.MANUAL].
2633
*/
27-
fun setEnableLocationTracking(enable: Boolean): Builder {
28-
this.enableLocationTracking = enable
34+
fun setLocationTrackingMode(mode: LocationTrackingMode): Builder {
35+
this.trackingMode = mode
2936
return this
3037
}
3138

3239
override fun build(): LocationModuleConfig {
3340
return LocationModuleConfig(
34-
enableLocationTracking = enableLocationTracking
41+
trackingMode = trackingMode
3542
)
3643
}
3744
}

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

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,8 @@ internal class LocationOrchestrator(
1717
private val locationProvider: LocationProvider
1818
) {
1919

20-
suspend fun requestLocationUpdate() {
21-
if (!config.enableLocationTracking) {
20+
suspend fun requestLocationUpdate(cacheOnly: Boolean = false) {
21+
if (!config.isEnabled) {
2222
logger.debug("Location tracking is disabled, ignoring requestLocationUpdate.")
2323
return
2424
}
@@ -34,7 +34,7 @@ internal class LocationOrchestrator(
3434
granularity = LocationGranularity.DEFAULT
3535
)
3636
logger.debug("Tracking location: lat=${snapshot.latitude}, lng=${snapshot.longitude}")
37-
locationTracker.onLocationReceived(snapshot.latitude, snapshot.longitude)
37+
locationTracker.onLocationReceived(snapshot.latitude, snapshot.longitude, cacheOnly)
3838
} catch (e: CancellationException) {
3939
logger.debug("Location request was cancelled.")
4040
throw e

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

Lines changed: 1 addition & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -41,20 +41,12 @@ interface LocationServices {
4141
fun setLastKnownLocation(location: Location)
4242

4343
/**
44-
* Starts a single location update and sends the result to Customer.io.
44+
* Requests a single location update and sends the result to Customer.io.
4545
*
4646
* 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.
4947
*
5048
* The SDK does not request location permission. The host app must request
5149
* runtime permissions and only call this when permission is granted.
5250
*/
5351
fun requestLocationUpdate()
54-
55-
/**
56-
* Cancels any in-flight location request.
57-
* No-op if nothing is in progress.
58-
*/
59-
fun stopLocationUpdates()
6052
}

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

Lines changed: 8 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,6 @@ package io.customer.location
22

33
import android.location.Location
44
import io.customer.sdk.core.util.Logger
5-
import java.util.concurrent.locks.ReentrantLock
6-
import kotlin.concurrent.withLock
75
import kotlinx.coroutines.CoroutineScope
86
import kotlinx.coroutines.Job
97
import kotlinx.coroutines.launch
@@ -21,11 +19,10 @@ internal class LocationServicesImpl(
2119
private val scope: CoroutineScope
2220
) : LocationServices {
2321

24-
private val lock = ReentrantLock()
2522
private var currentLocationJob: Job? = null
2623

2724
override fun setLastKnownLocation(latitude: Double, longitude: Double) {
28-
if (!config.enableLocationTracking) {
25+
if (!config.isEnabled) {
2926
logger.debug("Location tracking is disabled, ignoring setLastKnownLocation.")
3027
return
3128
}
@@ -45,36 +42,17 @@ internal class LocationServicesImpl(
4542
}
4643

4744
override fun requestLocationUpdate() {
48-
lock.withLock {
49-
// Cancel any previous in-flight request
50-
currentLocationJob?.cancel()
51-
52-
currentLocationJob = scope.launch {
53-
val thisJob = coroutineContext[Job]
54-
try {
55-
orchestrator.requestLocationUpdate()
56-
} finally {
57-
lock.withLock {
58-
if (currentLocationJob === thisJob) {
59-
currentLocationJob = null
60-
}
61-
}
62-
}
45+
// Cancel any previous in-flight request to avoid concurrent GPS calls
46+
currentLocationJob?.cancel()
47+
currentLocationJob = scope.launch {
48+
try {
49+
orchestrator.requestLocationUpdate()
50+
} finally {
51+
currentLocationJob = null
6352
}
6453
}
6554
}
6655

67-
override fun stopLocationUpdates() {
68-
val job: Job?
69-
lock.withLock {
70-
job = currentLocationJob
71-
currentLocationJob = null
72-
}
73-
// Cancelling the job triggers invokeOnCancellation in FusedLocationProvider's
74-
// suspendCancellableCoroutine, which cancels the CancellationTokenSource.
75-
job?.cancel()
76-
}
77-
7856
companion object {
7957
/**
8058
* Validates that latitude is within [-90, 90] and longitude is within [-180, 180].

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

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -74,16 +74,23 @@ internal class LocationTracker(
7474
}
7575

7676
/**
77-
* Processes an incoming location: caches in memory, persists
78-
* coordinates, and attempts to send a location track event.
77+
* Processes an incoming location: caches in memory, persists coordinates,
78+
* and optionally attempts to send a location track event.
79+
*
80+
* @param cacheOnly when true, updates the in-memory cache and persists
81+
* coordinates but does NOT send a "Location Update" track event.
82+
* Used by ON_APP_START auto-requests to refresh cached location for
83+
* identify context enrichment without generating a discrete event.
7984
*/
80-
fun onLocationReceived(latitude: Double, longitude: Double) {
81-
logger.debug("Location update received: lat=$latitude, lng=$longitude")
85+
fun onLocationReceived(latitude: Double, longitude: Double, cacheOnly: Boolean = false) {
86+
logger.debug("Location update received: lat=$latitude, lng=$longitude, cacheOnly=$cacheOnly")
8287

8388
lastLocation = LocationCoordinates(latitude = latitude, longitude = longitude)
8489
locationPreferenceStore.saveCachedLocation(latitude, longitude)
8590

86-
trySendLocationTrack(latitude, longitude)
91+
if (!cacheOnly) {
92+
trySendLocationTrack(latitude, longitude)
93+
}
8794
}
8895

8996
/**
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
package io.customer.location
2+
3+
/**
4+
* Defines the tracking mode for the location module.
5+
*/
6+
enum class LocationTrackingMode {
7+
/**
8+
* Location tracking is disabled.
9+
* All location operations will no-op silently.
10+
*/
11+
OFF,
12+
13+
/**
14+
* Host app controls when location is captured.
15+
* Use [LocationServices.setLastKnownLocation] or [LocationServices.requestLocationUpdate]
16+
* to manually trigger location capture. This is the default mode.
17+
*/
18+
MANUAL,
19+
20+
/**
21+
* SDK automatically captures location on cold start.
22+
*
23+
* The auto-captured location is cached for identify context enrichment
24+
* without immediately sending a track event. Manual location APIs
25+
* ([LocationServices.setLastKnownLocation], [LocationServices.requestLocationUpdate])
26+
* remain fully functional and will send track events as usual.
27+
*/
28+
ON_APP_START
29+
}

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

Lines changed: 34 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -12,21 +12,23 @@ import io.customer.sdk.core.module.CustomerIOModule
1212
import io.customer.sdk.core.pipeline.DataPipeline
1313
import io.customer.sdk.core.pipeline.identifyHookRegistry
1414
import io.customer.sdk.core.util.Logger
15+
import kotlinx.coroutines.launch
1516

1617
/**
1718
* Location module for Customer.io SDK.
1819
*
1920
* This module provides location tracking capabilities including:
2021
* - Manual location setting from host app's existing location system
2122
* - One-shot SDK-managed location capture
23+
* - Automatic location capture on app start (ON_APP_START mode)
2224
*
2325
* Usage:
2426
* ```
2527
* val config = CustomerIOConfigBuilder(appContext, "your-api-key")
2628
* .addCustomerIOModule(
2729
* ModuleLocation(
2830
* LocationModuleConfig.Builder()
29-
* .setEnableLocationTracking(true)
31+
* .setLocationTrackingMode(LocationTrackingMode.MANUAL)
3032
* .build()
3133
* )
3234
* )
@@ -67,6 +69,7 @@ class ModuleLocation @JvmOverloads constructor(
6769
val logger = SDKComponent.logger
6870
val eventBus = SDKComponent.eventBus
6971
val context = SDKComponent.android().applicationContext
72+
val locationScope = SDKComponent.scopeProvider.locationScope
7073

7174
val dataPipeline = SDKComponent.getOrNull<DataPipeline>()
7275
val store = LocationPreferenceStoreImpl(context, logger)
@@ -75,6 +78,27 @@ class ModuleLocation @JvmOverloads constructor(
7578
)
7679
val locationTracker = LocationTracker(dataPipeline, store, locationSyncFilter, logger)
7780

81+
val locationProvider = FusedLocationProvider(context)
82+
val orchestrator = LocationOrchestrator(
83+
config = moduleConfig,
84+
logger = logger,
85+
locationTracker = locationTracker,
86+
locationProvider = locationProvider
87+
)
88+
89+
_locationServices = LocationServicesImpl(
90+
config = moduleConfig,
91+
logger = logger,
92+
locationTracker = locationTracker,
93+
orchestrator = orchestrator,
94+
scope = locationScope
95+
)
96+
97+
// When OFF, skip all background machinery — no restoration, no enrichment,
98+
// no event subscriptions. LocationServicesImpl has its own isEnabled guards
99+
// for the public API, so callers get silent no-ops with helpful log messages.
100+
if (!moduleConfig.isEnabled) return
101+
78102
locationTracker.restorePersistedLocation()
79103

80104
// Register as IdentifyHook so location is added to identify event context
@@ -92,21 +116,15 @@ class ModuleLocation @JvmOverloads constructor(
92116
}
93117
}
94118

95-
val locationProvider = FusedLocationProvider(context)
96-
val orchestrator = LocationOrchestrator(
97-
config = moduleConfig,
98-
logger = logger,
99-
locationTracker = locationTracker,
100-
locationProvider = locationProvider
101-
)
102-
103-
_locationServices = LocationServicesImpl(
104-
config = moduleConfig,
105-
logger = logger,
106-
locationTracker = locationTracker,
107-
orchestrator = orchestrator,
108-
scope = SDKComponent.scopeProvider.locationScope
109-
)
119+
// ON_APP_START: auto-capture location on cold start.
120+
// Fire-and-forget — refreshes cached location so subsequent identify() calls
121+
// carry fresh data. If permissions are denied or services disabled, orchestrator
122+
// silently no-ops.
123+
if (moduleConfig.trackingMode == LocationTrackingMode.ON_APP_START) {
124+
locationScope.launch {
125+
orchestrator.requestLocationUpdate(cacheOnly = true)
126+
}
127+
}
110128
}
111129

112130
companion object {
@@ -143,6 +161,4 @@ private class UninitializedLocationServices(
143161
override fun setLastKnownLocation(location: Location) = logNotInitialized()
144162

145163
override fun requestLocationUpdate() = logNotInitialized()
146-
147-
override fun stopLocationUpdates() = logNotInitialized()
148164
}

0 commit comments

Comments
 (0)