Skip to content

Commit

Permalink
persist replays to shared preferences (#306)
Browse files Browse the repository at this point in the history
* persist replays to shared preferences

* Fixes

* fix radar.kt, 3.8.7-beta.2

* fix tracking

* fix race

* clear buffer

* move replay load from completion handler

* 3.8.9-beta.1

* 3.8.9-beta.2

* Fix feature settings, put persistence properly behind a featureSetting

* remove status from updateTrackingFromMeta

* more cleanup and aligning with ios, beta.2

* revert tracking options

* Fix getConfig callback

* fix apiclient callback

* use debug logger

* change log copy

* remove beta tag
  • Loading branch information
lmeier committed Aug 29, 2023
1 parent 4c0148b commit fe35d8b
Show file tree
Hide file tree
Showing 11 changed files with 86 additions and 22 deletions.
2 changes: 1 addition & 1 deletion sdk/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ apply plugin: "org.jetbrains.dokka"
apply plugin: 'io.radar.mvnpublish'

ext {
radarVersion = '3.8.8'
radarVersion = '3.8.9'
}

String buildNumber = ".${System.currentTimeMillis()}"
Expand Down
20 changes: 17 additions & 3 deletions sdk/src/main/java/io/radar/sdk/Radar.kt
Original file line number Diff line number Diff line change
Expand Up @@ -477,7 +477,7 @@ object Radar {
}

if (!this::replayBuffer.isInitialized) {
this.replayBuffer = RadarSimpleReplayBuffer()
this.replayBuffer = RadarSimpleReplayBuffer(this.context)
}

if (!this::logger.isInitialized) {
Expand Down Expand Up @@ -527,11 +527,16 @@ object Radar {
}
application?.registerActivityLifecycleCallbacks(RadarActivityLifecycleCallbacks(fraud))


val featureSettings = RadarSettings.getFeatureSettings(this.context)
if (featureSettings.usePersistence) {
Radar.loadReplayBufferFromSharedPreferences()
}
val usage = "initialize"
this.apiClient.getConfig(usage, false, object : RadarApiClient.RadarGetConfigApiCallback {
override fun onComplete(config: RadarConfig) {
locationManager.updateTrackingFromMeta(config.meta)
RadarSettings.setFeatureSettings(context, config.featureSettings)
locationManager.updateTrackingFromMeta(config?.meta)
RadarSettings.setFeatureSettings(context, config?.meta.featureSettings)
}
})

Expand Down Expand Up @@ -3077,6 +3082,15 @@ object Radar {
replayBuffer.write(replayParams)
}

@JvmStatic
internal fun loadReplayBufferFromSharedPreferences() {
replayBuffer.loadFromSharedPreferences()
val replays = getReplays()
val replayCount = replays.size
// TODO: revisit this log
logger.d("loaded replay buffer from shared preferences with $replayCount replays")
}

@JvmStatic
internal fun isTestKey(): Boolean {
val key = RadarSettings.getPublishableKey(this.context)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import android.view.InputDevice
import android.view.MotionEvent
import android.view.View
import android.view.ViewGroup
import io.radar.sdk.Radar.RadarStatus
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
import io.radar.sdk.model.RadarConfig
Expand Down Expand Up @@ -52,7 +53,7 @@ internal class RadarActivityLifecycleCallbacks(
Radar.apiClient.getConfig(usage, false, object : RadarApiClient.RadarGetConfigApiCallback {
override fun onComplete(config: RadarConfig) {
Radar.locationManager.updateTrackingFromMeta(config.meta)
RadarSettings.setFeatureSettings(activity.applicationContext, config.featureSettings)
RadarSettings.setFeatureSettings(activity.applicationContext, config.meta.featureSettings)
}
})
}
Expand Down
2 changes: 1 addition & 1 deletion sdk/src/main/java/io/radar/sdk/RadarLocationManager.kt
Original file line number Diff line number Diff line change
Expand Up @@ -240,8 +240,8 @@ internal class RadarLocationManager(
RadarSettings.removeRemoteTrackingOptions(context)
logger.d("Removed remote tracking options | trackingOptions = ${Radar.getTrackingOptions()}")
}
updateTracking()
}
updateTracking()
}

internal fun restartPreviousTrackingOptions() {
Expand Down
9 changes: 6 additions & 3 deletions sdk/src/main/java/io/radar/sdk/RadarSettings.kt
Original file line number Diff line number Diff line change
Expand Up @@ -252,12 +252,15 @@ internal object RadarSettings {
}

fun setFeatureSettings(context: Context, featureSettings: RadarFeatureSettings) {
getSharedPreferences(context).edit { putString(KEY_FEATURE_SETTINGS, featureSettings.toJson().toString()) }
val optionsJson = featureSettings.toJson().toString()

getSharedPreferences(context).edit { putString(KEY_FEATURE_SETTINGS, optionsJson) }
}

fun getFeatureSettings(context: Context): RadarFeatureSettings {
val optionsJson = getSharedPreferences(context).getString(KEY_FEATURE_SETTINGS, null)
?: return RadarFeatureSettings.default()
val sharedPrefFeatureSettings = getSharedPreferences(context).getString(KEY_FEATURE_SETTINGS, null)
Radar.logger.d("getFeatureSettings | featureSettings = $sharedPrefFeatureSettings")
val optionsJson = sharedPrefFeatureSettings ?: return RadarFeatureSettings.default()
return RadarFeatureSettings.fromJson(JSONObject(optionsJson))
}

Expand Down
3 changes: 0 additions & 3 deletions sdk/src/main/java/io/radar/sdk/model/RadarConfig.kt
Original file line number Diff line number Diff line change
Expand Up @@ -4,20 +4,17 @@ import org.json.JSONObject

internal data class RadarConfig(
val meta: RadarMeta,
val featureSettings: RadarFeatureSettings,
val googlePlayProjectNumber: Long?,
val nonce: String?
) {

companion object {
private const val META = "meta"
private const val FEATURE_SETTINGS = "settings"
private const val GOOGLE_CLOUD_PROJECT_NUMBER = "googleCloudProjectNumber"
private const val NONCE = "nonce"

fun fromJson(res: JSONObject?) = RadarConfig(
RadarMeta.fromJson(res?.optJSONObject(META)),
RadarFeatureSettings.fromJson(res?.optJSONObject(FEATURE_SETTINGS)),
res?.optLong(GOOGLE_CLOUD_PROJECT_NUMBER),
res?.optString(NONCE)
)
Expand Down
10 changes: 7 additions & 3 deletions sdk/src/main/java/io/radar/sdk/model/RadarFeatureSettings.kt
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,13 @@ import org.json.JSONObject
*/
internal data class RadarFeatureSettings(
val maxConcurrentJobs: Int,
val schedulerRequiresNetwork: Boolean
val schedulerRequiresNetwork: Boolean,
val usePersistence: Boolean
) {
companion object {
private const val MAX_CONCURRENT_JOBS = "maxConcurrentJobs"
private const val DEFAULT_MAX_CONCURRENT_JOBS = 1
private const val USE_PERSISTENCE = "usePersistence"
private const val SCHEDULER_REQUIRES_NETWORK = "networkAny"

fun fromJson(json: JSONObject?): RadarFeatureSettings {
Expand All @@ -20,20 +22,22 @@ internal data class RadarFeatureSettings(
} else {
RadarFeatureSettings(
json.optInt(MAX_CONCURRENT_JOBS, DEFAULT_MAX_CONCURRENT_JOBS),
json.optBoolean(SCHEDULER_REQUIRES_NETWORK)
json.optBoolean(SCHEDULER_REQUIRES_NETWORK),
json.optBoolean(USE_PERSISTENCE)
)
}
}

fun default(): RadarFeatureSettings {
return RadarFeatureSettings(DEFAULT_MAX_CONCURRENT_JOBS, false)
return RadarFeatureSettings(DEFAULT_MAX_CONCURRENT_JOBS, false, false)
}
}

fun toJson(): JSONObject {
return JSONObject().apply {
putOpt(MAX_CONCURRENT_JOBS, maxConcurrentJobs)
putOpt(SCHEDULER_REQUIRES_NETWORK, schedulerRequiresNetwork)
putOpt(USE_PERSISTENCE, usePersistence)
}
}
}
9 changes: 6 additions & 3 deletions sdk/src/main/java/io/radar/sdk/model/RadarMeta.kt
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,21 @@ import io.radar.sdk.RadarTrackingOptions
import org.json.JSONObject

internal data class RadarMeta(
val remoteTrackingOptions: RadarTrackingOptions?
val remoteTrackingOptions: RadarTrackingOptions?,
val featureSettings: RadarFeatureSettings,
) {
companion object {
private const val TRACKING_OPTIONS = "trackingOptions"
private const val FEATURE_SETTINGS = "featureSettings"

fun fromJson(meta: JSONObject?): RadarMeta {
val rawOptions = meta?.optJSONObject(TRACKING_OPTIONS)
val rawFeatureSettings = meta?.optJSONObject(FEATURE_SETTINGS)

return if (rawOptions == null) {
RadarMeta(null)
RadarMeta(null, RadarFeatureSettings.fromJson(rawFeatureSettings))
} else {
RadarMeta(RadarTrackingOptions.fromJson(rawOptions))
RadarMeta(RadarTrackingOptions.fromJson(rawOptions), RadarFeatureSettings.fromJson(rawFeatureSettings))
}
}
}
Expand Down
2 changes: 2 additions & 0 deletions sdk/src/main/java/io/radar/sdk/util/RadarReplayBuffer.kt
Original file line number Diff line number Diff line change
Expand Up @@ -19,4 +19,6 @@ internal interface RadarReplayBuffer {
* @return a [Flushable] containing all stored logs
*/
fun getFlushableReplaysStash(): Flushable<RadarReplay>

fun loadFromSharedPreferences()
}
40 changes: 38 additions & 2 deletions sdk/src/main/java/io/radar/sdk/util/RadarSimpleReplayBuffer.kt
Original file line number Diff line number Diff line change
@@ -1,19 +1,25 @@
package io.radar.sdk.util

import io.radar.sdk.Radar
import io.radar.sdk.RadarSettings
import io.radar.sdk.model.RadarReplay
// TODO: determine if we need the above and below
import android.content.Context
import android.content.SharedPreferences
import androidx.core.content.edit
import java.util.concurrent.LinkedBlockingDeque
import org.json.JSONObject
import org.json.JSONArray

/**
* A buffer for replay events.
*/

internal class RadarSimpleReplayBuffer : RadarReplayBuffer {
internal class RadarSimpleReplayBuffer(private val context: Context) : RadarReplayBuffer {

private companion object {
const val MAXIMUM_CAPACITY = 120
const val PREFERENCES_NAME = "RadarReplayBufferPreferences"
const val KEY_REPLAYS = "radar-replays"
}

private val buffer = LinkedBlockingDeque<RadarReplay>(MAXIMUM_CAPACITY)
Expand All @@ -23,6 +29,18 @@ internal class RadarSimpleReplayBuffer : RadarReplayBuffer {
buffer.removeFirst()
}
buffer.offer(RadarReplay(replayParams))
val featureSettings = RadarSettings.getFeatureSettings(context)
if (featureSettings.usePersistence) {
// If buffer length is above 50, remove every fifth replay from the persisted buffer
if (buffer.size > 50) {
val prunedBuffer = buffer.filterIndexed { index, _ -> index % 5 != 0 }
val prunedReplaysAsJsonArray = JSONArray(prunedBuffer.map { it.toJson() })
getSharedPreferences(context).edit { putString(KEY_REPLAYS, prunedReplaysAsJsonArray.toString()) }
} else {
val replaysAsJsonArray = JSONArray(buffer.map { it.toJson() })
getSharedPreferences(context).edit { putString(KEY_REPLAYS, replaysAsJsonArray.toString()) }
}
}
}

override fun getFlushableReplaysStash(): Flushable<RadarReplay> {
Expand All @@ -37,8 +55,26 @@ internal class RadarSimpleReplayBuffer : RadarReplayBuffer {
override fun onFlush(success: Boolean) {
if (success) {
buffer.clear()
// clear the shared preferences
getSharedPreferences(context).edit { remove(KEY_REPLAYS) }
}
}
}
}

override fun loadFromSharedPreferences() {
val replaysAsString = getSharedPreferences(context).getString(KEY_REPLAYS, null)
replaysAsString?.let { replays ->
val replaysAsJsonArray = JSONArray(replays)
for (i in 0 until replaysAsJsonArray.length()) {
val replayAsJsonObject = replaysAsJsonArray.getJSONObject(i)
val replay = RadarReplay.fromJson(replayAsJsonObject)
buffer.offer(replay)
}
}
}

private fun getSharedPreferences(context: Context): SharedPreferences {
return context.getSharedPreferences(PREFERENCES_NAME, Context.MODE_PRIVATE)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ class RadarFeatureSettingsTest {

private var maxConcurrentJobs = -1
private var requiresNetwork = false
private var usePersistence = true
private lateinit var jsonString: String

@Before
Expand All @@ -25,15 +26,16 @@ class RadarFeatureSettingsTest {
requiresNetwork = Random.nextBoolean()
jsonString = """{
"networkAny":$requiresNetwork,
"maxConcurrentJobs":$maxConcurrentJobs
"maxConcurrentJobs":$maxConcurrentJobs,
"usePersistence":$usePersistence
}""".trimIndent()
}

@Test
fun testToJson() {
assertEquals(
jsonString.removeWhitespace(),
RadarFeatureSettings(maxConcurrentJobs, requiresNetwork).toJson().toString().removeWhitespace()
RadarFeatureSettings(maxConcurrentJobs, requiresNetwork, usePersistence).toJson().toString().removeWhitespace()
)
}

Expand All @@ -42,13 +44,15 @@ class RadarFeatureSettingsTest {
val settings = RadarFeatureSettings.fromJson(JSONObject(jsonString))
assertEquals(maxConcurrentJobs, settings.maxConcurrentJobs)
assertEquals(requiresNetwork, settings.schedulerRequiresNetwork)
assertEquals(usePersistence, settings.usePersistence)
}

@Test
fun testDefault() {
val settings = RadarFeatureSettings.default()
assertEquals(1, settings.maxConcurrentJobs)
assertFalse(settings.schedulerRequiresNetwork)
assertFalse(settings.usePersistence)
}

private fun String.removeWhitespace(): String = replace("\\s".toRegex(), "")
Expand Down

0 comments on commit fe35d8b

Please sign in to comment.