diff --git a/launchdarkly-android-client-sdk/src/androidTest/java/com/launchdarkly/sdk/android/SharedPrefsSummaryEventStoreTest.java b/launchdarkly-android-client-sdk/src/androidTest/java/com/launchdarkly/sdk/android/InMemorySummaryEventStoreTest.java similarity index 94% rename from launchdarkly-android-client-sdk/src/androidTest/java/com/launchdarkly/sdk/android/SharedPrefsSummaryEventStoreTest.java rename to launchdarkly-android-client-sdk/src/androidTest/java/com/launchdarkly/sdk/android/InMemorySummaryEventStoreTest.java index 18043da4..5a443958 100644 --- a/launchdarkly-android-client-sdk/src/androidTest/java/com/launchdarkly/sdk/android/SharedPrefsSummaryEventStoreTest.java +++ b/launchdarkly-android-client-sdk/src/androidTest/java/com/launchdarkly/sdk/android/InMemorySummaryEventStoreTest.java @@ -7,6 +7,7 @@ import com.launchdarkly.sdk.LDValue; import com.launchdarkly.sdk.LDValueType; +import org.junit.After; import org.junit.Assert; import org.junit.Before; import org.junit.Rule; @@ -21,9 +22,8 @@ import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; - @RunWith(AndroidJUnit4.class) -public class SharedPrefsSummaryEventStoreTest { +public class InMemorySummaryEventStoreTest { @Rule public TimberLoggingRule timberLoggingRule = new TimberLoggingRule(); @@ -44,6 +44,11 @@ public void setUp() { summaryEventStore = ldClient.getSummaryEventStore(); } + @After + public void tearDown() throws Exception { + ldClient.close(); + } + @Test public void startDateIsSaved() { assertTrue(ldClient.isInitialized()); @@ -102,7 +107,7 @@ public void evaluationsAreSaved() { assertEquals(LDValueType.STRING, features.get("stringFlag").defaultValue.getType()); assertEquals("string", features.get("stringFlag").defaultValue.stringValue()); - assertNull(features.get("jsonFlag").defaultValue); + assertEquals(LDValue.ofNull(), features.get("jsonFlag").defaultValue); } @Test diff --git a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/DefaultUserManager.java b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/DefaultUserManager.java index 7b406c18..396d6b8e 100644 --- a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/DefaultUserManager.java +++ b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/DefaultUserManager.java @@ -46,8 +46,7 @@ static synchronized DefaultUserManager newInstance(Application application, Feat this.fetcher = fetcher; this.flagStoreManager = new SharedPrefsFlagStoreManager(application, mobileKey, new SharedPrefsFlagStoreFactory(application, logger), maxCachedUsers, logger); - this.summaryEventStore = new SharedPrefsSummaryEventStore(application, - LDConfig.SHARED_PREFS_BASE_KEY + mobileKey + "-summaryevents", logger); + this.summaryEventStore = new InMemorySummaryEventStore(); this.environmentName = environmentName; this.logger = logger; diff --git a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/Event.java b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/Event.java index 5d120455..e73f65ba 100644 --- a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/Event.java +++ b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/Event.java @@ -117,11 +117,11 @@ class FeatureRequestEvent extends GenericEvent { } class SummaryEvent extends Event { - @Expose Long startDate; - @Expose Long endDate; - @Expose Map features; + @Expose final long startDate; + @Expose final long endDate; + @Expose final Map features; - SummaryEvent(Long startDate, Long endDate, Map features) { + SummaryEvent(long startDate, long endDate, Map features) { super("summary"); this.startDate = startDate; this.endDate = endDate; diff --git a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/InMemorySummaryEventStore.java b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/InMemorySummaryEventStore.java new file mode 100644 index 00000000..d2e97915 --- /dev/null +++ b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/InMemorySummaryEventStore.java @@ -0,0 +1,114 @@ +package com.launchdarkly.sdk.android; + +import androidx.annotation.Nullable; + +import com.launchdarkly.sdk.LDValue; + +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; + +final class InMemorySummaryEventStore implements SummaryEventStore { + private final class CounterKey { + final String flagKey; + final Integer flagVersion; + final Integer variation; + + CounterKey(String flagKey, Integer flagVersion, Integer variation) { + this.flagKey = flagKey; + this.flagVersion = flagVersion; + this.variation = variation; + } + + @Override + public boolean equals(Object other) { + if (other instanceof CounterKey) { + CounterKey o = (CounterKey)other; + return flagKey.equals(o.flagKey) && Objects.equals(flagVersion, o.flagVersion) && + Objects.equals(variation, o.variation); + } + return false; + } + + @Override + public int hashCode() { + return ((flagKey.hashCode() * 31) + (flagVersion == null ? -1 : flagVersion.intValue())) * 31 + + (variation == null ? -1 : variation.intValue()); + } + } + + private final class CounterValue { + final LDValue value; + final LDValue defaultValue; + volatile int count; + + CounterValue(LDValue value, LDValue defaultValue) { + this.value = value; + this.defaultValue = defaultValue; + } + } + + private final Map data = new HashMap<>(); + private long startTimestamp = 0, endTimestamp = 0; + + public synchronized void clear() { + data.clear(); + startTimestamp = endTimestamp = 0; + } + + @Override + public synchronized void addOrUpdateEvent( + String flagKey, + LDValue value, + LDValue defaultVal, + @Nullable Integer version, + @Nullable Integer variation + ) { + long timestamp = System.currentTimeMillis(); + if (startTimestamp == 0) { + startTimestamp = timestamp; + } + if (timestamp > endTimestamp) { + endTimestamp = timestamp; + } + CounterKey counterKey = new CounterKey(flagKey, version, variation); + CounterValue counterValue = data.get(counterKey); + if (counterValue == null) { + counterValue = new CounterValue(value, defaultVal); + data.put(counterKey, counterValue); + } + counterValue.count++; + } + + @Override + public synchronized SummaryEvent getSummaryEvent() { + if (data.size() == 0) { + return null; + } + Map countersOut = new HashMap<>(); + for (Map.Entry kv: data.entrySet()) { + CounterKey counterKey = kv.getKey(); + String flagKey = counterKey.flagKey; + CounterValue counterValue = kv.getValue(); + SummaryEventStore.FlagCounters countersForFlag = countersOut.get(flagKey); + if (countersForFlag == null) { + countersForFlag = new FlagCounters(counterValue.defaultValue); + countersOut.put(flagKey, countersForFlag); + } + countersForFlag.counters.add(new FlagCounter( + counterValue.value, + counterKey.flagVersion, + counterKey.variation, + counterValue.count + )); + } + return new SummaryEvent(startTimestamp, endTimestamp, countersOut); + } + + @Override + public synchronized SummaryEvent getSummaryEventAndClear() { + SummaryEvent summary = getSummaryEvent(); + clear(); + return summary; + } +} diff --git a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/SharedPrefsSummaryEventStore.java b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/SharedPrefsSummaryEventStore.java deleted file mode 100644 index 6d4af3e9..00000000 --- a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/SharedPrefsSummaryEventStore.java +++ /dev/null @@ -1,101 +0,0 @@ -package com.launchdarkly.sdk.android; - -import android.app.Application; -import android.content.Context; -import android.content.SharedPreferences; - -import com.launchdarkly.logging.LDLogger; -import com.launchdarkly.sdk.LDValue; - -import java.util.HashMap; - -/** - * Used internally by the SDK. - */ -class SharedPrefsSummaryEventStore implements SummaryEventStore { - - private static final String START_DATE_KEY = "$startDate$"; - - private final SharedPreferences sharedPreferences; - private final LDLogger logger; - - SharedPrefsSummaryEventStore(Application application, String name, LDLogger logger) { - this.sharedPreferences = application.getSharedPreferences(name, Context.MODE_PRIVATE); - this.logger = logger; - } - - @Override - public synchronized void addOrUpdateEvent(String flagResponseKey, LDValue value, LDValue defaultVal, Integer version, Integer variation) { - FlagCounters storedCounters = getFlagCounters(flagResponseKey); - - if (storedCounters == null) { - storedCounters = new FlagCounters(defaultVal); - } - - boolean existingCounter = false; - for (FlagCounter counter: storedCounters.counters) { - if (counter.matches(version, variation)) { - counter.count++; - existingCounter = true; - break; - } - } - - if (!existingCounter) { - storedCounters.counters.add(new FlagCounter(value, version, variation)); - } - - SharedPreferences.Editor editor = sharedPreferences.edit(); - editor.putString(flagResponseKey, GsonCache.getGson().toJson(storedCounters)); - if (!sharedPreferences.contains(START_DATE_KEY)) { - editor.putLong(START_DATE_KEY, System.currentTimeMillis()).apply(); - } - editor.apply(); - - logger.debug("Updated summary for flagKey {} to {}", flagResponseKey, GsonCache.getGson().toJson(storedCounters)); - } - - @Override - public synchronized SummaryEvent getSummaryEvent() { - long startDate = sharedPreferences.getLong(START_DATE_KEY, -1); - HashMap features = new HashMap<>(); - for (String key: sharedPreferences.getAll().keySet()) { - if (START_DATE_KEY.equals(key)) { - continue; - } - features.put(key, getFlagCounters(key)); - } - - if (startDate == -1 || features.size() == 0) { - return null; - } - - return new SummaryEvent(startDate, System.currentTimeMillis(), features); - } - - synchronized FlagCounters getFlagCounters(String flagKey) { - try { - String storedJson = sharedPreferences.getString(flagKey, null); - if (storedJson == null) { - return null; - } - return GsonCache.getGson().fromJson(storedJson, FlagCounters.class); - } catch (Exception ignored) { - // Fallthrough to clear - } - // An old version of shared preferences is stored, so clear it. - clear(); - return null; - } - - @Override - public synchronized SummaryEvent getSummaryEventAndClear() { - SummaryEvent summaryEvent = getSummaryEvent(); - clear(); - return summaryEvent; - } - - public synchronized void clear() { - sharedPreferences.edit().clear().apply(); - } -} diff --git a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/SummaryEventStore.java b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/SummaryEventStore.java index 2b451333..1e259603 100644 --- a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/SummaryEventStore.java +++ b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/SummaryEventStore.java @@ -17,33 +17,18 @@ interface SummaryEventStore { SummaryEvent getSummaryEventAndClear(); class FlagCounter { - @Expose Integer version; - @Expose Integer variation; - @Expose Boolean unknown; - @Expose LDValue value; - @Expose int count; + @Expose final Integer version; + @Expose final Integer variation; + @Expose final Boolean unknown; + @Expose final LDValue value; + @Expose final int count; - FlagCounter(LDValue value, Integer version, Integer variation) { + FlagCounter(LDValue value, Integer version, Integer variation, int count) { this.version = version; this.variation = variation; - if (version == null) { - unknown = true; - } + unknown = version == null ? Boolean.TRUE : null; this.value = value; - this.count = 1; - } - - boolean isUnknown() { - return unknown != null && unknown; - } - - boolean matches(Integer version, Integer variation) { - if (isUnknown()) { - return version == null; - } - - return Objects.equals(this.version, version) && - Objects.equals(this.variation, variation); + this.count = count; } }