Skip to content

Commit e480d68

Browse files
Shahroz16claude
andcommitted
chore: split LocationTracker into enrichment and sync concerns, add NOT_DETERMINED auth status
Split the monolithic LocationTracker into LocationEnrichmentProvider (identify context enrichment) and LocationSyncCoordinator (persistence + sync filter + track events) to match iOS SDK architecture. Both register as IdentifyHook with their own reset responsibilities. Add NOT_DETERMINED to AuthorizationStatus and PERMISSION_NOT_DETERMINED to LocationProviderError for API completeness with iOS. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 93e89ba commit e480d68

12 files changed

+324
-252
lines changed
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
package io.customer.location
2+
3+
/**
4+
* Internal location coordinate holder.
5+
*/
6+
internal data class LocationCoordinates(
7+
val latitude: Double,
8+
val longitude: Double
9+
)
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
package io.customer.location
2+
3+
import io.customer.location.store.LocationPreferenceStore
4+
import io.customer.sdk.core.pipeline.IdentifyHook
5+
import io.customer.sdk.core.util.Logger
6+
7+
/**
8+
* Provides location data for identify event context enrichment.
9+
*
10+
* Every identify() call enriches the event context with the latest
11+
* location coordinates. This is unfiltered — a new user always gets
12+
* the device's current location on their profile immediately.
13+
*
14+
* On clearIdentify(), [resetContext] clears the in-memory cache and
15+
* persisted coordinates synchronously during analytics.reset().
16+
*/
17+
internal class LocationEnrichmentProvider(
18+
private val locationPreferenceStore: LocationPreferenceStore,
19+
private val logger: Logger
20+
) : IdentifyHook {
21+
22+
@Volatile
23+
private var lastLocation: LocationCoordinates? = null
24+
25+
override fun getIdentifyContext(): Map<String, Any> {
26+
val location = lastLocation ?: return emptyMap()
27+
return mapOf(
28+
"location_latitude" to location.latitude,
29+
"location_longitude" to location.longitude
30+
)
31+
}
32+
33+
override fun resetContext() {
34+
lastLocation = null
35+
locationPreferenceStore.clearCachedLocation()
36+
logger.debug("Location enrichment state reset")
37+
}
38+
39+
/**
40+
* Updates the in-memory location cache used for identify context.
41+
* Called by [LocationSyncCoordinator] when a new location is received.
42+
*/
43+
fun updateLocation(coordinates: LocationCoordinates) {
44+
lastLocation = coordinates
45+
}
46+
47+
/**
48+
* Reads persisted cached location from the preference store and sets the
49+
* in-memory cache so that identify events have location context
50+
* immediately after SDK restart.
51+
*/
52+
fun restorePersistedLocation() {
53+
val lat = locationPreferenceStore.getCachedLatitude() ?: return
54+
val lng = locationPreferenceStore.getCachedLongitude() ?: return
55+
lastLocation = LocationCoordinates(latitude = lat, longitude = lng)
56+
logger.debug("Restored persisted location: lat=$lat, lng=$lng")
57+
}
58+
}

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import kotlinx.coroutines.CancellationException
1313
internal class LocationOrchestrator(
1414
private val config: LocationModuleConfig,
1515
private val logger: Logger,
16-
private val locationTracker: LocationTracker,
16+
private val syncCoordinator: LocationSyncCoordinator,
1717
private val locationProvider: LocationProvider
1818
) {
1919

@@ -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+
syncCoordinator.onLocationReceived(snapshot.latitude, snapshot.longitude)
3838
} catch (e: CancellationException) {
3939
logger.debug("Location request was cancelled.")
4040
throw e

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ import kotlinx.coroutines.launch
1414
internal class LocationServicesImpl(
1515
private val config: LocationModuleConfig,
1616
private val logger: Logger,
17-
private val locationTracker: LocationTracker,
17+
private val syncCoordinator: LocationSyncCoordinator,
1818
private val orchestrator: LocationOrchestrator,
1919
private val scope: CoroutineScope
2020
) : LocationServices {
@@ -34,7 +34,7 @@ internal class LocationServicesImpl(
3434

3535
logger.debug("Tracking location: lat=$latitude, lng=$longitude")
3636

37-
locationTracker.onLocationReceived(latitude, longitude)
37+
syncCoordinator.onLocationReceived(latitude, longitude)
3838
}
3939

4040
override fun setLastKnownLocation(location: Location) {
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
package io.customer.location
2+
3+
import io.customer.location.store.LocationPreferenceStore
4+
import io.customer.location.sync.LocationSyncFilter
5+
import io.customer.sdk.core.pipeline.DataPipeline
6+
import io.customer.sdk.core.pipeline.IdentifyHook
7+
import io.customer.sdk.core.util.Logger
8+
import io.customer.sdk.util.EventNames
9+
10+
/**
11+
* Coordinates location persistence and "Location Update" track events.
12+
*
13+
* When a location is received, it is persisted, the enrichment provider's
14+
* in-memory cache is updated, and the sync filter is evaluated to decide
15+
* whether a track event should be sent. The track event is gated by a
16+
* userId check and the 24h / 1km sync filter to avoid redundant events.
17+
*
18+
* Implements [IdentifyHook] solely for [resetContext] — clears sync filter
19+
* state on clearIdentify() so the next user starts with a fresh baseline.
20+
*/
21+
internal class LocationSyncCoordinator(
22+
private val dataPipeline: DataPipeline?,
23+
private val locationPreferenceStore: LocationPreferenceStore,
24+
private val locationSyncFilter: LocationSyncFilter,
25+
private val enrichmentProvider: LocationEnrichmentProvider,
26+
private val logger: Logger
27+
) : IdentifyHook {
28+
29+
override fun getIdentifyContext(): Map<String, Any> = emptyMap()
30+
31+
override fun resetContext() {
32+
locationSyncFilter.clearSyncedData()
33+
logger.debug("Location sync state reset")
34+
}
35+
36+
/**
37+
* Processes an incoming location: updates the enrichment provider's
38+
* in-memory cache, persists coordinates, and attempts to send a
39+
* location track event.
40+
*/
41+
fun onLocationReceived(latitude: Double, longitude: Double) {
42+
logger.debug("Location update received: lat=$latitude, lng=$longitude")
43+
44+
enrichmentProvider.updateLocation(LocationCoordinates(latitude = latitude, longitude = longitude))
45+
locationPreferenceStore.saveCachedLocation(latitude, longitude)
46+
47+
trySendLocationTrack(latitude, longitude)
48+
}
49+
50+
/**
51+
* Called when a user is identified. Attempts to sync the cached
52+
* location as a track event for the newly identified user.
53+
*
54+
* The identify event itself already carries location via
55+
* [LocationEnrichmentProvider.getIdentifyContext] — this method handles
56+
* the supplementary "Location Update" track event, subject to the sync filter.
57+
*/
58+
fun onUserIdentified() {
59+
syncCachedLocationIfNeeded()
60+
}
61+
62+
/**
63+
* Re-evaluates the cached location for sending.
64+
* Called on identify (via [onUserIdentified]) and on cold start
65+
* (via replayed UserChangedEvent) to handle cases where a location
66+
* was cached but not yet sent for the current user.
67+
*/
68+
internal fun syncCachedLocationIfNeeded() {
69+
val lat = locationPreferenceStore.getCachedLatitude() ?: return
70+
val lng = locationPreferenceStore.getCachedLongitude() ?: return
71+
72+
logger.debug("Re-evaluating cached location: lat=$lat, lng=$lng")
73+
trySendLocationTrack(lat, lng)
74+
}
75+
76+
/**
77+
* Applies the userId gate and sync filter, then sends a location
78+
* track event via [DataPipeline] if both pass.
79+
*/
80+
private fun trySendLocationTrack(latitude: Double, longitude: Double) {
81+
val pipeline = dataPipeline ?: return
82+
if (!pipeline.isUserIdentified) return
83+
if (!locationSyncFilter.filterAndRecord(latitude, longitude)) return
84+
85+
logger.debug("Sending location track: lat=$latitude, lng=$longitude")
86+
pipeline.track(
87+
name = EventNames.LOCATION_UPDATE,
88+
properties = mapOf(
89+
"latitude" to latitude,
90+
"longitude" to longitude
91+
)
92+
)
93+
}
94+
}

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

Lines changed: 0 additions & 141 deletions
This file was deleted.

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

Lines changed: 16 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -76,20 +76,27 @@ class ModuleLocation @JvmOverloads constructor(
7676
val locationSyncFilter = LocationSyncFilter(
7777
LocationSyncStoreImpl(context, logger)
7878
)
79-
val locationTracker = LocationTracker(dataPipeline, store, locationSyncFilter, logger)
79+
val enrichmentProvider = LocationEnrichmentProvider(store, logger)
80+
val syncCoordinator = LocationSyncCoordinator(
81+
dataPipeline,
82+
store,
83+
locationSyncFilter,
84+
enrichmentProvider,
85+
logger
86+
)
8087

8188
val locationProvider = FusedLocationProvider(context)
8289
val orchestrator = LocationOrchestrator(
8390
config = moduleConfig,
8491
logger = logger,
85-
locationTracker = locationTracker,
92+
syncCoordinator = syncCoordinator,
8693
locationProvider = locationProvider
8794
)
8895

8996
_locationServices = LocationServicesImpl(
9097
config = moduleConfig,
9198
logger = logger,
92-
locationTracker = locationTracker,
99+
syncCoordinator = syncCoordinator,
93100
orchestrator = orchestrator,
94101
scope = locationScope
95102
)
@@ -99,20 +106,19 @@ class ModuleLocation @JvmOverloads constructor(
99106
// for the public API, so callers get silent no-ops with helpful log messages.
100107
if (!moduleConfig.isEnabled) return
101108

102-
locationTracker.restorePersistedLocation()
109+
enrichmentProvider.restorePersistedLocation()
103110

104-
// Register as IdentifyHook so location is added to identify event context
105-
// and cleared synchronously during analytics.reset(). This ensures every
106-
// identify() call carries the device's current location in the event context —
107-
// the primary way location reaches a user's profile.
108-
SDKComponent.identifyHookRegistry.register(locationTracker)
111+
// Register both as IdentifyHooks — enrichment provider adds location to
112+
// identify context, sync coordinator clears sync filter state on reset.
113+
SDKComponent.identifyHookRegistry.register(enrichmentProvider)
114+
SDKComponent.identifyHookRegistry.register(syncCoordinator)
109115

110116
// On identify, attempt to send a supplementary "Location Update" track event.
111117
// The identify event itself already carries location via context enrichment —
112118
// this track event is for journey/segment triggers in the user's timeline.
113119
eventBus.subscribe<Event.UserChangedEvent> {
114120
if (!it.userId.isNullOrEmpty()) {
115-
locationTracker.onUserIdentified()
121+
syncCoordinator.onUserIdentified()
116122
}
117123
}
118124

0 commit comments

Comments
 (0)