Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ class DevCycleClient private constructor(

private val configRequestQueue = ConcurrentLinkedQueue<UserAndCallback>()
private val configRequestMutex = Mutex()
private val defaultCacheTTL = 7 * 24 * 3600000L // 7 days
private val defaultCacheTTL = 30 * 24 * 3600000L // 30 days
private val configCacheTTL = options?.configCacheTTL ?: defaultCacheTTL
private val disableConfigCache = options?.disableConfigCache ?: false
private val disableRealtimeUpdates = options?.disableRealtimeUpdates ?: false
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,95 @@ internal class DVCSharedPrefs(context: Context) {
Context.MODE_PRIVATE
)

init {
migrateLegacyConfigs()
}

companion object {
const val UserKey = "USER"
const val AnonUserIdKey = "ANONYMOUS_USER_ID"
const val IdentifiedConfigKey = "IDENTIFIED_CONFIG"
const val AnonymousConfigKey = "ANONYMOUS_CONFIG"
const val FetchDateSuffix = "FETCH_DATE"
const val MigrationCompletedKey = "MIGRATION_COMPLETED"
}

private fun generateUserConfigKey(userId: String, isAnonymous: Boolean): String {
val prefix = if (isAnonymous) AnonymousConfigKey else IdentifiedConfigKey
return "$prefix.$userId"
}

private fun generateUserFetchDateKey(userId: String, isAnonymous: Boolean): String {
return "${generateUserConfigKey(userId, isAnonymous)}.$FetchDateSuffix"
}

@Synchronized
private fun migrateLegacyConfigs() {
// Check if migration has already been completed
if (preferences.getBoolean(MigrationCompletedKey, false)) {
return
}

try {
val legacyKeys = listOf(IdentifiedConfigKey, AnonymousConfigKey)
val editor = preferences.edit()
var migrationOccurred = false

for (legacyKey in legacyKeys) {
val legacyUserIdKey = "$legacyKey.USER_ID"
val legacyFetchDateKey = "$legacyKey.FETCH_DATE"

val userId = preferences.getString(legacyUserIdKey, null)
val fetchDateMs = preferences.getLong(legacyFetchDateKey, 0)
val configString = preferences.getString(legacyKey, null)

// Attempt migration if we have complete data
if (userId != null && configString != null && fetchDateMs > 0) {
val isAnonymous = legacyKey == AnonymousConfigKey
val userKey = generateUserConfigKey(userId, isAnonymous)
val userFetchDateKey = generateUserFetchDateKey(userId, isAnonymous)

// Only migrate if new format doesn't already exist
if (!preferences.contains(userKey)) {
editor.putString(userKey, configString)
editor.putLong(userFetchDateKey, fetchDateMs)
DevCycleLogger.d("Migrated legacy config for user ID $userId from key $legacyKey")
}
}

// Always clean up legacy keys if they exist, regardless of migration success
var keysRemoved = false
if (preferences.contains(legacyKey)) {
editor.remove(legacyKey)
keysRemoved = true
}
if (preferences.contains(legacyUserIdKey)) {
editor.remove(legacyUserIdKey)
keysRemoved = true
}
if (preferences.contains(legacyFetchDateKey)) {
editor.remove(legacyFetchDateKey)
keysRemoved = true
}

if (keysRemoved) {
migrationOccurred = true
}
}

// Mark migration as completed, regardless of whether data was migrated
editor.putBoolean(MigrationCompletedKey, true)

if (migrationOccurred) {
editor.apply()
DevCycleLogger.d("Legacy config migration completed")
} else {
editor.apply()
DevCycleLogger.d("Migration check completed - no legacy data found")
}
} catch (e: Exception) {
DevCycleLogger.e(e, "Error during legacy config migration: ${e.message}")
}
}

@Synchronized
Expand Down Expand Up @@ -65,12 +149,13 @@ internal class DVCSharedPrefs(context: Context) {
@Synchronized
fun saveConfig(configToSave: BucketedUserConfig, user: PopulatedUser) {
try {
val key = if (user.isAnonymous) AnonymousConfigKey else IdentifiedConfigKey
val userKey = generateUserConfigKey(user.userId, user.isAnonymous)
val userFetchDateKey = generateUserFetchDateKey(user.userId, user.isAnonymous)

val editor = preferences.edit()
val jsonString = JSONMapper.mapper.writeValueAsString(configToSave)
editor.putString(key, jsonString)
editor.putString("$key.USER_ID", user.userId)
editor.putLong("$key.FETCH_DATE", Calendar.getInstance().timeInMillis)
editor.putString(userKey, jsonString)
editor.putLong(userFetchDateKey, Calendar.getInstance().timeInMillis)
editor.apply()
} catch (e: JsonProcessingException) {
DevCycleLogger.e(e, e.message)
Expand All @@ -80,28 +165,29 @@ internal class DVCSharedPrefs(context: Context) {
@Synchronized
fun getConfig(user: PopulatedUser, ttlMs: Long): BucketedUserConfig? {
try {
val key = if (user.isAnonymous) AnonymousConfigKey else IdentifiedConfigKey
val userId = preferences.getString("$key.USER_ID", null)
val fetchDateMs = preferences.getLong("$key.FETCH_DATE", 0)

if (userId != user.userId) {
DevCycleLogger.d("Skipping cached config: no config for user ID ${user.userId}")
return null
}

val userKey = generateUserConfigKey(user.userId, user.isAnonymous)
val userConfigString = preferences.getString(userKey, null)
val userFetchDateKey = generateUserFetchDateKey(user.userId, user.isAnonymous)
val userFetchDateMs = preferences.getLong(userFetchDateKey, 0)

val oldestValidDateMs = Calendar.getInstance().timeInMillis - ttlMs
if (fetchDateMs < oldestValidDateMs) {
DevCycleLogger.d("Skipping cached config: last fetched date is too old")
return null
}

val configString = preferences.getString(key, null)
if (configString == null) {
DevCycleLogger.d("Skipping cached config: no config found")
return null

if (userConfigString != null) {
if (userFetchDateMs >= oldestValidDateMs) {
DevCycleLogger.d("Loaded config from cache for user ID ${user.userId}")
return JSONMapper.mapper.readValue(userConfigString)
} else {
// Config exists but is expired, remove it
val editor = preferences.edit()
editor.remove(userKey)
editor.remove(userFetchDateKey)
editor.apply()
DevCycleLogger.d("Removed expired config for user ID ${user.userId}")
}
}

return JSONMapper.mapper.readValue(configString)

DevCycleLogger.d("No valid config found for user ID ${user.userId}")
return null
} catch (e: JsonProcessingException) {
DevCycleLogger.e(e, e.message)
return null
Expand Down
Loading