diff --git a/example/src/main/AndroidManifest.xml b/example/src/main/AndroidManifest.xml index 156797b6..6897078a 100644 --- a/example/src/main/AndroidManifest.xml +++ b/example/src/main/AndroidManifest.xml @@ -14,6 +14,7 @@ + ?, user: RadarUser?) { // pass through flush replay onComplete for track callback - callback?.onComplete(status) + if (status != RadarStatus.SUCCESS && RadarSettings.getSdkConfiguration(context).useOfflineRTOUpdates) { + RadarOfflineManager().contextualizeLocation(context, trackLocation, object : RadarOfflineManager.RadarOfflineCallback { + override fun onComplete(config: RadarConfig?) { + if (config != null) { + callback?.onComplete(status, null, null, null, null, config) + } else { + callback?.onComplete(status) + } + } + }) + } else { + callback?.onComplete(status) + } } } ) @@ -425,9 +439,20 @@ internal class RadarApiClient( } Radar.sendError(status) - - callback?.onComplete(status) - + if (RadarSettings.getSdkConfiguration(context).useOfflineRTOUpdates) { + Radar.logger.d("network issue encountered, falling back to RadarOfflineManager") + RadarOfflineManager().contextualizeLocation(context, location, object : RadarOfflineManager.RadarOfflineCallback { + override fun onComplete(config: RadarConfig?) { + if (config != null) { + callback?.onComplete(status, null, null, null, null, config) + } else { + callback?.onComplete(status) + } + } + }) + } else { + callback?.onComplete(status) + } return } diff --git a/sdk/src/main/java/io/radar/sdk/RadarLocationManager.kt b/sdk/src/main/java/io/radar/sdk/RadarLocationManager.kt index c516105d..95dd01e1 100644 --- a/sdk/src/main/java/io/radar/sdk/RadarLocationManager.kt +++ b/sdk/src/main/java/io/radar/sdk/RadarLocationManager.kt @@ -353,7 +353,8 @@ internal class RadarLocationManager( } } - private fun replaceSyncedGeofences(radarGeofences: Array?) { + internal fun replaceSyncedGeofences(radarGeofences: Array?) { + RadarState.setNearbyGeofences(context, radarGeofences) this.removeSyncedGeofences() { success -> this.addSyncedGeofences(radarGeofences) } @@ -620,7 +621,9 @@ internal class RadarLocationManager( config: RadarConfig?, token: RadarVerifiedLocationToken? ) { - locationManager.replaceSyncedGeofences(nearbyGeofences) + if (status == RadarStatus.SUCCESS) { + locationManager.replaceSyncedGeofences(nearbyGeofences) + } if (options.foregroundServiceEnabled && foregroundService.updatesOnly) { locationManager.stopForegroundService() diff --git a/sdk/src/main/java/io/radar/sdk/RadarOfflineManager.kt b/sdk/src/main/java/io/radar/sdk/RadarOfflineManager.kt new file mode 100644 index 00000000..4c817d06 --- /dev/null +++ b/sdk/src/main/java/io/radar/sdk/RadarOfflineManager.kt @@ -0,0 +1,91 @@ +package io.radar.sdk + +import android.content.Context +import android.location.Location +import io.radar.sdk.model.RadarRemoteTrackingOptions +import io.radar.sdk.model.RadarCircleGeometry +import io.radar.sdk.model.RadarConfig +import io.radar.sdk.model.RadarCoordinate +import io.radar.sdk.model.RadarPolygonGeometry +import org.json.JSONObject + +class RadarOfflineManager { + interface RadarOfflineCallback { + fun onComplete(config: RadarConfig?) + } + internal fun contextualizeLocation(context: Context, location: Location, callback: RadarOfflineCallback) { + var newGeofenceIds = mutableSetOf() + var newGeofenceTags = mutableSetOf() + val nearbyGeofences = RadarState.getNearbyGeofences(context) + if (nearbyGeofences == null) { + Radar.logger.d("skipping as no synced nearby geofence") + callback.onComplete(null) + return + } + for (geofence in nearbyGeofences) { + var center: RadarCoordinate? = null + var radius = 100.0 + if (geofence.geometry is RadarCircleGeometry) { + center = geofence.geometry.center + radius = geofence.geometry.radius + } else if (geofence.geometry is RadarPolygonGeometry) { + center = geofence.geometry.center + radius = geofence.geometry.radius + } else { + Radar.logger.e("Unsupported geofence geometry type") + continue + } + if (isPointInsideCircle(center, radius, location)) { + newGeofenceIds.add(geofence._id) + if (geofence.tag != null) { + newGeofenceTags.add(geofence.tag) + } + } + } + RadarState.setGeofenceIds(context,newGeofenceIds) + val sdkConfiguration = RadarSettings.getSdkConfiguration(context) + val rampUpGeofenceTags = RadarRemoteTrackingOptions.getGeofenceTagsWithKey(sdkConfiguration.remoteTrackingOptions, "inGeofence") + var isRampedUp = false + if (!rampUpGeofenceTags.isNullOrEmpty()) { + for (tag in rampUpGeofenceTags) { + if (newGeofenceTags.contains(tag)) { + isRampedUp = true + break + } + } + } + var newTrackingOptions: RadarTrackingOptions? = null + if (isRampedUp) { + // ramp up + Radar.logger.d("Ramp up geofences with trackingOptions: $sdkConfiguration.inGeofenceTrackingOptions") + newTrackingOptions = RadarRemoteTrackingOptions.getRemoteTrackingOptionsWithKey(sdkConfiguration.remoteTrackingOptions, "inGeofence") + } else { + val tripOptions = RadarSettings.getTripOptions(context) + if (tripOptions != null && RadarRemoteTrackingOptions.getRemoteTrackingOptionsWithKey(sdkConfiguration.remoteTrackingOptions, "onTrip") != null){ + Radar.logger.d("Ramp down geofences with trackingOptions: $sdkConfiguration.6onTripTrackingOptions") + newTrackingOptions = RadarRemoteTrackingOptions.getRemoteTrackingOptionsWithKey(sdkConfiguration.remoteTrackingOptions, "onTrip") + } else { + Radar.logger.d("Ramp down geofences with trackingOptions: $sdkConfiguration.defaultTrackingOptions") + newTrackingOptions = RadarRemoteTrackingOptions.getRemoteTrackingOptionsWithKey(sdkConfiguration.remoteTrackingOptions, "default") + } + } + if (newTrackingOptions != null) { + val metaDict = JSONObject() + metaDict.put("trackingOptions", newTrackingOptions.toJson()) + val configDict = JSONObject() + configDict.put("meta", metaDict) + callback.onComplete(RadarConfig.fromJson(configDict)) + return + } + callback.onComplete(null) + return + } + + private fun isPointInsideCircle(center: RadarCoordinate, radius: Double, point: Location): Boolean { + val centerLocation = Location("centerLocation") + centerLocation.latitude = center.latitude + centerLocation.longitude = center.longitude + val distanceBetween = point.distanceTo(centerLocation) + return distanceBetween <= radius + } +} diff --git a/sdk/src/main/java/io/radar/sdk/RadarState.kt b/sdk/src/main/java/io/radar/sdk/RadarState.kt index 5710582c..d6703fe5 100644 --- a/sdk/src/main/java/io/radar/sdk/RadarState.kt +++ b/sdk/src/main/java/io/radar/sdk/RadarState.kt @@ -7,6 +7,8 @@ import android.os.Build import androidx.annotation.RequiresApi import androidx.core.content.edit import io.radar.sdk.model.RadarBeacon +import io.radar.sdk.model.RadarGeofence +import org.json.JSONArray import org.json.JSONObject internal object RadarState { @@ -39,6 +41,7 @@ internal object RadarState { private const val KEY_LAST_BEACON_UUIDS = "last_beacon_uuids" private const val KEY_LAST_BEACON_UIDS = "last_beacon_uids" private const val KEY_LAST_MOTION_ACTIVITY = "last_motion_activity" + private const val KEY_NEARBY_GEOFENCES = "nearby_geofences" private fun getSharedPreferences(context: Context): SharedPreferences { return context.getSharedPreferences("RadarSDK", Context.MODE_PRIVATE) @@ -277,4 +280,15 @@ internal object RadarState { } } + internal fun getNearbyGeofences(context: Context): Array? { + val jsonString = getSharedPreferences(context).getString(KEY_NEARBY_GEOFENCES, "") + val jsonArray = jsonString?.let { JSONArray(it) } + return RadarGeofence.fromJson(jsonArray) + } + + internal fun setNearbyGeofences(context: Context, nearbyGeofences: Array?) { + val jsonString = RadarGeofence.toJson(nearbyGeofences).toString() + getSharedPreferences(context).edit { putString(KEY_NEARBY_GEOFENCES, jsonString) } + } + } diff --git a/sdk/src/main/java/io/radar/sdk/RadarVerificationManager.kt b/sdk/src/main/java/io/radar/sdk/RadarVerificationManager.kt index 751e45cb..94ab0179 100644 --- a/sdk/src/main/java/io/radar/sdk/RadarVerificationManager.kt +++ b/sdk/src/main/java/io/radar/sdk/RadarVerificationManager.kt @@ -125,7 +125,7 @@ internal class RadarVerificationManager( config: RadarConfig?, token: RadarVerifiedLocationToken? ) { - if (status == Radar.RadarStatus.SUCCESS) { + if (config != null) { Radar.locationManager.updateTrackingFromMeta( config?.meta ) diff --git a/sdk/src/main/java/io/radar/sdk/model/RadarConfig.kt b/sdk/src/main/java/io/radar/sdk/model/RadarConfig.kt index 7630e721..43a6d176 100644 --- a/sdk/src/main/java/io/radar/sdk/model/RadarConfig.kt +++ b/sdk/src/main/java/io/radar/sdk/model/RadarConfig.kt @@ -2,7 +2,7 @@ package io.radar.sdk.model import org.json.JSONObject -internal data class RadarConfig( +data class RadarConfig( val meta: RadarMeta, val googlePlayProjectNumber: Long?, val nonce: String? diff --git a/sdk/src/main/java/io/radar/sdk/model/RadarGeofence.kt b/sdk/src/main/java/io/radar/sdk/model/RadarGeofence.kt index dacab0c9..5d447812 100644 --- a/sdk/src/main/java/io/radar/sdk/model/RadarGeofence.kt +++ b/sdk/src/main/java/io/radar/sdk/model/RadarGeofence.kt @@ -87,13 +87,13 @@ class RadarGeofence( } ?: RadarCoordinate(0.0, 0.0) val radius = obj.optDouble(FIELD_GEOMETRY_RADIUS) val geometry = when (obj.optString(FIELD_TYPE)) { - TYPE_CIRCLE -> { + TYPE_CIRCLE, TYPE_GEOMETRY_CIRCLE -> { RadarCircleGeometry( center, radius ) } - TYPE_POLYGON, TYPE_ISOCHRONE -> { + TYPE_POLYGON, TYPE_ISOCHRONE, TYPE_GEOMETRY_POLYGON -> { val geometryObj = obj.optJSONObject(FIELD_GEOMETRY) val coordinatesArr = geometryObj?.optJSONArray(FIELD_COORDINATES) if (coordinatesArr != null) { diff --git a/sdk/src/main/java/io/radar/sdk/model/RadarMeta.kt b/sdk/src/main/java/io/radar/sdk/model/RadarMeta.kt index bc3de1fb..7de26f9c 100644 --- a/sdk/src/main/java/io/radar/sdk/model/RadarMeta.kt +++ b/sdk/src/main/java/io/radar/sdk/model/RadarMeta.kt @@ -3,7 +3,7 @@ package io.radar.sdk.model import io.radar.sdk.RadarTrackingOptions import org.json.JSONObject -internal data class RadarMeta( +data class RadarMeta( val remoteTrackingOptions: RadarTrackingOptions?, val sdkConfiguration: RadarSdkConfiguration?, ) { diff --git a/sdk/src/main/java/io/radar/sdk/model/RadarRemoteTrackingOptions.kt b/sdk/src/main/java/io/radar/sdk/model/RadarRemoteTrackingOptions.kt new file mode 100644 index 00000000..dd33cb61 --- /dev/null +++ b/sdk/src/main/java/io/radar/sdk/model/RadarRemoteTrackingOptions.kt @@ -0,0 +1,94 @@ +package io.radar.sdk.model + +import io.radar.sdk.RadarTrackingOptions +import org.json.JSONArray +import org.json.JSONObject + +class RadarRemoteTrackingOptions( + val type: String, + val trackingOptions: RadarTrackingOptions, + val geofenceTags: Array? +) { + internal companion object { + private const val TYPE_ID = "type" + private const val TRACKING_OPTIONS = "trackingOptions" + private const val GEOFENCE_TAGS = "geofenceTags" + + @JvmStatic + fun fromJson(obj: JSONObject?): RadarRemoteTrackingOptions? { + if (obj == null) { + return null + } + val type = obj.optString(TYPE_ID) + val trackingOptions = RadarTrackingOptions.fromJson(obj.optJSONObject(TRACKING_OPTIONS)) + val geofenceTags = obj.optJSONArray(GEOFENCE_TAGS)?.let { tags -> + (0 until tags.length()).map { index -> + tags.getString(index) + }.toTypedArray() + } + return RadarRemoteTrackingOptions(type, trackingOptions, geofenceTags) + } + + @JvmStatic + fun fromJson(arr: JSONArray?): Array? { + if (arr == null) { + return null + } + return Array(arr.length()) { index -> + fromJson(arr.optJSONObject(index)) + }.filterNotNull().toTypedArray() + } + + @JvmStatic + fun toJson(remoteTrackingOptions: Array?): JSONArray? { + if (remoteTrackingOptions == null) { + return null + } + val arr = JSONArray() + remoteTrackingOptions.forEach { remoteTrackingOption -> + arr.put(remoteTrackingOption.toJson()) + } + return arr + } + + @JvmStatic + fun getRemoteTrackingOptionsWithKey(remoteTrackingOptions: Array?, key: String): RadarTrackingOptions? { + if (remoteTrackingOptions == null) { + return null + } + for (remoteTrackingOption in remoteTrackingOptions) { + if (remoteTrackingOption.type == key) { + return remoteTrackingOption.trackingOptions + } + } + return null + } + + @JvmStatic + fun getGeofenceTagsWithKey(remoteTrackingOptions: Array?, key: String): Array? { + if (remoteTrackingOptions == null) { + return null + } + var geofenceTags: Array? = null + for (remoteTrackingOption in remoteTrackingOptions) { + if (remoteTrackingOption.type == key) { + geofenceTags = remoteTrackingOption.geofenceTags + } + } + return geofenceTags + } + } + + fun toJson(): JSONObject { + val obj = JSONObject() + obj.putOpt(TYPE_ID, type) + obj.putOpt(TRACKING_OPTIONS, trackingOptions.toJson()) + val geofenceTagsArr = JSONArray() + if (geofenceTags != null) { + geofenceTags.forEach { geofenceTag -> geofenceTagsArr.put(geofenceTag) } + obj.putOpt(GEOFENCE_TAGS, geofenceTagsArr) + } + return obj + } + +} \ No newline at end of file diff --git a/sdk/src/main/java/io/radar/sdk/model/RadarSdkConfiguration.kt b/sdk/src/main/java/io/radar/sdk/model/RadarSdkConfiguration.kt index f6403ac0..f467708f 100644 --- a/sdk/src/main/java/io/radar/sdk/model/RadarSdkConfiguration.kt +++ b/sdk/src/main/java/io/radar/sdk/model/RadarSdkConfiguration.kt @@ -9,7 +9,7 @@ import org.json.JSONObject /** * Represents server-side configuration settings. */ -internal data class RadarSdkConfiguration( +data class RadarSdkConfiguration( val maxConcurrentJobs: Int, val schedulerRequiresNetwork: Boolean, val usePersistence: Boolean, @@ -21,7 +21,9 @@ internal data class RadarSdkConfiguration( val trackOnceOnAppOpen: Boolean, val useLocationMetadata: Boolean, val useOpenedAppConversion: Boolean = false, -) { + val useOfflineRTOUpdates: Boolean, + val remoteTrackingOptions: Array?, + ) { companion object { private const val MAX_CONCURRENT_JOBS = "maxConcurrentJobs" private const val DEFAULT_MAX_CONCURRENT_JOBS = 1 @@ -35,7 +37,8 @@ internal data class RadarSdkConfiguration( private const val TRACK_ONCE_ON_APP_OPEN = "trackOnceOnAppOpen" private const val USE_LOCATION_METADATA = "useLocationMetadata" private const val USE_OPENED_APP_CONVERSION = "useOpenedAppConversion" - + private const val USE_OFFLINE_RTO_UPDATES = "useOfflineRTOUpdates" + private const val ALTERNATIVE_TRACKING_OPTIONS = "remoteTrackingOptions" fun fromJson(json: JSONObject?): RadarSdkConfiguration { // set json as empty object if json is null, which uses fallback values @@ -53,6 +56,8 @@ internal data class RadarSdkConfiguration( config.optBoolean(TRACK_ONCE_ON_APP_OPEN, false), config.optBoolean(USE_LOCATION_METADATA, false), config.optBoolean(USE_OPENED_APP_CONVERSION, true), + config.optBoolean(USE_OFFLINE_RTO_UPDATES, false), + config.optJSONArray(ALTERNATIVE_TRACKING_OPTIONS)?.let { RadarRemoteTrackingOptions.fromJson(it) }, ) } @@ -82,6 +87,8 @@ internal data class RadarSdkConfiguration( putOpt(TRACK_ONCE_ON_APP_OPEN, trackOnceOnAppOpen) putOpt(USE_LOCATION_METADATA, useLocationMetadata) putOpt(USE_OPENED_APP_CONVERSION, useOpenedAppConversion) + putOpt(USE_OFFLINE_RTO_UPDATES, useOfflineRTOUpdates) + putOpt(ALTERNATIVE_TRACKING_OPTIONS, RadarRemoteTrackingOptions.toJson(remoteTrackingOptions)) } } } diff --git a/sdk/src/test/java/io/radar/sdk/RadarTest.kt b/sdk/src/test/java/io/radar/sdk/RadarTest.kt index e4245056..1f928091 100644 --- a/sdk/src/test/java/io/radar/sdk/RadarTest.kt +++ b/sdk/src/test/java/io/radar/sdk/RadarTest.kt @@ -5,6 +5,7 @@ import android.location.Location import android.os.Build import androidx.test.core.app.ApplicationProvider import androidx.test.ext.junit.runners.AndroidJUnit4 +import io.radar.sdk.Radar.locationManager import io.radar.sdk.model.* import org.json.JSONObject import org.junit.Assert.* @@ -331,7 +332,7 @@ class RadarTest { icon = 1337, updatesOnly = true, )) - // Radar.setNotificationOptions has side effects on foregroundServiceOptions. + // Radar.setNotificationOptions has side effects on foregroundServiceOptions. Radar.setNotificationOptions(RadarNotificationOptions( "foo", "red", @@ -591,15 +592,15 @@ class RadarTest { updatesOnly = true, iconColor = "#FF0000" )) - - val options = RadarTrackingOptions.EFFICIENT - options.desiredAccuracy = RadarTrackingOptions.RadarTrackingOptionsDesiredAccuracy.LOW val now = Date() - options.startTrackingAfter = now - options.stopTrackingAfter = Date(now.time + 1000) - options.sync = RadarTrackingOptions.RadarTrackingOptionsSync.NONE - options.syncGeofences = true - options.syncGeofencesLimit = 100 + val options = RadarTrackingOptions.EFFICIENT.copy( + desiredAccuracy = RadarTrackingOptions.RadarTrackingOptionsDesiredAccuracy.LOW, + startTrackingAfter = now, + stopTrackingAfter = Date(now.time + 1000), + sync = RadarTrackingOptions.RadarTrackingOptionsSync.NONE, + syncGeofences = true, + syncGeofencesLimit = 100 + ) Radar.startTracking(options) assertEquals(options, Radar.getTrackingOptions()) assertTrue(Radar.isTracking()) @@ -701,12 +702,18 @@ class RadarTest { @Test fun test_Radar_startTrip() { + apiHelperMock.mockStatus = Radar.RadarStatus.SUCCESS + permissionsHelperMock.mockFineLocationPermissionGranted = true val tripOptions = getTestTripOptions() + val latch = CountDownLatch(1) Radar.startTrip(tripOptions) { status, trip, events -> - assertEquals(tripOptions, Radar.getTripOptions()) - assertFalse(Radar.isTracking()) + latch.countDown() } + ShadowLooper.runUiThreadTasksIncludingDelayedTasks() + latch.await(LATCH_TIMEOUT, TimeUnit.SECONDS) + assertEquals(tripOptions, Radar.getTripOptions()) + assertTrue(Radar.isTracking()) } @Test @@ -798,7 +805,6 @@ class RadarTest { ShadowLooper.runUiThreadTasksIncludingDelayedTasks() latch.await(LATCH_TIMEOUT, TimeUnit.SECONDS) - assertEquals(Radar.RadarStatus.ERROR_PERMISSIONS, callbackStatus) } @@ -1549,7 +1555,7 @@ class RadarTest { @Test fun test_Radar_setSdkConfiguration() { - val sdkConfiguration = RadarSdkConfiguration(1, false, false, false, false, false, Radar.RadarLogLevel.WARNING, true, true, true) + val sdkConfiguration = RadarSdkConfiguration(1, false, false, false, false, false, Radar.RadarLogLevel.WARNING, true, true, true,true, true, null) RadarSettings.setUserDebug(context, false) RadarSettings.setSdkConfiguration(context, sdkConfiguration) @@ -1572,7 +1578,7 @@ class RadarTest { latch.countDown() } }) - + ShadowLooper.runUiThreadTasksIncludingDelayedTasks() latch.await(LATCH_TIMEOUT, TimeUnit.SECONDS) @@ -1586,5 +1592,184 @@ class RadarTest { assertEquals(true, savedSdkConfiguration?.startTrackingOnInitialize) assertEquals(true, savedSdkConfiguration?.trackOnceOnAppOpen) assertEquals(true,savedSdkConfiguration?.useLocationMetadata) + assertEquals(true, savedSdkConfiguration.useOfflineRTOUpdates) + assertEquals(RadarTrackingOptions.EFFICIENT, RadarRemoteTrackingOptions.getRemoteTrackingOptionsWithKey(savedSdkConfiguration.remoteTrackingOptions,"default")) + assertEquals(RadarTrackingOptions.RESPONSIVE, RadarRemoteTrackingOptions.getRemoteTrackingOptionsWithKey(savedSdkConfiguration.remoteTrackingOptions,"inGeofence")) + assertEquals(RadarTrackingOptions.CONTINUOUS, RadarRemoteTrackingOptions.getRemoteTrackingOptionsWithKey(savedSdkConfiguration.remoteTrackingOptions,"onTrip")) + assertEquals(arrayOf("venue")[0], + (RadarRemoteTrackingOptions.getGeofenceTagsWithKey(savedSdkConfiguration.remoteTrackingOptions,"inGeofence"))?.get(0) ?: "" + ) + } + + @Test + fun test_Radar_trackOnce_offlineRampUp() { + RadarSettings.setTripOptions(context, null) + apiHelperMock.mockStatus = Radar.RadarStatus.SUCCESS + apiHelperMock.mockResponse = RadarTestUtils.jsonObjectFromResource("/get_config_response.json") + + Radar.apiClient.getConfig("sdkConfigUpdate", false, object : RadarApiClient.RadarGetConfigApiCallback { + override fun onComplete(status: Radar.RadarStatus, config: RadarConfig?) { + + if (config != null) { + RadarSettings.setSdkConfiguration(context, config.meta.sdkConfiguration) + locationManager.updateTrackingFromMeta(config.meta) + } + assertEquals(RadarTrackingOptions.EFFICIENT, Radar.getTrackingOptions()) + assertEquals(status, Radar.RadarStatus.SUCCESS) + permissionsHelperMock.mockFineLocationPermissionGranted = true + val mockLocation = Location("RadarSDK") + mockLocation.latitude = 40.783825 + mockLocation.longitude = -73.975365 + mockLocation.accuracy = 65f + mockLocation.time = System.currentTimeMillis() + locationClientMock.mockLocation = mockLocation + apiHelperMock.mockStatus = Radar.RadarStatus.SUCCESS + apiHelperMock.mockResponse = RadarTestUtils.jsonObjectFromResource("/track.json") + val latch = CountDownLatch(1) + var firstTrackOnceStatus = Radar.RadarStatus.SUCCESS + Radar.trackOnce { status, _, _, _ -> + firstTrackOnceStatus = status + latch.countDown() + } + ShadowLooper.runUiThreadTasksIncludingDelayedTasks() + latch.await(LATCH_TIMEOUT, TimeUnit.SECONDS) + assertEquals(firstTrackOnceStatus, Radar.RadarStatus.SUCCESS) + var secondTrackOnceStatus = Radar.RadarStatus.SUCCESS + apiHelperMock.mockStatus = Radar.RadarStatus.ERROR_NETWORK + val latch2 = CountDownLatch(1) + Radar.trackOnce(mockLocation) { status, _, _, _ -> + secondTrackOnceStatus = status + latch2.countDown() + } + ShadowLooper.runUiThreadTasksIncludingDelayedTasks() + latch2.await(LATCH_TIMEOUT, TimeUnit.SECONDS) + assertEquals(secondTrackOnceStatus, Radar.RadarStatus.ERROR_NETWORK) + assertEquals(RadarTrackingOptions.RESPONSIVE,Radar.getTrackingOptions()) + } + }) + } + @Test + fun test_Radar_trackOnce_offlineRampDown_default() { + RadarSettings.setTripOptions(context, null) + apiHelperMock.mockStatus = Radar.RadarStatus.SUCCESS + apiHelperMock.mockResponse = + RadarTestUtils.jsonObjectFromResource("/get_config_response.json") + + Radar.apiClient.getConfig( + "sdkConfigUpdate", + false, + object : RadarApiClient.RadarGetConfigApiCallback { + override fun onComplete(status: Radar.RadarStatus, config: RadarConfig?) { + + if (config != null) { + RadarSettings.setSdkConfiguration(context, config.meta.sdkConfiguration) + locationManager.updateTrackingFromMeta(config.meta) + } + assertEquals(RadarTrackingOptions.EFFICIENT, Radar.getTrackingOptions()) + assertEquals(status, Radar.RadarStatus.SUCCESS) + permissionsHelperMock.mockFineLocationPermissionGranted = true + val mockLocation = Location("RadarSDK") + mockLocation.latitude = 40.783825 + mockLocation.longitude = -73.975365 + mockLocation.accuracy = 65f + mockLocation.time = System.currentTimeMillis() + locationClientMock.mockLocation = mockLocation + apiHelperMock.mockStatus = Radar.RadarStatus.SUCCESS + apiHelperMock.addMockResponse("v1/track", RadarTestUtils.jsonObjectFromResource("/rampup.json")!!) + val latch = CountDownLatch(1) + var firstTrackOnceStatus = Radar.RadarStatus.SUCCESS + Radar.trackOnce { status, _, _, _ -> + firstTrackOnceStatus = status + latch.countDown() + } + ShadowLooper.runUiThreadTasksIncludingDelayedTasks() + latch.await(LATCH_TIMEOUT, TimeUnit.SECONDS) + assertEquals(firstTrackOnceStatus, Radar.RadarStatus.SUCCESS) + // need to fix this + assertEquals( + RadarTrackingOptions.RESPONSIVE, + Radar.getTrackingOptions() + ) + apiHelperMock.mockStatus = Radar.RadarStatus.ERROR_NETWORK + mockLocation.latitude = 50.783825 + mockLocation.longitude = -63.975365 + val latch2 = CountDownLatch(1) + var secondTrackOnceStatus = Radar.RadarStatus.SUCCESS + Radar.trackOnce(mockLocation) { status, _, _, _ -> + secondTrackOnceStatus = status + latch2.countDown() + } + ShadowLooper.runUiThreadTasksIncludingDelayedTasks() + latch2.await(LATCH_TIMEOUT, TimeUnit.SECONDS) + assertEquals(secondTrackOnceStatus, Radar.RadarStatus.ERROR_NETWORK) + assertEquals( + RadarTrackingOptions.EFFICIENT, + Radar.getTrackingOptions() + ) + apiHelperMock.addMockResponse("v1/track", RadarTestUtils.jsonObjectFromResource("/track.json")!!) + } + } + ) + } + + @Test + fun test_Radar_trackOnce_offlineRampDown_trips() { + apiHelperMock.mockStatus = Radar.RadarStatus.SUCCESS + val tripOptions = getTestTripOptions() + val latch = CountDownLatch(1) + Radar.startTrip(tripOptions) { _, _, _ -> + latch.countDown() + } + ShadowLooper.runUiThreadTasksIncludingDelayedTasks() + latch.await(LATCH_TIMEOUT, TimeUnit.SECONDS) + apiHelperMock.mockStatus = Radar.RadarStatus.SUCCESS + apiHelperMock.mockResponse = RadarTestUtils.jsonObjectFromResource("/get_config_response.json") + + Radar.apiClient.getConfig("sdkConfigUpdate", false, object : RadarApiClient.RadarGetConfigApiCallback { + override fun onComplete(status: Radar.RadarStatus, config: RadarConfig?) { + + if (config != null) { + RadarSettings.setSdkConfiguration(context, config.meta.sdkConfiguration) + locationManager.updateTrackingFromMeta(config.meta) + } + assertEquals(RadarTrackingOptions.EFFICIENT, Radar.getTrackingOptions()) + assertEquals(status, Radar.RadarStatus.SUCCESS) + permissionsHelperMock.mockFineLocationPermissionGranted = true + val mockLocation = Location("RadarSDK") + mockLocation.latitude = 40.783825 + mockLocation.longitude = -73.975365 + mockLocation.accuracy = 65f + mockLocation.time = System.currentTimeMillis() + locationClientMock.mockLocation = mockLocation + apiHelperMock.mockStatus = Radar.RadarStatus.SUCCESS + apiHelperMock.addMockResponse("v1/track", RadarTestUtils.jsonObjectFromResource("/rampup.json")!!) + val latch2 = CountDownLatch(1) + var firstTrackOnceStatus = Radar.RadarStatus.SUCCESS + Radar.trackOnce { status, _, _, _ -> + firstTrackOnceStatus = status + latch2.countDown() + } + ShadowLooper.runUiThreadTasksIncludingDelayedTasks() + latch2.await(LATCH_TIMEOUT, TimeUnit.SECONDS) + assertEquals(firstTrackOnceStatus, Radar.RadarStatus.SUCCESS) + assertEquals(RadarTrackingOptions.RESPONSIVE,Radar.getTrackingOptions()) + mockLocation.latitude = 50.783825 + mockLocation.longitude = -63.975365 + apiHelperMock.mockStatus = Radar.RadarStatus.ERROR_NETWORK + var secondTrackOnceStatus = Radar.RadarStatus.SUCCESS + val latch3 = CountDownLatch(1) + Radar.trackOnce(mockLocation) { status, _, _, _ -> + secondTrackOnceStatus = status + latch3.countDown() + } + ShadowLooper.runUiThreadTasksIncludingDelayedTasks() + latch3.await(LATCH_TIMEOUT, TimeUnit.SECONDS) + assertEquals(secondTrackOnceStatus, Radar.RadarStatus.ERROR_NETWORK) + assertEquals(RadarTrackingOptions.CONTINUOUS,Radar.getTrackingOptions()) + // clean up + RadarSettings.setTripOptions(context, null) + apiHelperMock.addMockResponse("v1/track", RadarTestUtils.jsonObjectFromResource("/track.json")!!) + } + }) } } diff --git a/sdk/src/test/java/io/radar/sdk/model/RadarMetaTest.kt b/sdk/src/test/java/io/radar/sdk/model/RadarMetaTest.kt index 85bc7a56..cf4c6766 100644 --- a/sdk/src/test/java/io/radar/sdk/model/RadarMetaTest.kt +++ b/sdk/src/test/java/io/radar/sdk/model/RadarMetaTest.kt @@ -25,24 +25,24 @@ class RadarMetaTest { val obj = config.meta assertNotNull(obj.remoteTrackingOptions) assertEquals(trackingOptions.desiredStoppedUpdateInterval, obj.remoteTrackingOptions!!.desiredStoppedUpdateInterval) - assertEquals(trackingOptions.fastestStoppedUpdateInterval, obj.remoteTrackingOptions.fastestStoppedUpdateInterval) - assertEquals(trackingOptions.desiredMovingUpdateInterval, obj.remoteTrackingOptions.desiredMovingUpdateInterval) - assertEquals(trackingOptions.fastestMovingUpdateInterval, obj.remoteTrackingOptions.fastestMovingUpdateInterval) - assertEquals(trackingOptions.desiredSyncInterval, obj.remoteTrackingOptions.desiredSyncInterval) - assertEquals(trackingOptions.desiredAccuracy, obj.remoteTrackingOptions.desiredAccuracy) - assertEquals(trackingOptions.stopDuration, obj.remoteTrackingOptions.stopDuration) - assertEquals(trackingOptions.stopDistance, obj.remoteTrackingOptions.stopDistance) - assertEquals(trackingOptions.startTrackingAfter, obj.remoteTrackingOptions.startTrackingAfter) - assertEquals(trackingOptions.stopTrackingAfter, obj.remoteTrackingOptions.stopTrackingAfter) - assertEquals(trackingOptions.replay, obj.remoteTrackingOptions.replay) - assertEquals(trackingOptions.sync, obj.remoteTrackingOptions.sync) - assertEquals(trackingOptions.useStoppedGeofence, obj.remoteTrackingOptions.useStoppedGeofence) - assertEquals(trackingOptions.stoppedGeofenceRadius, obj.remoteTrackingOptions.stoppedGeofenceRadius) - assertEquals(trackingOptions.useMovingGeofence, obj.remoteTrackingOptions.useMovingGeofence) - assertEquals(trackingOptions.movingGeofenceRadius, obj.remoteTrackingOptions.movingGeofenceRadius) - assertEquals(trackingOptions.syncGeofences, obj.remoteTrackingOptions.syncGeofences) - assertEquals(trackingOptions.syncGeofencesLimit, obj.remoteTrackingOptions.syncGeofencesLimit) - assertEquals(trackingOptions.foregroundServiceEnabled, obj.remoteTrackingOptions.foregroundServiceEnabled) - assertEquals(trackingOptions.beacons, obj.remoteTrackingOptions.beacons) + assertEquals(trackingOptions.fastestStoppedUpdateInterval, obj.remoteTrackingOptions!!.fastestStoppedUpdateInterval) + assertEquals(trackingOptions.desiredMovingUpdateInterval, obj.remoteTrackingOptions!!.desiredMovingUpdateInterval) + assertEquals(trackingOptions.fastestMovingUpdateInterval, obj.remoteTrackingOptions!!.fastestMovingUpdateInterval) + assertEquals(trackingOptions.desiredSyncInterval, obj.remoteTrackingOptions!!.desiredSyncInterval) + assertEquals(trackingOptions.desiredAccuracy, obj.remoteTrackingOptions!!.desiredAccuracy) + assertEquals(trackingOptions.stopDuration, obj.remoteTrackingOptions!!.stopDuration) + assertEquals(trackingOptions.stopDistance, obj.remoteTrackingOptions!!.stopDistance) + assertEquals(trackingOptions.startTrackingAfter, obj.remoteTrackingOptions!!.startTrackingAfter) + assertEquals(trackingOptions.stopTrackingAfter, obj.remoteTrackingOptions!!.stopTrackingAfter) + assertEquals(trackingOptions.replay, obj.remoteTrackingOptions!!.replay) + assertEquals(trackingOptions.sync, obj.remoteTrackingOptions!!.sync) + assertEquals(trackingOptions.useStoppedGeofence, obj.remoteTrackingOptions!!.useStoppedGeofence) + assertEquals(trackingOptions.stoppedGeofenceRadius, obj.remoteTrackingOptions!!.stoppedGeofenceRadius) + assertEquals(trackingOptions.useMovingGeofence, obj.remoteTrackingOptions!!.useMovingGeofence) + assertEquals(trackingOptions.movingGeofenceRadius, obj.remoteTrackingOptions!!.movingGeofenceRadius) + assertEquals(trackingOptions.syncGeofences, obj.remoteTrackingOptions!!.syncGeofences) + assertEquals(trackingOptions.syncGeofencesLimit, obj.remoteTrackingOptions!!.syncGeofencesLimit) + assertEquals(trackingOptions.foregroundServiceEnabled, obj.remoteTrackingOptions!!.foregroundServiceEnabled) + assertEquals(trackingOptions.beacons, obj.remoteTrackingOptions!!.beacons) } } \ No newline at end of file diff --git a/sdk/src/test/java/io/radar/sdk/model/RadarSdkConfigurationTest.kt b/sdk/src/test/java/io/radar/sdk/model/RadarSdkConfigurationTest.kt index 8d6b076f..9b45f101 100644 --- a/sdk/src/test/java/io/radar/sdk/model/RadarSdkConfigurationTest.kt +++ b/sdk/src/test/java/io/radar/sdk/model/RadarSdkConfigurationTest.kt @@ -1,9 +1,12 @@ package io.radar.sdk.model import io.radar.sdk.Radar +import io.radar.sdk.RadarTrackingOptions +import org.json.JSONArray import org.json.JSONObject import org.junit.Assert.assertEquals import org.junit.Assert.assertFalse +import org.junit.Assert.assertNull import org.junit.Assert.assertTrue import org.junit.Before import org.junit.Test @@ -29,6 +32,12 @@ class RadarSdkConfigurationTest { private var trackOnceOnAppOpen = false private var useLocationMetadata = false private var useOpenedAppConversion = false + private var useOfflineRTOUpdates = false + private var remoteTrackingOptions = + arrayOf(RadarRemoteTrackingOptions("default",RadarTrackingOptions.EFFICIENT,null), + RadarRemoteTrackingOptions("onTrip", RadarTrackingOptions.CONTINUOUS,null), + RadarRemoteTrackingOptions("inGeofence", RadarTrackingOptions.RESPONSIVE, arrayOf("venue")) + ) @Before fun setUp() { @@ -46,14 +55,87 @@ class RadarSdkConfigurationTest { "startTrackingOnInitialize":$startTrackingOnInitialize, "trackOnceOnAppOpen":$trackOnceOnAppOpen, "useLocationMetadata":$useLocationMetadata, - "useOpenedAppConversion":$useOpenedAppConversion + "useOpenedAppConversion":$useOpenedAppConversion, + "useOfflineRTOUpdates":$useOfflineRTOUpdates, + "remoteTrackingOptions": [ + { + "type": "default", + "trackingOptions":{ + "desiredStoppedUpdateInterval": 3600, + "fastestStoppedUpdateInterval": 1200, + "desiredMovingUpdateInterval": 1200, + "fastestMovingUpdateInterval": 360, + "desiredSyncInterval": 140, + "desiredAccuracy": "medium", + "stopDuration": 140, + "stopDistance": 70, + "replay": "stops", + "sync": "all", + "useStoppedGeofence": false, + "stoppedGeofenceRadius": 0, + "useMovingGeofence": false, + "movingGeofenceRadius": 0, + "syncGeofences": true, + "syncGeofencesLimit": 10, + "foregroundServiceEnabled": false, + "beacons": false + } + }, + { + "type": "onTrip", + "trackingOptions":{ + "desiredStoppedUpdateInterval": 30, + "fastestStoppedUpdateInterval": 30, + "desiredMovingUpdateInterval": 30, + "fastestMovingUpdateInterval": 30, + "desiredSyncInterval": 20, + "desiredAccuracy": "high", + "stopDuration": 140, + "stopDistance": 70, + "replay": "none", + "sync": "all", + "useStoppedGeofence": false, + "stoppedGeofenceRadius": 0, + "useMovingGeofence": false, + "movingGeofenceRadius": 0, + "syncGeofences": true, + "syncGeofencesLimit": 0, + "foregroundServiceEnabled": true, + "beacons": false + } + }, + { + "type":"inGeofence", + "trackingOptions":{ + "desiredStoppedUpdateInterval": 0, + "fastestStoppedUpdateInterval": 0, + "desiredMovingUpdateInterval": 150, + "fastestMovingUpdateInterval": 30, + "desiredSyncInterval": 20, + "desiredAccuracy": "medium", + "stopDuration": 140, + "stopDistance": 70, + "replay": "stops", + "sync": "all", + "useStoppedGeofence": true, + "stoppedGeofenceRadius": 100, + "useMovingGeofence": true, + "movingGeofenceRadius": 100, + "syncGeofences": true, + "syncGeofencesLimit": 10, + "foregroundServiceEnabled": false, + "beacons": false + }, + "geofenceTags":["venue"] + } + ] }""".trimIndent() } @Test fun testToJson() { assertEquals( - JSONObject(jsonString).toString(), + JSONObject(jsonString).toMap(), RadarSdkConfiguration( maxConcurrentJobs, requiresNetwork, @@ -65,11 +147,29 @@ class RadarSdkConfigurationTest { startTrackingOnInitialize, trackOnceOnAppOpen, useLocationMetadata, - useOpenedAppConversion - ).toJson().toString() + useOpenedAppConversion, + useOfflineRTOUpdates, + remoteTrackingOptions + ).toJson().toMap() ) } + fun JSONObject.toMap(): Map = keys().asSequence().associateWith { key -> + when (val value = this[key]) { + is JSONArray -> value.toList() + is JSONObject -> value.toMap() + else -> value + } + } + + fun JSONArray.toList(): List = (0 until length()).map { index -> + when (val value = get(index)) { + is JSONArray -> value.toList() + is JSONObject -> value.toMap() + else -> value + } + } + @Test fun testFromJson() { val settings = RadarSdkConfiguration.fromJson(JSONObject(jsonString)) @@ -84,6 +184,14 @@ class RadarSdkConfigurationTest { assertEquals(trackOnceOnAppOpen, settings.trackOnceOnAppOpen) assertEquals(useLocationMetadata, settings.useLocationMetadata) assertEquals(useOpenedAppConversion, settings.useOpenedAppConversion) + assertEquals(useOfflineRTOUpdates, settings.useOfflineRTOUpdates) + assertEquals(RadarTrackingOptions.EFFICIENT, RadarRemoteTrackingOptions.getRemoteTrackingOptionsWithKey(settings.remoteTrackingOptions,"default")) + assertEquals(RadarTrackingOptions.RESPONSIVE, RadarRemoteTrackingOptions.getRemoteTrackingOptionsWithKey(settings.remoteTrackingOptions,"inGeofence")) + assertEquals(RadarTrackingOptions.CONTINUOUS, RadarRemoteTrackingOptions.getRemoteTrackingOptionsWithKey(settings.remoteTrackingOptions,"onTrip")) + assertEquals(arrayOf("venue")[0], + RadarRemoteTrackingOptions.getGeofenceTagsWithKey(settings.remoteTrackingOptions,"inGeofence") + ?.get(0) ?: "" + ) } @Test @@ -100,6 +208,9 @@ class RadarSdkConfigurationTest { assertFalse(settings.trackOnceOnAppOpen) assertFalse(settings.useLocationMetadata) assertTrue(settings.useOpenedAppConversion) + assertFalse(settings.useOfflineRTOUpdates) + assertNull(settings.remoteTrackingOptions) + } private fun String.removeWhitespace(): String = replace("\\s".toRegex(), "") diff --git a/sdk/src/test/resources/get_config_response.json b/sdk/src/test/resources/get_config_response.json index 38eafa83..aadd5b6e 100644 --- a/sdk/src/test/resources/get_config_response.json +++ b/sdk/src/test/resources/get_config_response.json @@ -1,18 +1,119 @@ { - "meta": { - "code": 200, - "featureSettings": { - "useLocationMetadata": false, - "useRadarKVStore": true, - "useRadarModifiedBeacon": true, - "radarLowPowerManagerDesiredAccuracy": 3000, - "radarLowPowerManagerDistanceFilter": 3000 + "meta": { + "code": 200, + "featureSettings": { + "useLocationMetadata": false, + "useRadarKVStore": true, + "useRadarModifiedBeacon": true, + "radarLowPowerManagerDesiredAccuracy": 3000, + "radarLowPowerManagerDistanceFilter": 3000 + }, + "sdkConfiguration": { + "logLevel": "info", + "startTrackingOnInitialize": true, + "trackOnceOnAppOpen": true, + "useLocationMetadata": true, + "useOfflineRTOUpdates": true, + "remoteTrackingOptions": [ + { + "type": "inGeofence", + "trackingOptions": { + "desiredStoppedUpdateInterval": 0, + "fastestStoppedUpdateInterval": 0, + "desiredMovingUpdateInterval": 150, + "fastestMovingUpdateInterval": 30, + "desiredSyncInterval": 20, + "desiredAccuracy": "medium", + "stopDuration": 140, + "stopDistance": 70, + "startTrackingAfter": null, + "stopTrackingAfter": null, + "replay": "stops", + "sync": "all", + "useStoppedGeofence": true, + "stoppedGeofenceRadius": 100, + "useMovingGeofence": true, + "movingGeofenceRadius": 100, + "syncGeofences": true, + "syncGeofencesLimit": 10, + "foregroundServiceEnabled": false, + "beacons": false + }, + "geofenceTags": ["venue"] }, - "sdkConfiguration": { - "logLevel": "info", - "startTrackingOnInitialize": true, - "trackOnceOnAppOpen": true, - "useLocationMetadata": true + { + "type": "onTrip", + "trackingOptions": { + "desiredStoppedUpdateInterval": 30, + "fastestStoppedUpdateInterval": 30, + "desiredMovingUpdateInterval": 30, + "fastestMovingUpdateInterval": 30, + "desiredSyncInterval": 20, + "desiredAccuracy": "high", + "stopDuration": 140, + "stopDistance": 70, + "startTrackingAfter": null, + "stopTrackingAfter": null, + "replay": "none", + "sync": "all", + "useStoppedGeofence": false, + "stoppedGeofenceRadius": 0, + "useMovingGeofence": false, + "movingGeofenceRadius": 0, + "syncGeofences": true, + "syncGeofencesLimit": 0, + "foregroundServiceEnabled": true, + "beacons": false + } + }, + { + "type": "default", + "trackingOptions": { + "desiredStoppedUpdateInterval": 3600, + "fastestStoppedUpdateInterval": 1200, + "desiredMovingUpdateInterval": 1200, + "fastestMovingUpdateInterval": 360, + "desiredSyncInterval": 140, + "desiredAccuracy": "medium", + "stopDuration": 140, + "stopDistance": 70, + "startTrackingAfter": null, + "stopTrackingAfter": null, + "replay": "stops", + "sync": "all", + "useStoppedGeofence": false, + "stoppedGeofenceRadius": 0, + "useMovingGeofence": false, + "movingGeofenceRadius": 0, + "syncGeofences": true, + "syncGeofencesLimit": 10, + "foregroundServiceEnabled": false, + "beacons": false + } } + ] + }, + "trackingOptions": { + "desiredStoppedUpdateInterval": 3600, + "fastestStoppedUpdateInterval": 1200, + "desiredMovingUpdateInterval": 1200, + "fastestMovingUpdateInterval": 360, + "desiredSyncInterval": 140, + "desiredAccuracy": "medium", + "stopDuration": 140, + "stopDistance": 70, + "startTrackingAfter": null, + "stopTrackingAfter": null, + "replay": "stops", + "sync": "all", + "useStoppedGeofence": false, + "stoppedGeofenceRadius": 0, + "useMovingGeofence": false, + "movingGeofenceRadius": 0, + "syncGeofences": true, + "syncGeofencesLimit": 10, + "foregroundServiceEnabled": false, + "beacons": false } + } } diff --git a/sdk/src/test/resources/rampup.json b/sdk/src/test/resources/rampup.json new file mode 100644 index 00000000..26f07498 --- /dev/null +++ b/sdk/src/test/resources/rampup.json @@ -0,0 +1,1091 @@ +{ + "meta": { + "code": 200, + "trackingOptions": { + "desiredStoppedUpdateInterval": 0, + "fastestStoppedUpdateInterval": 0, + "desiredMovingUpdateInterval": 150, + "fastestMovingUpdateInterval": 30, + "desiredSyncInterval": 20, + "desiredAccuracy": "medium", + "stopDuration": 140, + "stopDistance": 70, + "startTrackingAfter": null, + "stopTrackingAfter": null, + "replay": "stops", + "sync": "all", + "useStoppedGeofence": true, + "stoppedGeofenceRadius": 100, + "useMovingGeofence": true, + "movingGeofenceRadius": 100, + "syncGeofences": true, + "syncGeofencesLimit": 10, + "foregroundServiceEnabled": false, + "beacons": false + } + }, + "user": { + "location": { + "type": "Point", + "coordinates": [-73.9753651, 40.7838251] + }, + "live": false, + "geofences": [ + { + "geometryCenter": { + "coordinates": [-73.975365, 40.783825], + "type": "Point" + }, + "_id": "5ca7dd72208530002b30683c", + "description": "S3 Test Monk's Café", + "type": "circle", + "metadata": { + "category": "restaurant" + }, + "geometryRadius": 50, + "tag": "venue", + "externalId": "2" + } + ], + "segments": [ + { + "description": "Starbucks Visitors", + "externalId": "starbucks-visitors" + } + ], + "topChains": [ + { + "domain": "starbucks.com", + "name": "Starbucks", + "slug": "starbucks", + "externalId": "811", + "metadata": { + "category": "Coffee & Tea", + "loyalty": false + } + }, + { + "domain": "walgreens.com", + "name": "Walgreens", + "slug": "walgreens", + "externalId": "5", + "metadata": { + "category": "Pharmacy", + "loyalty": false + } + } + ], + "metadata": { + "customId": "123", + "customFlag": false + }, + "description": "User 1", + "_id": "5bb8d4fbfd58d5002103ff5c", + "ip": "68.129.209.28", + "locationAccuracy": 5, + "stopped": true, + "foreground": false, + "deviceId": "A", + "userId": "1", + "actualUpdatedAt": "2019-11-15T17:31:18.454Z", + "updatedAt": "2019-11-15T17:31:18.454Z", + "createdAt": "2018-10-06T15:30:03.713Z", + "place": { + "location": { + "coordinates": [-73.97541, 40.78377], + "type": "Point" + }, + "osmGeometry": { + "coordinates": [] + }, + "categories": [ + "real-estate", + "residence-other", + "apartment-condo-building" + ], + "_id": "59bb1dc10d8998dc02510033", + "name": "129 West 81st Street" + }, + "deviceType": "iOS", + "nearbyPlaceChains": [ + { + "domain": "starbucks.com", + "name": "Starbucks", + "slug": "starbucks", + "externalId": "811", + "metadata": { + "category": "Coffee & Tea", + "loyalty": false, + "pwi": false, + "tlog": false + } + }, + { + "domain": "walgreens.com", + "name": "Walgreens", + "slug": "walgreens", + "externalId": "5", + "metadata": { + "category": "Pharmacy", + "loyalty": false, + "pwi": false, + "tlog": false + } + } + ], + "country": { + "_id": "5cf694f66da6a800683f4d71", + "type": "country", + "name": "United States", + "code": "US" + }, + "state": { + "_id": "5cf695096da6a800683f4e7f", + "type": "state", + "name": "New York", + "code": "NY" + }, + "dma": { + "_id": "5cf695016da6a800683f4e06", + "type": "dma", + "name": "New York", + "code": "501" + }, + "postalCode": { + "_id": "5cf695266da6a800683f5817", + "type": "postalCode", + "name": "10024", + "code": "10024" + }, + "source": "BACKGROUND_LOCATION", + "fraud": { + "passed": true, + "bypassed": true, + "proxy": true, + "mocked": true, + "compromised": true, + "jumped": true + }, + "trip": { + "_id": "5f3e50491c2b7d005c81f5d9", + "live": true, + "externalId": "299", + "metadata": { + "Customer Name": "Jacob Pena", + "Car Model": "Green Honda Civic" + }, + "mode": "car", + "destinationGeofenceTag": "store", + "destinationGeofenceExternalId": "123", + "destinationLocation": { + "coordinates": [-105.061198, 39.779366], + "type": "Point" + }, + "eta": { + "duration": 5.5, + "distance": 1331 + }, + "status": "started" + } + }, + "events": [ + { + "createdAt": "2019-11-15T17:31:18.454Z", + "live": false, + "type": "user.exited_region_state", + "location": { + "type": "Point", + "coordinates": [-73.975365, 40.783825] + }, + "locationAccuracy": 5, + "confidence": 3, + "actualCreatedAt": "2019-11-15T17:31:18.454Z", + "user": { + "segments": [], + "topChains": [ + { + "name": "TravelCenters of America", + "slug": "travelcenters-of-america" + } + ], + "_id": "5bb8d4fbfd58d5002103ff5c", + "deviceId": "A", + "userId": "1", + "state": { + "_id": "5cf695096da6a800683f4e7f", + "type": "state", + "name": "New York", + "code": "NY" + }, + "dma": { + "_id": "5cf695016da6a800683f4e06", + "type": "dma", + "name": "New York", + "code": "501" + }, + "postalCode": { + "_id": "5cf695266da6a800683f5817", + "type": "postalCode", + "name": "10024", + "code": "10024" + } + }, + "region": { + "_id": "5cf695086da6a800683f4e69", + "type": "state", + "name": "Maryland", + "code": "MD" + }, + "_id": "5dcee0e67e71e80027a7eec3", + "country": { + "_id": "5cf694f66da6a800683f4d71", + "type": "country", + "name": "United States", + "code": "US" + }, + "state": { + "_id": "5cf695096da6a800683f4e7f", + "type": "state", + "name": "New York", + "code": "NY" + }, + "postalCode": { + "_id": "5cf695266da6a800683f5817", + "type": "postalCode", + "name": "10024", + "code": "10024" + }, + "dma": { + "_id": "5cf695016da6a800683f4e06", + "type": "dma", + "name": "New York", + "code": "501" + } + }, + { + "createdAt": "2019-11-15T17:31:18.454Z", + "live": false, + "type": "user.entered_region_state", + "location": { + "type": "Point", + "coordinates": [-73.975365, 40.783825] + }, + "locationAccuracy": 5, + "confidence": 3, + "actualCreatedAt": "2019-11-15T17:31:18.454Z", + "user": { + "segments": [], + "topChains": [ + { + "name": "TravelCenters of America", + "slug": "travelcenters-of-america" + } + ], + "_id": "5bb8d4fbfd58d5002103ff5c", + "deviceId": "A", + "userId": "1", + "state": { + "_id": "5cf695096da6a800683f4e7f", + "type": "state", + "name": "New York", + "code": "NY" + }, + "dma": { + "_id": "5cf695016da6a800683f4e06", + "type": "dma", + "name": "New York", + "code": "501" + }, + "postalCode": { + "_id": "5cf695266da6a800683f5817", + "type": "postalCode", + "name": "10024", + "code": "10024" + } + }, + "region": { + "_id": "5cf695096da6a800683f4e7f", + "type": "state", + "name": "New York", + "code": "NY" + }, + "_id": "5dcee0e67e71e80027a7eec4", + "country": { + "_id": "5cf694f66da6a800683f4d71", + "type": "country", + "name": "United States", + "code": "US" + }, + "state": { + "_id": "5cf695096da6a800683f4e7f", + "type": "state", + "name": "New York", + "code": "NY" + }, + "postalCode": { + "_id": "5cf695266da6a800683f5817", + "type": "postalCode", + "name": "10024", + "code": "10024" + }, + "dma": { + "_id": "5cf695016da6a800683f4e06", + "type": "dma", + "name": "New York", + "code": "501" + } + }, + { + "createdAt": "2019-11-15T17:31:18.454Z", + "live": false, + "type": "user.exited_region_dma", + "location": { + "type": "Point", + "coordinates": [-73.975365, 40.783825] + }, + "locationAccuracy": 5, + "confidence": 3, + "actualCreatedAt": "2019-11-15T17:31:18.454Z", + "user": { + "segments": [], + "topChains": [ + { + "name": "TravelCenters of America", + "slug": "travelcenters-of-america" + } + ], + "_id": "5bb8d4fbfd58d5002103ff5c", + "deviceId": "A", + "userId": "1", + "state": { + "_id": "5cf695096da6a800683f4e7f", + "type": "state", + "name": "New York", + "code": "NY" + }, + "dma": { + "_id": "5cf695016da6a800683f4e06", + "type": "dma", + "name": "New York", + "code": "501" + }, + "postalCode": { + "_id": "5cf695266da6a800683f5817", + "type": "postalCode", + "name": "10024", + "code": "10024" + } + }, + "region": { + "_id": "5cf694fb6da6a800683f4d92", + "type": "dma", + "name": "Baltimore", + "code": "512" + }, + "_id": "5dcee0e67e71e80027a7eec5", + "country": { + "_id": "5cf694f66da6a800683f4d71", + "type": "country", + "name": "United States", + "code": "US" + }, + "state": { + "_id": "5cf695096da6a800683f4e7f", + "type": "state", + "name": "New York", + "code": "NY" + }, + "postalCode": { + "_id": "5cf695266da6a800683f5817", + "type": "postalCode", + "name": "10024", + "code": "10024" + }, + "dma": { + "_id": "5cf695016da6a800683f4e06", + "type": "dma", + "name": "New York", + "code": "501" + } + }, + { + "createdAt": "2019-11-15T17:31:18.454Z", + "live": false, + "type": "user.entered_region_dma", + "location": { + "type": "Point", + "coordinates": [-73.975365, 40.783825] + }, + "locationAccuracy": 5, + "confidence": 3, + "actualCreatedAt": "2019-11-15T17:31:18.454Z", + "user": { + "segments": [], + "topChains": [ + { + "name": "TravelCenters of America", + "slug": "travelcenters-of-america" + } + ], + "_id": "5bb8d4fbfd58d5002103ff5c", + "deviceId": "A", + "userId": "1", + "state": { + "_id": "5cf695096da6a800683f4e7f", + "type": "state", + "name": "New York", + "code": "NY" + }, + "dma": { + "_id": "5cf695016da6a800683f4e06", + "type": "dma", + "name": "New York", + "code": "501" + }, + "postalCode": { + "_id": "5cf695266da6a800683f5817", + "type": "postalCode", + "name": "10024", + "code": "10024" + } + }, + "region": { + "_id": "5cf695016da6a800683f4e06", + "type": "dma", + "name": "New York", + "code": "501" + }, + "_id": "5dcee0e67e71e80027a7eec6", + "country": { + "_id": "5cf694f66da6a800683f4d71", + "type": "country", + "name": "United States", + "code": "US" + }, + "state": { + "_id": "5cf695096da6a800683f4e7f", + "type": "state", + "name": "New York", + "code": "NY" + }, + "postalCode": { + "_id": "5cf695266da6a800683f5817", + "type": "postalCode", + "name": "10024", + "code": "10024" + }, + "dma": { + "_id": "5cf695016da6a800683f4e06", + "type": "dma", + "name": "New York", + "code": "501" + } + }, + { + "createdAt": "2019-11-15T17:31:18.454Z", + "live": false, + "type": "user.exited_geofence", + "location": { + "type": "Point", + "coordinates": [-73.975365, 40.783825] + }, + "locationAccuracy": 5, + "confidence": 3, + "actualCreatedAt": "2019-11-15T17:31:18.454Z", + "user": { + "segments": [], + "topChains": [ + { + "name": "TravelCenters of America", + "slug": "travelcenters-of-america" + } + ], + "_id": "5bb8d4fbfd58d5002103ff5c", + "deviceId": "A", + "userId": "1", + "state": { + "_id": "5cf695096da6a800683f4e7f", + "type": "state", + "name": "New York", + "code": "NY" + }, + "dma": { + "_id": "5cf695016da6a800683f4e06", + "type": "dma", + "name": "New York", + "code": "501" + }, + "postalCode": { + "_id": "5cf695266da6a800683f5817", + "type": "postalCode", + "name": "10024", + "code": "10024" + } + }, + "duration": 84491.305, + "geofence": { + "geometryCenter": { + "coordinates": [-76.782486, 39.165325], + "type": "Point" + }, + "_id": "5d818607dc9243002682d6da", + "description": "TA Jessup", + "type": "circle", + "geometryRadius": 100, + "tag": "ta-petro", + "externalId": "123", + "metadata": { + "foo": "bar" + } + }, + "_id": "5dcee0e67e71e80027a7eec7", + "country": { + "_id": "5cf694f66da6a800683f4d71", + "type": "country", + "name": "United States", + "code": "US" + }, + "state": { + "_id": "5cf695096da6a800683f4e7f", + "type": "state", + "name": "New York", + "code": "NY" + }, + "postalCode": { + "_id": "5cf695266da6a800683f5817", + "type": "postalCode", + "name": "10024", + "code": "10024" + }, + "dma": { + "_id": "5cf695016da6a800683f4e06", + "type": "dma", + "name": "New York", + "code": "501" + } + }, + { + "createdAt": "2019-11-15T17:31:18.454Z", + "live": false, + "type": "user.entered_geofence", + "location": { + "type": "Point", + "coordinates": [-73.975365, 40.783825] + }, + "locationAccuracy": 5, + "confidence": 3, + "actualCreatedAt": "2019-11-15T17:31:18.454Z", + "user": { + "segments": [], + "topChains": [ + { + "name": "TravelCenters of America", + "slug": "travelcenters-of-america" + } + ], + "_id": "5bb8d4fbfd58d5002103ff5c", + "deviceId": "A", + "userId": "1", + "state": { + "_id": "5cf695096da6a800683f4e7f", + "type": "state", + "name": "New York", + "code": "NY" + }, + "dma": { + "_id": "5cf695016da6a800683f4e06", + "type": "dma", + "name": "New York", + "code": "501" + }, + "postalCode": { + "_id": "5cf695266da6a800683f5817", + "type": "postalCode", + "name": "10024", + "code": "10024" + } + }, + "geofence": { + "geometryCenter": { + "coordinates": [-73.975365, 40.783825], + "type": "Point" + }, + "_id": "5ca7dd72208530002b30683c", + "description": "S3 Test Monk's Café", + "type": "circle", + "metadata": { + "category": "restaurant" + }, + "geometryRadius": 50, + "tag": "venue", + "externalId": "2" + }, + "_id": "5dcee0e67e71e80027a7eec8", + "country": { + "_id": "5cf694f66da6a800683f4d71", + "type": "country", + "name": "United States", + "code": "US" + }, + "state": { + "_id": "5cf695096da6a800683f4e7f", + "type": "state", + "name": "New York", + "code": "NY" + }, + "postalCode": { + "_id": "5cf695266da6a800683f5817", + "type": "postalCode", + "name": "10024", + "code": "10024" + }, + "dma": { + "_id": "5cf695016da6a800683f4e06", + "type": "dma", + "name": "New York", + "code": "501" + } + }, + { + "createdAt": "2019-11-15T17:31:18.454Z", + "live": false, + "type": "user.entered_place", + "location": { + "type": "Point", + "coordinates": [-73.975365, 40.783825] + }, + "locationAccuracy": 5, + "confidence": 1, + "actualCreatedAt": "2019-11-15T17:31:18.454Z", + "user": { + "segments": [], + "topChains": [ + { + "name": "TravelCenters of America", + "slug": "travelcenters-of-america" + } + ], + "_id": "5bb8d4fbfd58d5002103ff5c", + "deviceId": "A", + "userId": "1", + "state": { + "_id": "5cf695096da6a800683f4e7f", + "type": "state", + "name": "New York", + "code": "NY" + }, + "dma": { + "_id": "5cf695016da6a800683f4e06", + "type": "dma", + "name": "New York", + "code": "501" + }, + "postalCode": { + "_id": "5cf695266da6a800683f5817", + "type": "postalCode", + "name": "10024", + "code": "10024" + } + }, + "place": { + "location": { + "coordinates": [-73.97541, 40.78377], + "type": "Point" + }, + "osmGeometry": { + "coordinates": [] + }, + "categories": [ + "real-estate", + "residence-other", + "apartment-condo-building" + ], + "_id": "59bb1dc10d8998dc02510033", + "name": "129 West 81st Street" + }, + "alternatePlaces": [ + { + "location": { + "coordinates": [-73.97541, 40.78377], + "type": "Point" + }, + "osmGeometry": { + "coordinates": [] + }, + "categories": [ + "outdoor-places", + "landmark", + "public-services-government" + ], + "_id": "59e289a9a064b45aefe78b82", + "name": "Things in Jerry Seinfeld's Apartment" + }, + { + "location": { + "coordinates": [-73.97536, 40.78387], + "type": "Point" + }, + "osmGeometry": { + "coordinates": [] + }, + "categories": [ + "local-services", + "business-services", + "commercial-industrial", + "commercial-industrial-equipment" + ], + "_id": "59e289a9a064b45aefe78b85", + "name": "Amazon", + "chain": { + "domain": "amazon.com", + "name": "Amazon", + "slug": "amazon" + } + }, + { + "location": { + "coordinates": [-73.97501, 40.78358], + "type": "Point" + }, + "osmGeometry": { + "coordinates": [] + }, + "categories": [ + "real-estate", + "real-estate-service", + "shopping-retail", + "mobile-phone-shop" + ], + "_id": "59bc68ec8be4c5ce94087563", + "name": "Endicott" + }, + { + "location": { + "coordinates": [-73.97512, 40.78341], + "type": "Point" + }, + "osmGeometry": { + "coordinates": [] + }, + "categories": [ + "travel-transportation", + "travel-company", + "travel-agency" + ], + "_id": "59da3785a064b45aefe1e2ef", + "name": "Cook Travel" + } + ], + "_id": "5dcee0e67e71e80027a7eec9", + "country": { + "_id": "5cf694f66da6a800683f4d71", + "type": "country", + "name": "United States", + "code": "US" + }, + "state": { + "_id": "5cf695096da6a800683f4e7f", + "type": "state", + "name": "New York", + "code": "NY" + }, + "postalCode": { + "_id": "5cf695266da6a800683f5817", + "type": "postalCode", + "name": "10024", + "code": "10024" + }, + "dma": { + "_id": "5cf695016da6a800683f4e06", + "type": "dma", + "name": "New York", + "code": "501" + } + }, + { + "createdAt": "2019-11-15T17:31:18.454Z", + "live": false, + "type": "user.exited_place", + "location": { + "type": "Point", + "coordinates": [-73.975365, 40.783825] + }, + "locationAccuracy": 5, + "confidence": 2, + "actualCreatedAt": "2019-11-15T17:31:18.454Z", + "user": { + "segments": [], + "topChains": [ + { + "name": "TravelCenters of America", + "slug": "travelcenters-of-america" + } + ], + "_id": "5bb8d4fbfd58d5002103ff5c", + "deviceId": "A", + "userId": "1", + "state": { + "_id": "5cf695096da6a800683f4e7f", + "type": "state", + "name": "New York", + "code": "NY" + }, + "dma": { + "_id": "5cf695016da6a800683f4e06", + "type": "dma", + "name": "New York", + "code": "501" + }, + "postalCode": { + "_id": "5cf695266da6a800683f5817", + "type": "postalCode", + "name": "10024", + "code": "10024" + } + }, + "duration": 84491.305, + "place": { + "location": { + "coordinates": [-76.782486, 39.165436], + "type": "Point" + }, + "categories": ["food-beverage", "restaurant"], + "_id": "5adc6759a4e7ae6c68b63307", + "name": "TravelCenters of America", + "chain": { + "domain": "ta-petro.com", + "name": "TravelCenters of America", + "slug": "travelcenters-of-america", + "externalId": "TA", + "metadata": { + "category": "gas" + } + } + }, + "_id": "5dcee0e67e71e80027a7eeca", + "country": { + "_id": "5cf694f66da6a800683f4d71", + "type": "country", + "name": "United States", + "code": "US" + }, + "state": { + "_id": "5cf695096da6a800683f4e7f", + "type": "state", + "name": "New York", + "code": "NY" + }, + "postalCode": { + "_id": "5cf695266da6a800683f5817", + "type": "postalCode", + "name": "10024", + "code": "10024" + }, + "dma": { + "_id": "5cf695016da6a800683f4e06", + "type": "dma", + "name": "New York", + "code": "501" + } + }, + { + "createdAt": "2019-11-15T17:31:18.454Z", + "live": false, + "type": "user.nearby_place_chain", + "location": { + "type": "Point", + "coordinates": [-73.975365, 40.783825] + }, + "locationAccuracy": 5, + "confidence": 2, + "actualCreatedAt": "2019-11-15T17:31:18.454Z", + "user": { + "segments": [], + "topChains": [ + { + "name": "TravelCenters of America", + "slug": "travelcenters-of-america" + } + ], + "_id": "5bb8d4fbfd58d5002103ff5c", + "deviceId": "A", + "userId": "1", + "state": { + "_id": "5cf695096da6a800683f4e7f", + "type": "state", + "name": "New York", + "code": "NY" + }, + "dma": { + "_id": "5cf695016da6a800683f4e06", + "type": "dma", + "name": "New York", + "code": "501" + }, + "postalCode": { + "_id": "5cf695266da6a800683f5817", + "type": "postalCode", + "name": "10024", + "code": "10024" + } + }, + "place": { + "location": { + "coordinates": [-73.97869, 40.783066], + "type": "Point" + }, + "osmGeometry": { + "coordinates": [] + }, + "categories": ["medical-health", "pharmacy"], + "_id": "5dc9b3422004860034bfc412", + "name": "Walgreens", + "chain": { + "domain": "walgreens.com", + "name": "Walgreens", + "slug": "walgreens", + "externalId": "5", + "metadata": { + "category": "Pharmacy", + "loyalty": false, + "pwi": false, + "tlog": false + } + } + }, + "_id": "5dcee0e67e71e80027a7eecb", + "country": { + "_id": "5cf694f66da6a800683f4d71", + "type": "country", + "name": "United States", + "code": "US" + }, + "state": { + "_id": "5cf695096da6a800683f4e7f", + "type": "state", + "name": "New York", + "code": "NY" + }, + "postalCode": { + "_id": "5cf695266da6a800683f5817", + "type": "postalCode", + "name": "10024", + "code": "10024" + }, + "dma": { + "_id": "5cf695016da6a800683f4e06", + "type": "dma", + "name": "New York", + "code": "501" + } + }, + { + "createdAt": "2019-11-15T17:31:18.454Z", + "live": false, + "type": "user.nearby_place_chain", + "location": { + "type": "Point", + "coordinates": [-73.975365, 40.783825] + }, + "locationAccuracy": 5, + "confidence": 3, + "actualCreatedAt": "2019-11-15T17:31:18.454Z", + "user": { + "segments": [], + "topChains": [ + { + "name": "TravelCenters of America", + "slug": "travelcenters-of-america" + } + ], + "_id": "5bb8d4fbfd58d5002103ff5c", + "deviceId": "A", + "userId": "1", + "state": { + "_id": "5cf695096da6a800683f4e7f", + "type": "state", + "name": "New York", + "code": "NY" + }, + "dma": { + "_id": "5cf695016da6a800683f4e06", + "type": "dma", + "name": "New York", + "code": "501" + }, + "postalCode": { + "_id": "5cf695266da6a800683f5817", + "type": "postalCode", + "name": "10024", + "code": "10024" + } + }, + "place": { + "location": { + "coordinates": [-73.97453, 40.78356], + "type": "Point" + }, + "osmGeometry": { + "coordinates": [] + }, + "categories": ["food-beverage", "cafe", "coffee-shop"], + "_id": "59bc68ec8be4c5ce9408755f", + "name": "Starbucks", + "chain": { + "domain": "starbucks.com", + "name": "Starbucks", + "slug": "starbucks", + "externalId": "811", + "metadata": { + "category": "Coffee & Tea", + "loyalty": false, + "pwi": false, + "tlog": false + } + } + }, + "_id": "5dcee0e67e71e80027a7eecc", + "country": { + "_id": "5cf694f66da6a800683f4d71", + "type": "country", + "name": "United States", + "code": "US" + }, + "state": { + "_id": "5cf695096da6a800683f4e7f", + "type": "state", + "name": "New York", + "code": "NY" + }, + "postalCode": { + "_id": "5cf695266da6a800683f5817", + "type": "postalCode", + "name": "10024", + "code": "10024" + }, + "dma": { + "_id": "5cf695016da6a800683f4e06", + "type": "dma", + "name": "New York", + "code": "501" + } + } + ], + "nearbyGeofences": [ + { + "geometryCenter": { + "coordinates": [-73.975365, 40.783825], + "type": "Point" + }, + "_id": "5ca7dd72208530002b30683c", + "description": "S3 Test Monk's Café", + "type": "circle", + "metadata": { + "category": "restaurant" + }, + "geometryRadius": 50, + "tag": "venue", + "externalId": "2" + } + ] +} diff --git a/sdk/src/test/resources/track.json b/sdk/src/test/resources/track.json index b3aae16d..adab968b 100644 --- a/sdk/src/test/resources/track.json +++ b/sdk/src/test/resources/track.json @@ -1130,5 +1130,25 @@ "code": "501" } } + ], + "nearbyGeofences": [ + { + "geometryCenter": { + "coordinates": [ + -73.975365, + 40.783825 + ], + "type": "Point" + }, + "_id": "5ca7dd72208530002b30683c", + "description": "S3 Test Monk's Café", + "type": "circle", + "metadata": { + "category": "restaurant" + }, + "geometryRadius": 50, + "tag": "venue", + "externalId": "2" + } ] }