|
|
|
@ -1,42 +1,113 @@
|
|
|
|
|
package com.bugsnag.android
|
|
|
|
|
|
|
|
|
|
import java.io.IOException
|
|
|
|
|
import kotlin.math.max
|
|
|
|
|
|
|
|
|
|
internal class FeatureFlags(
|
|
|
|
|
internal val store: MutableMap<String, String?> = mutableMapOf()
|
|
|
|
|
internal class FeatureFlags private constructor(
|
|
|
|
|
@Volatile
|
|
|
|
|
private var flags: Array<FeatureFlag>
|
|
|
|
|
) : JsonStream.Streamable, FeatureFlagAware {
|
|
|
|
|
private val emptyVariant = "__EMPTY_VARIANT_SENTINEL__"
|
|
|
|
|
|
|
|
|
|
@Synchronized override fun addFeatureFlag(name: String) {
|
|
|
|
|
/*
|
|
|
|
|
* Implemented as *effectively* a CopyOnWriteArrayList - but since FeatureFlags are
|
|
|
|
|
* key/value pairs, CopyOnWriteArrayList would require external locking (in addition to it's
|
|
|
|
|
* internal locking) for us to be sure we are not adding duplicates.
|
|
|
|
|
*
|
|
|
|
|
* This class aims to have similar performance while also ensuring that the FeatureFlag object
|
|
|
|
|
* themselves don't leak, as they are mutable and we want 'copy' to be an O(1) snapshot
|
|
|
|
|
* operation for when an Event is created.
|
|
|
|
|
*
|
|
|
|
|
* It's assumed that *most* FeatureFlags will be added up-front, or during the normal app
|
|
|
|
|
* lifecycle (not during an Event).
|
|
|
|
|
*
|
|
|
|
|
* As such a copy-on-write structure allows an Event to simply capture a reference to the
|
|
|
|
|
* "snapshot" of FeatureFlags that were active when the Event was created.
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
constructor() : this(emptyArray<FeatureFlag>())
|
|
|
|
|
|
|
|
|
|
override fun addFeatureFlag(name: String) {
|
|
|
|
|
addFeatureFlag(name, null)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@Synchronized override fun addFeatureFlag(name: String, variant: String?) {
|
|
|
|
|
store[name] = variant ?: emptyVariant
|
|
|
|
|
override fun addFeatureFlag(name: String, variant: String?) {
|
|
|
|
|
synchronized(this) {
|
|
|
|
|
val flagArray = flags
|
|
|
|
|
val index = flagArray.indexOfFirst { it.name == name }
|
|
|
|
|
|
|
|
|
|
flags = when {
|
|
|
|
|
// this is a new FeatureFlag
|
|
|
|
|
index == -1 -> flagArray + FeatureFlag(name, variant)
|
|
|
|
|
|
|
|
|
|
// this is a change to an existing FeatureFlag
|
|
|
|
|
flagArray[index].variant != variant -> flagArray.copyOf().also {
|
|
|
|
|
// replace the existing FeatureFlag in-place
|
|
|
|
|
it[index] = FeatureFlag(name, variant)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// no actual change, so we return
|
|
|
|
|
else -> return
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@Synchronized override fun addFeatureFlags(featureFlags: Iterable<FeatureFlag>) {
|
|
|
|
|
featureFlags.forEach { (name, variant) ->
|
|
|
|
|
addFeatureFlag(name, variant)
|
|
|
|
|
override fun addFeatureFlags(featureFlags: Iterable<FeatureFlag>) {
|
|
|
|
|
synchronized(this) {
|
|
|
|
|
val flagArray = flags
|
|
|
|
|
|
|
|
|
|
val newFlags = ArrayList<FeatureFlag>(
|
|
|
|
|
// try to guess a reasonable upper-bound for the output array
|
|
|
|
|
if (featureFlags is Collection<*>) flagArray.size + featureFlags.size
|
|
|
|
|
else max(flagArray.size * 2, flagArray.size)
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
newFlags.addAll(flagArray)
|
|
|
|
|
|
|
|
|
|
featureFlags.forEach { (name, variant) ->
|
|
|
|
|
val existingIndex = newFlags.indexOfFirst { it.name == name }
|
|
|
|
|
when (existingIndex) {
|
|
|
|
|
// add a new flag to the end of the list
|
|
|
|
|
-1 -> newFlags.add(FeatureFlag(name, variant))
|
|
|
|
|
// replace the existing flag
|
|
|
|
|
else -> newFlags[existingIndex] = FeatureFlag(name, variant)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
flags = newFlags.toTypedArray()
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@Synchronized override fun clearFeatureFlag(name: String) {
|
|
|
|
|
store.remove(name)
|
|
|
|
|
override fun clearFeatureFlag(name: String) {
|
|
|
|
|
synchronized(this) {
|
|
|
|
|
val flagArray = flags
|
|
|
|
|
val index = flagArray.indexOfFirst { it.name == name }
|
|
|
|
|
if (index == -1) {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
val out = arrayOfNulls<FeatureFlag>(flagArray.size - 1)
|
|
|
|
|
flagArray.copyInto(out, 0, 0, index)
|
|
|
|
|
flagArray.copyInto(out, index, index + 1)
|
|
|
|
|
|
|
|
|
|
@Suppress("UNCHECKED_CAST")
|
|
|
|
|
flags = out as Array<FeatureFlag>
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@Synchronized override fun clearFeatureFlags() {
|
|
|
|
|
store.clear()
|
|
|
|
|
override fun clearFeatureFlags() {
|
|
|
|
|
synchronized(this) {
|
|
|
|
|
flags = emptyArray()
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@Throws(IOException::class)
|
|
|
|
|
override fun toStream(stream: JsonStream) {
|
|
|
|
|
val storeCopy = synchronized(this) { store.toMap() }
|
|
|
|
|
val storeCopy = flags
|
|
|
|
|
stream.beginArray()
|
|
|
|
|
storeCopy.forEach { (name, variant) ->
|
|
|
|
|
stream.beginObject()
|
|
|
|
|
stream.name("featureFlag").value(name)
|
|
|
|
|
if (variant != emptyVariant) {
|
|
|
|
|
if (variant != null) {
|
|
|
|
|
stream.name("variant").value(variant)
|
|
|
|
|
}
|
|
|
|
|
stream.endObject()
|
|
|
|
@ -44,9 +115,7 @@ internal class FeatureFlags(
|
|
|
|
|
stream.endArray()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@Synchronized fun toList(): List<FeatureFlag> = store.entries.map { (name, variant) ->
|
|
|
|
|
FeatureFlag(name, variant.takeUnless { it == emptyVariant })
|
|
|
|
|
}
|
|
|
|
|
fun toList(): List<FeatureFlag> = flags.map { (name, variant) -> FeatureFlag(name, variant) }
|
|
|
|
|
|
|
|
|
|
@Synchronized fun copy() = FeatureFlags(store.toMutableMap())
|
|
|
|
|
fun copy() = FeatureFlags(flags)
|
|
|
|
|
}
|
|
|
|
|