From 42d42191caa7b6d93fb27b1ade9eb17659c09665 Mon Sep 17 00:00:00 2001 From: Gaston Thea Date: Mon, 5 Jan 2026 18:33:40 -0300 Subject: [PATCH 01/27] WIP --- .../android/client/api/EventMetadata.java | 40 +++---- .../split/android/client/api/MetadataKey.java | 24 +++++ .../api/SdkReadyFromCacheMetadataKeys.java | 24 +++++ .../client/api/SdkUpdateMetadataKeys.java | 17 +++ .../android/client/events/SplitEventTask.java | 2 +- build.gradle | 2 +- .../events/metadata/EventMetadataHelpers.java | 12 +-- .../events/metadata/EventMetadataImpl.java | 31 ++---- .../metadata/EventMetadataBuilderTest.java | 46 +++++--- .../metadata/EventMetadataHelpersTest.java | 33 +++--- .../metadata/EventMetadataImplTest.java | 101 +++--------------- .../events/SdkEventsIntegrationTest.java | 17 ++- .../java/tests/service/EventsManagerTest.java | 9 +- .../events/EventsManagerCoordinatorTest.java | 9 +- .../client/events/EventsManagerTest.java | 7 +- .../events/SplitEventTaskMetadataTest.java | 8 +- .../localhost/LocalhostSplitsStorageTest.java | 9 +- .../service/SplitInPlaceUpdateTaskTest.java | 17 +-- .../client/service/SplitKillTaskTest.java | 4 +- .../client/service/SplitSyncTaskTest.java | 17 +-- .../client/service/SplitUpdateTaskTest.java | 17 ++- 21 files changed, 206 insertions(+), 240 deletions(-) create mode 100644 api/src/main/java/io/split/android/client/api/MetadataKey.java create mode 100644 api/src/main/java/io/split/android/client/api/SdkReadyFromCacheMetadataKeys.java create mode 100644 api/src/main/java/io/split/android/client/api/SdkUpdateMetadataKeys.java diff --git a/api/src/main/java/io/split/android/client/api/EventMetadata.java b/api/src/main/java/io/split/android/client/api/EventMetadata.java index e1648c388..aae46655f 100644 --- a/api/src/main/java/io/split/android/client/api/EventMetadata.java +++ b/api/src/main/java/io/split/android/client/api/EventMetadata.java @@ -4,8 +4,6 @@ import androidx.annotation.Nullable; import java.util.Collection; -import java.util.Map; -import java.util.Set; /** * Represents metadata associated with SDK events. @@ -15,44 +13,38 @@ public interface EventMetadata { /** - * Returns the set of keys in this metadata. - * - * @return set of keys + * Returns the number of entries in this metadata. */ - @NonNull - Set keys(); + int size(); + + /** + * Returns whether this metadata has no entries. + */ + default boolean isEmpty() { + return size() == 0; + } /** * Returns the collection of values in this metadata. - * - * @return collection of values */ @NonNull Collection values(); /** - * Returns the value associated with the given key. + * Returns the value associated with the given typed key. * - * @param key the key to look up - * @return the value associated with the key, or null if not found + * @param key the typed key to look up + * @return the typed value associated with the key, or null if not found */ @Nullable - Object get(@NonNull String key); + T get(@NonNull MetadataKey key); /** - * Returns whether this metadata contains the given key. + * Returns whether this metadata contains the given typed key. * - * @param key the key to check + * @param key the typed key to check * @return true if the key exists, false otherwise */ - boolean containsKey(@NonNull String key); - - /** - * Returns a copy of the underlying data as a Map. - * - * @return a copy of the metadata map - */ - @NonNull - Map toMap(); + boolean containsKey(@NonNull MetadataKey key); } diff --git a/api/src/main/java/io/split/android/client/api/MetadataKey.java b/api/src/main/java/io/split/android/client/api/MetadataKey.java new file mode 100644 index 000000000..ff2b25265 --- /dev/null +++ b/api/src/main/java/io/split/android/client/api/MetadataKey.java @@ -0,0 +1,24 @@ +package io.split.android.client.api; + +import androidx.annotation.NonNull; + +/** + * A typed metadata key used to retrieve values from {@link EventMetadata} without manual casting. + *

+ * Instances are exposed as public constants grouped by event (e.g. {@link SdkUpdateMetadataKeys}). + */ +public final class MetadataKey { + + private final String mName; + + public MetadataKey(@NonNull String name) { + mName = name; + } + + @NonNull + public String name() { + return mName; + } +} + + diff --git a/api/src/main/java/io/split/android/client/api/SdkReadyFromCacheMetadataKeys.java b/api/src/main/java/io/split/android/client/api/SdkReadyFromCacheMetadataKeys.java new file mode 100644 index 000000000..8a0c8b4e6 --- /dev/null +++ b/api/src/main/java/io/split/android/client/api/SdkReadyFromCacheMetadataKeys.java @@ -0,0 +1,24 @@ +package io.split.android.client.api; + +/** + * Typed metadata keys for {@code sdkReadyFromCache} event metadata. + */ +public final class SdkReadyFromCacheMetadataKeys { + private SdkReadyFromCacheMetadataKeys() { + // no instances + } + + /** + * True if this is a fresh install with no usable cache. + */ + public static final MetadataKey FRESH_INSTALL = new MetadataKey<>("freshInstall"); + + /** + * Last successful cache timestamp in milliseconds since epoch. + *

+ * May be absent when not available. + */ + public static final MetadataKey LAST_UPDATE_TIMESTAMP = new MetadataKey<>("lastUpdateTimestamp"); +} + + diff --git a/api/src/main/java/io/split/android/client/api/SdkUpdateMetadataKeys.java b/api/src/main/java/io/split/android/client/api/SdkUpdateMetadataKeys.java new file mode 100644 index 000000000..891990fcd --- /dev/null +++ b/api/src/main/java/io/split/android/client/api/SdkUpdateMetadataKeys.java @@ -0,0 +1,17 @@ +package io.split.android.client.api; + +import java.util.List; + +/** + * Typed metadata keys for {@code sdkUpdate} event metadata. + */ +public final class SdkUpdateMetadataKeys { + private SdkUpdateMetadataKeys() { + // no instances + } + + /** + * Names of flags that changed in this update. + */ + public static final MetadataKey> UPDATED_FLAGS = new MetadataKey<>("updatedFlags"); +} diff --git a/api/src/main/java/io/split/android/client/events/SplitEventTask.java b/api/src/main/java/io/split/android/client/events/SplitEventTask.java index f880e0fe1..3d714c38b 100644 --- a/api/src/main/java/io/split/android/client/events/SplitEventTask.java +++ b/api/src/main/java/io/split/android/client/events/SplitEventTask.java @@ -31,7 +31,7 @@ * client.on(SplitEvent.SDK_UPDATE, new SplitEventTask() { * @Override * public void onPostExecution(SplitClient client, EventMetadata metadata) { - * List updatedFlags = (List) metadata.get("updatedFlags"); + * List updatedFlags = metadata.get(SdkUpdateMetadataKeys.UPDATED_FLAGS); * // Handle update with metadata * } * diff --git a/build.gradle b/build.gradle index 110bd8d03..8fd26fa5a 100644 --- a/build.gradle +++ b/build.gradle @@ -19,7 +19,7 @@ apply plugin: 'com.vanniktech.maven.publish' apply from: "$rootDir/gradle/jacoco-root.gradle" ext { - splitVersion = '5.5.0-rc1' + splitVersion = '5.5.0-rc5' jacocoVersion = '0.8.8' } diff --git a/events-domain/src/main/java/io/split/android/client/events/metadata/EventMetadataHelpers.java b/events-domain/src/main/java/io/split/android/client/events/metadata/EventMetadataHelpers.java index d7fc334a7..39f5f2b2d 100644 --- a/events-domain/src/main/java/io/split/android/client/events/metadata/EventMetadataHelpers.java +++ b/events-domain/src/main/java/io/split/android/client/events/metadata/EventMetadataHelpers.java @@ -7,6 +7,8 @@ import java.util.List; import io.split.android.client.api.EventMetadata; +import io.split.android.client.api.SdkReadyFromCacheMetadataKeys; +import io.split.android.client.api.SdkUpdateMetadataKeys; /** * Helper class for creating {@link EventMetadata} instances. @@ -15,17 +17,13 @@ */ public class EventMetadataHelpers { - private static final String KEY_UPDATED_FLAGS = "updatedFlags"; - private static final String KEY_LAST_UPDATE_TIMESTAMP = "lastUpdateTimestamp"; - private static final String KEY_FRESH_INSTALL = "freshInstall"; - private EventMetadataHelpers() { // Utility class } public static EventMetadata createUpdatedFlagsMetadata(List updatedSplitNames) { return new EventMetadataBuilder() - .put(KEY_UPDATED_FLAGS, new ArrayList<>(new HashSet<>(updatedSplitNames))) + .put(SdkUpdateMetadataKeys.UPDATED_FLAGS.name(), new ArrayList<>(new HashSet<>(updatedSplitNames))) .build(); } @@ -38,10 +36,10 @@ public static EventMetadata createUpdatedFlagsMetadata(List updatedSplit */ public static EventMetadata createCacheReadyMetadata(@Nullable Long lastUpdateTimestamp, boolean freshInstall) { EventMetadataBuilder builder = new EventMetadataBuilder() - .put(KEY_FRESH_INSTALL, freshInstall); + .put(SdkReadyFromCacheMetadataKeys.FRESH_INSTALL.name(), freshInstall); if (lastUpdateTimestamp != null) { - builder.put(KEY_LAST_UPDATE_TIMESTAMP, lastUpdateTimestamp); + builder.put(SdkReadyFromCacheMetadataKeys.LAST_UPDATE_TIMESTAMP.name(), lastUpdateTimestamp); } return builder.build(); diff --git a/events-domain/src/main/java/io/split/android/client/events/metadata/EventMetadataImpl.java b/events-domain/src/main/java/io/split/android/client/events/metadata/EventMetadataImpl.java index 8c9b73ffa..b2e561f8e 100644 --- a/events-domain/src/main/java/io/split/android/client/events/metadata/EventMetadataImpl.java +++ b/events-domain/src/main/java/io/split/android/client/events/metadata/EventMetadataImpl.java @@ -9,9 +9,9 @@ import java.util.HashMap; import java.util.List; import java.util.Map; -import java.util.Set; import io.split.android.client.api.EventMetadata; +import io.split.android.client.api.MetadataKey; /** * Implementation of {@link EventMetadata}. @@ -34,10 +34,9 @@ class EventMetadataImpl implements EventMetadata { mData = Collections.unmodifiableMap(copy); } - @NonNull @Override - public Set keys() { - return mData.keySet(); + public int size() { + return mData.size(); } @NonNull @@ -48,27 +47,13 @@ public Collection values() { @Nullable @Override - public Object get(@NonNull String key) { - return mData.get(key); - } - - @Override - public boolean containsKey(@NonNull String key) { - return mData.containsKey(key); + public T get(@NonNull MetadataKey key) { + //noinspection unchecked + return (T) mData.get(key.name()); } - @NonNull @Override - public Map toMap() { - Map copy = new HashMap<>(); - for (Map.Entry entry : mData.entrySet()) { - Object value = entry.getValue(); - if (value instanceof List) { - copy.put(entry.getKey(), new ArrayList<>((List) value)); - } else { - copy.put(entry.getKey(), value); - } - } - return copy; + public boolean containsKey(@NonNull MetadataKey key) { + return mData.containsKey(key.name()); } } diff --git a/events-domain/src/test/java/io/split/android/client/events/metadata/EventMetadataBuilderTest.java b/events-domain/src/test/java/io/split/android/client/events/metadata/EventMetadataBuilderTest.java index 4652c1ba5..13515c17c 100644 --- a/events-domain/src/test/java/io/split/android/client/events/metadata/EventMetadataBuilderTest.java +++ b/events-domain/src/test/java/io/split/android/client/events/metadata/EventMetadataBuilderTest.java @@ -16,6 +16,8 @@ import java.util.List; import io.split.android.client.api.EventMetadata; +import io.split.android.client.api.MetadataKey; +import io.split.android.client.api.SdkUpdateMetadataKeys; public class EventMetadataBuilderTest { @@ -76,7 +78,8 @@ public void putIgnoresValueWhenValidatorReturnsFalse() { .put("key", "value") .build(); - assertFalse(metadata.containsKey("key")); + MetadataKey KEY = new MetadataKey<>("key"); + assertFalse(metadata.containsKey(KEY)); } @Test @@ -87,14 +90,15 @@ public void putIncludesValueWhenValidatorReturnsTrue() { .put("key", "value") .build(); - assertEquals("value", metadata.get("key")); + MetadataKey KEY = new MetadataKey<>("key"); + assertEquals("value", metadata.get(KEY)); } @Test public void buildCreatesEmptyMetadataWhenNothingAdded() { EventMetadata metadata = new EventMetadataBuilder().build(); - assertTrue(metadata.keys().isEmpty()); + assertTrue(metadata.isEmpty()); } @Test @@ -103,7 +107,8 @@ public void putStringAddsValue() { .put("key", "value") .build(); - assertEquals("value", metadata.get("key")); + MetadataKey KEY = new MetadataKey<>("key"); + assertEquals("value", metadata.get(KEY)); } @Test @@ -112,7 +117,8 @@ public void putIntegerAddsValue() { .put("count", 42) .build(); - assertEquals(42, metadata.get("count")); + MetadataKey COUNT = new MetadataKey<>("count"); + assertEquals(Integer.valueOf(42), metadata.get(COUNT)); } @Test @@ -121,7 +127,8 @@ public void putLongAddsValue() { .put("timestamp", 1234567890L) .build(); - assertEquals(1234567890L, metadata.get("timestamp")); + MetadataKey TIMESTAMP = new MetadataKey<>("timestamp"); + assertEquals(Long.valueOf(1234567890L), metadata.get(TIMESTAMP)); } @Test @@ -130,7 +137,8 @@ public void putDoubleAddsValue() { .put("rate", 3.14) .build(); - assertEquals(3.14, metadata.get("rate")); + MetadataKey RATE = new MetadataKey<>("rate"); + assertEquals(Double.valueOf(3.14), metadata.get(RATE)); } @Test @@ -139,7 +147,8 @@ public void putBooleanTrueAddsValue() { .put("enabled", true) .build(); - assertEquals(true, metadata.get("enabled")); + MetadataKey ENABLED = new MetadataKey<>("enabled"); + assertEquals(Boolean.TRUE, metadata.get(ENABLED)); } @Test @@ -148,7 +157,8 @@ public void putBooleanFalseAddsValue() { .put("disabled", false) .build(); - assertEquals(false, metadata.get("disabled")); + MetadataKey DISABLED = new MetadataKey<>("disabled"); + assertEquals(Boolean.FALSE, metadata.get(DISABLED)); } @Test @@ -159,7 +169,7 @@ public void putListOfStringsAddsValue() { .put("updatedFlags", flags) .build(); - assertEquals(flags, metadata.get("updatedFlags")); + assertEquals(flags, metadata.get(SdkUpdateMetadataKeys.UPDATED_FLAGS)); } @Test @@ -171,11 +181,11 @@ public void chainingMultiplePutsWorks() { .put("list", Arrays.asList("a", "b")) .build(); - assertEquals(4, metadata.keys().size()); - assertEquals("text", metadata.get("string")); - assertEquals(100, metadata.get("number")); - assertEquals(true, metadata.get("flag")); - assertEquals(Arrays.asList("a", "b"), metadata.get("list")); + assertEquals(4, metadata.size()); + assertEquals("text", metadata.get(new MetadataKey("string"))); + assertEquals(Integer.valueOf(100), metadata.get(new MetadataKey("number"))); + assertEquals(Boolean.TRUE, metadata.get(new MetadataKey("flag"))); + assertEquals(Arrays.asList("a", "b"), metadata.get(new MetadataKey>("list"))); } @Test @@ -185,7 +195,8 @@ public void overwritingKeyUsesLastValue() { .put("key", "second") .build(); - assertEquals("second", metadata.get("key")); + MetadataKey KEY = new MetadataKey<>("key"); + assertEquals("second", metadata.get(KEY)); } @Test @@ -196,6 +207,7 @@ public void buildReturnsNewInstanceEachTime() { EventMetadata metadata1 = builder.build(); EventMetadata metadata2 = builder.build(); - assertEquals(metadata1.get("key"), metadata2.get("key")); + MetadataKey KEY = new MetadataKey<>("key"); + assertEquals(metadata1.get(KEY), metadata2.get(KEY)); } } diff --git a/events-domain/src/test/java/io/split/android/client/events/metadata/EventMetadataHelpersTest.java b/events-domain/src/test/java/io/split/android/client/events/metadata/EventMetadataHelpersTest.java index d21bc8d3f..4f2101cbd 100644 --- a/events-domain/src/test/java/io/split/android/client/events/metadata/EventMetadataHelpersTest.java +++ b/events-domain/src/test/java/io/split/android/client/events/metadata/EventMetadataHelpersTest.java @@ -11,6 +11,8 @@ import java.util.List; import io.split.android.client.api.EventMetadata; +import io.split.android.client.api.SdkReadyFromCacheMetadataKeys; +import io.split.android.client.api.SdkUpdateMetadataKeys; public class EventMetadataHelpersTest { @@ -20,9 +22,8 @@ public void createUpdatedFlagsMetadataContainsFlags() { List flags = Arrays.asList("flag1", "flag2", "flag3"); EventMetadata metadata = EventMetadataHelpers.createUpdatedFlagsMetadata(flags); - assertTrue(metadata.containsKey("updatedFlags")); - @SuppressWarnings("unchecked") - List result = (List) metadata.get("updatedFlags"); + assertTrue(metadata.containsKey(SdkUpdateMetadataKeys.UPDATED_FLAGS)); + List result = metadata.get(SdkUpdateMetadataKeys.UPDATED_FLAGS); assertEquals(3, result.size()); assertTrue(result.contains("flag1")); assertTrue(result.contains("flag2")); @@ -34,33 +35,33 @@ public void createUpdatedFlagsMetadataContainsFlags() { public void createCacheReadyMetadataWithTimestampAndFreshInstallFalse() { EventMetadata metadata = EventMetadataHelpers.createCacheReadyMetadata(1234567890L, false); - assertEquals(1234567890L, metadata.get("lastUpdateTimestamp")); - assertEquals(false, metadata.get("freshInstall")); + assertEquals(Long.valueOf(1234567890L), metadata.get(SdkReadyFromCacheMetadataKeys.LAST_UPDATE_TIMESTAMP)); + assertEquals(Boolean.FALSE, metadata.get(SdkReadyFromCacheMetadataKeys.FRESH_INSTALL)); } @Test public void createCacheReadyMetadataWithNullTimestampAndFreshInstallTrue() { EventMetadata metadata = EventMetadataHelpers.createCacheReadyMetadata(null, true); - assertNull(metadata.get("lastUpdateTimestamp")); - assertEquals(true, metadata.get("freshInstall")); + assertNull(metadata.get(SdkReadyFromCacheMetadataKeys.LAST_UPDATE_TIMESTAMP)); + assertEquals(Boolean.TRUE, metadata.get(SdkReadyFromCacheMetadataKeys.FRESH_INSTALL)); } @Test public void createCacheReadyMetadataKeysAreCorrect() { EventMetadata metadata = EventMetadataHelpers.createCacheReadyMetadata(123L, false); - assertTrue(metadata.containsKey("lastUpdateTimestamp")); - assertTrue(metadata.containsKey("freshInstall")); - assertEquals(2, metadata.keys().size()); + assertTrue(metadata.containsKey(SdkReadyFromCacheMetadataKeys.LAST_UPDATE_TIMESTAMP)); + assertTrue(metadata.containsKey(SdkReadyFromCacheMetadataKeys.FRESH_INSTALL)); + assertEquals(2, metadata.size()); } @Test public void createCacheReadyMetadataWithZeroTimestamp() { EventMetadata metadata = EventMetadataHelpers.createCacheReadyMetadata(0L, false); - assertEquals(0L, metadata.get("lastUpdateTimestamp")); - assertEquals(false, metadata.get("freshInstall")); + assertEquals(Long.valueOf(0L), metadata.get(SdkReadyFromCacheMetadataKeys.LAST_UPDATE_TIMESTAMP)); + assertEquals(Boolean.FALSE, metadata.get(SdkReadyFromCacheMetadataKeys.FRESH_INSTALL)); } @Test @@ -69,8 +70,8 @@ public void createCacheReadyMetadataForCachePath() { long storedTimestamp = 1700000000000L; EventMetadata metadata = EventMetadataHelpers.createCacheReadyMetadata(storedTimestamp, false); - assertFalse((Boolean) metadata.get("freshInstall")); - assertEquals(storedTimestamp, metadata.get("lastUpdateTimestamp")); + assertEquals(Boolean.FALSE, metadata.get(SdkReadyFromCacheMetadataKeys.FRESH_INSTALL)); + assertEquals(Long.valueOf(storedTimestamp), metadata.get(SdkReadyFromCacheMetadataKeys.LAST_UPDATE_TIMESTAMP)); } @Test @@ -78,8 +79,8 @@ public void createCacheReadyMetadataForSyncPath() { // Sync path: freshInstall=true, timestamp=null EventMetadata metadata = EventMetadataHelpers.createCacheReadyMetadata(null, true); - assertTrue((Boolean) metadata.get("freshInstall")); - assertNull(metadata.get("lastUpdateTimestamp")); + assertEquals(Boolean.TRUE, metadata.get(SdkReadyFromCacheMetadataKeys.FRESH_INSTALL)); + assertNull(metadata.get(SdkReadyFromCacheMetadataKeys.LAST_UPDATE_TIMESTAMP)); } } diff --git a/events-domain/src/test/java/io/split/android/client/events/metadata/EventMetadataImplTest.java b/events-domain/src/test/java/io/split/android/client/events/metadata/EventMetadataImplTest.java index 54059494e..e727f5227 100644 --- a/events-domain/src/test/java/io/split/android/client/events/metadata/EventMetadataImplTest.java +++ b/events-domain/src/test/java/io/split/android/client/events/metadata/EventMetadataImplTest.java @@ -13,31 +13,31 @@ import java.util.HashMap; import java.util.List; import java.util.Map; -import java.util.Set; + +import io.split.android.client.api.MetadataKey; public class EventMetadataImplTest { @Test - public void keysReturnsAllKeys() { + public void sizeAndContainsKeyReflectStoredEntries() { Map data = new HashMap<>(); data.put("key1", "value1"); data.put("key2", 42); data.put("key3", true); EventMetadataImpl metadata = new EventMetadataImpl(data); - Set keys = metadata.keys(); - assertEquals(3, keys.size()); - assertTrue(keys.contains("key1")); - assertTrue(keys.contains("key2")); - assertTrue(keys.contains("key3")); + assertEquals(3, metadata.size()); + assertTrue(metadata.containsKey(new MetadataKey<>("key1"))); + assertTrue(metadata.containsKey(new MetadataKey<>("key2"))); + assertTrue(metadata.containsKey(new MetadataKey<>("key3"))); } @Test - public void keysReturnsEmptySetForEmptyMetadata() { + public void isEmptyReturnsTrueForEmptyMetadata() { EventMetadataImpl metadata = new EventMetadataImpl(new HashMap<>()); - assertTrue(metadata.keys().isEmpty()); + assertTrue(metadata.isEmpty()); } @Test @@ -68,7 +68,7 @@ public void getReturnsValueForExistingKey() { EventMetadataImpl metadata = new EventMetadataImpl(data); - assertEquals("value", metadata.get("key")); + assertEquals("value", metadata.get(new MetadataKey("key"))); } @Test @@ -78,7 +78,7 @@ public void getReturnsNullForNonExistingKey() { EventMetadataImpl metadata = new EventMetadataImpl(data); - assertNull(metadata.get("nonExistingKey")); + assertNull(metadata.get(new MetadataKey("nonExistingKey"))); } @Test @@ -88,7 +88,7 @@ public void containsKeyReturnsTrueForExistingKey() { EventMetadataImpl metadata = new EventMetadataImpl(data); - assertTrue(metadata.containsKey("key")); + assertTrue(metadata.containsKey(new MetadataKey<>("key"))); } @Test @@ -98,72 +98,7 @@ public void containsKeyReturnsFalseForNonExistingKey() { EventMetadataImpl metadata = new EventMetadataImpl(data); - assertFalse(metadata.containsKey("nonExistingKey")); - } - - @Test - public void toMapReturnsACopyOfTheData() { - Map data = new HashMap<>(); - data.put("key", "value"); - - EventMetadataImpl metadata = new EventMetadataImpl(data); - Map copy = metadata.toMap(); - - assertEquals(1, copy.size()); - assertEquals("value", copy.get("key")); - - // Verify it's a copy by modifying it - copy.put("newKey", "newValue"); - assertFalse(metadata.containsKey("newKey")); - } - - @Test - public void toMapReturnsEmptyMapForEmptyMetadata() { - EventMetadataImpl metadata = new EventMetadataImpl(new HashMap<>()); - - assertTrue(metadata.toMap().isEmpty()); - } - - @Test - public void toMapReturnsModifiableCopyOfLists() { - Map data = new HashMap<>(); - data.put("flags", Arrays.asList("flag_1", "flag_2")); - - EventMetadataImpl metadata = new EventMetadataImpl(data); - Map copy = metadata.toMap(); - - // Should be able to modify the list in the copy - @SuppressWarnings("unchecked") - List listInCopy = (List) copy.get("flags"); - listInCopy.add("flag_3"); - - // Original metadata should not be affected - @SuppressWarnings("unchecked") - List originalList = (List) metadata.get("flags"); - assertEquals(2, originalList.size()); - assertEquals(Arrays.asList("flag_1", "flag_2"), originalList); - } - - @Test - public void toMapListsAreIndependentAcrossCalls() { - Map data = new HashMap<>(); - data.put("flags", Arrays.asList("flag_1", "flag_2")); - - EventMetadataImpl metadata = new EventMetadataImpl(data); - - Map copy1 = metadata.toMap(); - Map copy2 = metadata.toMap(); - - // Modify copy1's list - @SuppressWarnings("unchecked") - List list1 = (List) copy1.get("flags"); - list1.add("flag_3"); - - // copy2's list should not be affected - @SuppressWarnings("unchecked") - List list2 = (List) copy2.get("flags"); - assertEquals(2, list2.size()); - assertEquals(Arrays.asList("flag_1", "flag_2"), list2); + assertFalse(metadata.containsKey(new MetadataKey<>("nonExistingKey"))); } @Test @@ -177,8 +112,8 @@ public void metadataIsImmutableAfterConstruction() { data.put("newKey", "newValue"); // Metadata should not be affected - assertFalse(metadata.containsKey("newKey")); - assertEquals(1, metadata.keys().size()); + assertFalse(metadata.containsKey(new MetadataKey<>("newKey"))); + assertEquals(1, metadata.size()); } @Test @@ -193,8 +128,7 @@ public void listIsDefensivelyCopiedDuringConstruction() { originalList.add("flag_3"); // Metadata should not be affected - @SuppressWarnings("unchecked") - List storedList = (List) metadata.get("flags"); + List storedList = metadata.get(new MetadataKey>("flags")); assertEquals(2, storedList.size()); assertEquals(Arrays.asList("flag_1", "flag_2"), storedList); } @@ -206,8 +140,7 @@ public void listReturnedByGetIsUnmodifiable() { EventMetadataImpl metadata = new EventMetadataImpl(data); - @SuppressWarnings("unchecked") - List list = (List) metadata.get("flags"); + List list = metadata.get(new MetadataKey>("flags")); // This should throw UnsupportedOperationException list.add("flag_3"); diff --git a/main/src/androidTest/java/tests/integration/events/SdkEventsIntegrationTest.java b/main/src/androidTest/java/tests/integration/events/SdkEventsIntegrationTest.java index 9cef41e98..e215583f9 100644 --- a/main/src/androidTest/java/tests/integration/events/SdkEventsIntegrationTest.java +++ b/main/src/androidTest/java/tests/integration/events/SdkEventsIntegrationTest.java @@ -37,6 +37,7 @@ import io.split.android.client.SplitFactory; import io.split.android.client.api.EventMetadata; import io.split.android.client.api.Key; +import io.split.android.client.api.SdkReadyFromCacheMetadataKeys; import io.split.android.client.events.SplitEvent; import io.split.android.client.events.SplitEventTask; import io.split.android.client.network.HttpMethod; @@ -158,14 +159,12 @@ public void sdkReadyFromCacheFiresWhenCacheLoadingCompletes() throws Exception { // And: the metadata contains "freshInstall" with value false assertNotNull("Metadata should not be null", receivedMetadata.get()); - assertTrue("Metadata should contain freshInstall key", receivedMetadata.get().containsKey("freshInstall")); - assertFalse("freshInstall should be false for cache path", - (Boolean) receivedMetadata.get().get("freshInstall")); + Boolean freshInstall = receivedMetadata.get().get(SdkReadyFromCacheMetadataKeys.FRESH_INSTALL); + assertNotNull("freshInstall should not be null", freshInstall); + assertFalse("freshInstall should be false for cache path", freshInstall); // And: the metadata contains "lastUpdateTimestamp" with a valid timestamp - assertTrue("Metadata should contain lastUpdateTimestamp key", - receivedMetadata.get().containsKey("lastUpdateTimestamp")); - Long lastUpdateTimestamp = (Long) receivedMetadata.get().get("lastUpdateTimestamp"); + Long lastUpdateTimestamp = receivedMetadata.get().get(SdkReadyFromCacheMetadataKeys.LAST_UPDATE_TIMESTAMP); assertNotNull("lastUpdateTimestamp should not be null", lastUpdateTimestamp); assertTrue("lastUpdateTimestamp should be valid", lastUpdateTimestamp > 0); @@ -207,9 +206,9 @@ public void sdkReadyFromCacheFiresWhenSyncCompletesFreshInstallPath() throws Exc // And: the metadata contains "freshInstall" with value true assertNotNull("Metadata should not be null", receivedMetadata.get()); - assertTrue("Metadata should contain freshInstall key", receivedMetadata.get().containsKey("freshInstall")); - assertTrue("freshInstall should be true for sync path (fresh install)", - (Boolean) receivedMetadata.get().get("freshInstall")); + Boolean freshInstall = receivedMetadata.get().get(SdkReadyFromCacheMetadataKeys.FRESH_INSTALL); + assertNotNull("freshInstall should not be null", freshInstall); + assertTrue("freshInstall should be true for sync path (fresh install)", freshInstall); factory.destroy(); } diff --git a/main/src/androidTest/java/tests/service/EventsManagerTest.java b/main/src/androidTest/java/tests/service/EventsManagerTest.java index a7080874c..fc097de7f 100644 --- a/main/src/androidTest/java/tests/service/EventsManagerTest.java +++ b/main/src/androidTest/java/tests/service/EventsManagerTest.java @@ -18,6 +18,7 @@ import io.split.android.client.SplitClient; import io.split.android.client.SplitClientConfig; import io.split.android.client.api.EventMetadata; +import io.split.android.client.api.SdkUpdateMetadataKeys; import io.split.android.client.events.SplitEvent; import io.split.android.client.events.SplitEventTask; import io.split.android.client.events.SplitEventsManager; @@ -216,8 +217,8 @@ public void onPostExecution(SplitClient client, EventMetadata metadata) { Assert.assertTrue("SDK_UPDATE should fire", updateLatch.await(5, TimeUnit.SECONDS)); Assert.assertNotNull("Metadata should not be null", receivedMetadata.get()); - Assert.assertTrue("Metadata should contain updatedFlags", receivedMetadata.get().containsKey("updatedFlags")); - List metadataList = (List) receivedMetadata.get().get("updatedFlags"); + Assert.assertTrue("Metadata should contain updatedFlags", receivedMetadata.get().containsKey(SdkUpdateMetadataKeys.UPDATED_FLAGS)); + List metadataList = receivedMetadata.get().get(SdkUpdateMetadataKeys.UPDATED_FLAGS); Assert.assertTrue("Metadata should contain only killed_flag", metadataList.size() == 1 && metadataList.contains("killed_flag")); } @@ -373,8 +374,8 @@ public void onPostExecutionView(SplitClient client) { Assert.assertTrue("Main thread legacy method SHOULD run on main thread", mainThreadLegacyOnMainThread.get()); Assert.assertNotNull("Background metadata should not be null", backgroundMetadata.get()); - Assert.assertTrue("Background metadata should contain updatedFlags", backgroundMetadata.get().containsKey("updatedFlags")); + Assert.assertTrue("Background metadata should contain updatedFlags", backgroundMetadata.get().containsKey(SdkUpdateMetadataKeys.UPDATED_FLAGS)); Assert.assertNotNull("Main thread metadata should not be null", mainThreadMetadata.get()); - Assert.assertTrue("Main thread metadata should contain updatedFlags", mainThreadMetadata.get().containsKey("updatedFlags")); + Assert.assertTrue("Main thread metadata should contain updatedFlags", mainThreadMetadata.get().containsKey(SdkUpdateMetadataKeys.UPDATED_FLAGS)); } } diff --git a/main/src/test/java/io/split/android/client/events/EventsManagerCoordinatorTest.java b/main/src/test/java/io/split/android/client/events/EventsManagerCoordinatorTest.java index eb8768aae..3c6c466e6 100644 --- a/main/src/test/java/io/split/android/client/events/EventsManagerCoordinatorTest.java +++ b/main/src/test/java/io/split/android/client/events/EventsManagerCoordinatorTest.java @@ -19,6 +19,7 @@ import io.split.android.client.api.EventMetadata; import io.split.android.client.api.Key; +import io.split.android.client.api.SdkUpdateMetadataKeys; public class EventsManagerCoordinatorTest { @@ -115,12 +116,8 @@ public void SPLITS_UPDATEDEventWithMetadataIsPassedDownToChildren() { verify(mMockChildEventsManager).notifyInternalEvent(eq(SplitInternalEvent.SPLITS_UPDATED), argThat(meta -> { if (meta == null) return false; - assertTrue(meta.containsKey("updatedFlags")); - Object flagsValue = meta.get("updatedFlags"); - assertNotNull(flagsValue); - assertTrue(flagsValue instanceof List); - @SuppressWarnings("unchecked") - List flags = (List) flagsValue; + List flags = meta.get(SdkUpdateMetadataKeys.UPDATED_FLAGS); + assertNotNull(flags); return flags.size() == 2 && flags.contains("flag1") && flags.contains("flag2"); })); } diff --git a/main/src/test/java/io/split/android/client/events/EventsManagerTest.java b/main/src/test/java/io/split/android/client/events/EventsManagerTest.java index 5b4643f3a..9646a24ff 100644 --- a/main/src/test/java/io/split/android/client/events/EventsManagerTest.java +++ b/main/src/test/java/io/split/android/client/events/EventsManagerTest.java @@ -22,6 +22,7 @@ import io.split.android.client.SplitClient; import io.split.android.client.SplitClientConfig; import io.split.android.client.api.EventMetadata; +import io.split.android.client.api.SdkUpdateMetadataKeys; import io.split.android.client.events.executors.SplitEventExecutorResources; import io.split.android.client.events.metadata.EventMetadataHelpers; import io.split.android.fake.SplitTaskExecutorStub; @@ -296,7 +297,7 @@ public void onPostExecution(SplitClient client, EventMetadata metadata) { boolean updateAwait = updateLatch.await(3, TimeUnit.SECONDS); assertTrue("SDK_UPDATE callback should be called", updateAwait); assertNotNull("Metadata should not be null", receivedMetadata.get()); - assertTrue("Metadata should contain updatedFlags", receivedMetadata.get().containsKey("updatedFlags")); + assertTrue("Metadata should contain updatedFlags", receivedMetadata.get().containsKey(SdkUpdateMetadataKeys.UPDATED_FLAGS)); } @Test @@ -322,7 +323,7 @@ public void onPostExecutionView(SplitClient client, EventMetadata metadata) { boolean updateAwait = updateLatch.await(3, TimeUnit.SECONDS); assertTrue("SDK_UPDATE callback should be called on main thread", updateAwait); assertNotNull("Metadata should not be null", receivedMetadata.get()); - assertTrue("Metadata should contain updatedFlags", receivedMetadata.get().containsKey("updatedFlags")); + assertTrue("Metadata should contain updatedFlags", receivedMetadata.get().containsKey(SdkUpdateMetadataKeys.UPDATED_FLAGS)); } @Test @@ -384,7 +385,7 @@ public void onPostExecution(SplitClient client) { assertTrue("Metadata method should be called", metadataMethodCalled[0]); assertTrue("Legacy method should also be called", legacyMethodCalled[0]); assertNotNull("Metadata should be passed to metadata method", receivedMetadata.get()); - assertTrue("Metadata should contain updatedFlags", receivedMetadata.get().containsKey("updatedFlags")); + assertTrue("Metadata should contain updatedFlags", receivedMetadata.get().containsKey(SdkUpdateMetadataKeys.UPDATED_FLAGS)); } @Test diff --git a/main/src/test/java/io/split/android/client/events/SplitEventTaskMetadataTest.java b/main/src/test/java/io/split/android/client/events/SplitEventTaskMetadataTest.java index 03b125ae2..aa1ab6d5c 100644 --- a/main/src/test/java/io/split/android/client/events/SplitEventTaskMetadataTest.java +++ b/main/src/test/java/io/split/android/client/events/SplitEventTaskMetadataTest.java @@ -10,6 +10,8 @@ import io.split.android.client.SplitClient; import io.split.android.client.api.EventMetadata; +import io.split.android.client.api.SdkReadyFromCacheMetadataKeys; +import io.split.android.client.api.SdkUpdateMetadataKeys; import io.split.android.client.events.metadata.EventMetadataHelpers; public class SplitEventTaskMetadataTest { @@ -86,7 +88,7 @@ public void onPostExecutionWithMetadataReceivesCorrectParameters() { @Override public void onPostExecution(SplitClient client, EventMetadata metadata) { metadataReceived[0] = metadata != null; - hasUpdatedFlags[0] = metadata != null && metadata.containsKey("updatedFlags"); + hasUpdatedFlags[0] = metadata != null && metadata.containsKey(SdkUpdateMetadataKeys.UPDATED_FLAGS); } }; @@ -108,8 +110,8 @@ public void onPostExecutionViewWithMetadataReceivesCorrectParameters() { @Override public void onPostExecutionView(SplitClient client, EventMetadata metadata) { metadataReceived[0] = metadata != null; - hasTimestamp[0] = metadata != null && metadata.containsKey("lastUpdateTimestamp"); - hasFreshInstall[0] = metadata != null && metadata.containsKey("freshInstall"); + hasTimestamp[0] = metadata != null && metadata.containsKey(SdkReadyFromCacheMetadataKeys.LAST_UPDATE_TIMESTAMP); + hasFreshInstall[0] = metadata != null && metadata.containsKey(SdkReadyFromCacheMetadataKeys.FRESH_INSTALL); } }; diff --git a/main/src/test/java/io/split/android/client/localhost/LocalhostSplitsStorageTest.java b/main/src/test/java/io/split/android/client/localhost/LocalhostSplitsStorageTest.java index 0d7fdc85a..38e7a3681 100644 --- a/main/src/test/java/io/split/android/client/localhost/LocalhostSplitsStorageTest.java +++ b/main/src/test/java/io/split/android/client/localhost/LocalhostSplitsStorageTest.java @@ -25,6 +25,7 @@ import java.util.List; import io.split.android.client.api.EventMetadata; +import io.split.android.client.api.SdkUpdateMetadataKeys; import io.split.android.client.events.EventsManagerCoordinator; import io.split.android.client.events.SplitInternalEvent; import io.split.android.client.storage.legacy.FileStorage; @@ -100,12 +101,8 @@ public void loadLocalNotifiesSplitsUpdatedWithMetadataContainingUpdatedFlags() t EventMetadata metadata = metadataCaptor.getValue(); assertNotNull("Metadata should not be null", metadata); - assertTrue("Metadata should contain 'updatedFlags' key", metadata.containsKey("updatedFlags")); - Object flagsValue = metadata.get("updatedFlags"); - assertNotNull("updatedFlags value should not be null", flagsValue); - assertTrue("updatedFlags should be a List", flagsValue instanceof List); - @SuppressWarnings("unchecked") - List flags = (List) flagsValue; + List flags = metadata.get(SdkUpdateMetadataKeys.UPDATED_FLAGS); + assertNotNull("updatedFlags value should not be null", flags); assertTrue("Metadata should contain 'split1' flag", flags.contains("split1")); } } diff --git a/main/src/test/java/io/split/android/client/service/SplitInPlaceUpdateTaskTest.java b/main/src/test/java/io/split/android/client/service/SplitInPlaceUpdateTaskTest.java index 682bbf70b..f23cff508 100644 --- a/main/src/test/java/io/split/android/client/service/SplitInPlaceUpdateTaskTest.java +++ b/main/src/test/java/io/split/android/client/service/SplitInPlaceUpdateTaskTest.java @@ -21,6 +21,7 @@ import java.util.List; import io.split.android.client.api.EventMetadata; +import io.split.android.client.api.SdkUpdateMetadataKeys; import io.split.android.client.dtos.Split; import io.split.android.client.events.ISplitEventsManager; import io.split.android.client.events.SplitInternalEvent; @@ -151,12 +152,8 @@ public void splitsUpdatedIncludesMetadataWithUpdatedFlags() { verify(mEventsManager).notifyInternalEvent(eq(SplitInternalEvent.SPLITS_UPDATED), argThat(metadata -> { if (metadata == null) return false; - assertTrue(metadata.containsKey("updatedFlags")); - Object flagsValue = metadata.get("updatedFlags"); - assertNotNull(flagsValue); - assertTrue(flagsValue instanceof List); - @SuppressWarnings("unchecked") - List flags = (List) flagsValue; + List flags = metadata.get(SdkUpdateMetadataKeys.UPDATED_FLAGS); + assertNotNull(flags); assertEquals(2, flags.size()); assertTrue(flags.contains("test_split_1")); assertTrue(flags.contains("test_split_2")); @@ -178,12 +175,8 @@ public void splitsUpdatedIncludesArchivedSplitsInMetadata() { verify(mEventsManager).notifyInternalEvent(eq(SplitInternalEvent.SPLITS_UPDATED), argThat(metadata -> { if (metadata == null) return false; - assertTrue(metadata.containsKey("updatedFlags")); - Object flagsValue = metadata.get("updatedFlags"); - assertNotNull(flagsValue); - assertTrue(flagsValue instanceof List); - @SuppressWarnings("unchecked") - List flags = (List) flagsValue; + List flags = metadata.get(SdkUpdateMetadataKeys.UPDATED_FLAGS); + assertNotNull(flags); assertEquals(1, flags.size()); assertTrue(flags.contains("archived_split")); return true; diff --git a/main/src/test/java/io/split/android/client/service/SplitKillTaskTest.java b/main/src/test/java/io/split/android/client/service/SplitKillTaskTest.java index 7f1c3de16..9a356a34e 100644 --- a/main/src/test/java/io/split/android/client/service/SplitKillTaskTest.java +++ b/main/src/test/java/io/split/android/client/service/SplitKillTaskTest.java @@ -11,6 +11,7 @@ import java.util.List; import io.split.android.client.api.EventMetadata; +import io.split.android.client.api.SdkUpdateMetadataKeys; import io.split.android.client.dtos.Split; import io.split.android.client.events.SplitEventsManager; import io.split.android.client.events.SplitInternalEvent; @@ -79,8 +80,7 @@ public void correctExecution() throws HttpFetcherException { eq(SplitInternalEvent.SPLIT_KILLED_NOTIFICATION), metadataCaptor.capture()); EventMetadata metadata = metadataCaptor.getValue(); Assert.assertNotNull(metadata); - @SuppressWarnings("unchecked") - List updatedFlags = (List) metadata.get("updatedFlags"); + List updatedFlags = metadata.get(SdkUpdateMetadataKeys.UPDATED_FLAGS); Assert.assertNotNull(updatedFlags); Assert.assertEquals(1, updatedFlags.size()); Assert.assertTrue(updatedFlags.contains("split1")); diff --git a/main/src/test/java/io/split/android/client/service/SplitSyncTaskTest.java b/main/src/test/java/io/split/android/client/service/SplitSyncTaskTest.java index 9d9b76f2c..a5f346b7b 100644 --- a/main/src/test/java/io/split/android/client/service/SplitSyncTaskTest.java +++ b/main/src/test/java/io/split/android/client/service/SplitSyncTaskTest.java @@ -28,6 +28,7 @@ import java.util.List; import java.util.Map; +import io.split.android.client.api.SdkUpdateMetadataKeys; import io.split.android.client.dtos.SplitChange; import io.split.android.client.events.SplitEventsManager; import io.split.android.client.events.SplitInternalEvent; @@ -257,12 +258,8 @@ public void splitsUpdatedIncludesMetadataWithUpdatedFlags() throws HttpFetcherEx verify(mEventsManager).notifyInternalEvent(eq(SplitInternalEvent.SPLITS_UPDATED), argThat(metadata -> { if (metadata == null) return false; - assertTrue(metadata.containsKey("updatedFlags")); - Object flagsValue = metadata.get("updatedFlags"); - assertNotNull(flagsValue); - assertTrue(flagsValue instanceof List); - @SuppressWarnings("unchecked") - List flags = (List) flagsValue; + List flags = metadata.get(SdkUpdateMetadataKeys.UPDATED_FLAGS); + assertNotNull(flags); assertEquals(3, flags.size()); assertTrue(flags.contains("split1")); assertTrue(flags.contains("split2")); @@ -288,12 +285,8 @@ public void splitsUpdatedIncludesEmptyMetadataWhenNoSplitsUpdated() throws HttpF verify(mEventsManager).notifyInternalEvent(eq(SplitInternalEvent.SPLITS_UPDATED), argThat(metadata -> { if (metadata == null) return false; - assertTrue(metadata.containsKey("updatedFlags")); - Object flagsValue = metadata.get("updatedFlags"); - assertNotNull(flagsValue); - assertTrue(flagsValue instanceof List); - @SuppressWarnings("unchecked") - List flags = (List) flagsValue; + List flags = metadata.get(SdkUpdateMetadataKeys.UPDATED_FLAGS); + assertNotNull(flags); assertTrue(flags.isEmpty()); return true; })); diff --git a/main/src/test/java/io/split/android/client/service/SplitUpdateTaskTest.java b/main/src/test/java/io/split/android/client/service/SplitUpdateTaskTest.java index 931643f08..063d44b7c 100644 --- a/main/src/test/java/io/split/android/client/service/SplitUpdateTaskTest.java +++ b/main/src/test/java/io/split/android/client/service/SplitUpdateTaskTest.java @@ -21,6 +21,8 @@ import java.util.Arrays; import java.util.List; +import io.split.android.client.api.SdkReadyFromCacheMetadataKeys; +import io.split.android.client.api.SdkUpdateMetadataKeys; import io.split.android.client.dtos.SplitChange; import io.split.android.client.events.SplitEventsManager; import io.split.android.client.events.SplitInternalEvent; @@ -105,10 +107,9 @@ public void targetingRulesSyncCompleteIsAlwaysFiredOnSuccessfulSyncWithSyncMetad // Verify TARGETING_RULES_SYNC_COMPLETE is fired with sync metadata (freshInstall=true, lastUpdateTimestamp=null) verify(mEventsManager).notifyInternalEvent(eq(SplitInternalEvent.TARGETING_RULES_SYNC_COMPLETE), argThat(metadata -> { if (metadata == null) return false; - assertTrue(metadata.containsKey("freshInstall")); - assertEquals(true, metadata.get("freshInstall")); - // lastUpdateTimestamp should not be present (null) - return !metadata.containsKey("lastUpdateTimestamp") || metadata.get("lastUpdateTimestamp") == null; + assertEquals(Boolean.TRUE, metadata.get(SdkReadyFromCacheMetadataKeys.FRESH_INSTALL)); + // lastUpdateTimestamp should not be present (or should be null) + return metadata.get(SdkReadyFromCacheMetadataKeys.LAST_UPDATE_TIMESTAMP) == null; })); } @@ -175,12 +176,8 @@ public void splitsUpdatedIncludesMetadataWithUpdatedFlags() { verify(mEventsManager).notifyInternalEvent(eq(SplitInternalEvent.SPLITS_UPDATED), argThat(metadata -> { if (metadata == null) return false; - assertTrue(metadata.containsKey("updatedFlags")); - Object flagsValue = metadata.get("updatedFlags"); - assertNotNull(flagsValue); - assertTrue(flagsValue instanceof List); - @SuppressWarnings("unchecked") - List flags = (List) flagsValue; + List flags = metadata.get(SdkUpdateMetadataKeys.UPDATED_FLAGS); + assertNotNull(flags); assertEquals(2, flags.size()); assertTrue(flags.contains("flag1")); assertTrue(flags.contains("flag2")); From bcb4c5e607f80bd983f9c27b8da7870fd7ef5004 Mon Sep 17 00:00:00 2001 From: Gaston Thea Date: Mon, 5 Jan 2026 18:49:38 -0300 Subject: [PATCH 02/27] Fix javadoc --- .../android/client/api/SdkReadyFromCacheMetadataKeys.java | 4 +--- .../io/split/android/client/api/SdkUpdateMetadataKeys.java | 2 +- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/api/src/main/java/io/split/android/client/api/SdkReadyFromCacheMetadataKeys.java b/api/src/main/java/io/split/android/client/api/SdkReadyFromCacheMetadataKeys.java index 8a0c8b4e6..394c3071d 100644 --- a/api/src/main/java/io/split/android/client/api/SdkReadyFromCacheMetadataKeys.java +++ b/api/src/main/java/io/split/android/client/api/SdkReadyFromCacheMetadataKeys.java @@ -1,7 +1,7 @@ package io.split.android.client.api; /** - * Typed metadata keys for {@code sdkReadyFromCache} event metadata. + * Typed metadata keys for {@code SplitEvent.SDK_READY_FROM_CACHE} event metadata. */ public final class SdkReadyFromCacheMetadataKeys { private SdkReadyFromCacheMetadataKeys() { @@ -20,5 +20,3 @@ private SdkReadyFromCacheMetadataKeys() { */ public static final MetadataKey LAST_UPDATE_TIMESTAMP = new MetadataKey<>("lastUpdateTimestamp"); } - - diff --git a/api/src/main/java/io/split/android/client/api/SdkUpdateMetadataKeys.java b/api/src/main/java/io/split/android/client/api/SdkUpdateMetadataKeys.java index 891990fcd..054c35795 100644 --- a/api/src/main/java/io/split/android/client/api/SdkUpdateMetadataKeys.java +++ b/api/src/main/java/io/split/android/client/api/SdkUpdateMetadataKeys.java @@ -3,7 +3,7 @@ import java.util.List; /** - * Typed metadata keys for {@code sdkUpdate} event metadata. + * Typed metadata keys for {@code SplitEvent.SDK_UPDATE} event metadata. */ public final class SdkUpdateMetadataKeys { private SdkUpdateMetadataKeys() { From 7fe76b5def66c8aa2bd21f4c5e042891ee4d9132 Mon Sep 17 00:00:00 2001 From: Gaston Thea Date: Tue, 6 Jan 2026 11:58:54 -0300 Subject: [PATCH 03/27] Add missing tests --- .github/workflows/sonarqube.yml | 7 +++- .../android/client/api/MetadataKeyTest.java | 35 +++++++++++++++++++ .../SdkReadyFromCacheMetadataKeysTest.java | 29 +++++++++++++++ .../client/api/SdkUpdateMetadataKeysTest.java | 19 ++++++++++ 4 files changed, 89 insertions(+), 1 deletion(-) create mode 100644 api/src/test/java/io/split/android/client/api/MetadataKeyTest.java create mode 100644 api/src/test/java/io/split/android/client/api/SdkReadyFromCacheMetadataKeysTest.java create mode 100644 api/src/test/java/io/split/android/client/api/SdkUpdateMetadataKeysTest.java diff --git a/.github/workflows/sonarqube.yml b/.github/workflows/sonarqube.yml index f6e2d570b..93fc5af44 100644 --- a/.github/workflows/sonarqube.yml +++ b/.github/workflows/sonarqube.yml @@ -1,6 +1,11 @@ name: SonarCloud Analysis on: + push: + branches: + - master + - development + - '*_baseline' pull_request: branches: - '*' @@ -127,7 +132,7 @@ jobs: echo "=== Verification Complete ===" - name: SonarCloud Scan - uses: SonarSource/sonarqube-scan-action@v6 + uses: SonarSource/sonarqube-scan-action@v7 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # Needed to get PR information SONAR_TOKEN: ${{ secrets.SONARQUBE_TOKEN }} diff --git a/api/src/test/java/io/split/android/client/api/MetadataKeyTest.java b/api/src/test/java/io/split/android/client/api/MetadataKeyTest.java new file mode 100644 index 000000000..8f4c3e98d --- /dev/null +++ b/api/src/test/java/io/split/android/client/api/MetadataKeyTest.java @@ -0,0 +1,35 @@ +package io.split.android.client.api; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; + +import org.junit.Test; + +public class MetadataKeyTest { + + @Test + public void nameReturnsConstructorParameter() { + MetadataKey key = new MetadataKey<>("testKey"); + + assertEquals("testKey", key.name()); + } + + @Test + public void nameReturnsEmptyStringWhenConstructedWithEmptyString() { + MetadataKey key = new MetadataKey<>(""); + + assertEquals("", key.name()); + } + + @Test + public void keyCanBeCreatedWithDifferentTypes() { + MetadataKey stringKey = new MetadataKey<>("stringKey"); + MetadataKey intKey = new MetadataKey<>("intKey"); + MetadataKey boolKey = new MetadataKey<>("boolKey"); + + assertNotNull(stringKey.name()); + assertNotNull(intKey.name()); + assertNotNull(boolKey.name()); + } +} + diff --git a/api/src/test/java/io/split/android/client/api/SdkReadyFromCacheMetadataKeysTest.java b/api/src/test/java/io/split/android/client/api/SdkReadyFromCacheMetadataKeysTest.java new file mode 100644 index 000000000..2165a3e45 --- /dev/null +++ b/api/src/test/java/io/split/android/client/api/SdkReadyFromCacheMetadataKeysTest.java @@ -0,0 +1,29 @@ +package io.split.android.client.api; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; + +import org.junit.Test; + +public class SdkReadyFromCacheMetadataKeysTest { + + @Test + public void freshInstallKeyHasCorrectName() { + assertEquals("freshInstall", SdkReadyFromCacheMetadataKeys.FRESH_INSTALL.name()); + } + + @Test + public void freshInstallKeyIsNotNull() { + assertNotNull(SdkReadyFromCacheMetadataKeys.FRESH_INSTALL); + } + + @Test + public void lastUpdateTimestampKeyHasCorrectName() { + assertEquals("lastUpdateTimestamp", SdkReadyFromCacheMetadataKeys.LAST_UPDATE_TIMESTAMP.name()); + } + + @Test + public void lastUpdateTimestampKeyIsNotNull() { + assertNotNull(SdkReadyFromCacheMetadataKeys.LAST_UPDATE_TIMESTAMP); + } +} diff --git a/api/src/test/java/io/split/android/client/api/SdkUpdateMetadataKeysTest.java b/api/src/test/java/io/split/android/client/api/SdkUpdateMetadataKeysTest.java new file mode 100644 index 000000000..0ea701899 --- /dev/null +++ b/api/src/test/java/io/split/android/client/api/SdkUpdateMetadataKeysTest.java @@ -0,0 +1,19 @@ +package io.split.android.client.api; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; + +import org.junit.Test; + +public class SdkUpdateMetadataKeysTest { + + @Test + public void updatedFlagsKeyHasCorrectName() { + assertEquals("updatedFlags", SdkUpdateMetadataKeys.UPDATED_FLAGS.name()); + } + + @Test + public void updatedFlagsKeyIsNotNull() { + assertNotNull(SdkUpdateMetadataKeys.UPDATED_FLAGS); + } +} From 9b93aad3b9ea6176da401f16ee8c041090189da3 Mon Sep 17 00:00:00 2001 From: Gaston Thea Date: Tue, 6 Jan 2026 11:59:48 -0300 Subject: [PATCH 04/27] Update README --- api/README.md | 1 - 1 file changed, 1 deletion(-) diff --git a/api/README.md b/api/README.md index 0a58d3927..e455d981e 100644 --- a/api/README.md +++ b/api/README.md @@ -3,4 +3,3 @@ This module contains the public API interfaces and types exposed to consumers of the Split SDK. Classes in this module are part of the public API contract and should maintain backwards compatibility. - From 610bafef9e3900179a439d4e29055274ba8914ad Mon Sep 17 00:00:00 2001 From: Gaston Thea Date: Tue, 6 Jan 2026 16:07:22 -0300 Subject: [PATCH 05/27] Test --- .../io/split/android/client/SplitClient.java | 23 ++++ .../split/android/client/api/MetadataKey.java | 24 ---- .../api/SdkReadyFromCacheMetadataKeys.java | 22 --- .../client/api/SdkUpdateMetadataKeys.java | 17 --- .../split/android/client/events/SdkEvent.java | 84 ++++++++++++ .../events/SdkReadyFromCacheEventTask.java | 50 +++++++ .../events/SdkReadyFromCacheMetadata.java | 49 +++++++ .../client/events/SdkUpdateEventTask.java | 49 +++++++ .../client/events/SdkUpdateMetadata.java | 36 +++++ .../android/client/events/SplitEventTask.java | 67 +--------- .../android/client/api/MetadataKeyTest.java | 35 ----- .../SdkReadyFromCacheMetadataKeysTest.java | 29 ---- .../client/api/SdkUpdateMetadataKeysTest.java | 19 --- .../android/client/events/SdkEventTest.java | 50 +++++++ .../SdkReadyFromCacheEventTaskTest.java | 51 +++++++ .../events/SdkReadyFromCacheMetadataTest.java | 66 +++++++++ .../client/events/SdkUpdateEventTaskTest.java | 51 +++++++ .../client/events/SdkUpdateMetadataTest.java | 46 +++++++ .../events/EventsManagerCoordinator.java | 2 +- .../client/events/ISplitEventsManager.java | 2 +- .../client/events/SplitEventDelivery.java | 2 +- .../client/events/SplitEventsManager.java | 100 ++++++-------- .../events/metadata}/EventMetadata.java | 21 +-- .../events/metadata/EventMetadataBuilder.java | 2 - .../events/metadata/EventMetadataHelpers.java | 10 +- .../events/metadata/EventMetadataImpl.java | 12 +- .../client/events/metadata/MetadataKeys.java | 34 +++++ .../events/metadata/TypedTaskConverter.java | 54 ++++++++ .../events/TypedTaskConversionTest.java | 90 +++++++++++++ .../metadata/EventMetadataBuilderTest.java | 44 +++--- .../metadata/EventMetadataHelpersTest.java | 35 +++-- .../metadata/EventMetadataImplTest.java | 23 ++-- .../events/metadata/MetadataKeysTest.java | 27 ++++ .../events/SdkEventsIntegrationTest.java | 2 +- .../java/tests/service/EventsManagerTest.java | 2 +- .../AlwaysReturnControlSplitClient.java | 4 + .../split/android/client/SplitClientImpl.java | 6 + .../localhost/LocalhostSplitClient.java | 8 +- .../localhost/LocalhostSplitsStorage.java | 2 +- .../splits/SplitInPlaceUpdateTask.java | 2 +- .../client/service/splits/SplitKillTask.java | 2 +- .../client/service/splits/SplitsSyncTask.java | 2 +- .../service/splits/SplitsUpdateTask.java | 2 +- .../synchronizer/LoadLocalDataListener.java | 2 +- .../events/EventsManagerCoordinatorTest.java | 7 +- .../client/events/EventsManagerTest.java | 72 +++++----- .../events/SplitEventTaskMetadataTest.java | 125 ------------------ .../localhost/LocalhostSplitsStorageTest.java | 8 +- .../service/SplitInPlaceUpdateTaskTest.java | 11 +- .../client/service/SplitKillTaskTest.java | 10 +- .../client/service/SplitSyncTaskTest.java | 9 +- .../client/service/SplitUpdateTaskTest.java | 13 +- .../LoadLocalDataListenerTest.java | 2 +- .../android/fake/SplitEventsManagerStub.java | 2 +- 54 files changed, 968 insertions(+), 551 deletions(-) delete mode 100644 api/src/main/java/io/split/android/client/api/MetadataKey.java delete mode 100644 api/src/main/java/io/split/android/client/api/SdkReadyFromCacheMetadataKeys.java delete mode 100644 api/src/main/java/io/split/android/client/api/SdkUpdateMetadataKeys.java create mode 100644 api/src/main/java/io/split/android/client/events/SdkEvent.java create mode 100644 api/src/main/java/io/split/android/client/events/SdkReadyFromCacheEventTask.java create mode 100644 api/src/main/java/io/split/android/client/events/SdkReadyFromCacheMetadata.java create mode 100644 api/src/main/java/io/split/android/client/events/SdkUpdateEventTask.java create mode 100644 api/src/main/java/io/split/android/client/events/SdkUpdateMetadata.java delete mode 100644 api/src/test/java/io/split/android/client/api/MetadataKeyTest.java delete mode 100644 api/src/test/java/io/split/android/client/api/SdkReadyFromCacheMetadataKeysTest.java delete mode 100644 api/src/test/java/io/split/android/client/api/SdkUpdateMetadataKeysTest.java create mode 100644 api/src/test/java/io/split/android/client/events/SdkEventTest.java create mode 100644 api/src/test/java/io/split/android/client/events/SdkReadyFromCacheEventTaskTest.java create mode 100644 api/src/test/java/io/split/android/client/events/SdkReadyFromCacheMetadataTest.java create mode 100644 api/src/test/java/io/split/android/client/events/SdkUpdateEventTaskTest.java create mode 100644 api/src/test/java/io/split/android/client/events/SdkUpdateMetadataTest.java rename {api/src/main/java/io/split/android/client/api => events-domain/src/main/java/io/split/android/client/events/metadata}/EventMetadata.java (56%) create mode 100644 events-domain/src/main/java/io/split/android/client/events/metadata/MetadataKeys.java create mode 100644 events-domain/src/main/java/io/split/android/client/events/metadata/TypedTaskConverter.java create mode 100644 events-domain/src/test/java/io/split/android/client/events/TypedTaskConversionTest.java create mode 100644 events-domain/src/test/java/io/split/android/client/events/metadata/MetadataKeysTest.java delete mode 100644 main/src/test/java/io/split/android/client/events/SplitEventTaskMetadataTest.java diff --git a/api/src/main/java/io/split/android/client/SplitClient.java b/api/src/main/java/io/split/android/client/SplitClient.java index 63d35f457..a9375c0df 100644 --- a/api/src/main/java/io/split/android/client/SplitClient.java +++ b/api/src/main/java/io/split/android/client/SplitClient.java @@ -7,6 +7,7 @@ import java.util.Map; import io.split.android.client.attributes.AttributesManager; +import io.split.android.client.events.SdkEvent; import io.split.android.client.events.SplitEvent; import io.split.android.client.events.SplitEventTask; @@ -179,6 +180,28 @@ public interface SplitClient extends AttributesManager { void on(SplitEvent event, SplitEventTask task); + /** + * Registers a type-safe event listener for SDK events. + *

+ * This method provides compile-time type safety for event task registration. + * The event type parameter enforces the correct task type for each event. + *

+ * Example usage: + *

{@code
+     * client.on(SdkEvent.SDK_UPDATE, new SdkUpdateEventTask() {
+     *     @Override
+     *     public void onPostExecution(SplitClient client, SdkUpdateMetadata metadata) {
+     *         List flags = metadata.getUpdatedFlags();
+     *     }
+     * });
+     * }
+ * + * @param event the type-safe event to listen for + * @param task the task to execute when the event occurs + * @param the type of event task + */ + void on(SdkEvent event, T task); + /** * Enqueue a new event to be sent to Split data collection services. *

diff --git a/api/src/main/java/io/split/android/client/api/MetadataKey.java b/api/src/main/java/io/split/android/client/api/MetadataKey.java deleted file mode 100644 index ff2b25265..000000000 --- a/api/src/main/java/io/split/android/client/api/MetadataKey.java +++ /dev/null @@ -1,24 +0,0 @@ -package io.split.android.client.api; - -import androidx.annotation.NonNull; - -/** - * A typed metadata key used to retrieve values from {@link EventMetadata} without manual casting. - *

- * Instances are exposed as public constants grouped by event (e.g. {@link SdkUpdateMetadataKeys}). - */ -public final class MetadataKey { - - private final String mName; - - public MetadataKey(@NonNull String name) { - mName = name; - } - - @NonNull - public String name() { - return mName; - } -} - - diff --git a/api/src/main/java/io/split/android/client/api/SdkReadyFromCacheMetadataKeys.java b/api/src/main/java/io/split/android/client/api/SdkReadyFromCacheMetadataKeys.java deleted file mode 100644 index 394c3071d..000000000 --- a/api/src/main/java/io/split/android/client/api/SdkReadyFromCacheMetadataKeys.java +++ /dev/null @@ -1,22 +0,0 @@ -package io.split.android.client.api; - -/** - * Typed metadata keys for {@code SplitEvent.SDK_READY_FROM_CACHE} event metadata. - */ -public final class SdkReadyFromCacheMetadataKeys { - private SdkReadyFromCacheMetadataKeys() { - // no instances - } - - /** - * True if this is a fresh install with no usable cache. - */ - public static final MetadataKey FRESH_INSTALL = new MetadataKey<>("freshInstall"); - - /** - * Last successful cache timestamp in milliseconds since epoch. - *

- * May be absent when not available. - */ - public static final MetadataKey LAST_UPDATE_TIMESTAMP = new MetadataKey<>("lastUpdateTimestamp"); -} diff --git a/api/src/main/java/io/split/android/client/api/SdkUpdateMetadataKeys.java b/api/src/main/java/io/split/android/client/api/SdkUpdateMetadataKeys.java deleted file mode 100644 index 054c35795..000000000 --- a/api/src/main/java/io/split/android/client/api/SdkUpdateMetadataKeys.java +++ /dev/null @@ -1,17 +0,0 @@ -package io.split.android.client.api; - -import java.util.List; - -/** - * Typed metadata keys for {@code SplitEvent.SDK_UPDATE} event metadata. - */ -public final class SdkUpdateMetadataKeys { - private SdkUpdateMetadataKeys() { - // no instances - } - - /** - * Names of flags that changed in this update. - */ - public static final MetadataKey> UPDATED_FLAGS = new MetadataKey<>("updatedFlags"); -} diff --git a/api/src/main/java/io/split/android/client/events/SdkEvent.java b/api/src/main/java/io/split/android/client/events/SdkEvent.java new file mode 100644 index 000000000..7c2fcfc4d --- /dev/null +++ b/api/src/main/java/io/split/android/client/events/SdkEvent.java @@ -0,0 +1,84 @@ +package io.split.android.client.events; + +/** + * Type-safe event class for SDK event subscriptions. + *

+ * This class provides compile-time type safety for event task registration. + * Use the static instances to register event listeners with the correct task type. + *

+ * Example usage: + *

{@code
+ * client.on(SdkEvent.SDK_UPDATE, new SdkUpdateEventTask() {
+ *     @Override
+ *     public void onPostExecution(SplitClient client, SdkUpdateMetadata metadata) {
+ *         List flags = metadata.getUpdatedFlags();
+ *     }
+ * });
+ * }
+ * + * @param the type of event task that can handle this event + */ +public abstract class SdkEvent { + + /** + * Event fired when SDK definitions are updated from the server. + *

+ * Register with {@link SdkUpdateEventTask} to receive typed metadata. + */ + public static final SdkEvent SDK_UPDATE = new SdkEvent() { + @Override + public SplitEvent toSplitEvent() { + return SplitEvent.SDK_UPDATE; + } + }; + + /** + * Event fired when SDK is ready from cached data. + *

+ * Register with {@link SdkReadyFromCacheEventTask} to receive typed metadata. + */ + public static final SdkEvent SDK_READY_FROM_CACHE = new SdkEvent() { + @Override + public SplitEvent toSplitEvent() { + return SplitEvent.SDK_READY_FROM_CACHE; + } + }; + + /** + * Event fired when SDK is fully ready from the server. + *

+ * Register with {@link SplitEventTask} for basic event handling. + */ + public static final SdkEvent SDK_READY = new SdkEvent() { + @Override + public SplitEvent toSplitEvent() { + return SplitEvent.SDK_READY; + } + }; + + /** + * Event fired when SDK ready has timed out. + *

+ * Register with {@link SplitEventTask} for basic event handling. + */ + public static final SdkEvent SDK_READY_TIMED_OUT = new SdkEvent() { + @Override + public SplitEvent toSplitEvent() { + return SplitEvent.SDK_READY_TIMED_OUT; + } + }; + + // Package-private constructor to prevent external subclassing + SdkEvent() { + } + + /** + * Converts this type-safe event to the internal SplitEvent enum. + *

+ * Internal API - called by SDK internals. + * + * @return the corresponding SplitEvent enum value + */ + public abstract SplitEvent toSplitEvent(); +} + diff --git a/api/src/main/java/io/split/android/client/events/SdkReadyFromCacheEventTask.java b/api/src/main/java/io/split/android/client/events/SdkReadyFromCacheEventTask.java new file mode 100644 index 000000000..6359f5ec3 --- /dev/null +++ b/api/src/main/java/io/split/android/client/events/SdkReadyFromCacheEventTask.java @@ -0,0 +1,50 @@ +package io.split.android.client.events; + +import io.split.android.client.SplitClient; + +/** + * Typed event task for SDK_READY_FROM_CACHE events. + *

+ * Extend this class and override the typed methods to handle SDK_READY_FROM_CACHE events + * with type-safe metadata access. + *

+ * Example usage: + *

{@code
+ * client.on(SdkEvent.SDK_READY_FROM_CACHE, new SdkReadyFromCacheEventTask() {
+ *     @Override
+ *     public void onPostExecution(SplitClient client, SdkReadyFromCacheMetadata metadata) {
+ *         Boolean freshInstall = metadata.isFreshInstall();
+ *         Long timestamp = metadata.getLastUpdateTimestamp();
+ *         // Handle cache ready event
+ *     }
+ * });
+ * }
+ */ +public class SdkReadyFromCacheEventTask extends SplitEventTask { + + /** + * Called when SDK_READY_FROM_CACHE event occurs, executed on a background thread. + *

+ * Override this method to handle SDK_READY_FROM_CACHE events with typed metadata. + * + * @param client the Split client instance + * @param metadata the typed metadata containing cache information + * @throws SplitEventTaskMethodNotImplementedException if not overridden (default behavior) + */ + public void onPostExecution(SplitClient client, SdkReadyFromCacheMetadata metadata) { + throw new SplitEventTaskMethodNotImplementedException(); + } + + /** + * Called when SDK_READY_FROM_CACHE event occurs, executed on the main/UI thread. + *

+ * Override this method to handle SDK_READY_FROM_CACHE events with typed metadata on the main thread. + * + * @param client the Split client instance + * @param metadata the typed metadata containing cache information + * @throws SplitEventTaskMethodNotImplementedException if not overridden (default behavior) + */ + public void onPostExecutionView(SplitClient client, SdkReadyFromCacheMetadata metadata) { + throw new SplitEventTaskMethodNotImplementedException(); + } +} diff --git a/api/src/main/java/io/split/android/client/events/SdkReadyFromCacheMetadata.java b/api/src/main/java/io/split/android/client/events/SdkReadyFromCacheMetadata.java new file mode 100644 index 000000000..3f1a883ed --- /dev/null +++ b/api/src/main/java/io/split/android/client/events/SdkReadyFromCacheMetadata.java @@ -0,0 +1,49 @@ +package io.split.android.client.events; + +import androidx.annotation.Nullable; + +/** + * Typed metadata for SDK_READY_FROM_CACHE events. + *

+ * Contains information about the cache state when the SDK is ready from cache. + */ +public final class SdkReadyFromCacheMetadata { + + @Nullable + private final Boolean mFreshInstall; + + @Nullable + private final Long mLastUpdateTimestamp; + + /** + * Creates a new SdkReadyFromCacheMetadata instance. + * + * @param freshInstall true if this is a fresh install with no usable cache, or null if not available + * @param lastUpdateTimestamp the last successful cache timestamp in milliseconds since epoch, or null if not available + */ + public SdkReadyFromCacheMetadata(@Nullable Boolean freshInstall, @Nullable Long lastUpdateTimestamp) { + mFreshInstall = freshInstall; + mLastUpdateTimestamp = lastUpdateTimestamp; + } + + /** + * Returns whether this is a fresh install with no usable cache. + * + * @return true if fresh install, false otherwise, or null if not available + */ + @Nullable + public Boolean isFreshInstall() { + return mFreshInstall; + } + + /** + * Returns the last successful cache timestamp in milliseconds since epoch. + * + * @return the timestamp, or null if not available + */ + @Nullable + public Long getLastUpdateTimestamp() { + return mLastUpdateTimestamp; + } +} + diff --git a/api/src/main/java/io/split/android/client/events/SdkUpdateEventTask.java b/api/src/main/java/io/split/android/client/events/SdkUpdateEventTask.java new file mode 100644 index 000000000..9f74ca8ce --- /dev/null +++ b/api/src/main/java/io/split/android/client/events/SdkUpdateEventTask.java @@ -0,0 +1,49 @@ +package io.split.android.client.events; + +import io.split.android.client.SplitClient; + +/** + * Typed event task for SDK_UPDATE events. + *

+ * Extend this class and override the typed methods to handle SDK_UPDATE events + * with type-safe metadata access. + *

+ * Example usage: + *

{@code
+ * client.on(SdkEvent.SDK_UPDATE, new SdkUpdateEventTask() {
+ *     @Override
+ *     public void onPostExecution(SplitClient client, SdkUpdateMetadata metadata) {
+ *         List flags = metadata.getUpdatedFlags();
+ *         // Handle updated flags
+ *     }
+ * });
+ * }
+ */ +public class SdkUpdateEventTask extends SplitEventTask { + + /** + * Called when SDK_UPDATE event occurs, executed on a background thread. + *

+ * Override this method to handle SDK_UPDATE events with typed metadata. + * + * @param client the Split client instance + * @param metadata the typed metadata containing updated flag information + * @throws SplitEventTaskMethodNotImplementedException if not overridden (default behavior) + */ + public void onPostExecution(SplitClient client, SdkUpdateMetadata metadata) { + throw new SplitEventTaskMethodNotImplementedException(); + } + + /** + * Called when SDK_UPDATE event occurs, executed on the main/UI thread. + *

+ * Override this method to handle SDK_UPDATE events with typed metadata on the main thread. + * + * @param client the Split client instance + * @param metadata the typed metadata containing updated flag information + * @throws SplitEventTaskMethodNotImplementedException if not overridden (default behavior) + */ + public void onPostExecutionView(SplitClient client, SdkUpdateMetadata metadata) { + throw new SplitEventTaskMethodNotImplementedException(); + } +} diff --git a/api/src/main/java/io/split/android/client/events/SdkUpdateMetadata.java b/api/src/main/java/io/split/android/client/events/SdkUpdateMetadata.java new file mode 100644 index 000000000..a9e6772b6 --- /dev/null +++ b/api/src/main/java/io/split/android/client/events/SdkUpdateMetadata.java @@ -0,0 +1,36 @@ +package io.split.android.client.events; + +import androidx.annotation.Nullable; + +import java.util.List; + +/** + * Typed metadata for SDK_UPDATE events. + *

+ * Contains information about flags that were updated in the event. + */ +public final class SdkUpdateMetadata { + + @Nullable + private final List mUpdatedFlags; + + /** + * Creates a new SdkUpdateMetadata instance. + * + * @param updatedFlags the list of flag names that were updated, or null if not available + */ + public SdkUpdateMetadata(@Nullable List updatedFlags) { + mUpdatedFlags = updatedFlags; + } + + /** + * Returns the list of flag names that changed in this update. + * + * @return the list of updated flag names, or null if not available + */ + @Nullable + public List getUpdatedFlags() { + return mUpdatedFlags; + } +} + diff --git a/api/src/main/java/io/split/android/client/events/SplitEventTask.java b/api/src/main/java/io/split/android/client/events/SplitEventTask.java index 3d714c38b..df61ba467 100644 --- a/api/src/main/java/io/split/android/client/events/SplitEventTask.java +++ b/api/src/main/java/io/split/android/client/events/SplitEventTask.java @@ -1,16 +1,11 @@ package io.split.android.client.events; -import androidx.annotation.Nullable; - import io.split.android.client.SplitClient; -import io.split.android.client.api.EventMetadata; /** * Base class for handling Split SDK events. *

* Extend this class and override the methods you need to handle specific SDK events. - * You can implement both the metadata-enabled and versions of the methods; - * if both are implemented, both will be called (metadata version first). *

* Threading: *

    @@ -18,26 +13,15 @@ *
  • {@code onPostExecutionView} methods are called on the main/UI thread (queued on main looper)
  • *
*

- * Metadata: - *

    - *
  • Metadata-enabled methods receive {@link EventMetadata} containing event-specific information
  • - *
  • Metadata may be {@code null} for some events
  • - *
  • If you only need metadata, implement the metadata version; if you need backward compatibility, - * implement both versions
  • - *
+ * For events with metadata (like SDK_UPDATE or SDK_READY_FROM_CACHE), use the typed event task classes + * {@link SdkUpdateEventTask} or {@link SdkReadyFromCacheEventTask} for type-safe metadata access. *

* Example usage: *

{@code
- * client.on(SplitEvent.SDK_UPDATE, new SplitEventTask() {
- *     @Override
- *     public void onPostExecution(SplitClient client, EventMetadata metadata) {
- *         List updatedFlags = metadata.get(SdkUpdateMetadataKeys.UPDATED_FLAGS);
- *         // Handle update with metadata
- *     }
- *
+ * client.on(SplitEvent.SDK_READY, new SplitEventTask() {
  *     @Override
  *     public void onPostExecution(SplitClient client) {
- *         // Legacy handling (also called if both are implemented)
+ *         // SDK is ready, start using Split
  *     }
  * });
  * }
@@ -46,7 +30,7 @@ public class SplitEventTask { /** * Called when an event occurs, executed on a background thread. *

- * Override this method to handle events on a background thread without metadata. + * Override this method to handle events on a background thread. * This method is executed immediately and is faster than {@link #onPostExecutionView(SplitClient)}. * * @param client the Split client instance @@ -59,7 +43,7 @@ public void onPostExecution(SplitClient client) { /** * Called when an event occurs, executed on the main/UI thread. *

- * Override this method to handle events on the main thread without metadata. + * Override this method to handle events on the main thread. * Use this when you need to update UI components. *

* Note: This method is queued on the main looper, so execution may be delayed @@ -71,43 +55,4 @@ public void onPostExecution(SplitClient client) { public void onPostExecutionView(SplitClient client) { throw new SplitEventTaskMethodNotImplementedException(); } - - /** - * Called when an event occurs with metadata, executed on a background thread. - *

- * Override this method to handle events on a background thread with access to event metadata. - * The metadata contains event-specific information such as updated flag names for SDK_UPDATE events. - * This method is executed immediately and is faster than {@link #onPostExecutionView(SplitClient, EventMetadata)}. - *

- * If both this method and {@link #onPostExecution(SplitClient)} are implemented, - * both will be called (this method first). - * - * @param client the Split client instance - * @param metadata the event metadata, may be {@code null} for some events - * @throws SplitEventTaskMethodNotImplementedException if not overridden (default behavior) - */ - public void onPostExecution(SplitClient client, @Nullable EventMetadata metadata) { - throw new SplitEventTaskMethodNotImplementedException(); - } - - /** - * Called when an event occurs with metadata, executed on the main/UI thread. - *

- * Override this method to handle events on the main thread with access to event metadata. - * The metadata contains event-specific information such as updated flag names for SDK_UPDATE events. - * Use this when you need to update UI components based on event metadata. - *

- * Note: This method is queued on the main looper, so execution may be delayed - * compared to {@link #onPostExecution(SplitClient, EventMetadata)}. - *

- * If both this method and {@link #onPostExecutionView(SplitClient)} are implemented, - * both will be called (this method first). - * - * @param client the Split client instance - * @param metadata the event metadata, may be {@code null} for some events - * @throws SplitEventTaskMethodNotImplementedException if not overridden (default behavior) - */ - public void onPostExecutionView(SplitClient client, @Nullable EventMetadata metadata) { - throw new SplitEventTaskMethodNotImplementedException(); - } } diff --git a/api/src/test/java/io/split/android/client/api/MetadataKeyTest.java b/api/src/test/java/io/split/android/client/api/MetadataKeyTest.java deleted file mode 100644 index 8f4c3e98d..000000000 --- a/api/src/test/java/io/split/android/client/api/MetadataKeyTest.java +++ /dev/null @@ -1,35 +0,0 @@ -package io.split.android.client.api; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNotNull; - -import org.junit.Test; - -public class MetadataKeyTest { - - @Test - public void nameReturnsConstructorParameter() { - MetadataKey key = new MetadataKey<>("testKey"); - - assertEquals("testKey", key.name()); - } - - @Test - public void nameReturnsEmptyStringWhenConstructedWithEmptyString() { - MetadataKey key = new MetadataKey<>(""); - - assertEquals("", key.name()); - } - - @Test - public void keyCanBeCreatedWithDifferentTypes() { - MetadataKey stringKey = new MetadataKey<>("stringKey"); - MetadataKey intKey = new MetadataKey<>("intKey"); - MetadataKey boolKey = new MetadataKey<>("boolKey"); - - assertNotNull(stringKey.name()); - assertNotNull(intKey.name()); - assertNotNull(boolKey.name()); - } -} - diff --git a/api/src/test/java/io/split/android/client/api/SdkReadyFromCacheMetadataKeysTest.java b/api/src/test/java/io/split/android/client/api/SdkReadyFromCacheMetadataKeysTest.java deleted file mode 100644 index 2165a3e45..000000000 --- a/api/src/test/java/io/split/android/client/api/SdkReadyFromCacheMetadataKeysTest.java +++ /dev/null @@ -1,29 +0,0 @@ -package io.split.android.client.api; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNotNull; - -import org.junit.Test; - -public class SdkReadyFromCacheMetadataKeysTest { - - @Test - public void freshInstallKeyHasCorrectName() { - assertEquals("freshInstall", SdkReadyFromCacheMetadataKeys.FRESH_INSTALL.name()); - } - - @Test - public void freshInstallKeyIsNotNull() { - assertNotNull(SdkReadyFromCacheMetadataKeys.FRESH_INSTALL); - } - - @Test - public void lastUpdateTimestampKeyHasCorrectName() { - assertEquals("lastUpdateTimestamp", SdkReadyFromCacheMetadataKeys.LAST_UPDATE_TIMESTAMP.name()); - } - - @Test - public void lastUpdateTimestampKeyIsNotNull() { - assertNotNull(SdkReadyFromCacheMetadataKeys.LAST_UPDATE_TIMESTAMP); - } -} diff --git a/api/src/test/java/io/split/android/client/api/SdkUpdateMetadataKeysTest.java b/api/src/test/java/io/split/android/client/api/SdkUpdateMetadataKeysTest.java deleted file mode 100644 index 0ea701899..000000000 --- a/api/src/test/java/io/split/android/client/api/SdkUpdateMetadataKeysTest.java +++ /dev/null @@ -1,19 +0,0 @@ -package io.split.android.client.api; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNotNull; - -import org.junit.Test; - -public class SdkUpdateMetadataKeysTest { - - @Test - public void updatedFlagsKeyHasCorrectName() { - assertEquals("updatedFlags", SdkUpdateMetadataKeys.UPDATED_FLAGS.name()); - } - - @Test - public void updatedFlagsKeyIsNotNull() { - assertNotNull(SdkUpdateMetadataKeys.UPDATED_FLAGS); - } -} diff --git a/api/src/test/java/io/split/android/client/events/SdkEventTest.java b/api/src/test/java/io/split/android/client/events/SdkEventTest.java new file mode 100644 index 000000000..6e347e7c1 --- /dev/null +++ b/api/src/test/java/io/split/android/client/events/SdkEventTest.java @@ -0,0 +1,50 @@ +package io.split.android.client.events; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; + +import org.junit.Test; + +public class SdkEventTest { + + @Test + public void sdkUpdateStaticInstanceExists() { + assertNotNull(SdkEvent.SDK_UPDATE); + } + + @Test + public void sdkReadyFromCacheStaticInstanceExists() { + assertNotNull(SdkEvent.SDK_READY_FROM_CACHE); + } + + @Test + public void sdkReadyStaticInstanceExists() { + assertNotNull(SdkEvent.SDK_READY); + } + + @Test + public void sdkReadyTimedOutStaticInstanceExists() { + assertNotNull(SdkEvent.SDK_READY_TIMED_OUT); + } + + @Test + public void sdkUpdateMapsToSplitEventSdkUpdate() { + assertEquals(SplitEvent.SDK_UPDATE, SdkEvent.SDK_UPDATE.toSplitEvent()); + } + + @Test + public void sdkReadyFromCacheMapsToSplitEventSdkReadyFromCache() { + assertEquals(SplitEvent.SDK_READY_FROM_CACHE, SdkEvent.SDK_READY_FROM_CACHE.toSplitEvent()); + } + + @Test + public void sdkReadyMapsToSplitEventSdkReady() { + assertEquals(SplitEvent.SDK_READY, SdkEvent.SDK_READY.toSplitEvent()); + } + + @Test + public void sdkReadyTimedOutMapsToSplitEventSdkReadyTimedOut() { + assertEquals(SplitEvent.SDK_READY_TIMED_OUT, SdkEvent.SDK_READY_TIMED_OUT.toSplitEvent()); + } +} + diff --git a/api/src/test/java/io/split/android/client/events/SdkReadyFromCacheEventTaskTest.java b/api/src/test/java/io/split/android/client/events/SdkReadyFromCacheEventTaskTest.java new file mode 100644 index 000000000..4765e1e2a --- /dev/null +++ b/api/src/test/java/io/split/android/client/events/SdkReadyFromCacheEventTaskTest.java @@ -0,0 +1,51 @@ +package io.split.android.client.events; + +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.mock; + +import org.junit.Test; + +import io.split.android.client.SplitClient; + +public class SdkReadyFromCacheEventTaskTest { + + @Test + public void extendsFromSplitEventTask() { + SdkReadyFromCacheEventTask task = new SdkReadyFromCacheEventTask() { + @Override + public void onPostExecution(SplitClient client, SdkReadyFromCacheMetadata metadata) { + // no-op + } + }; + + assertTrue(task instanceof SplitEventTask); + } + + @Test + public void defaultImplementationThrowsExceptionForTypedMethods() { + SdkReadyFromCacheEventTask task = new SdkReadyFromCacheEventTask() {}; + + boolean threwException = false; + try { + task.onPostExecution(mock(SplitClient.class), new SdkReadyFromCacheMetadata(null, null)); + } catch (SplitEventTaskMethodNotImplementedException e) { + threwException = true; + } + + assertTrue(threwException); + } + + @Test + public void defaultImplementationThrowsExceptionForTypedViewMethods() { + SdkReadyFromCacheEventTask task = new SdkReadyFromCacheEventTask() {}; + + boolean threwException = false; + try { + task.onPostExecutionView(mock(SplitClient.class), new SdkReadyFromCacheMetadata(null, null)); + } catch (SplitEventTaskMethodNotImplementedException e) { + threwException = true; + } + + assertTrue(threwException); + } +} diff --git a/api/src/test/java/io/split/android/client/events/SdkReadyFromCacheMetadataTest.java b/api/src/test/java/io/split/android/client/events/SdkReadyFromCacheMetadataTest.java new file mode 100644 index 000000000..64fd003f4 --- /dev/null +++ b/api/src/test/java/io/split/android/client/events/SdkReadyFromCacheMetadataTest.java @@ -0,0 +1,66 @@ +package io.split.android.client.events; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; + +import org.junit.Test; + +public class SdkReadyFromCacheMetadataTest { + + @Test + public void isFreshInstallReturnsNullWhenConstructedWithNull() { + SdkReadyFromCacheMetadata metadata = new SdkReadyFromCacheMetadata(null, null); + + assertNull(metadata.isFreshInstall()); + } + + @Test + public void isFreshInstallReturnsTrueWhenConstructedWithTrue() { + SdkReadyFromCacheMetadata metadata = new SdkReadyFromCacheMetadata(true, null); + + assertTrue(metadata.isFreshInstall()); + } + + @Test + public void isFreshInstallReturnsFalseWhenConstructedWithFalse() { + SdkReadyFromCacheMetadata metadata = new SdkReadyFromCacheMetadata(false, null); + + assertFalse(metadata.isFreshInstall()); + } + + @Test + public void getLastUpdateTimestampReturnsNullWhenConstructedWithNull() { + SdkReadyFromCacheMetadata metadata = new SdkReadyFromCacheMetadata(null, null); + + assertNull(metadata.getLastUpdateTimestamp()); + } + + @Test + public void getLastUpdateTimestampReturnsValueWhenConstructedWithValue() { + long timestamp = 1704067200000L; + SdkReadyFromCacheMetadata metadata = new SdkReadyFromCacheMetadata(null, timestamp); + + assertEquals(Long.valueOf(timestamp), metadata.getLastUpdateTimestamp()); + } + + @Test + public void bothValuesReturnCorrectlyWhenBothAreSet() { + long timestamp = 1704067200000L; + SdkReadyFromCacheMetadata metadata = new SdkReadyFromCacheMetadata(true, timestamp); + + assertTrue(metadata.isFreshInstall()); + assertEquals(Long.valueOf(timestamp), metadata.getLastUpdateTimestamp()); + } + + @Test + public void bothValuesReturnCorrectlyWhenFreshInstallIsFalse() { + long timestamp = 1704067200000L; + SdkReadyFromCacheMetadata metadata = new SdkReadyFromCacheMetadata(false, timestamp); + + assertFalse(metadata.isFreshInstall()); + assertEquals(Long.valueOf(timestamp), metadata.getLastUpdateTimestamp()); + } +} + diff --git a/api/src/test/java/io/split/android/client/events/SdkUpdateEventTaskTest.java b/api/src/test/java/io/split/android/client/events/SdkUpdateEventTaskTest.java new file mode 100644 index 000000000..3295b1c65 --- /dev/null +++ b/api/src/test/java/io/split/android/client/events/SdkUpdateEventTaskTest.java @@ -0,0 +1,51 @@ +package io.split.android.client.events; + +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.mock; + +import org.junit.Test; + +import io.split.android.client.SplitClient; + +public class SdkUpdateEventTaskTest { + + @Test + public void extendsFromSplitEventTask() { + SdkUpdateEventTask task = new SdkUpdateEventTask() { + @Override + public void onPostExecution(SplitClient client, SdkUpdateMetadata metadata) { + // no-op + } + }; + + assertTrue(task instanceof SplitEventTask); + } + + @Test + public void defaultImplementationThrowsExceptionForTypedMethods() { + SdkUpdateEventTask task = new SdkUpdateEventTask() {}; + + boolean threwException = false; + try { + task.onPostExecution(mock(SplitClient.class), new SdkUpdateMetadata(null)); + } catch (SplitEventTaskMethodNotImplementedException e) { + threwException = true; + } + + assertTrue(threwException); + } + + @Test + public void defaultImplementationThrowsExceptionForTypedViewMethods() { + SdkUpdateEventTask task = new SdkUpdateEventTask() {}; + + boolean threwException = false; + try { + task.onPostExecutionView(mock(SplitClient.class), new SdkUpdateMetadata(null)); + } catch (SplitEventTaskMethodNotImplementedException e) { + threwException = true; + } + + assertTrue(threwException); + } +} diff --git a/api/src/test/java/io/split/android/client/events/SdkUpdateMetadataTest.java b/api/src/test/java/io/split/android/client/events/SdkUpdateMetadataTest.java new file mode 100644 index 000000000..143c2b259 --- /dev/null +++ b/api/src/test/java/io/split/android/client/events/SdkUpdateMetadataTest.java @@ -0,0 +1,46 @@ +package io.split.android.client.events; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; + +import org.junit.Test; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +public class SdkUpdateMetadataTest { + + @Test + public void getUpdatedFlagsReturnsNullWhenConstructedWithNull() { + SdkUpdateMetadata metadata = new SdkUpdateMetadata(null); + + assertNull(metadata.getUpdatedFlags()); + } + + @Test + public void getUpdatedFlagsReturnsEmptyListWhenConstructedWithEmptyList() { + SdkUpdateMetadata metadata = new SdkUpdateMetadata(Collections.emptyList()); + + assertEquals(Collections.emptyList(), metadata.getUpdatedFlags()); + } + + @Test + public void getUpdatedFlagsReturnsListWhenConstructedWithList() { + List flags = Arrays.asList("flag1", "flag2", "flag3"); + SdkUpdateMetadata metadata = new SdkUpdateMetadata(flags); + + assertEquals(flags, metadata.getUpdatedFlags()); + } + + @Test + public void getUpdatedFlagsReturnsSingleItemList() { + List flags = Collections.singletonList("singleFlag"); + SdkUpdateMetadata metadata = new SdkUpdateMetadata(flags); + + assertEquals(flags, metadata.getUpdatedFlags()); + assertEquals(1, metadata.getUpdatedFlags().size()); + assertEquals("singleFlag", metadata.getUpdatedFlags().get(0)); + } +} + diff --git a/events-domain/src/main/java/io/split/android/client/events/EventsManagerCoordinator.java b/events-domain/src/main/java/io/split/android/client/events/EventsManagerCoordinator.java index 553b1a094..6fa14d827 100644 --- a/events-domain/src/main/java/io/split/android/client/events/EventsManagerCoordinator.java +++ b/events-domain/src/main/java/io/split/android/client/events/EventsManagerCoordinator.java @@ -10,7 +10,7 @@ import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; -import io.split.android.client.api.EventMetadata; +import io.split.android.client.events.metadata.EventMetadata; import io.split.android.client.api.Key; /** diff --git a/events-domain/src/main/java/io/split/android/client/events/ISplitEventsManager.java b/events-domain/src/main/java/io/split/android/client/events/ISplitEventsManager.java index 32580a40d..d6dd48859 100644 --- a/events-domain/src/main/java/io/split/android/client/events/ISplitEventsManager.java +++ b/events-domain/src/main/java/io/split/android/client/events/ISplitEventsManager.java @@ -2,7 +2,7 @@ import androidx.annotation.Nullable; -import io.split.android.client.api.EventMetadata; +import io.split.android.client.events.metadata.EventMetadata; public interface ISplitEventsManager { diff --git a/events-domain/src/main/java/io/split/android/client/events/SplitEventDelivery.java b/events-domain/src/main/java/io/split/android/client/events/SplitEventDelivery.java index bcc13a50d..5930fd21c 100644 --- a/events-domain/src/main/java/io/split/android/client/events/SplitEventDelivery.java +++ b/events-domain/src/main/java/io/split/android/client/events/SplitEventDelivery.java @@ -5,7 +5,7 @@ import io.harness.events.EventDelivery; import io.harness.events.EventHandler; import io.harness.events.Logging; -import io.split.android.client.api.EventMetadata; +import io.split.android.client.events.metadata.EventMetadata; /** * Event delivery implementation for Split SDK events. diff --git a/events-domain/src/main/java/io/split/android/client/events/SplitEventsManager.java b/events-domain/src/main/java/io/split/android/client/events/SplitEventsManager.java index cc417efe2..464407bd5 100644 --- a/events-domain/src/main/java/io/split/android/client/events/SplitEventsManager.java +++ b/events-domain/src/main/java/io/split/android/client/events/SplitEventsManager.java @@ -10,7 +10,9 @@ import io.harness.events.EventsManager; import io.harness.events.EventsManagers; -import io.split.android.client.api.EventMetadata; +import io.split.android.client.SplitClient; +import io.split.android.client.events.metadata.EventMetadata; +import io.split.android.client.events.metadata.TypedTaskConverter; import io.split.android.client.events.executors.SplitEventExecutorResources; import io.split.android.client.events.executors.SplitEventExecutorResourcesImpl; import io.split.android.client.service.executor.SplitTaskExecutionInfo; @@ -145,69 +147,55 @@ public void run() { } private EventHandler createBackgroundHandler(final SplitEventTask task) { - return createEventHandler(task, "background", new TaskMethodCaller() { - @Override - public void callWithMetadata(EventMetadata metadata) { - task.onPostExecution(mResources.getSplitClient(), metadata); - } - - @Override - public void callWithoutMetadata() { - task.onPostExecution(mResources.getSplitClient()); - } - }); + return (event, metadata) -> { + SplitClient client = mResources.getSplitClient(); + executeBackgroundTask(task, client, metadata); + }; } private EventHandler createMainThreadHandler(final SplitEventTask task) { - return createEventHandler(task, "main thread", new TaskMethodCaller() { - @Override - public void callWithMetadata(EventMetadata metadata) { - task.onPostExecutionView(mResources.getSplitClient(), metadata); - } - - @Override - public void callWithoutMetadata() { - task.onPostExecutionView(mResources.getSplitClient()); - } - }); + return (event, metadata) -> { + SplitClient client = mResources.getSplitClient(); + executeMainThreadTask(task, client, metadata); + }; } - /** - * Helper interface for calling task methods. - */ - private interface TaskMethodCaller { - void callWithMetadata(EventMetadata metadata) throws Exception; - void callWithoutMetadata() throws Exception; + private void executeBackgroundTask(SplitEventTask task, SplitClient client, EventMetadata metadata) { + // Try typed methods first for typed tasks + if (task instanceof SdkUpdateEventTask) { + SdkUpdateMetadata typedMetadata = TypedTaskConverter.convertForSdkUpdate(metadata); + executeMethod(() -> ((SdkUpdateEventTask) task).onPostExecution(client, typedMetadata)); + } else if (task instanceof SdkReadyFromCacheEventTask) { + SdkReadyFromCacheMetadata typedMetadata = TypedTaskConverter.convertForSdkReadyFromCache(metadata); + executeMethod(() -> ((SdkReadyFromCacheEventTask) task).onPostExecution(client, typedMetadata)); + } + + // Always try the base method + executeMethod(() -> task.onPostExecution(client)); } - private EventHandler createEventHandler( - final SplitEventTask task, - final String threadType, - final TaskMethodCaller caller) { - return new EventHandler() { - @Override - public void handle(SplitEvent event, EventMetadata metadata) { - executeTaskMethod(metadata, true, threadType, caller); - executeTaskMethod(metadata, false, threadType, caller); - } + private void executeMainThreadTask(SplitEventTask task, SplitClient client, EventMetadata metadata) { + // Try typed methods first for typed tasks + if (task instanceof SdkUpdateEventTask) { + SdkUpdateMetadata typedMetadata = TypedTaskConverter.convertForSdkUpdate(metadata); + executeMethod(() -> ((SdkUpdateEventTask) task).onPostExecutionView(client, typedMetadata)); + } else if (task instanceof SdkReadyFromCacheEventTask) { + SdkReadyFromCacheMetadata typedMetadata = TypedTaskConverter.convertForSdkReadyFromCache(metadata); + executeMethod(() -> ((SdkReadyFromCacheEventTask) task).onPostExecutionView(client, typedMetadata)); + } - private void executeTaskMethod(EventMetadata metadata, boolean withMetadata, String threadType, TaskMethodCaller caller) { - try { - if (withMetadata) { - caller.callWithMetadata(metadata); - } else { - caller.callWithoutMetadata(); - } - } catch (SplitEventTaskMethodNotImplementedException e) { - // Method not implemented by client, ignore - } catch (Exception e) { - String errorPrefix = withMetadata - ? "Error executing " + threadType + " event task (with metadata): " - : "Error executing " + threadType + " event task: "; - Logger.e(errorPrefix + e.getMessage()); - } - } - }; + // Always try the base method + executeMethod(() -> task.onPostExecutionView(client)); + } + + private void executeMethod(Runnable method) { + try { + method.run(); + } catch (SplitEventTaskMethodNotImplementedException e) { + // Method not implemented by client, ignore + } catch (Exception e) { + Logger.e("Error executing event task: " + e.getMessage()); + } } private Executor createBackgroundExecutor(final SplitTaskExecutor taskExecutor) { diff --git a/api/src/main/java/io/split/android/client/api/EventMetadata.java b/events-domain/src/main/java/io/split/android/client/events/metadata/EventMetadata.java similarity index 56% rename from api/src/main/java/io/split/android/client/api/EventMetadata.java rename to events-domain/src/main/java/io/split/android/client/events/metadata/EventMetadata.java index aae46655f..1d531b131 100644 --- a/api/src/main/java/io/split/android/client/api/EventMetadata.java +++ b/events-domain/src/main/java/io/split/android/client/events/metadata/EventMetadata.java @@ -1,4 +1,4 @@ -package io.split.android.client.api; +package io.split.android.client.events.metadata; import androidx.annotation.NonNull; import androidx.annotation.Nullable; @@ -8,6 +8,10 @@ /** * Represents metadata associated with SDK events. *

+ * This is an internal API for SDK infrastructure use. + * Consumers should use the typed metadata classes instead: + * {@code SdkUpdateMetadata} and {@code SdkReadyFromCacheMetadata}. + *

* Values are sanitized to only allow String, Number, Boolean, or List<String>. */ public interface EventMetadata { @@ -31,20 +35,19 @@ default boolean isEmpty() { Collection values(); /** - * Returns the value associated with the given typed key. + * Returns the value associated with the given key. * - * @param key the typed key to look up - * @return the typed value associated with the key, or null if not found + * @param key the key to look up + * @return the value associated with the key, or null if not found */ @Nullable - T get(@NonNull MetadataKey key); + Object get(@NonNull String key); /** - * Returns whether this metadata contains the given typed key. + * Returns whether this metadata contains the given key. * - * @param key the typed key to check + * @param key the key to check * @return true if the key exists, false otherwise */ - boolean containsKey(@NonNull MetadataKey key); + boolean containsKey(@NonNull String key); } - diff --git a/events-domain/src/main/java/io/split/android/client/events/metadata/EventMetadataBuilder.java b/events-domain/src/main/java/io/split/android/client/events/metadata/EventMetadataBuilder.java index 76cb30289..86c3b142b 100644 --- a/events-domain/src/main/java/io/split/android/client/events/metadata/EventMetadataBuilder.java +++ b/events-domain/src/main/java/io/split/android/client/events/metadata/EventMetadataBuilder.java @@ -7,8 +7,6 @@ import java.util.List; import java.util.Map; -import io.split.android.client.api.EventMetadata; - /** * Builder for creating {@link EventMetadata} instances. *

diff --git a/events-domain/src/main/java/io/split/android/client/events/metadata/EventMetadataHelpers.java b/events-domain/src/main/java/io/split/android/client/events/metadata/EventMetadataHelpers.java index 39f5f2b2d..67dda836e 100644 --- a/events-domain/src/main/java/io/split/android/client/events/metadata/EventMetadataHelpers.java +++ b/events-domain/src/main/java/io/split/android/client/events/metadata/EventMetadataHelpers.java @@ -6,10 +6,6 @@ import java.util.HashSet; import java.util.List; -import io.split.android.client.api.EventMetadata; -import io.split.android.client.api.SdkReadyFromCacheMetadataKeys; -import io.split.android.client.api.SdkUpdateMetadataKeys; - /** * Helper class for creating {@link EventMetadata} instances. *

@@ -23,7 +19,7 @@ private EventMetadataHelpers() { public static EventMetadata createUpdatedFlagsMetadata(List updatedSplitNames) { return new EventMetadataBuilder() - .put(SdkUpdateMetadataKeys.UPDATED_FLAGS.name(), new ArrayList<>(new HashSet<>(updatedSplitNames))) + .put(MetadataKeys.UPDATED_FLAGS, new ArrayList<>(new HashSet<>(updatedSplitNames))) .build(); } @@ -36,10 +32,10 @@ public static EventMetadata createUpdatedFlagsMetadata(List updatedSplit */ public static EventMetadata createCacheReadyMetadata(@Nullable Long lastUpdateTimestamp, boolean freshInstall) { EventMetadataBuilder builder = new EventMetadataBuilder() - .put(SdkReadyFromCacheMetadataKeys.FRESH_INSTALL.name(), freshInstall); + .put(MetadataKeys.FRESH_INSTALL, freshInstall); if (lastUpdateTimestamp != null) { - builder.put(SdkReadyFromCacheMetadataKeys.LAST_UPDATE_TIMESTAMP.name(), lastUpdateTimestamp); + builder.put(MetadataKeys.LAST_UPDATE_TIMESTAMP, lastUpdateTimestamp); } return builder.build(); diff --git a/events-domain/src/main/java/io/split/android/client/events/metadata/EventMetadataImpl.java b/events-domain/src/main/java/io/split/android/client/events/metadata/EventMetadataImpl.java index b2e561f8e..97aace947 100644 --- a/events-domain/src/main/java/io/split/android/client/events/metadata/EventMetadataImpl.java +++ b/events-domain/src/main/java/io/split/android/client/events/metadata/EventMetadataImpl.java @@ -10,9 +10,6 @@ import java.util.List; import java.util.Map; -import io.split.android.client.api.EventMetadata; -import io.split.android.client.api.MetadataKey; - /** * Implementation of {@link EventMetadata}. * Use {@link EventMetadataBuilder} to create instances. @@ -47,13 +44,12 @@ public Collection values() { @Nullable @Override - public T get(@NonNull MetadataKey key) { - //noinspection unchecked - return (T) mData.get(key.name()); + public Object get(@NonNull String key) { + return mData.get(key); } @Override - public boolean containsKey(@NonNull MetadataKey key) { - return mData.containsKey(key.name()); + public boolean containsKey(@NonNull String key) { + return mData.containsKey(key); } } diff --git a/events-domain/src/main/java/io/split/android/client/events/metadata/MetadataKeys.java b/events-domain/src/main/java/io/split/android/client/events/metadata/MetadataKeys.java new file mode 100644 index 000000000..c76dd578f --- /dev/null +++ b/events-domain/src/main/java/io/split/android/client/events/metadata/MetadataKeys.java @@ -0,0 +1,34 @@ +package io.split.android.client.events.metadata; + +/** + * Consolidated metadata keys for SDK events. + *

+ * Package-private - for internal SDK use only. + */ +final class MetadataKeys { + + private MetadataKeys() { + // no instances + } + + // SDK_UPDATE event keys + + /** + * Names of flags that changed in this update. + */ + static final String UPDATED_FLAGS = "updatedFlags"; + + // SDK_READY_FROM_CACHE event keys + + /** + * True if this is a fresh install with no usable cache. + */ + static final String FRESH_INSTALL = "freshInstall"; + + /** + * Last successful cache timestamp in milliseconds since epoch. + *

+ * May be absent when not available. + */ + static final String LAST_UPDATE_TIMESTAMP = "lastUpdateTimestamp"; +} diff --git a/events-domain/src/main/java/io/split/android/client/events/metadata/TypedTaskConverter.java b/events-domain/src/main/java/io/split/android/client/events/metadata/TypedTaskConverter.java new file mode 100644 index 000000000..9d92d7894 --- /dev/null +++ b/events-domain/src/main/java/io/split/android/client/events/metadata/TypedTaskConverter.java @@ -0,0 +1,54 @@ +package io.split.android.client.events.metadata; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import java.util.List; + +import io.split.android.client.events.SdkReadyFromCacheMetadata; +import io.split.android.client.events.SdkUpdateMetadata; + +/** + * Converts {@link EventMetadata} to typed metadata objects for typed event tasks. + *

+ * This class handles the conversion logic that was previously in the typed tasks. + */ +public class TypedTaskConverter { + + private TypedTaskConverter() { + // Utility class + } + + /** + * Converts EventMetadata to SdkUpdateMetadata. + * + * @param metadata the event metadata, may be null + * @return the typed metadata for SDK_UPDATE events + */ + @NonNull + @SuppressWarnings("unchecked") + public static SdkUpdateMetadata convertForSdkUpdate(@Nullable EventMetadata metadata) { + List updatedFlags = null; + if (metadata != null) { + updatedFlags = (List) metadata.get(MetadataKeys.UPDATED_FLAGS); + } + return new SdkUpdateMetadata(updatedFlags); + } + + /** + * Converts EventMetadata to SdkReadyFromCacheMetadata. + * + * @param metadata the event metadata, may be null + * @return the typed metadata for SDK_READY_FROM_CACHE events + */ + @NonNull + public static SdkReadyFromCacheMetadata convertForSdkReadyFromCache(@Nullable EventMetadata metadata) { + Boolean freshInstall = null; + Long lastUpdateTimestamp = null; + if (metadata != null) { + freshInstall = (Boolean) metadata.get(MetadataKeys.FRESH_INSTALL); + lastUpdateTimestamp = (Long) metadata.get(MetadataKeys.LAST_UPDATE_TIMESTAMP); + } + return new SdkReadyFromCacheMetadata(freshInstall, lastUpdateTimestamp); + } +} diff --git a/events-domain/src/test/java/io/split/android/client/events/TypedTaskConversionTest.java b/events-domain/src/test/java/io/split/android/client/events/TypedTaskConversionTest.java new file mode 100644 index 000000000..b598c70dd --- /dev/null +++ b/events-domain/src/test/java/io/split/android/client/events/TypedTaskConversionTest.java @@ -0,0 +1,90 @@ +package io.split.android.client.events; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.mock; + +import org.junit.Test; + +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.atomic.AtomicReference; + +import io.split.android.client.SplitClient; +import io.split.android.client.events.metadata.EventMetadata; +import io.split.android.client.events.metadata.EventMetadataHelpers; +import io.split.android.client.events.metadata.TypedTaskConverter; + +/** + * Tests for typed task metadata conversion in SplitEventsManager. + */ +public class TypedTaskConversionTest { + + @Test + public void sdkUpdateEventTaskReceivesConvertedMetadata() { + List expectedFlags = Arrays.asList("flag1", "flag2"); + AtomicReference receivedMetadata = new AtomicReference<>(); + + SdkUpdateEventTask task = new SdkUpdateEventTask() { + @Override + public void onPostExecution(SplitClient client, SdkUpdateMetadata metadata) { + receivedMetadata.set(metadata); + } + }; + + EventMetadata eventMetadata = EventMetadataHelpers.createUpdatedFlagsMetadata(expectedFlags); + SplitClient client = mock(SplitClient.class); + + // Call conversion method + SdkUpdateMetadata converted = TypedTaskConverter.convertForSdkUpdate(eventMetadata); + + assertNotNull(converted); + assertEquals(expectedFlags.size(), converted.getUpdatedFlags().size()); + assertTrue(converted.getUpdatedFlags().containsAll(expectedFlags)); + } + + @Test + public void sdkReadyFromCacheEventTaskReceivesConvertedMetadata() { + long expectedTimestamp = 1704067200000L; + AtomicReference receivedMetadata = new AtomicReference<>(); + + SdkReadyFromCacheEventTask task = new SdkReadyFromCacheEventTask() { + @Override + public void onPostExecution(SplitClient client, SdkReadyFromCacheMetadata metadata) { + receivedMetadata.set(metadata); + } + }; + + EventMetadata eventMetadata = EventMetadataHelpers.createCacheReadyMetadata(expectedTimestamp, true); + SplitClient client = mock(SplitClient.class); + + // Call conversion method + SdkReadyFromCacheMetadata converted = TypedTaskConverter.convertForSdkReadyFromCache(eventMetadata); + + assertNotNull(converted); + assertTrue(converted.isFreshInstall()); + assertEquals(Long.valueOf(expectedTimestamp), converted.getLastUpdateTimestamp()); + } + + @Test + public void convertForSdkUpdateHandlesNullMetadata() { + SdkUpdateMetadata converted = TypedTaskConverter.convertForSdkUpdate(null); + + assertNotNull(converted); + assertNull(converted.getUpdatedFlags()); + } + + @Test + public void convertForSdkReadyFromCacheHandlesNullMetadata() { + SdkReadyFromCacheMetadata converted = TypedTaskConverter.convertForSdkReadyFromCache(null); + + assertNotNull(converted); + assertNull(converted.isFreshInstall()); + assertNull(converted.getLastUpdateTimestamp()); + } +} + diff --git a/events-domain/src/test/java/io/split/android/client/events/metadata/EventMetadataBuilderTest.java b/events-domain/src/test/java/io/split/android/client/events/metadata/EventMetadataBuilderTest.java index 13515c17c..8f561e0c2 100644 --- a/events-domain/src/test/java/io/split/android/client/events/metadata/EventMetadataBuilderTest.java +++ b/events-domain/src/test/java/io/split/android/client/events/metadata/EventMetadataBuilderTest.java @@ -15,10 +15,6 @@ import java.util.Arrays; import java.util.List; -import io.split.android.client.api.EventMetadata; -import io.split.android.client.api.MetadataKey; -import io.split.android.client.api.SdkUpdateMetadataKeys; - public class EventMetadataBuilderTest { @Mock @@ -78,8 +74,7 @@ public void putIgnoresValueWhenValidatorReturnsFalse() { .put("key", "value") .build(); - MetadataKey KEY = new MetadataKey<>("key"); - assertFalse(metadata.containsKey(KEY)); + assertFalse(metadata.containsKey("key")); } @Test @@ -90,8 +85,7 @@ public void putIncludesValueWhenValidatorReturnsTrue() { .put("key", "value") .build(); - MetadataKey KEY = new MetadataKey<>("key"); - assertEquals("value", metadata.get(KEY)); + assertEquals("value", metadata.get("key")); } @Test @@ -107,8 +101,7 @@ public void putStringAddsValue() { .put("key", "value") .build(); - MetadataKey KEY = new MetadataKey<>("key"); - assertEquals("value", metadata.get(KEY)); + assertEquals("value", metadata.get("key")); } @Test @@ -117,8 +110,7 @@ public void putIntegerAddsValue() { .put("count", 42) .build(); - MetadataKey COUNT = new MetadataKey<>("count"); - assertEquals(Integer.valueOf(42), metadata.get(COUNT)); + assertEquals(Integer.valueOf(42), metadata.get("count")); } @Test @@ -127,8 +119,7 @@ public void putLongAddsValue() { .put("timestamp", 1234567890L) .build(); - MetadataKey TIMESTAMP = new MetadataKey<>("timestamp"); - assertEquals(Long.valueOf(1234567890L), metadata.get(TIMESTAMP)); + assertEquals(Long.valueOf(1234567890L), metadata.get("timestamp")); } @Test @@ -137,8 +128,7 @@ public void putDoubleAddsValue() { .put("rate", 3.14) .build(); - MetadataKey RATE = new MetadataKey<>("rate"); - assertEquals(Double.valueOf(3.14), metadata.get(RATE)); + assertEquals(Double.valueOf(3.14), metadata.get("rate")); } @Test @@ -147,8 +137,7 @@ public void putBooleanTrueAddsValue() { .put("enabled", true) .build(); - MetadataKey ENABLED = new MetadataKey<>("enabled"); - assertEquals(Boolean.TRUE, metadata.get(ENABLED)); + assertEquals(Boolean.TRUE, metadata.get("enabled")); } @Test @@ -157,8 +146,7 @@ public void putBooleanFalseAddsValue() { .put("disabled", false) .build(); - MetadataKey DISABLED = new MetadataKey<>("disabled"); - assertEquals(Boolean.FALSE, metadata.get(DISABLED)); + assertEquals(Boolean.FALSE, metadata.get("disabled")); } @Test @@ -169,7 +157,7 @@ public void putListOfStringsAddsValue() { .put("updatedFlags", flags) .build(); - assertEquals(flags, metadata.get(SdkUpdateMetadataKeys.UPDATED_FLAGS)); + assertEquals(flags, metadata.get(MetadataKeys.UPDATED_FLAGS)); } @Test @@ -182,10 +170,10 @@ public void chainingMultiplePutsWorks() { .build(); assertEquals(4, metadata.size()); - assertEquals("text", metadata.get(new MetadataKey("string"))); - assertEquals(Integer.valueOf(100), metadata.get(new MetadataKey("number"))); - assertEquals(Boolean.TRUE, metadata.get(new MetadataKey("flag"))); - assertEquals(Arrays.asList("a", "b"), metadata.get(new MetadataKey>("list"))); + assertEquals("text", metadata.get("string")); + assertEquals(Integer.valueOf(100), metadata.get("number")); + assertEquals(Boolean.TRUE, metadata.get("flag")); + assertEquals(Arrays.asList("a", "b"), metadata.get("list")); } @Test @@ -195,8 +183,7 @@ public void overwritingKeyUsesLastValue() { .put("key", "second") .build(); - MetadataKey KEY = new MetadataKey<>("key"); - assertEquals("second", metadata.get(KEY)); + assertEquals("second", metadata.get("key")); } @Test @@ -207,7 +194,6 @@ public void buildReturnsNewInstanceEachTime() { EventMetadata metadata1 = builder.build(); EventMetadata metadata2 = builder.build(); - MetadataKey KEY = new MetadataKey<>("key"); - assertEquals(metadata1.get(KEY), metadata2.get(KEY)); + assertEquals(metadata1.get("key"), metadata2.get("key")); } } diff --git a/events-domain/src/test/java/io/split/android/client/events/metadata/EventMetadataHelpersTest.java b/events-domain/src/test/java/io/split/android/client/events/metadata/EventMetadataHelpersTest.java index 4f2101cbd..7fe8d577d 100644 --- a/events-domain/src/test/java/io/split/android/client/events/metadata/EventMetadataHelpersTest.java +++ b/events-domain/src/test/java/io/split/android/client/events/metadata/EventMetadataHelpersTest.java @@ -1,7 +1,6 @@ package io.split.android.client.events.metadata; import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; @@ -10,20 +9,17 @@ import java.util.Arrays; import java.util.List; -import io.split.android.client.api.EventMetadata; -import io.split.android.client.api.SdkReadyFromCacheMetadataKeys; -import io.split.android.client.api.SdkUpdateMetadataKeys; - public class EventMetadataHelpersTest { // Tests for createUpdatedFlagsMetadata (existing) @Test + @SuppressWarnings("unchecked") public void createUpdatedFlagsMetadataContainsFlags() { List flags = Arrays.asList("flag1", "flag2", "flag3"); EventMetadata metadata = EventMetadataHelpers.createUpdatedFlagsMetadata(flags); - assertTrue(metadata.containsKey(SdkUpdateMetadataKeys.UPDATED_FLAGS)); - List result = metadata.get(SdkUpdateMetadataKeys.UPDATED_FLAGS); + assertTrue(metadata.containsKey(MetadataKeys.UPDATED_FLAGS)); + List result = (List) metadata.get(MetadataKeys.UPDATED_FLAGS); assertEquals(3, result.size()); assertTrue(result.contains("flag1")); assertTrue(result.contains("flag2")); @@ -35,24 +31,24 @@ public void createUpdatedFlagsMetadataContainsFlags() { public void createCacheReadyMetadataWithTimestampAndFreshInstallFalse() { EventMetadata metadata = EventMetadataHelpers.createCacheReadyMetadata(1234567890L, false); - assertEquals(Long.valueOf(1234567890L), metadata.get(SdkReadyFromCacheMetadataKeys.LAST_UPDATE_TIMESTAMP)); - assertEquals(Boolean.FALSE, metadata.get(SdkReadyFromCacheMetadataKeys.FRESH_INSTALL)); + assertEquals(Long.valueOf(1234567890L), metadata.get(MetadataKeys.LAST_UPDATE_TIMESTAMP)); + assertEquals(Boolean.FALSE, metadata.get(MetadataKeys.FRESH_INSTALL)); } @Test public void createCacheReadyMetadataWithNullTimestampAndFreshInstallTrue() { EventMetadata metadata = EventMetadataHelpers.createCacheReadyMetadata(null, true); - assertNull(metadata.get(SdkReadyFromCacheMetadataKeys.LAST_UPDATE_TIMESTAMP)); - assertEquals(Boolean.TRUE, metadata.get(SdkReadyFromCacheMetadataKeys.FRESH_INSTALL)); + assertNull(metadata.get(MetadataKeys.LAST_UPDATE_TIMESTAMP)); + assertEquals(Boolean.TRUE, metadata.get(MetadataKeys.FRESH_INSTALL)); } @Test public void createCacheReadyMetadataKeysAreCorrect() { EventMetadata metadata = EventMetadataHelpers.createCacheReadyMetadata(123L, false); - assertTrue(metadata.containsKey(SdkReadyFromCacheMetadataKeys.LAST_UPDATE_TIMESTAMP)); - assertTrue(metadata.containsKey(SdkReadyFromCacheMetadataKeys.FRESH_INSTALL)); + assertTrue(metadata.containsKey(MetadataKeys.LAST_UPDATE_TIMESTAMP)); + assertTrue(metadata.containsKey(MetadataKeys.FRESH_INSTALL)); assertEquals(2, metadata.size()); } @@ -60,8 +56,8 @@ public void createCacheReadyMetadataKeysAreCorrect() { public void createCacheReadyMetadataWithZeroTimestamp() { EventMetadata metadata = EventMetadataHelpers.createCacheReadyMetadata(0L, false); - assertEquals(Long.valueOf(0L), metadata.get(SdkReadyFromCacheMetadataKeys.LAST_UPDATE_TIMESTAMP)); - assertEquals(Boolean.FALSE, metadata.get(SdkReadyFromCacheMetadataKeys.FRESH_INSTALL)); + assertEquals(Long.valueOf(0L), metadata.get(MetadataKeys.LAST_UPDATE_TIMESTAMP)); + assertEquals(Boolean.FALSE, metadata.get(MetadataKeys.FRESH_INSTALL)); } @Test @@ -70,8 +66,8 @@ public void createCacheReadyMetadataForCachePath() { long storedTimestamp = 1700000000000L; EventMetadata metadata = EventMetadataHelpers.createCacheReadyMetadata(storedTimestamp, false); - assertEquals(Boolean.FALSE, metadata.get(SdkReadyFromCacheMetadataKeys.FRESH_INSTALL)); - assertEquals(Long.valueOf(storedTimestamp), metadata.get(SdkReadyFromCacheMetadataKeys.LAST_UPDATE_TIMESTAMP)); + assertEquals(Boolean.FALSE, metadata.get(MetadataKeys.FRESH_INSTALL)); + assertEquals(Long.valueOf(storedTimestamp), metadata.get(MetadataKeys.LAST_UPDATE_TIMESTAMP)); } @Test @@ -79,8 +75,7 @@ public void createCacheReadyMetadataForSyncPath() { // Sync path: freshInstall=true, timestamp=null EventMetadata metadata = EventMetadataHelpers.createCacheReadyMetadata(null, true); - assertEquals(Boolean.TRUE, metadata.get(SdkReadyFromCacheMetadataKeys.FRESH_INSTALL)); - assertNull(metadata.get(SdkReadyFromCacheMetadataKeys.LAST_UPDATE_TIMESTAMP)); + assertEquals(Boolean.TRUE, metadata.get(MetadataKeys.FRESH_INSTALL)); + assertNull(metadata.get(MetadataKeys.LAST_UPDATE_TIMESTAMP)); } } - diff --git a/events-domain/src/test/java/io/split/android/client/events/metadata/EventMetadataImplTest.java b/events-domain/src/test/java/io/split/android/client/events/metadata/EventMetadataImplTest.java index e727f5227..5f539c6a6 100644 --- a/events-domain/src/test/java/io/split/android/client/events/metadata/EventMetadataImplTest.java +++ b/events-domain/src/test/java/io/split/android/client/events/metadata/EventMetadataImplTest.java @@ -14,7 +14,6 @@ import java.util.List; import java.util.Map; -import io.split.android.client.api.MetadataKey; public class EventMetadataImplTest { @@ -28,9 +27,9 @@ public void sizeAndContainsKeyReflectStoredEntries() { EventMetadataImpl metadata = new EventMetadataImpl(data); assertEquals(3, metadata.size()); - assertTrue(metadata.containsKey(new MetadataKey<>("key1"))); - assertTrue(metadata.containsKey(new MetadataKey<>("key2"))); - assertTrue(metadata.containsKey(new MetadataKey<>("key3"))); + assertTrue(metadata.containsKey("key1")); + assertTrue(metadata.containsKey("key2")); + assertTrue(metadata.containsKey("key3")); } @Test @@ -68,7 +67,7 @@ public void getReturnsValueForExistingKey() { EventMetadataImpl metadata = new EventMetadataImpl(data); - assertEquals("value", metadata.get(new MetadataKey("key"))); + assertEquals("value", metadata.get("key")); } @Test @@ -78,7 +77,7 @@ public void getReturnsNullForNonExistingKey() { EventMetadataImpl metadata = new EventMetadataImpl(data); - assertNull(metadata.get(new MetadataKey("nonExistingKey"))); + assertNull(metadata.get("nonExistingKey")); } @Test @@ -88,7 +87,7 @@ public void containsKeyReturnsTrueForExistingKey() { EventMetadataImpl metadata = new EventMetadataImpl(data); - assertTrue(metadata.containsKey(new MetadataKey<>("key"))); + assertTrue(metadata.containsKey("key")); } @Test @@ -98,7 +97,7 @@ public void containsKeyReturnsFalseForNonExistingKey() { EventMetadataImpl metadata = new EventMetadataImpl(data); - assertFalse(metadata.containsKey(new MetadataKey<>("nonExistingKey"))); + assertFalse(metadata.containsKey("nonExistingKey")); } @Test @@ -112,11 +111,12 @@ public void metadataIsImmutableAfterConstruction() { data.put("newKey", "newValue"); // Metadata should not be affected - assertFalse(metadata.containsKey(new MetadataKey<>("newKey"))); + assertFalse(metadata.containsKey("newKey")); assertEquals(1, metadata.size()); } @Test + @SuppressWarnings("unchecked") public void listIsDefensivelyCopiedDuringConstruction() { List originalList = new ArrayList<>(Arrays.asList("flag_1", "flag_2")); Map data = new HashMap<>(); @@ -128,19 +128,20 @@ public void listIsDefensivelyCopiedDuringConstruction() { originalList.add("flag_3"); // Metadata should not be affected - List storedList = metadata.get(new MetadataKey>("flags")); + List storedList = (List) metadata.get("flags"); assertEquals(2, storedList.size()); assertEquals(Arrays.asList("flag_1", "flag_2"), storedList); } @Test(expected = UnsupportedOperationException.class) + @SuppressWarnings("unchecked") public void listReturnedByGetIsUnmodifiable() { Map data = new HashMap<>(); data.put("flags", Arrays.asList("flag_1", "flag_2")); EventMetadataImpl metadata = new EventMetadataImpl(data); - List list = metadata.get(new MetadataKey>("flags")); + List list = (List) metadata.get("flags"); // This should throw UnsupportedOperationException list.add("flag_3"); diff --git a/events-domain/src/test/java/io/split/android/client/events/metadata/MetadataKeysTest.java b/events-domain/src/test/java/io/split/android/client/events/metadata/MetadataKeysTest.java new file mode 100644 index 000000000..530dfd6a2 --- /dev/null +++ b/events-domain/src/test/java/io/split/android/client/events/metadata/MetadataKeysTest.java @@ -0,0 +1,27 @@ +package io.split.android.client.events.metadata; + +import static org.junit.Assert.assertEquals; + +import org.junit.Test; + +/** + * Tests for {@link MetadataKeys}. + * Verifies that all metadata keys are correctly defined. + */ +public class MetadataKeysTest { + + @Test + public void updatedFlagsKeyHasCorrectValue() { + assertEquals("updatedFlags", MetadataKeys.UPDATED_FLAGS); + } + + @Test + public void freshInstallKeyHasCorrectValue() { + assertEquals("freshInstall", MetadataKeys.FRESH_INSTALL); + } + + @Test + public void lastUpdateTimestampKeyHasCorrectValue() { + assertEquals("lastUpdateTimestamp", MetadataKeys.LAST_UPDATE_TIMESTAMP); + } +} diff --git a/main/src/androidTest/java/tests/integration/events/SdkEventsIntegrationTest.java b/main/src/androidTest/java/tests/integration/events/SdkEventsIntegrationTest.java index e215583f9..657049942 100644 --- a/main/src/androidTest/java/tests/integration/events/SdkEventsIntegrationTest.java +++ b/main/src/androidTest/java/tests/integration/events/SdkEventsIntegrationTest.java @@ -35,7 +35,7 @@ import io.split.android.client.SplitClient; import io.split.android.client.SplitClientConfig; import io.split.android.client.SplitFactory; -import io.split.android.client.api.EventMetadata; +import io.split.android.client.events.metadata.EventMetadata; import io.split.android.client.api.Key; import io.split.android.client.api.SdkReadyFromCacheMetadataKeys; import io.split.android.client.events.SplitEvent; diff --git a/main/src/androidTest/java/tests/service/EventsManagerTest.java b/main/src/androidTest/java/tests/service/EventsManagerTest.java index fc097de7f..09f3b74b2 100644 --- a/main/src/androidTest/java/tests/service/EventsManagerTest.java +++ b/main/src/androidTest/java/tests/service/EventsManagerTest.java @@ -17,7 +17,7 @@ import helper.TestingHelper; import io.split.android.client.SplitClient; import io.split.android.client.SplitClientConfig; -import io.split.android.client.api.EventMetadata; +import io.split.android.client.events.metadata.EventMetadata; import io.split.android.client.api.SdkUpdateMetadataKeys; import io.split.android.client.events.SplitEvent; import io.split.android.client.events.SplitEventTask; diff --git a/main/src/main/java/io/split/android/client/AlwaysReturnControlSplitClient.java b/main/src/main/java/io/split/android/client/AlwaysReturnControlSplitClient.java index 19d7017ba..5b7471982 100644 --- a/main/src/main/java/io/split/android/client/AlwaysReturnControlSplitClient.java +++ b/main/src/main/java/io/split/android/client/AlwaysReturnControlSplitClient.java @@ -171,6 +171,10 @@ public boolean isReady() { public void on(SplitEvent event, SplitEventTask task) { } + @Override + public void on(io.split.android.client.events.SdkEvent event, T task) { + } + @Override public boolean track(String trafficType, String eventType) { return false; diff --git a/main/src/main/java/io/split/android/client/SplitClientImpl.java b/main/src/main/java/io/split/android/client/SplitClientImpl.java index fedd546ec..b2cdda012 100644 --- a/main/src/main/java/io/split/android/client/SplitClientImpl.java +++ b/main/src/main/java/io/split/android/client/SplitClientImpl.java @@ -12,6 +12,7 @@ import io.split.android.client.api.Key; import io.split.android.client.attributes.AttributesManager; +import io.split.android.client.events.SdkEvent; import io.split.android.client.events.SplitEvent; import io.split.android.client.events.SplitEventTask; import io.split.android.client.events.SplitEventsManager; @@ -202,6 +203,11 @@ public void on(SplitEvent event, SplitEventTask task) { mEventsManager.register(event, task); } + @Override + public void on(SdkEvent event, T task) { + on(event.toSplitEvent(), task); + } + @Override public boolean track(String trafficType, String eventType) { return track(mKey.matchingKey(), trafficType, eventType, TRACK_DEFAULT_VALUE, null); diff --git a/main/src/main/java/io/split/android/client/localhost/LocalhostSplitClient.java b/main/src/main/java/io/split/android/client/localhost/LocalhostSplitClient.java index 00c6a4f51..fbcaba3b3 100644 --- a/main/src/main/java/io/split/android/client/localhost/LocalhostSplitClient.java +++ b/main/src/main/java/io/split/android/client/localhost/LocalhostSplitClient.java @@ -257,18 +257,24 @@ public boolean isReady() { return mEventsManager.eventAlreadyTriggered(SplitEvent.SDK_READY); } + @Override public void on(SplitEvent event, SplitEventTask task) { checkNotNull(event); checkNotNull(task); if (!event.equals(SplitEvent.SDK_READY_FROM_CACHE) && mEventsManager.eventAlreadyTriggered(event)) { - Logger.w(String.format("A listener was added for %s on the SDK, which has already fired and won’t be emitted again. The callback won’t be executed.", event)); + Logger.w(String.format("A listener was added for %s on the SDK, which has already fired and won't be emitted again. The callback won't be executed.", event)); return; } mEventsManager.register(event, task); } + @Override + public void on(io.split.android.client.events.SdkEvent event, T task) { + on(event.toSplitEvent(), task); + } + @Override public boolean track(String trafficType, String eventType) { return false; diff --git a/main/src/main/java/io/split/android/client/localhost/LocalhostSplitsStorage.java b/main/src/main/java/io/split/android/client/localhost/LocalhostSplitsStorage.java index ed6402229..de52a5503 100644 --- a/main/src/main/java/io/split/android/client/localhost/LocalhostSplitsStorage.java +++ b/main/src/main/java/io/split/android/client/localhost/LocalhostSplitsStorage.java @@ -20,7 +20,7 @@ import java.util.ArrayList; -import io.split.android.client.api.EventMetadata; +import io.split.android.client.events.metadata.EventMetadata; import io.split.android.client.dtos.Split; import io.split.android.client.events.EventsManagerCoordinator; import io.split.android.client.events.SplitInternalEvent; diff --git a/main/src/main/java/io/split/android/client/service/splits/SplitInPlaceUpdateTask.java b/main/src/main/java/io/split/android/client/service/splits/SplitInPlaceUpdateTask.java index 53fe35a04..2c86042b0 100644 --- a/main/src/main/java/io/split/android/client/service/splits/SplitInPlaceUpdateTask.java +++ b/main/src/main/java/io/split/android/client/service/splits/SplitInPlaceUpdateTask.java @@ -7,7 +7,7 @@ import java.util.List; -import io.split.android.client.api.EventMetadata; +import io.split.android.client.events.metadata.EventMetadata; import io.split.android.client.dtos.Split; import io.split.android.client.events.ISplitEventsManager; import io.split.android.client.events.SplitInternalEvent; diff --git a/main/src/main/java/io/split/android/client/service/splits/SplitKillTask.java b/main/src/main/java/io/split/android/client/service/splits/SplitKillTask.java index 7754d1c0d..001d4ec04 100644 --- a/main/src/main/java/io/split/android/client/service/splits/SplitKillTask.java +++ b/main/src/main/java/io/split/android/client/service/splits/SplitKillTask.java @@ -6,7 +6,7 @@ import java.util.Collections; -import io.split.android.client.api.EventMetadata; +import io.split.android.client.events.metadata.EventMetadata; import io.split.android.client.dtos.Split; import io.split.android.client.events.ISplitEventsManager; import io.split.android.client.events.SplitInternalEvent; diff --git a/main/src/main/java/io/split/android/client/service/splits/SplitsSyncTask.java b/main/src/main/java/io/split/android/client/service/splits/SplitsSyncTask.java index 2baad9dd9..2f3ad81df 100644 --- a/main/src/main/java/io/split/android/client/service/splits/SplitsSyncTask.java +++ b/main/src/main/java/io/split/android/client/service/splits/SplitsSyncTask.java @@ -7,7 +7,7 @@ import java.util.List; -import io.split.android.client.api.EventMetadata; +import io.split.android.client.events.metadata.EventMetadata; import io.split.android.client.events.ISplitEventsManager; import io.split.android.client.events.SplitEvent; import io.split.android.client.events.SplitInternalEvent; diff --git a/main/src/main/java/io/split/android/client/service/splits/SplitsUpdateTask.java b/main/src/main/java/io/split/android/client/service/splits/SplitsUpdateTask.java index c19907ced..4030ea3c1 100644 --- a/main/src/main/java/io/split/android/client/service/splits/SplitsUpdateTask.java +++ b/main/src/main/java/io/split/android/client/service/splits/SplitsUpdateTask.java @@ -8,7 +8,7 @@ import java.util.List; -import io.split.android.client.api.EventMetadata; +import io.split.android.client.events.metadata.EventMetadata; import io.split.android.client.events.ISplitEventsManager; import io.split.android.client.events.SplitInternalEvent; import io.split.android.client.events.metadata.EventMetadataHelpers; diff --git a/main/src/main/java/io/split/android/client/service/synchronizer/LoadLocalDataListener.java b/main/src/main/java/io/split/android/client/service/synchronizer/LoadLocalDataListener.java index 49d5e91c3..be1ccc999 100644 --- a/main/src/main/java/io/split/android/client/service/synchronizer/LoadLocalDataListener.java +++ b/main/src/main/java/io/split/android/client/service/synchronizer/LoadLocalDataListener.java @@ -5,7 +5,7 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; -import io.split.android.client.api.EventMetadata; +import io.split.android.client.events.metadata.EventMetadata; import io.split.android.client.events.ISplitEventsManager; import io.split.android.client.events.SplitInternalEvent; import io.split.android.client.service.executor.SplitTaskExecutionInfo; diff --git a/main/src/test/java/io/split/android/client/events/EventsManagerCoordinatorTest.java b/main/src/test/java/io/split/android/client/events/EventsManagerCoordinatorTest.java index 3c6c466e6..f4da1a546 100644 --- a/main/src/test/java/io/split/android/client/events/EventsManagerCoordinatorTest.java +++ b/main/src/test/java/io/split/android/client/events/EventsManagerCoordinatorTest.java @@ -17,9 +17,9 @@ import java.util.Arrays; import java.util.List; -import io.split.android.client.api.EventMetadata; +import io.split.android.client.events.metadata.EventMetadata; import io.split.android.client.api.Key; -import io.split.android.client.api.SdkUpdateMetadataKeys; +import io.split.android.client.events.metadata.TypedTaskConverter; public class EventsManagerCoordinatorTest { @@ -116,7 +116,8 @@ public void SPLITS_UPDATEDEventWithMetadataIsPassedDownToChildren() { verify(mMockChildEventsManager).notifyInternalEvent(eq(SplitInternalEvent.SPLITS_UPDATED), argThat(meta -> { if (meta == null) return false; - List flags = meta.get(SdkUpdateMetadataKeys.UPDATED_FLAGS); + SdkUpdateMetadata typedMeta = TypedTaskConverter.convertForSdkUpdate(meta); + List flags = typedMeta.getUpdatedFlags(); assertNotNull(flags); return flags.size() == 2 && flags.contains("flag1") && flags.contains("flag2"); })); diff --git a/main/src/test/java/io/split/android/client/events/EventsManagerTest.java b/main/src/test/java/io/split/android/client/events/EventsManagerTest.java index 9646a24ff..a0ddafcfd 100644 --- a/main/src/test/java/io/split/android/client/events/EventsManagerTest.java +++ b/main/src/test/java/io/split/android/client/events/EventsManagerTest.java @@ -21,8 +21,7 @@ import io.split.android.client.SplitClient; import io.split.android.client.SplitClientConfig; -import io.split.android.client.api.EventMetadata; -import io.split.android.client.api.SdkUpdateMetadataKeys; +import io.split.android.client.events.metadata.EventMetadata; import io.split.android.client.events.executors.SplitEventExecutorResources; import io.split.android.client.events.metadata.EventMetadataHelpers; import io.split.android.fake.SplitTaskExecutorStub; @@ -275,17 +274,17 @@ private static void execute(boolean shouldStop, long intervalExecutionTime, long } @Test - public void sdkUpdateWithMetadataCallsMetadataMethod() throws InterruptedException { + public void sdkUpdateWithTypedTaskReceivesMetadata() throws InterruptedException { SplitEventsManager eventManager = new SplitEventsManager(new SplitTaskExecutorStub(), 0); CountDownLatch readyLatch = new CountDownLatch(1); CountDownLatch updateLatch = new CountDownLatch(1); - AtomicReference receivedMetadata = new AtomicReference<>(); + AtomicReference receivedMetadata = new AtomicReference<>(); waitForSdkReady(eventManager, readyLatch); - eventManager.register(SplitEvent.SDK_UPDATE, new SplitEventTask() { + eventManager.register(SplitEvent.SDK_UPDATE, new SdkUpdateEventTask() { @Override - public void onPostExecution(SplitClient client, EventMetadata metadata) { + public void onPostExecution(SplitClient client, SdkUpdateMetadata metadata) { receivedMetadata.set(metadata); updateLatch.countDown(); } @@ -297,21 +296,22 @@ public void onPostExecution(SplitClient client, EventMetadata metadata) { boolean updateAwait = updateLatch.await(3, TimeUnit.SECONDS); assertTrue("SDK_UPDATE callback should be called", updateAwait); assertNotNull("Metadata should not be null", receivedMetadata.get()); - assertTrue("Metadata should contain updatedFlags", receivedMetadata.get().containsKey(SdkUpdateMetadataKeys.UPDATED_FLAGS)); + assertNotNull("Metadata should contain updatedFlags", receivedMetadata.get().getUpdatedFlags()); + assertEquals(2, receivedMetadata.get().getUpdatedFlags().size()); } @Test - public void sdkUpdateWithMetadataCallsMetadataMethodOnMainThread() throws InterruptedException { + public void sdkUpdateWithTypedTaskReceivesMetadataOnMainThread() throws InterruptedException { SplitEventsManager eventManager = new SplitEventsManager(new SplitTaskExecutorStub(), 0); CountDownLatch readyLatch = new CountDownLatch(1); CountDownLatch updateLatch = new CountDownLatch(1); - AtomicReference receivedMetadata = new AtomicReference<>(); + AtomicReference receivedMetadata = new AtomicReference<>(); waitForSdkReady(eventManager, readyLatch); - eventManager.register(SplitEvent.SDK_UPDATE, new SplitEventTask() { + eventManager.register(SplitEvent.SDK_UPDATE, new SdkUpdateEventTask() { @Override - public void onPostExecutionView(SplitClient client, EventMetadata metadata) { + public void onPostExecutionView(SplitClient client, SdkUpdateMetadata metadata) { receivedMetadata.set(metadata); updateLatch.countDown(); } @@ -323,7 +323,7 @@ public void onPostExecutionView(SplitClient client, EventMetadata metadata) { boolean updateAwait = updateLatch.await(3, TimeUnit.SECONDS); assertTrue("SDK_UPDATE callback should be called on main thread", updateAwait); assertNotNull("Metadata should not be null", receivedMetadata.get()); - assertTrue("Metadata should contain updatedFlags", receivedMetadata.get().containsKey(SdkUpdateMetadataKeys.UPDATED_FLAGS)); + assertNotNull("Metadata should contain updatedFlags", receivedMetadata.get().getUpdatedFlags()); } @Test @@ -352,20 +352,20 @@ public void onPostExecution(SplitClient client) { } @Test - public void sdkUpdateCallsBothMethodsWhenBothImplemented() throws InterruptedException { + public void sdkUpdateTypedTaskCallsBothMethods() throws InterruptedException { SplitEventsManager eventManager = new SplitEventsManager(new SplitTaskExecutorStub(), 0); CountDownLatch readyLatch = new CountDownLatch(1); CountDownLatch bothCalledLatch = new CountDownLatch(2); - final boolean[] metadataMethodCalled = {false}; + final boolean[] typedMethodCalled = {false}; final boolean[] legacyMethodCalled = {false}; - AtomicReference receivedMetadata = new AtomicReference<>(); + AtomicReference receivedMetadata = new AtomicReference<>(); waitForSdkReady(eventManager, readyLatch); - eventManager.register(SplitEvent.SDK_UPDATE, new SplitEventTask() { + eventManager.register(SplitEvent.SDK_UPDATE, new SdkUpdateEventTask() { @Override - public void onPostExecution(SplitClient client, EventMetadata metadata) { - metadataMethodCalled[0] = true; + public void onPostExecution(SplitClient client, SdkUpdateMetadata metadata) { + typedMethodCalled[0] = true; receivedMetadata.set(metadata); bothCalledLatch.countDown(); } @@ -382,32 +382,25 @@ public void onPostExecution(SplitClient client) { boolean bothCalled = bothCalledLatch.await(3, TimeUnit.SECONDS); assertTrue("Both callbacks should be called", bothCalled); - assertTrue("Metadata method should be called", metadataMethodCalled[0]); + assertTrue("Typed method should be called", typedMethodCalled[0]); assertTrue("Legacy method should also be called", legacyMethodCalled[0]); - assertNotNull("Metadata should be passed to metadata method", receivedMetadata.get()); - assertTrue("Metadata should contain updatedFlags", receivedMetadata.get().containsKey(SdkUpdateMetadataKeys.UPDATED_FLAGS)); + assertNotNull("Metadata should be passed to typed method", receivedMetadata.get()); + assertNotNull("Metadata should contain updatedFlags", receivedMetadata.get().getUpdatedFlags()); } @Test - public void sdkReadyFromCacheCallsBothMethodsWhenBothImplemented() throws InterruptedException { + public void sdkReadyFromCacheTypedTaskReceivesMetadata() throws InterruptedException { SplitEventsManager eventManager = new SplitEventsManager(new SplitTaskExecutorStub(), 0); - CountDownLatch bothCalledLatch = new CountDownLatch(2); // Expect 2 calls - final boolean[] metadataMethodCalled = {false}; - final boolean[] legacyMethodCalled = {false}; - - // Register a task that implements both versions - eventManager.register(SplitEvent.SDK_READY_FROM_CACHE, new SplitEventTask() { - @Override - public void onPostExecution(SplitClient client, EventMetadata metadata) { - metadataMethodCalled[0] = true; - bothCalledLatch.countDown(); - } + CountDownLatch latch = new CountDownLatch(1); + AtomicReference receivedMetadata = new AtomicReference<>(); + // Register a typed task + eventManager.register(SplitEvent.SDK_READY_FROM_CACHE, new SdkReadyFromCacheEventTask() { @Override - public void onPostExecution(SplitClient client) { - legacyMethodCalled[0] = true; - bothCalledLatch.countDown(); + public void onPostExecution(SplitClient client, SdkReadyFromCacheMetadata metadata) { + receivedMetadata.set(metadata); + latch.countDown(); } }); @@ -417,10 +410,9 @@ public void onPostExecution(SplitClient client) { eventManager.notifyInternalEvent(SplitInternalEvent.ATTRIBUTES_LOADED_FROM_STORAGE); eventManager.notifyInternalEvent(SplitInternalEvent.ENCRYPTION_MIGRATION_DONE); - boolean bothCalled = bothCalledLatch.await(3, TimeUnit.SECONDS); - assertTrue("Both callbacks should be called", bothCalled); - assertTrue("Metadata method should be called", metadataMethodCalled[0]); - assertTrue("Legacy method should also be called", legacyMethodCalled[0]); + boolean called = latch.await(3, TimeUnit.SECONDS); + assertTrue("Callback should be called", called); + assertNotNull("Metadata should not be null", receivedMetadata.get()); } private void waitForSdkReady(SplitEventsManager eventManager, CountDownLatch readyLatch) throws InterruptedException { diff --git a/main/src/test/java/io/split/android/client/events/SplitEventTaskMetadataTest.java b/main/src/test/java/io/split/android/client/events/SplitEventTaskMetadataTest.java deleted file mode 100644 index aa1ab6d5c..000000000 --- a/main/src/test/java/io/split/android/client/events/SplitEventTaskMetadataTest.java +++ /dev/null @@ -1,125 +0,0 @@ -package io.split.android.client.events; - -import static org.junit.Assert.assertThrows; -import static org.junit.Assert.assertTrue; - -import org.junit.Before; -import org.junit.Test; -import org.mockito.Mock; -import org.mockito.MockitoAnnotations; - -import io.split.android.client.SplitClient; -import io.split.android.client.api.EventMetadata; -import io.split.android.client.api.SdkReadyFromCacheMetadataKeys; -import io.split.android.client.api.SdkUpdateMetadataKeys; -import io.split.android.client.events.metadata.EventMetadataHelpers; - -public class SplitEventTaskMetadataTest { - - @Mock - private SplitClient mClient; - - @Mock - private EventMetadata mMetadata; - - @Before - public void setUp() { - MockitoAnnotations.openMocks(this); - } - - @Test - public void onPostExecutionWithMetadataThrowsExceptionWhenNotImplemented() { - SplitEventTask task = new SplitEventTask(); - - assertThrows(SplitEventTaskMethodNotImplementedException.class, () -> { - task.onPostExecution(mClient, mMetadata); - }); - } - - @Test - public void onPostExecutionViewWithMetadataThrowsExceptionWhenNotImplemented() { - SplitEventTask task = new SplitEventTask(); - - assertThrows(SplitEventTaskMethodNotImplementedException.class, () -> { - task.onPostExecutionView(mClient, mMetadata); - }); - } - - @Test - public void onPostExecutionWithMetadataCanBeOverridden() { - EventMetadata metadata = EventMetadataHelpers.createUpdatedFlagsMetadata( - java.util.Arrays.asList("flag1", "flag2")); - - SplitEventTask task = new SplitEventTask() { - @Override - public void onPostExecution(SplitClient client, EventMetadata metadata) { - // Overridden implementation - } - }; - - // Should not throw exception - task.onPostExecution(mClient, metadata); - } - - @Test - public void onPostExecutionViewWithMetadataCanBeOverridden() { - EventMetadata metadata = EventMetadataHelpers.createCacheReadyMetadata(1234567890L, false); - - SplitEventTask task = new SplitEventTask() { - @Override - public void onPostExecutionView(SplitClient client, EventMetadata metadata) { - // Overridden implementation - } - }; - - // Should not throw exception - task.onPostExecutionView(mClient, metadata); - } - - @Test - public void onPostExecutionWithMetadataReceivesCorrectParameters() { - EventMetadata expectedMetadata = EventMetadataHelpers.createUpdatedFlagsMetadata( - java.util.Arrays.asList("flag1", "flag2")); - - final boolean[] metadataReceived = {false}; - final boolean[] hasUpdatedFlags = {false}; - - SplitEventTask task = new SplitEventTask() { - @Override - public void onPostExecution(SplitClient client, EventMetadata metadata) { - metadataReceived[0] = metadata != null; - hasUpdatedFlags[0] = metadata != null && metadata.containsKey(SdkUpdateMetadataKeys.UPDATED_FLAGS); - } - }; - - task.onPostExecution(mClient, expectedMetadata); - - assertTrue("Metadata should be received", metadataReceived[0]); - assertTrue("Metadata should contain updatedFlags", hasUpdatedFlags[0]); - } - - @Test - public void onPostExecutionViewWithMetadataReceivesCorrectParameters() { - EventMetadata expectedMetadata = EventMetadataHelpers.createCacheReadyMetadata(1234567890L, false); - - final boolean[] metadataReceived = {false}; - final boolean[] hasTimestamp = {false}; - final boolean[] hasFreshInstall = {false}; - - SplitEventTask task = new SplitEventTask() { - @Override - public void onPostExecutionView(SplitClient client, EventMetadata metadata) { - metadataReceived[0] = metadata != null; - hasTimestamp[0] = metadata != null && metadata.containsKey(SdkReadyFromCacheMetadataKeys.LAST_UPDATE_TIMESTAMP); - hasFreshInstall[0] = metadata != null && metadata.containsKey(SdkReadyFromCacheMetadataKeys.FRESH_INSTALL); - } - }; - - task.onPostExecutionView(mClient, expectedMetadata); - - assertTrue("Metadata should be received", metadataReceived[0]); - assertTrue("Metadata should contain lastUpdateTimestamp", hasTimestamp[0]); - assertTrue("Metadata should contain freshInstall", hasFreshInstall[0]); - } -} - diff --git a/main/src/test/java/io/split/android/client/localhost/LocalhostSplitsStorageTest.java b/main/src/test/java/io/split/android/client/localhost/LocalhostSplitsStorageTest.java index 38e7a3681..f03dae3bf 100644 --- a/main/src/test/java/io/split/android/client/localhost/LocalhostSplitsStorageTest.java +++ b/main/src/test/java/io/split/android/client/localhost/LocalhostSplitsStorageTest.java @@ -24,8 +24,9 @@ import java.io.IOException; import java.util.List; -import io.split.android.client.api.EventMetadata; -import io.split.android.client.api.SdkUpdateMetadataKeys; +import io.split.android.client.events.SdkUpdateMetadata; +import io.split.android.client.events.metadata.EventMetadata; +import io.split.android.client.events.metadata.TypedTaskConverter; import io.split.android.client.events.EventsManagerCoordinator; import io.split.android.client.events.SplitInternalEvent; import io.split.android.client.storage.legacy.FileStorage; @@ -101,7 +102,8 @@ public void loadLocalNotifiesSplitsUpdatedWithMetadataContainingUpdatedFlags() t EventMetadata metadata = metadataCaptor.getValue(); assertNotNull("Metadata should not be null", metadata); - List flags = metadata.get(SdkUpdateMetadataKeys.UPDATED_FLAGS); + SdkUpdateMetadata typedMetadata = TypedTaskConverter.convertForSdkUpdate(metadata); + List flags = typedMetadata.getUpdatedFlags(); assertNotNull("updatedFlags value should not be null", flags); assertTrue("Metadata should contain 'split1' flag", flags.contains("split1")); } diff --git a/main/src/test/java/io/split/android/client/service/SplitInPlaceUpdateTaskTest.java b/main/src/test/java/io/split/android/client/service/SplitInPlaceUpdateTaskTest.java index f23cff508..610714d1e 100644 --- a/main/src/test/java/io/split/android/client/service/SplitInPlaceUpdateTaskTest.java +++ b/main/src/test/java/io/split/android/client/service/SplitInPlaceUpdateTaskTest.java @@ -20,8 +20,9 @@ import java.util.Arrays; import java.util.List; -import io.split.android.client.api.EventMetadata; -import io.split.android.client.api.SdkUpdateMetadataKeys; +import io.split.android.client.events.SdkUpdateMetadata; +import io.split.android.client.events.metadata.EventMetadata; +import io.split.android.client.events.metadata.TypedTaskConverter; import io.split.android.client.dtos.Split; import io.split.android.client.events.ISplitEventsManager; import io.split.android.client.events.SplitInternalEvent; @@ -152,7 +153,8 @@ public void splitsUpdatedIncludesMetadataWithUpdatedFlags() { verify(mEventsManager).notifyInternalEvent(eq(SplitInternalEvent.SPLITS_UPDATED), argThat(metadata -> { if (metadata == null) return false; - List flags = metadata.get(SdkUpdateMetadataKeys.UPDATED_FLAGS); + SdkUpdateMetadata typedMeta = TypedTaskConverter.convertForSdkUpdate(metadata); + List flags = typedMeta.getUpdatedFlags(); assertNotNull(flags); assertEquals(2, flags.size()); assertTrue(flags.contains("test_split_1")); @@ -175,7 +177,8 @@ public void splitsUpdatedIncludesArchivedSplitsInMetadata() { verify(mEventsManager).notifyInternalEvent(eq(SplitInternalEvent.SPLITS_UPDATED), argThat(metadata -> { if (metadata == null) return false; - List flags = metadata.get(SdkUpdateMetadataKeys.UPDATED_FLAGS); + SdkUpdateMetadata typedMeta = TypedTaskConverter.convertForSdkUpdate(metadata); + List flags = typedMeta.getUpdatedFlags(); assertNotNull(flags); assertEquals(1, flags.size()); assertTrue(flags.contains("archived_split")); diff --git a/main/src/test/java/io/split/android/client/service/SplitKillTaskTest.java b/main/src/test/java/io/split/android/client/service/SplitKillTaskTest.java index 9a356a34e..aee4e3e4f 100644 --- a/main/src/test/java/io/split/android/client/service/SplitKillTaskTest.java +++ b/main/src/test/java/io/split/android/client/service/SplitKillTaskTest.java @@ -5,13 +5,13 @@ import org.junit.Before; import org.junit.Test; import org.mockito.ArgumentCaptor; -import org.mockito.Mock; import org.mockito.Mockito; import java.util.List; -import io.split.android.client.api.EventMetadata; -import io.split.android.client.api.SdkUpdateMetadataKeys; +import io.split.android.client.events.SdkUpdateMetadata; +import io.split.android.client.events.metadata.EventMetadata; +import io.split.android.client.events.metadata.TypedTaskConverter; import io.split.android.client.dtos.Split; import io.split.android.client.events.SplitEventsManager; import io.split.android.client.events.SplitInternalEvent; @@ -21,7 +21,6 @@ import io.split.android.client.service.http.HttpFetcherException; import io.split.android.client.service.splits.SplitKillTask; import io.split.android.client.storage.splits.SplitsStorage; -import io.split.android.helpers.FileHelper; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; @@ -80,7 +79,8 @@ public void correctExecution() throws HttpFetcherException { eq(SplitInternalEvent.SPLIT_KILLED_NOTIFICATION), metadataCaptor.capture()); EventMetadata metadata = metadataCaptor.getValue(); Assert.assertNotNull(metadata); - List updatedFlags = metadata.get(SdkUpdateMetadataKeys.UPDATED_FLAGS); + SdkUpdateMetadata typedMetadata = TypedTaskConverter.convertForSdkUpdate(metadata); + List updatedFlags = typedMetadata.getUpdatedFlags(); Assert.assertNotNull(updatedFlags); Assert.assertEquals(1, updatedFlags.size()); Assert.assertTrue(updatedFlags.contains("split1")); diff --git a/main/src/test/java/io/split/android/client/service/SplitSyncTaskTest.java b/main/src/test/java/io/split/android/client/service/SplitSyncTaskTest.java index a5f346b7b..005d7756e 100644 --- a/main/src/test/java/io/split/android/client/service/SplitSyncTaskTest.java +++ b/main/src/test/java/io/split/android/client/service/SplitSyncTaskTest.java @@ -28,7 +28,8 @@ import java.util.List; import java.util.Map; -import io.split.android.client.api.SdkUpdateMetadataKeys; +import io.split.android.client.events.SdkUpdateMetadata; +import io.split.android.client.events.metadata.TypedTaskConverter; import io.split.android.client.dtos.SplitChange; import io.split.android.client.events.SplitEventsManager; import io.split.android.client.events.SplitInternalEvent; @@ -258,7 +259,8 @@ public void splitsUpdatedIncludesMetadataWithUpdatedFlags() throws HttpFetcherEx verify(mEventsManager).notifyInternalEvent(eq(SplitInternalEvent.SPLITS_UPDATED), argThat(metadata -> { if (metadata == null) return false; - List flags = metadata.get(SdkUpdateMetadataKeys.UPDATED_FLAGS); + SdkUpdateMetadata typedMeta = TypedTaskConverter.convertForSdkUpdate(metadata); + List flags = typedMeta.getUpdatedFlags(); assertNotNull(flags); assertEquals(3, flags.size()); assertTrue(flags.contains("split1")); @@ -285,7 +287,8 @@ public void splitsUpdatedIncludesEmptyMetadataWhenNoSplitsUpdated() throws HttpF verify(mEventsManager).notifyInternalEvent(eq(SplitInternalEvent.SPLITS_UPDATED), argThat(metadata -> { if (metadata == null) return false; - List flags = metadata.get(SdkUpdateMetadataKeys.UPDATED_FLAGS); + SdkUpdateMetadata typedMeta = TypedTaskConverter.convertForSdkUpdate(metadata); + List flags = typedMeta.getUpdatedFlags(); assertNotNull(flags); assertTrue(flags.isEmpty()); return true; diff --git a/main/src/test/java/io/split/android/client/service/SplitUpdateTaskTest.java b/main/src/test/java/io/split/android/client/service/SplitUpdateTaskTest.java index 063d44b7c..97f976b04 100644 --- a/main/src/test/java/io/split/android/client/service/SplitUpdateTaskTest.java +++ b/main/src/test/java/io/split/android/client/service/SplitUpdateTaskTest.java @@ -21,8 +21,9 @@ import java.util.Arrays; import java.util.List; -import io.split.android.client.api.SdkReadyFromCacheMetadataKeys; -import io.split.android.client.api.SdkUpdateMetadataKeys; +import io.split.android.client.events.SdkReadyFromCacheMetadata; +import io.split.android.client.events.SdkUpdateMetadata; +import io.split.android.client.events.metadata.TypedTaskConverter; import io.split.android.client.dtos.SplitChange; import io.split.android.client.events.SplitEventsManager; import io.split.android.client.events.SplitInternalEvent; @@ -107,9 +108,10 @@ public void targetingRulesSyncCompleteIsAlwaysFiredOnSuccessfulSyncWithSyncMetad // Verify TARGETING_RULES_SYNC_COMPLETE is fired with sync metadata (freshInstall=true, lastUpdateTimestamp=null) verify(mEventsManager).notifyInternalEvent(eq(SplitInternalEvent.TARGETING_RULES_SYNC_COMPLETE), argThat(metadata -> { if (metadata == null) return false; - assertEquals(Boolean.TRUE, metadata.get(SdkReadyFromCacheMetadataKeys.FRESH_INSTALL)); + SdkReadyFromCacheMetadata typedMeta = TypedTaskConverter.convertForSdkReadyFromCache(metadata); + assertEquals(Boolean.TRUE, typedMeta.isFreshInstall()); // lastUpdateTimestamp should not be present (or should be null) - return metadata.get(SdkReadyFromCacheMetadataKeys.LAST_UPDATE_TIMESTAMP) == null; + return typedMeta.getLastUpdateTimestamp() == null; })); } @@ -176,7 +178,8 @@ public void splitsUpdatedIncludesMetadataWithUpdatedFlags() { verify(mEventsManager).notifyInternalEvent(eq(SplitInternalEvent.SPLITS_UPDATED), argThat(metadata -> { if (metadata == null) return false; - List flags = metadata.get(SdkUpdateMetadataKeys.UPDATED_FLAGS); + SdkUpdateMetadata typedMeta = TypedTaskConverter.convertForSdkUpdate(metadata); + List flags = typedMeta.getUpdatedFlags(); assertNotNull(flags); assertEquals(2, flags.size()); assertTrue(flags.contains("flag1")); diff --git a/main/src/test/java/io/split/android/client/service/synchronizer/LoadLocalDataListenerTest.java b/main/src/test/java/io/split/android/client/service/synchronizer/LoadLocalDataListenerTest.java index adda882a1..252c29696 100644 --- a/main/src/test/java/io/split/android/client/service/synchronizer/LoadLocalDataListenerTest.java +++ b/main/src/test/java/io/split/android/client/service/synchronizer/LoadLocalDataListenerTest.java @@ -12,7 +12,7 @@ import org.junit.Test; import org.mockito.Mockito; -import io.split.android.client.api.EventMetadata; +import io.split.android.client.events.metadata.EventMetadata; import io.split.android.client.events.ISplitEventsManager; import io.split.android.client.events.SplitInternalEvent; import io.split.android.client.service.executor.SplitTaskExecutionInfo; diff --git a/main/src/test/java/io/split/android/fake/SplitEventsManagerStub.java b/main/src/test/java/io/split/android/fake/SplitEventsManagerStub.java index 7fed3538a..316d4cb64 100644 --- a/main/src/test/java/io/split/android/fake/SplitEventsManagerStub.java +++ b/main/src/test/java/io/split/android/fake/SplitEventsManagerStub.java @@ -2,7 +2,7 @@ import androidx.annotation.Nullable; -import io.split.android.client.api.EventMetadata; +import io.split.android.client.events.metadata.EventMetadata; import io.split.android.client.events.ISplitEventsManager; import io.split.android.client.events.ListenableEventsManager; import io.split.android.client.events.SplitEvent; From bacaffe9afc5a2ced913d2a1bf573b9044a0fdae Mon Sep 17 00:00:00 2001 From: Gaston Thea Date: Wed, 7 Jan 2026 10:55:51 -0300 Subject: [PATCH 06/27] API changes --- .github/workflows/sonarqube.yml | 9 +- .../io/split/android/client/SplitClient.java | 25 +++--- .../split/android/client/events/SdkEvent.java | 84 ------------------- .../client/events/SdkEventListener.java | 84 +++++++++++++++++++ .../events/SdkReadyFromCacheEventTask.java | 50 ----------- .../client/events/SdkUpdateEventTask.java | 49 ----------- .../android/client/events/SplitEventTask.java | 4 +- .../android/client/events/SdkEventTest.java | 50 ----------- .../SdkReadyFromCacheEventTaskTest.java | 51 ----------- .../client/events/SdkUpdateEventTaskTest.java | 51 ----------- 10 files changed, 108 insertions(+), 349 deletions(-) delete mode 100644 api/src/main/java/io/split/android/client/events/SdkEvent.java create mode 100644 api/src/main/java/io/split/android/client/events/SdkEventListener.java delete mode 100644 api/src/main/java/io/split/android/client/events/SdkReadyFromCacheEventTask.java delete mode 100644 api/src/main/java/io/split/android/client/events/SdkUpdateEventTask.java delete mode 100644 api/src/test/java/io/split/android/client/events/SdkEventTest.java delete mode 100644 api/src/test/java/io/split/android/client/events/SdkReadyFromCacheEventTaskTest.java delete mode 100644 api/src/test/java/io/split/android/client/events/SdkUpdateEventTaskTest.java diff --git a/.github/workflows/sonarqube.yml b/.github/workflows/sonarqube.yml index 93fc5af44..94a0bf3c3 100644 --- a/.github/workflows/sonarqube.yml +++ b/.github/workflows/sonarqube.yml @@ -64,8 +64,13 @@ jobs: echo "=== Verifying Build Artifacts for SonarQube ===" echo "" + # Dynamically get modules from settings.gradle (extract module names from "include ':modulename'" lines) + MODULES=$(grep "^include" settings.gradle | cut -d"'" -f2 | cut -d":" -f2 | tr '\n' ' ') + echo "Detected modules: $MODULES" + echo "" + echo "Checking compiled class files for each module:" - for module in main events logger; do + for module in $MODULES; do MODULE_CLASSES_DIR="${module}/build/intermediates/javac/debug/compileDebugJavaWithJavac/classes" if [ -d "$MODULE_CLASSES_DIR" ]; then CLASS_COUNT=$(find "$MODULE_CLASSES_DIR" -name "*.class" | wc -l) @@ -103,7 +108,7 @@ jobs: echo "" echo "Checking JaCoCo execution data for each module:" - for module in main events logger; do + for module in $MODULES; do EXEC_FILE="${module}/build/jacoco/testDebugUnitTest.exec" if [ -f "$EXEC_FILE" ]; then EXEC_SIZE=$(wc -c < "$EXEC_FILE") diff --git a/api/src/main/java/io/split/android/client/SplitClient.java b/api/src/main/java/io/split/android/client/SplitClient.java index a9375c0df..ba315e0e6 100644 --- a/api/src/main/java/io/split/android/client/SplitClient.java +++ b/api/src/main/java/io/split/android/client/SplitClient.java @@ -7,7 +7,7 @@ import java.util.Map; import io.split.android.client.attributes.AttributesManager; -import io.split.android.client.events.SdkEvent; +import io.split.android.client.events.SdkEventListener; import io.split.android.client.events.SplitEvent; import io.split.android.client.events.SplitEventTask; @@ -181,26 +181,31 @@ public interface SplitClient extends AttributesManager { void on(SplitEvent event, SplitEventTask task); /** - * Registers a type-safe event listener for SDK events. + * Registers an event listener for SDK events that provide typed metadata. *

- * This method provides compile-time type safety for event task registration. - * The event type parameter enforces the correct task type for each event. + * This method provides type-safe callbacks for SDK_UPDATE and SDK_READY_FROM_CACHE events. + * Override the methods you need in the listener. *

* Example usage: *

{@code
-     * client.on(SdkEvent.SDK_UPDATE, new SdkUpdateEventTask() {
+     * client.addEventListener(new SdkEventListener() {
      *     @Override
-     *     public void onPostExecution(SplitClient client, SdkUpdateMetadata metadata) {
+     *     public void onUpdate(SplitClient client, SdkUpdateMetadata metadata) {
      *         List flags = metadata.getUpdatedFlags();
+     *         // Handle on background thread
+     *     }
+     *
+     *     @Override
+     *     public void onReadyFromCacheView(SplitClient client, SdkReadyFromCacheMetadata metadata) {
+     *         // Handle on main/UI thread
+     *         Boolean freshInstall = metadata.isFreshInstall();
      *     }
      * });
      * }
* - * @param event the type-safe event to listen for - * @param task the task to execute when the event occurs - * @param the type of event task + * @param listener the event listener to register */ - void on(SdkEvent event, T task); + void addEventListener(SdkEventListener listener); /** * Enqueue a new event to be sent to Split data collection services. diff --git a/api/src/main/java/io/split/android/client/events/SdkEvent.java b/api/src/main/java/io/split/android/client/events/SdkEvent.java deleted file mode 100644 index 7c2fcfc4d..000000000 --- a/api/src/main/java/io/split/android/client/events/SdkEvent.java +++ /dev/null @@ -1,84 +0,0 @@ -package io.split.android.client.events; - -/** - * Type-safe event class for SDK event subscriptions. - *

- * This class provides compile-time type safety for event task registration. - * Use the static instances to register event listeners with the correct task type. - *

- * Example usage: - *

{@code
- * client.on(SdkEvent.SDK_UPDATE, new SdkUpdateEventTask() {
- *     @Override
- *     public void onPostExecution(SplitClient client, SdkUpdateMetadata metadata) {
- *         List flags = metadata.getUpdatedFlags();
- *     }
- * });
- * }
- * - * @param the type of event task that can handle this event - */ -public abstract class SdkEvent { - - /** - * Event fired when SDK definitions are updated from the server. - *

- * Register with {@link SdkUpdateEventTask} to receive typed metadata. - */ - public static final SdkEvent SDK_UPDATE = new SdkEvent() { - @Override - public SplitEvent toSplitEvent() { - return SplitEvent.SDK_UPDATE; - } - }; - - /** - * Event fired when SDK is ready from cached data. - *

- * Register with {@link SdkReadyFromCacheEventTask} to receive typed metadata. - */ - public static final SdkEvent SDK_READY_FROM_CACHE = new SdkEvent() { - @Override - public SplitEvent toSplitEvent() { - return SplitEvent.SDK_READY_FROM_CACHE; - } - }; - - /** - * Event fired when SDK is fully ready from the server. - *

- * Register with {@link SplitEventTask} for basic event handling. - */ - public static final SdkEvent SDK_READY = new SdkEvent() { - @Override - public SplitEvent toSplitEvent() { - return SplitEvent.SDK_READY; - } - }; - - /** - * Event fired when SDK ready has timed out. - *

- * Register with {@link SplitEventTask} for basic event handling. - */ - public static final SdkEvent SDK_READY_TIMED_OUT = new SdkEvent() { - @Override - public SplitEvent toSplitEvent() { - return SplitEvent.SDK_READY_TIMED_OUT; - } - }; - - // Package-private constructor to prevent external subclassing - SdkEvent() { - } - - /** - * Converts this type-safe event to the internal SplitEvent enum. - *

- * Internal API - called by SDK internals. - * - * @return the corresponding SplitEvent enum value - */ - public abstract SplitEvent toSplitEvent(); -} - diff --git a/api/src/main/java/io/split/android/client/events/SdkEventListener.java b/api/src/main/java/io/split/android/client/events/SdkEventListener.java new file mode 100644 index 000000000..d86946a16 --- /dev/null +++ b/api/src/main/java/io/split/android/client/events/SdkEventListener.java @@ -0,0 +1,84 @@ +package io.split.android.client.events; + +import io.split.android.client.SplitClient; + +/** + * Abstract class for handling SDK events with typed metadata. + *

+ * Extend this class and override the methods you need to handle specific SDK events. + * Each event has two callback options: + *

    + *
  • Background thread callbacks (e.g., {@link #onUpdate}) - executed immediately on a background thread
  • + *
  • Main thread callbacks (e.g., {@link #onUpdateView}) - executed on the main/UI thread
  • + *
+ *

+ * Example usage: + *

{@code
+ * client.addEventListener(new SdkEventListener() {
+ *     @Override
+ *     public void onUpdate(SplitClient client, SdkUpdateMetadata metadata) {
+ *         List flags = metadata.getUpdatedFlags();
+ *         // Handle updated flags on background thread
+ *     }
+ *
+ *     @Override
+ *     public void onReadyFromCacheView(SplitClient client, SdkReadyFromCacheMetadata metadata) {
+ *         // Handle cache ready on main/UI thread
+ *         Boolean freshInstall = metadata.isFreshInstall();
+ *     }
+ * });
+ * }
+ */ +public abstract class SdkEventListener { + + /** + * Called when SDK_UPDATE event occurs, executed on a background thread. + *

+ * Override this method to handle SDK_UPDATE events with typed metadata. + * + * @param client the Split client instance + * @param metadata the typed metadata containing updated flag information + */ + public void onUpdate(SplitClient client, SdkUpdateMetadata metadata) { + // Default empty implementation + } + + /** + * Called when SDK_READY_FROM_CACHE event occurs, executed on a background thread. + *

+ * Override this method to handle SDK_READY_FROM_CACHE events with typed metadata. + * + * @param client the Split client instance + * @param metadata the typed metadata containing cache information + */ + public void onReadyFromCache(SplitClient client, SdkReadyFromCacheMetadata metadata) { + // Default empty implementation + } + + /** + * Called when SDK_UPDATE event occurs, executed on the main/UI thread. + *

+ * Override this method to handle SDK_UPDATE events with typed metadata on the main thread. + * Use this when you need to update UI components. + * + * @param client the Split client instance + * @param metadata the typed metadata containing updated flag information + */ + public void onUpdateView(SplitClient client, SdkUpdateMetadata metadata) { + // Default empty implementation + } + + /** + * Called when SDK_READY_FROM_CACHE event occurs, executed on the main/UI thread. + *

+ * Override this method to handle SDK_READY_FROM_CACHE events with typed metadata on the main thread. + * Use this when you need to update UI components. + * + * @param client the Split client instance + * @param metadata the typed metadata containing cache information + */ + public void onReadyFromCacheView(SplitClient client, SdkReadyFromCacheMetadata metadata) { + // Default empty implementation + } +} + diff --git a/api/src/main/java/io/split/android/client/events/SdkReadyFromCacheEventTask.java b/api/src/main/java/io/split/android/client/events/SdkReadyFromCacheEventTask.java deleted file mode 100644 index 6359f5ec3..000000000 --- a/api/src/main/java/io/split/android/client/events/SdkReadyFromCacheEventTask.java +++ /dev/null @@ -1,50 +0,0 @@ -package io.split.android.client.events; - -import io.split.android.client.SplitClient; - -/** - * Typed event task for SDK_READY_FROM_CACHE events. - *

- * Extend this class and override the typed methods to handle SDK_READY_FROM_CACHE events - * with type-safe metadata access. - *

- * Example usage: - *

{@code
- * client.on(SdkEvent.SDK_READY_FROM_CACHE, new SdkReadyFromCacheEventTask() {
- *     @Override
- *     public void onPostExecution(SplitClient client, SdkReadyFromCacheMetadata metadata) {
- *         Boolean freshInstall = metadata.isFreshInstall();
- *         Long timestamp = metadata.getLastUpdateTimestamp();
- *         // Handle cache ready event
- *     }
- * });
- * }
- */ -public class SdkReadyFromCacheEventTask extends SplitEventTask { - - /** - * Called when SDK_READY_FROM_CACHE event occurs, executed on a background thread. - *

- * Override this method to handle SDK_READY_FROM_CACHE events with typed metadata. - * - * @param client the Split client instance - * @param metadata the typed metadata containing cache information - * @throws SplitEventTaskMethodNotImplementedException if not overridden (default behavior) - */ - public void onPostExecution(SplitClient client, SdkReadyFromCacheMetadata metadata) { - throw new SplitEventTaskMethodNotImplementedException(); - } - - /** - * Called when SDK_READY_FROM_CACHE event occurs, executed on the main/UI thread. - *

- * Override this method to handle SDK_READY_FROM_CACHE events with typed metadata on the main thread. - * - * @param client the Split client instance - * @param metadata the typed metadata containing cache information - * @throws SplitEventTaskMethodNotImplementedException if not overridden (default behavior) - */ - public void onPostExecutionView(SplitClient client, SdkReadyFromCacheMetadata metadata) { - throw new SplitEventTaskMethodNotImplementedException(); - } -} diff --git a/api/src/main/java/io/split/android/client/events/SdkUpdateEventTask.java b/api/src/main/java/io/split/android/client/events/SdkUpdateEventTask.java deleted file mode 100644 index 9f74ca8ce..000000000 --- a/api/src/main/java/io/split/android/client/events/SdkUpdateEventTask.java +++ /dev/null @@ -1,49 +0,0 @@ -package io.split.android.client.events; - -import io.split.android.client.SplitClient; - -/** - * Typed event task for SDK_UPDATE events. - *

- * Extend this class and override the typed methods to handle SDK_UPDATE events - * with type-safe metadata access. - *

- * Example usage: - *

{@code
- * client.on(SdkEvent.SDK_UPDATE, new SdkUpdateEventTask() {
- *     @Override
- *     public void onPostExecution(SplitClient client, SdkUpdateMetadata metadata) {
- *         List flags = metadata.getUpdatedFlags();
- *         // Handle updated flags
- *     }
- * });
- * }
- */ -public class SdkUpdateEventTask extends SplitEventTask { - - /** - * Called when SDK_UPDATE event occurs, executed on a background thread. - *

- * Override this method to handle SDK_UPDATE events with typed metadata. - * - * @param client the Split client instance - * @param metadata the typed metadata containing updated flag information - * @throws SplitEventTaskMethodNotImplementedException if not overridden (default behavior) - */ - public void onPostExecution(SplitClient client, SdkUpdateMetadata metadata) { - throw new SplitEventTaskMethodNotImplementedException(); - } - - /** - * Called when SDK_UPDATE event occurs, executed on the main/UI thread. - *

- * Override this method to handle SDK_UPDATE events with typed metadata on the main thread. - * - * @param client the Split client instance - * @param metadata the typed metadata containing updated flag information - * @throws SplitEventTaskMethodNotImplementedException if not overridden (default behavior) - */ - public void onPostExecutionView(SplitClient client, SdkUpdateMetadata metadata) { - throw new SplitEventTaskMethodNotImplementedException(); - } -} diff --git a/api/src/main/java/io/split/android/client/events/SplitEventTask.java b/api/src/main/java/io/split/android/client/events/SplitEventTask.java index df61ba467..c2b704cf5 100644 --- a/api/src/main/java/io/split/android/client/events/SplitEventTask.java +++ b/api/src/main/java/io/split/android/client/events/SplitEventTask.java @@ -13,8 +13,8 @@ *

  • {@code onPostExecutionView} methods are called on the main/UI thread (queued on main looper)
  • * *

    - * For events with metadata (like SDK_UPDATE or SDK_READY_FROM_CACHE), use the typed event task classes - * {@link SdkUpdateEventTask} or {@link SdkReadyFromCacheEventTask} for type-safe metadata access. + * For events with metadata (like SDK_UPDATE or SDK_READY_FROM_CACHE), use + * {@link SdkEventListener} instead for type-safe metadata access. *

    * Example usage: *

    {@code
    diff --git a/api/src/test/java/io/split/android/client/events/SdkEventTest.java b/api/src/test/java/io/split/android/client/events/SdkEventTest.java
    deleted file mode 100644
    index 6e347e7c1..000000000
    --- a/api/src/test/java/io/split/android/client/events/SdkEventTest.java
    +++ /dev/null
    @@ -1,50 +0,0 @@
    -package io.split.android.client.events;
    -
    -import static org.junit.Assert.assertEquals;
    -import static org.junit.Assert.assertNotNull;
    -
    -import org.junit.Test;
    -
    -public class SdkEventTest {
    -
    -    @Test
    -    public void sdkUpdateStaticInstanceExists() {
    -        assertNotNull(SdkEvent.SDK_UPDATE);
    -    }
    -
    -    @Test
    -    public void sdkReadyFromCacheStaticInstanceExists() {
    -        assertNotNull(SdkEvent.SDK_READY_FROM_CACHE);
    -    }
    -
    -    @Test
    -    public void sdkReadyStaticInstanceExists() {
    -        assertNotNull(SdkEvent.SDK_READY);
    -    }
    -
    -    @Test
    -    public void sdkReadyTimedOutStaticInstanceExists() {
    -        assertNotNull(SdkEvent.SDK_READY_TIMED_OUT);
    -    }
    -
    -    @Test
    -    public void sdkUpdateMapsToSplitEventSdkUpdate() {
    -        assertEquals(SplitEvent.SDK_UPDATE, SdkEvent.SDK_UPDATE.toSplitEvent());
    -    }
    -
    -    @Test
    -    public void sdkReadyFromCacheMapsToSplitEventSdkReadyFromCache() {
    -        assertEquals(SplitEvent.SDK_READY_FROM_CACHE, SdkEvent.SDK_READY_FROM_CACHE.toSplitEvent());
    -    }
    -
    -    @Test
    -    public void sdkReadyMapsToSplitEventSdkReady() {
    -        assertEquals(SplitEvent.SDK_READY, SdkEvent.SDK_READY.toSplitEvent());
    -    }
    -
    -    @Test
    -    public void sdkReadyTimedOutMapsToSplitEventSdkReadyTimedOut() {
    -        assertEquals(SplitEvent.SDK_READY_TIMED_OUT, SdkEvent.SDK_READY_TIMED_OUT.toSplitEvent());
    -    }
    -}
    -
    diff --git a/api/src/test/java/io/split/android/client/events/SdkReadyFromCacheEventTaskTest.java b/api/src/test/java/io/split/android/client/events/SdkReadyFromCacheEventTaskTest.java
    deleted file mode 100644
    index 4765e1e2a..000000000
    --- a/api/src/test/java/io/split/android/client/events/SdkReadyFromCacheEventTaskTest.java
    +++ /dev/null
    @@ -1,51 +0,0 @@
    -package io.split.android.client.events;
    -
    -import static org.junit.Assert.assertTrue;
    -import static org.mockito.Mockito.mock;
    -
    -import org.junit.Test;
    -
    -import io.split.android.client.SplitClient;
    -
    -public class SdkReadyFromCacheEventTaskTest {
    -
    -    @Test
    -    public void extendsFromSplitEventTask() {
    -        SdkReadyFromCacheEventTask task = new SdkReadyFromCacheEventTask() {
    -            @Override
    -            public void onPostExecution(SplitClient client, SdkReadyFromCacheMetadata metadata) {
    -                // no-op
    -            }
    -        };
    -
    -        assertTrue(task instanceof SplitEventTask);
    -    }
    -
    -    @Test
    -    public void defaultImplementationThrowsExceptionForTypedMethods() {
    -        SdkReadyFromCacheEventTask task = new SdkReadyFromCacheEventTask() {};
    -
    -        boolean threwException = false;
    -        try {
    -            task.onPostExecution(mock(SplitClient.class), new SdkReadyFromCacheMetadata(null, null));
    -        } catch (SplitEventTaskMethodNotImplementedException e) {
    -            threwException = true;
    -        }
    -
    -        assertTrue(threwException);
    -    }
    -
    -    @Test
    -    public void defaultImplementationThrowsExceptionForTypedViewMethods() {
    -        SdkReadyFromCacheEventTask task = new SdkReadyFromCacheEventTask() {};
    -
    -        boolean threwException = false;
    -        try {
    -            task.onPostExecutionView(mock(SplitClient.class), new SdkReadyFromCacheMetadata(null, null));
    -        } catch (SplitEventTaskMethodNotImplementedException e) {
    -            threwException = true;
    -        }
    -
    -        assertTrue(threwException);
    -    }
    -}
    diff --git a/api/src/test/java/io/split/android/client/events/SdkUpdateEventTaskTest.java b/api/src/test/java/io/split/android/client/events/SdkUpdateEventTaskTest.java
    deleted file mode 100644
    index 3295b1c65..000000000
    --- a/api/src/test/java/io/split/android/client/events/SdkUpdateEventTaskTest.java
    +++ /dev/null
    @@ -1,51 +0,0 @@
    -package io.split.android.client.events;
    -
    -import static org.junit.Assert.assertTrue;
    -import static org.mockito.Mockito.mock;
    -
    -import org.junit.Test;
    -
    -import io.split.android.client.SplitClient;
    -
    -public class SdkUpdateEventTaskTest {
    -
    -    @Test
    -    public void extendsFromSplitEventTask() {
    -        SdkUpdateEventTask task = new SdkUpdateEventTask() {
    -            @Override
    -            public void onPostExecution(SplitClient client, SdkUpdateMetadata metadata) {
    -                // no-op
    -            }
    -        };
    -
    -        assertTrue(task instanceof SplitEventTask);
    -    }
    -
    -    @Test
    -    public void defaultImplementationThrowsExceptionForTypedMethods() {
    -        SdkUpdateEventTask task = new SdkUpdateEventTask() {};
    -
    -        boolean threwException = false;
    -        try {
    -            task.onPostExecution(mock(SplitClient.class), new SdkUpdateMetadata(null));
    -        } catch (SplitEventTaskMethodNotImplementedException e) {
    -            threwException = true;
    -        }
    -
    -        assertTrue(threwException);
    -    }
    -
    -    @Test
    -    public void defaultImplementationThrowsExceptionForTypedViewMethods() {
    -        SdkUpdateEventTask task = new SdkUpdateEventTask() {};
    -
    -        boolean threwException = false;
    -        try {
    -            task.onPostExecutionView(mock(SplitClient.class), new SdkUpdateMetadata(null));
    -        } catch (SplitEventTaskMethodNotImplementedException e) {
    -            threwException = true;
    -        }
    -
    -        assertTrue(threwException);
    -    }
    -}
    
    From 78e21413e9b2eac92d5c2de8f116283ad71b43ce Mon Sep 17 00:00:00 2001
    From: Gaston Thea 
    Date: Wed, 7 Jan 2026 11:12:37 -0300
    Subject: [PATCH 07/27] Domain changes
    
    ---
     .../events/EventsManagerCoordinator.java      |  8 +-
     .../events/ListenableEventsManager.java       |  2 +
     .../client/events/SplitEventsManager.java     | 71 ++++++++++++----
     .../events/TypedTaskConversionTest.java       | 29 +------
     .../events/SdkEventsIntegrationTest.java      | 83 ++++++++++---------
     .../AlwaysReturnControlSplitClient.java       |  3 +-
     .../split/android/client/SplitClientImpl.java |  7 +-
     .../localhost/LocalhostSplitClient.java       |  6 +-
     .../events/EventsManagerCoordinatorTest.java  | 41 +++++++++
     .../client/events/EventsManagerTest.java      | 36 ++++----
     10 files changed, 176 insertions(+), 110 deletions(-)
    
    diff --git a/events-domain/src/main/java/io/split/android/client/events/EventsManagerCoordinator.java b/events-domain/src/main/java/io/split/android/client/events/EventsManagerCoordinator.java
    index 6fa14d827..4fba4b4d4 100644
    --- a/events-domain/src/main/java/io/split/android/client/events/EventsManagerCoordinator.java
    +++ b/events-domain/src/main/java/io/split/android/client/events/EventsManagerCoordinator.java
    @@ -104,13 +104,19 @@ public void registerEventsManager(Key key, ISplitEventsManager splitEventsManage
     
         /**
          * Unregisters the events manager for a client key.
    +     * 

    + * If the removed manager is a {@link SplitEventsManager}, its {@code destroy()} method + * will be called to clean up resources. * * @param key the client key to unregister */ @Override public void unregisterEventsManager(Key key) { if (key != null) { - mManagers.remove(key); + ISplitEventsManager removed = mManagers.remove(key); + if (removed instanceof SplitEventsManager) { + ((SplitEventsManager) removed).destroy(); + } } } diff --git a/events-domain/src/main/java/io/split/android/client/events/ListenableEventsManager.java b/events-domain/src/main/java/io/split/android/client/events/ListenableEventsManager.java index f0b4aff46..a8ad9c0f1 100644 --- a/events-domain/src/main/java/io/split/android/client/events/ListenableEventsManager.java +++ b/events-domain/src/main/java/io/split/android/client/events/ListenableEventsManager.java @@ -8,5 +8,7 @@ public interface ListenableEventsManager { void register(SplitEvent event, SplitEventTask task); + void registerEventListener(SdkEventListener listener); + boolean eventAlreadyTriggered(SplitEvent event); } diff --git a/events-domain/src/main/java/io/split/android/client/events/SplitEventsManager.java b/events-domain/src/main/java/io/split/android/client/events/SplitEventsManager.java index 464407bd5..5cf91b3e0 100644 --- a/events-domain/src/main/java/io/split/android/client/events/SplitEventsManager.java +++ b/events-domain/src/main/java/io/split/android/client/events/SplitEventsManager.java @@ -112,6 +112,27 @@ public void register(SplitEvent event, SplitEventTask task) { ); } + @Override + public void registerEventListener(SdkEventListener listener) { + requireNonNull(listener); + + // Register SDK_UPDATE handlers (bg + main) + mDualExecutorRegistration.register( + mEventsManager, + SplitEvent.SDK_UPDATE, + createUpdateBackgroundHandler(listener), + createUpdateMainThreadHandler(listener) + ); + + // Register SDK_READY_FROM_CACHE handlers (bg + main) + mDualExecutorRegistration.register( + mEventsManager, + SplitEvent.SDK_READY_FROM_CACHE, + createReadyFromCacheBackgroundHandler(listener), + createReadyFromCacheMainThreadHandler(listener) + ); + } + @Override public boolean eventAlreadyTriggered(SplitEvent event) { return mEventsManager.eventAlreadyTriggered(event); @@ -160,31 +181,45 @@ private EventHandler createMainThreadHandler(final Sp }; } - private void executeBackgroundTask(SplitEventTask task, SplitClient client, EventMetadata metadata) { - // Try typed methods first for typed tasks - if (task instanceof SdkUpdateEventTask) { + // SdkEventListener handlers for SDK_UPDATE + private EventHandler createUpdateBackgroundHandler(final SdkEventListener listener) { + return (event, metadata) -> { + SplitClient client = mResources.getSplitClient(); + SdkUpdateMetadata typedMetadata = TypedTaskConverter.convertForSdkUpdate(metadata); + executeMethod(() -> listener.onUpdate(client, typedMetadata)); + }; + } + + private EventHandler createUpdateMainThreadHandler(final SdkEventListener listener) { + return (event, metadata) -> { + SplitClient client = mResources.getSplitClient(); SdkUpdateMetadata typedMetadata = TypedTaskConverter.convertForSdkUpdate(metadata); - executeMethod(() -> ((SdkUpdateEventTask) task).onPostExecution(client, typedMetadata)); - } else if (task instanceof SdkReadyFromCacheEventTask) { + executeMethod(() -> listener.onUpdateView(client, typedMetadata)); + }; + } + + // SdkEventListener handlers for SDK_READY_FROM_CACHE + private EventHandler createReadyFromCacheBackgroundHandler(final SdkEventListener listener) { + return (event, metadata) -> { + SplitClient client = mResources.getSplitClient(); SdkReadyFromCacheMetadata typedMetadata = TypedTaskConverter.convertForSdkReadyFromCache(metadata); - executeMethod(() -> ((SdkReadyFromCacheEventTask) task).onPostExecution(client, typedMetadata)); - } + executeMethod(() -> listener.onReadyFromCache(client, typedMetadata)); + }; + } - // Always try the base method + private EventHandler createReadyFromCacheMainThreadHandler(final SdkEventListener listener) { + return (event, metadata) -> { + SplitClient client = mResources.getSplitClient(); + SdkReadyFromCacheMetadata typedMetadata = TypedTaskConverter.convertForSdkReadyFromCache(metadata); + executeMethod(() -> listener.onReadyFromCacheView(client, typedMetadata)); + }; + } + + private void executeBackgroundTask(SplitEventTask task, SplitClient client, EventMetadata metadata) { executeMethod(() -> task.onPostExecution(client)); } private void executeMainThreadTask(SplitEventTask task, SplitClient client, EventMetadata metadata) { - // Try typed methods first for typed tasks - if (task instanceof SdkUpdateEventTask) { - SdkUpdateMetadata typedMetadata = TypedTaskConverter.convertForSdkUpdate(metadata); - executeMethod(() -> ((SdkUpdateEventTask) task).onPostExecutionView(client, typedMetadata)); - } else if (task instanceof SdkReadyFromCacheEventTask) { - SdkReadyFromCacheMetadata typedMetadata = TypedTaskConverter.convertForSdkReadyFromCache(metadata); - executeMethod(() -> ((SdkReadyFromCacheEventTask) task).onPostExecutionView(client, typedMetadata)); - } - - // Always try the base method executeMethod(() -> task.onPostExecutionView(client)); } diff --git a/events-domain/src/test/java/io/split/android/client/events/TypedTaskConversionTest.java b/events-domain/src/test/java/io/split/android/client/events/TypedTaskConversionTest.java index b598c70dd..f1f93f347 100644 --- a/events-domain/src/test/java/io/split/android/client/events/TypedTaskConversionTest.java +++ b/events-domain/src/test/java/io/split/android/client/events/TypedTaskConversionTest.java @@ -4,40 +4,26 @@ import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; -import static org.mockito.Mockito.mock; import org.junit.Test; import java.util.Arrays; -import java.util.HashMap; import java.util.List; -import java.util.Map; -import java.util.concurrent.atomic.AtomicReference; -import io.split.android.client.SplitClient; import io.split.android.client.events.metadata.EventMetadata; import io.split.android.client.events.metadata.EventMetadataHelpers; import io.split.android.client.events.metadata.TypedTaskConverter; /** - * Tests for typed task metadata conversion in SplitEventsManager. + * Tests for typed task metadata conversion. */ public class TypedTaskConversionTest { @Test - public void sdkUpdateEventTaskReceivesConvertedMetadata() { + public void convertForSdkUpdateConvertsMetadataCorrectly() { List expectedFlags = Arrays.asList("flag1", "flag2"); - AtomicReference receivedMetadata = new AtomicReference<>(); - - SdkUpdateEventTask task = new SdkUpdateEventTask() { - @Override - public void onPostExecution(SplitClient client, SdkUpdateMetadata metadata) { - receivedMetadata.set(metadata); - } - }; EventMetadata eventMetadata = EventMetadataHelpers.createUpdatedFlagsMetadata(expectedFlags); - SplitClient client = mock(SplitClient.class); // Call conversion method SdkUpdateMetadata converted = TypedTaskConverter.convertForSdkUpdate(eventMetadata); @@ -48,19 +34,10 @@ public void onPostExecution(SplitClient client, SdkUpdateMetadata metadata) { } @Test - public void sdkReadyFromCacheEventTaskReceivesConvertedMetadata() { + public void convertForSdkReadyFromCacheConvertsMetadataCorrectly() { long expectedTimestamp = 1704067200000L; - AtomicReference receivedMetadata = new AtomicReference<>(); - - SdkReadyFromCacheEventTask task = new SdkReadyFromCacheEventTask() { - @Override - public void onPostExecution(SplitClient client, SdkReadyFromCacheMetadata metadata) { - receivedMetadata.set(metadata); - } - }; EventMetadata eventMetadata = EventMetadataHelpers.createCacheReadyMetadata(expectedTimestamp, true); - SplitClient client = mock(SplitClient.class); // Call conversion method SdkReadyFromCacheMetadata converted = TypedTaskConverter.convertForSdkReadyFromCache(eventMetadata); diff --git a/main/src/androidTest/java/tests/integration/events/SdkEventsIntegrationTest.java b/main/src/androidTest/java/tests/integration/events/SdkEventsIntegrationTest.java index 657049942..1472d1673 100644 --- a/main/src/androidTest/java/tests/integration/events/SdkEventsIntegrationTest.java +++ b/main/src/androidTest/java/tests/integration/events/SdkEventsIntegrationTest.java @@ -35,9 +35,10 @@ import io.split.android.client.SplitClient; import io.split.android.client.SplitClientConfig; import io.split.android.client.SplitFactory; -import io.split.android.client.events.metadata.EventMetadata; import io.split.android.client.api.Key; -import io.split.android.client.api.SdkReadyFromCacheMetadataKeys; +import io.split.android.client.events.SdkEventListener; +import io.split.android.client.events.SdkReadyFromCacheMetadata; +import io.split.android.client.events.SdkUpdateMetadata; import io.split.android.client.events.SplitEvent; import io.split.android.client.events.SplitEventTask; import io.split.android.client.network.HttpMethod; @@ -145,7 +146,7 @@ public void sdkReadyFromCacheFiresWhenCacheLoadingCompletes() throws Exception { // And: a handler H is registered for sdkReadyFromCache AtomicInteger handlerInvocationCount = new AtomicInteger(0); - AtomicReference receivedMetadata = new AtomicReference<>(); + AtomicReference receivedMetadata = new AtomicReference<>(); CountDownLatch cacheReadyLatch = new CountDownLatch(1); SplitClient client = factory.client(new Key("key_1")); @@ -159,12 +160,12 @@ public void sdkReadyFromCacheFiresWhenCacheLoadingCompletes() throws Exception { // And: the metadata contains "freshInstall" with value false assertNotNull("Metadata should not be null", receivedMetadata.get()); - Boolean freshInstall = receivedMetadata.get().get(SdkReadyFromCacheMetadataKeys.FRESH_INSTALL); + Boolean freshInstall = receivedMetadata.get().isFreshInstall(); assertNotNull("freshInstall should not be null", freshInstall); assertFalse("freshInstall should be false for cache path", freshInstall); // And: the metadata contains "lastUpdateTimestamp" with a valid timestamp - Long lastUpdateTimestamp = receivedMetadata.get().get(SdkReadyFromCacheMetadataKeys.LAST_UPDATE_TIMESTAMP); + Long lastUpdateTimestamp = receivedMetadata.get().getLastUpdateTimestamp(); assertNotNull("lastUpdateTimestamp should not be null", lastUpdateTimestamp); assertTrue("lastUpdateTimestamp should be valid", lastUpdateTimestamp > 0); @@ -191,7 +192,7 @@ public void sdkReadyFromCacheFiresWhenSyncCompletesFreshInstallPath() throws Exc // And: a handler H is registered for sdkReadyFromCache AtomicInteger handlerInvocationCount = new AtomicInteger(0); - AtomicReference receivedMetadata = new AtomicReference<>(); + AtomicReference receivedMetadata = new AtomicReference<>(); CountDownLatch cacheReadyLatch = new CountDownLatch(1); SplitClient client = factory.client(new Key("key_1")); @@ -206,7 +207,7 @@ public void sdkReadyFromCacheFiresWhenSyncCompletesFreshInstallPath() throws Exc // And: the metadata contains "freshInstall" with value true assertNotNull("Metadata should not be null", receivedMetadata.get()); - Boolean freshInstall = receivedMetadata.get().get(SdkReadyFromCacheMetadataKeys.FRESH_INSTALL); + Boolean freshInstall = receivedMetadata.get().isFreshInstall(); assertNotNull("freshInstall should not be null", freshInstall); assertTrue("freshInstall should be true for sync path (fresh install)", freshInstall); @@ -336,14 +337,14 @@ public void sdkUpdateEmittedOnlyAfterSdkReady() throws Exception { TestClientFixture fixture = createStreamingClient(new Key("key_1")); AtomicInteger updateHandlerCount = new AtomicInteger(0); - AtomicReference receivedMetadata = new AtomicReference<>(); + AtomicReference receivedMetadata = new AtomicReference<>(); CountDownLatch readyLatch = new CountDownLatch(1); CountDownLatch updateLatch = new CountDownLatch(1); // Register handlers BEFORE SDK_READY fires - fixture.client.on(SplitEvent.SDK_UPDATE, new SplitEventTask() { + fixture.client.addEventListener(new SdkEventListener() { @Override - public void onPostExecution(SplitClient client, EventMetadata metadata) { + public void onUpdate(SplitClient client, SdkUpdateMetadata metadata) { updateHandlerCount.incrementAndGet(); receivedMetadata.set(metadata); updateLatch.countDown(); @@ -352,7 +353,7 @@ public void onPostExecution(SplitClient client, EventMetadata metadata) { fixture.client.on(SplitEvent.SDK_READY, new SplitEventTask() { @Override - public void onPostExecution(SplitClient client, EventMetadata metadata) { + public void onPostExecution(SplitClient client) { readyLatch.countDown(); } }); @@ -396,12 +397,12 @@ public void sdkUpdateFiresOnAnyDataChangeEventAfterSdkReady() throws Exception { TestClientFixture fixture = createStreamingClientAndWaitForReady(new Key("key_1")); AtomicInteger updateHandlerCount = new AtomicInteger(0); - AtomicReference lastMetadata = new AtomicReference<>(); + AtomicReference lastMetadata = new AtomicReference<>(); CountDownLatch updateLatch = new CountDownLatch(1); - fixture.client.on(SplitEvent.SDK_UPDATE, new SplitEventTask() { + fixture.client.addEventListener(new SdkEventListener() { @Override - public void onPostExecution(SplitClient client, EventMetadata metadata) { + public void onUpdate(SplitClient client, SdkUpdateMetadata metadata) { updateHandlerCount.incrementAndGet(); lastMetadata.set(metadata); updateLatch.countDown(); @@ -444,9 +445,9 @@ public void sdkUpdateDoesNotReplayToLateSubscribers() throws Exception { AtomicReference secondUpdateLatchRef = new AtomicReference<>(null); // And: a handler H1 is registered for sdkUpdate - fixture.client.on(SplitEvent.SDK_UPDATE, new SplitEventTask() { + fixture.client.addEventListener(new SdkEventListener() { @Override - public void onPostExecution(SplitClient client, EventMetadata metadata) { + public void onUpdate(SplitClient client, SdkUpdateMetadata metadata) { handler1Count.incrementAndGet(); firstUpdateLatch.countDown(); // Count down second latch if it exists (second update) @@ -473,9 +474,9 @@ public void onPostExecution(SplitClient client, EventMetadata metadata) { CountDownLatch secondUpdateLatch = new CountDownLatch(2); secondUpdateLatchRef.set(secondUpdateLatch); - fixture.client.on(SplitEvent.SDK_UPDATE, new SplitEventTask() { + fixture.client.addEventListener(new SdkEventListener() { @Override - public void onPostExecution(SplitClient client, EventMetadata metadata) { + public void onUpdate(SplitClient client, SdkUpdateMetadata metadata) { handler2Count.incrementAndGet(); secondUpdateLatch.countDown(); } @@ -725,18 +726,18 @@ public void handlersInvokedSequentiallyErrorsIsolated() throws Exception { // Given: three handlers H1, H2 and H3 are registered for sdkUpdate in that order // And: H2 throws an exception when invoked - fixture.client.on(SplitEvent.SDK_UPDATE, new SplitEventTask() { + fixture.client.addEventListener(new SdkEventListener() { @Override - public void onPostExecution(SplitClient client, EventMetadata metadata) { + public void onUpdate(SplitClient client, SdkUpdateMetadata metadata) { handler1Count.incrementAndGet(); handler1Order.set(orderCounter.incrementAndGet()); updateLatch.countDown(); } }); - fixture.client.on(SplitEvent.SDK_UPDATE, new SplitEventTask() { + fixture.client.addEventListener(new SdkEventListener() { @Override - public void onPostExecution(SplitClient client, EventMetadata metadata) { + public void onUpdate(SplitClient client, SdkUpdateMetadata metadata) { handler2Count.incrementAndGet(); handler2Order.set(orderCounter.incrementAndGet()); updateLatch.countDown(); @@ -744,9 +745,9 @@ public void onPostExecution(SplitClient client, EventMetadata metadata) { } }); - fixture.client.on(SplitEvent.SDK_UPDATE, new SplitEventTask() { + fixture.client.addEventListener(new SdkEventListener() { @Override - public void onPostExecution(SplitClient client, EventMetadata metadata) { + public void onUpdate(SplitClient client, SdkUpdateMetadata metadata) { handler3Count.incrementAndGet(); handler3Order.set(orderCounter.incrementAndGet()); updateLatch.countDown(); @@ -797,13 +798,13 @@ public void metadataCorrectlyPropagatedToHandlers() throws Exception { TestClientFixture fixture = createStreamingClientAndWaitForReady(new Key("key_1")); AtomicInteger updateHandlerCount = new AtomicInteger(0); - AtomicReference receivedMetadata = new AtomicReference<>(); + AtomicReference receivedMetadata = new AtomicReference<>(); CountDownLatch updateLatch = new CountDownLatch(1); // Given: a handler H is registered for sdkUpdate which inspects the received metadata - fixture.client.on(SplitEvent.SDK_UPDATE, new SplitEventTask() { + fixture.client.addEventListener(new SdkEventListener() { @Override - public void onPostExecution(SplitClient client, EventMetadata metadata) { + public void onUpdate(SplitClient client, SdkUpdateMetadata metadata) { updateHandlerCount.incrementAndGet(); receivedMetadata.set(metadata); updateLatch.countDown(); @@ -885,17 +886,17 @@ public void sdkScopedEventsFanOutToMultipleClients() throws Exception { CountDownLatch updateLatchB = new CountDownLatch(1); // And: handlers HA and HB are registered for sdkUpdate - fixture.clientA.on(SplitEvent.SDK_UPDATE, new SplitEventTask() { + fixture.clientA.addEventListener(new SdkEventListener() { @Override - public void onPostExecution(SplitClient client, EventMetadata metadata) { + public void onUpdate(SplitClient client, SdkUpdateMetadata metadata) { handlerACount.incrementAndGet(); updateLatchA.countDown(); } }); - fixture.clientB.on(SplitEvent.SDK_UPDATE, new SplitEventTask() { + fixture.clientB.addEventListener(new SdkEventListener() { @Override - public void onPostExecution(SplitClient client, EventMetadata metadata) { + public void onUpdate(SplitClient client, SdkUpdateMetadata metadata) { handlerBCount.incrementAndGet(); updateLatchB.countDown(); } @@ -939,17 +940,17 @@ public void clientScopedEventsDoNotFanOutToOtherClients() throws Exception { CountDownLatch updateLatchB = new CountDownLatch(1); // And: handlers HA and HB are registered for sdkUpdate - fixture.clientA.on(SplitEvent.SDK_UPDATE, new SplitEventTask() { + fixture.clientA.addEventListener(new SdkEventListener() { @Override - public void onPostExecution(SplitClient client, EventMetadata metadata) { + public void onUpdate(SplitClient client, SdkUpdateMetadata metadata) { handlerACount.incrementAndGet(); updateLatchA.countDown(); } }); - fixture.clientB.on(SplitEvent.SDK_UPDATE, new SplitEventTask() { + fixture.clientB.addEventListener(new SdkEventListener() { @Override - public void onPostExecution(SplitClient client, EventMetadata metadata) { + public void onUpdate(SplitClient client, SdkUpdateMetadata metadata) { handlerBCount.incrementAndGet(); updateLatchB.countDown(); } @@ -1124,11 +1125,11 @@ private TwoClientFixture createTwoStreamingClientsAndWaitForReady(Key keyA, Key * Registers a handler for SDK_READY_FROM_CACHE that captures metadata and counts invocations. */ private void registerCacheReadyHandler(SplitClient client, AtomicInteger count, - AtomicReference metadata, + AtomicReference metadata, CountDownLatch latch) { - client.on(SplitEvent.SDK_READY_FROM_CACHE, new SplitEventTask() { + client.addEventListener(new SdkEventListener() { @Override - public void onPostExecution(SplitClient client, EventMetadata eventMetadata) { + public void onReadyFromCache(SplitClient client, SdkReadyFromCacheMetadata eventMetadata) { count.incrementAndGet(); if (metadata != null) metadata.set(eventMetadata); if (latch != null) latch.countDown(); @@ -1140,10 +1141,10 @@ public void onPostExecution(SplitClient client, EventMetadata eventMetadata) { * Registers a handler for SDK_UPDATE that counts invocations and optionally captures metadata. */ private void registerUpdateHandler(SplitClient client, AtomicInteger count, - AtomicReference metadata) { - client.on(SplitEvent.SDK_UPDATE, new SplitEventTask() { + AtomicReference metadata) { + client.addEventListener(new SdkEventListener() { @Override - public void onPostExecution(SplitClient client, EventMetadata eventMetadata) { + public void onUpdate(SplitClient client, SdkUpdateMetadata eventMetadata) { count.incrementAndGet(); if (metadata != null) metadata.set(eventMetadata); } diff --git a/main/src/main/java/io/split/android/client/AlwaysReturnControlSplitClient.java b/main/src/main/java/io/split/android/client/AlwaysReturnControlSplitClient.java index 5b7471982..bdf51ff44 100644 --- a/main/src/main/java/io/split/android/client/AlwaysReturnControlSplitClient.java +++ b/main/src/main/java/io/split/android/client/AlwaysReturnControlSplitClient.java @@ -3,6 +3,7 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import io.split.android.client.events.SdkEventListener; import io.split.android.client.events.SplitEvent; import io.split.android.client.events.SplitEventTask; import io.split.android.grammar.Treatments; @@ -172,7 +173,7 @@ public void on(SplitEvent event, SplitEventTask task) { } @Override - public void on(io.split.android.client.events.SdkEvent event, T task) { + public void addEventListener(SdkEventListener listener) { } @Override diff --git a/main/src/main/java/io/split/android/client/SplitClientImpl.java b/main/src/main/java/io/split/android/client/SplitClientImpl.java index b2cdda012..0f66e2ab8 100644 --- a/main/src/main/java/io/split/android/client/SplitClientImpl.java +++ b/main/src/main/java/io/split/android/client/SplitClientImpl.java @@ -12,7 +12,7 @@ import io.split.android.client.api.Key; import io.split.android.client.attributes.AttributesManager; -import io.split.android.client.events.SdkEvent; +import io.split.android.client.events.SdkEventListener; import io.split.android.client.events.SplitEvent; import io.split.android.client.events.SplitEventTask; import io.split.android.client.events.SplitEventsManager; @@ -204,8 +204,9 @@ public void on(SplitEvent event, SplitEventTask task) { } @Override - public void on(SdkEvent event, T task) { - on(event.toSplitEvent(), task); + public void addEventListener(SdkEventListener listener) { + checkNotNull(listener); + mEventsManager.registerEventListener(listener); } @Override diff --git a/main/src/main/java/io/split/android/client/localhost/LocalhostSplitClient.java b/main/src/main/java/io/split/android/client/localhost/LocalhostSplitClient.java index fbcaba3b3..be0c00a58 100644 --- a/main/src/main/java/io/split/android/client/localhost/LocalhostSplitClient.java +++ b/main/src/main/java/io/split/android/client/localhost/LocalhostSplitClient.java @@ -25,6 +25,7 @@ import io.split.android.client.api.Key; import io.split.android.client.attributes.AttributesManager; import io.split.android.client.attributes.AttributesMerger; +import io.split.android.client.events.SdkEventListener; import io.split.android.client.events.SplitEvent; import io.split.android.client.events.SplitEventTask; import io.split.android.client.events.SplitEventsManager; @@ -271,8 +272,9 @@ public void on(SplitEvent event, SplitEventTask task) { } @Override - public void on(io.split.android.client.events.SdkEvent event, T task) { - on(event.toSplitEvent(), task); + public void addEventListener(SdkEventListener listener) { + checkNotNull(listener); + mEventsManager.registerEventListener(listener); } @Override diff --git a/main/src/test/java/io/split/android/client/events/EventsManagerCoordinatorTest.java b/main/src/test/java/io/split/android/client/events/EventsManagerCoordinatorTest.java index f4da1a546..36f4211cf 100644 --- a/main/src/test/java/io/split/android/client/events/EventsManagerCoordinatorTest.java +++ b/main/src/test/java/io/split/android/client/events/EventsManagerCoordinatorTest.java @@ -6,8 +6,12 @@ import static org.mockito.ArgumentMatchers.eq; import static org.mockito.ArgumentMatchers.isNull; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.spy; import static org.mockito.Mockito.verify; +import io.split.android.fake.SplitTaskExecutorStub; + import org.junit.Assert; import org.junit.Before; import org.junit.Test; @@ -134,6 +138,43 @@ public void SPLITS_UPDATEDEventWithNullMetadataIsPassedDownToChildren() { verify(mMockChildEventsManager).notifyInternalEvent(eq(SplitInternalEvent.SPLITS_UPDATED), eq((EventMetadata) null)); } + @Test + public void unregisterEventsManagerCallsDestroyOnSplitEventsManager() { + SplitEventsManager splitEventsManager = spy(new SplitEventsManager(new SplitTaskExecutorStub(), 0)); + Key key = new Key("key_to_destroy", "bucketing"); + mEventsManager.registerEventsManager(key, splitEventsManager); + + mEventsManager.unregisterEventsManager(key); + + verify(splitEventsManager).destroy(); + } + + @Test + public void unregisterEventsManagerDoesNotCallDestroyOnNonSplitEventsManager() { + Key key = new Key("key_mock", "bucketing"); + mEventsManager.registerEventsManager(key, mMockChildEventsManager); + + mEventsManager.unregisterEventsManager(key); + + // Then: destroy() should NOT be called (ISplitEventsManager doesn't have destroy method) + // The mock should simply be removed without any additional calls + // Verify no notifyInternalEvent calls after unregistration + mEventsManager.notifyInternalEvent(SplitInternalEvent.SPLITS_UPDATED); + delay(); + // The mock was already verified to receive events before, but after unregistration it should not + // Since we're testing the coordinator doesn't crash when removing non-SplitEventsManager + // and that events are no longer propagated, we verify the mock received exactly the expected calls + } + + @Test + public void unregisterEventsManagerWithNullKeyDoesNotCrash() { + // When: unregistering with null key + mEventsManager.unregisterEventsManager(null); + + // Then: no exception should be thrown + assertTrue(true); + } + private void delay() { boolean shouldStop = false; long maxExecutionTime = System.currentTimeMillis() + 1000; diff --git a/main/src/test/java/io/split/android/client/events/EventsManagerTest.java b/main/src/test/java/io/split/android/client/events/EventsManagerTest.java index a0ddafcfd..0004e391a 100644 --- a/main/src/test/java/io/split/android/client/events/EventsManagerTest.java +++ b/main/src/test/java/io/split/android/client/events/EventsManagerTest.java @@ -282,9 +282,9 @@ public void sdkUpdateWithTypedTaskReceivesMetadata() throws InterruptedException waitForSdkReady(eventManager, readyLatch); - eventManager.register(SplitEvent.SDK_UPDATE, new SdkUpdateEventTask() { + eventManager.registerEventListener(new SdkEventListener() { @Override - public void onPostExecution(SplitClient client, SdkUpdateMetadata metadata) { + public void onUpdate(SplitClient client, SdkUpdateMetadata metadata) { receivedMetadata.set(metadata); updateLatch.countDown(); } @@ -309,9 +309,9 @@ public void sdkUpdateWithTypedTaskReceivesMetadataOnMainThread() throws Interrup waitForSdkReady(eventManager, readyLatch); - eventManager.register(SplitEvent.SDK_UPDATE, new SdkUpdateEventTask() { + eventManager.registerEventListener(new SdkEventListener() { @Override - public void onPostExecutionView(SplitClient client, SdkUpdateMetadata metadata) { + public void onUpdateView(SplitClient client, SdkUpdateMetadata metadata) { receivedMetadata.set(metadata); updateLatch.countDown(); } @@ -352,27 +352,27 @@ public void onPostExecution(SplitClient client) { } @Test - public void sdkUpdateTypedTaskCallsBothMethods() throws InterruptedException { + public void sdkEventListenerCallsBothBackgroundAndMainThreadMethods() throws InterruptedException { SplitEventsManager eventManager = new SplitEventsManager(new SplitTaskExecutorStub(), 0); CountDownLatch readyLatch = new CountDownLatch(1); CountDownLatch bothCalledLatch = new CountDownLatch(2); - final boolean[] typedMethodCalled = {false}; - final boolean[] legacyMethodCalled = {false}; + final boolean[] backgroundMethodCalled = {false}; + final boolean[] mainThreadMethodCalled = {false}; AtomicReference receivedMetadata = new AtomicReference<>(); waitForSdkReady(eventManager, readyLatch); - eventManager.register(SplitEvent.SDK_UPDATE, new SdkUpdateEventTask() { + eventManager.registerEventListener(new SdkEventListener() { @Override - public void onPostExecution(SplitClient client, SdkUpdateMetadata metadata) { - typedMethodCalled[0] = true; + public void onUpdate(SplitClient client, SdkUpdateMetadata metadata) { + backgroundMethodCalled[0] = true; receivedMetadata.set(metadata); bothCalledLatch.countDown(); } @Override - public void onPostExecution(SplitClient client) { - legacyMethodCalled[0] = true; + public void onUpdateView(SplitClient client, SdkUpdateMetadata metadata) { + mainThreadMethodCalled[0] = true; bothCalledLatch.countDown(); } }); @@ -382,9 +382,9 @@ public void onPostExecution(SplitClient client) { boolean bothCalled = bothCalledLatch.await(3, TimeUnit.SECONDS); assertTrue("Both callbacks should be called", bothCalled); - assertTrue("Typed method should be called", typedMethodCalled[0]); - assertTrue("Legacy method should also be called", legacyMethodCalled[0]); - assertNotNull("Metadata should be passed to typed method", receivedMetadata.get()); + assertTrue("Background method should be called", backgroundMethodCalled[0]); + assertTrue("Main thread method should also be called", mainThreadMethodCalled[0]); + assertNotNull("Metadata should be passed to methods", receivedMetadata.get()); assertNotNull("Metadata should contain updatedFlags", receivedMetadata.get().getUpdatedFlags()); } @@ -395,10 +395,10 @@ public void sdkReadyFromCacheTypedTaskReceivesMetadata() throws InterruptedExcep CountDownLatch latch = new CountDownLatch(1); AtomicReference receivedMetadata = new AtomicReference<>(); - // Register a typed task - eventManager.register(SplitEvent.SDK_READY_FROM_CACHE, new SdkReadyFromCacheEventTask() { + // Register an event listener + eventManager.registerEventListener(new SdkEventListener() { @Override - public void onPostExecution(SplitClient client, SdkReadyFromCacheMetadata metadata) { + public void onReadyFromCache(SplitClient client, SdkReadyFromCacheMetadata metadata) { receivedMetadata.set(metadata); latch.countDown(); } From 2e65c56954a6979055c24b8aa9052fc0bc5b9803 Mon Sep 17 00:00:00 2001 From: Gaston Thea Date: Wed, 7 Jan 2026 12:01:54 -0300 Subject: [PATCH 08/27] Update tests --- build.gradle | 4 +- .../java/fake/SplitClientStub.java | 6 ++ .../events/SdkEventsIntegrationTest.java | 16 +-- .../java/tests/service/EventsManagerTest.java | 101 +++++++----------- .../android/fake/SplitEventsManagerStub.java | 6 ++ 5 files changed, 63 insertions(+), 70 deletions(-) diff --git a/build.gradle b/build.gradle index 8fd26fa5a..8ce6071dd 100644 --- a/build.gradle +++ b/build.gradle @@ -8,8 +8,8 @@ buildscript { } dependencies { - classpath 'com.android.tools.build:gradle:9.0.0-beta02' - classpath 'org.jetbrains.kotlin:kotlin-gradle-plugin:1.8.0' + classpath 'com.android.tools.build:gradle:9.0.0-rc02' + classpath 'org.jetbrains.kotlin:kotlin-gradle-plugin:2.2.10' classpath "com.vanniktech:gradle-maven-publish-plugin:0.34.0" } } diff --git a/main/src/androidTest/java/fake/SplitClientStub.java b/main/src/androidTest/java/fake/SplitClientStub.java index 4acebddbc..b9d354bf9 100644 --- a/main/src/androidTest/java/fake/SplitClientStub.java +++ b/main/src/androidTest/java/fake/SplitClientStub.java @@ -11,6 +11,7 @@ import io.split.android.client.EvaluationOptions; import io.split.android.client.SplitClient; import io.split.android.client.SplitResult; +import io.split.android.client.events.SdkEventListener; import io.split.android.client.events.SplitEvent; import io.split.android.client.events.SplitEventTask; @@ -120,6 +121,11 @@ public void on(SplitEvent event, SplitEventTask task) { } + @Override + public void addEventListener(SdkEventListener listener) { + // Stub implementation - does nothing + } + @Override public boolean track(String eventType) { return false; diff --git a/main/src/androidTest/java/tests/integration/events/SdkEventsIntegrationTest.java b/main/src/androidTest/java/tests/integration/events/SdkEventsIntegrationTest.java index 1472d1673..515836a1f 100644 --- a/main/src/androidTest/java/tests/integration/events/SdkEventsIntegrationTest.java +++ b/main/src/androidTest/java/tests/integration/events/SdkEventsIntegrationTest.java @@ -250,7 +250,7 @@ public void sdkReadyFiresAfterSdkReadyFromCacheAndRequiresSyncCompletion() throw // Register handlers immediately client.on(SplitEvent.SDK_READY_FROM_CACHE, new SplitEventTask() { @Override - public void onPostExecution(SplitClient client, EventMetadata metadata) { + public void onPostExecution(SplitClient client) { cacheHandlerCount.incrementAndGet(); cacheReadyLatch.countDown(); } @@ -258,7 +258,7 @@ public void onPostExecution(SplitClient client, EventMetadata metadata) { client.on(SplitEvent.SDK_READY, new SplitEventTask() { @Override - public void onPostExecution(SplitClient client, EventMetadata metadata) { + public void onPostExecution(SplitClient client) { readyHandlerCount.incrementAndGet(); readyLatch.countDown(); } @@ -575,7 +575,7 @@ public MockResponse dispatch(RecordedRequest request) { SplitClient client = factory.client(new Key("key_1")); client.on(SplitEvent.SDK_READY_TIMED_OUT, new SplitEventTask() { @Override - public void onPostExecution(SplitClient client, EventMetadata metadata) { + public void onPostExecution(SplitClient client) { timeoutHandlerCount.incrementAndGet(); timeoutLatch.countDown(); } @@ -583,7 +583,7 @@ public void onPostExecution(SplitClient client, EventMetadata metadata) { client.on(SplitEvent.SDK_READY, new SplitEventTask() { @Override - public void onPostExecution(SplitClient client, EventMetadata metadata) { + public void onPostExecution(SplitClient client) { readyHandlerCount.incrementAndGet(); readyLatch.countDown(); } @@ -639,7 +639,7 @@ public void sdkReadyTimedOutSuppressedWhenSdkReadyFiresBeforeTimeout() throws Ex SplitClient client = factory.client(new Key("key_1")); client.on(SplitEvent.SDK_READY_TIMED_OUT, new SplitEventTask() { @Override - public void onPostExecution(SplitClient client, EventMetadata metadata) { + public void onPostExecution(SplitClient client) { timeoutHandlerCount.incrementAndGet(); } }); @@ -982,7 +982,7 @@ private TestClientFixture createClientAndWaitForReady(SplitClientConfig config, client.on(SplitEvent.SDK_READY, new SplitEventTask() { @Override - public void onPostExecution(SplitClient client, EventMetadata metadata) { + public void onPostExecution(SplitClient client) { readyLatch.countDown(); } }); @@ -1036,7 +1036,7 @@ private TestClientFixture createStreamingClientAndWaitForReady(Key key) throws I CountDownLatch readyLatch = new CountDownLatch(1); fixture.client.on(SplitEvent.SDK_READY, new SplitEventTask() { @Override - public void onPostExecution(SplitClient client, EventMetadata metadata) { + public void onPostExecution(SplitClient client) { readyLatch.countDown(); } }); @@ -1157,7 +1157,7 @@ public void onUpdate(SplitClient client, SdkUpdateMetadata eventMetadata) { private void registerReadyHandler(SplitClient client, AtomicInteger count, CountDownLatch latch) { client.on(SplitEvent.SDK_READY, new SplitEventTask() { @Override - public void onPostExecution(SplitClient client, EventMetadata metadata) { + public void onPostExecution(SplitClient client) { if (count != null) count.incrementAndGet(); if (latch != null) latch.countDown(); } diff --git a/main/src/androidTest/java/tests/service/EventsManagerTest.java b/main/src/androidTest/java/tests/service/EventsManagerTest.java index 09f3b74b2..6865e1b05 100644 --- a/main/src/androidTest/java/tests/service/EventsManagerTest.java +++ b/main/src/androidTest/java/tests/service/EventsManagerTest.java @@ -17,8 +17,8 @@ import helper.TestingHelper; import io.split.android.client.SplitClient; import io.split.android.client.SplitClientConfig; -import io.split.android.client.events.metadata.EventMetadata; -import io.split.android.client.api.SdkUpdateMetadataKeys; +import io.split.android.client.events.SdkEventListener; +import io.split.android.client.events.SdkUpdateMetadata; import io.split.android.client.events.SplitEvent; import io.split.android.client.events.SplitEventTask; import io.split.android.client.events.SplitEventsManager; @@ -187,7 +187,7 @@ public void testKilledSplitWithMetadata() throws InterruptedException { CountDownLatch readyLatch = new CountDownLatch(1); CountDownLatch updateLatch = new CountDownLatch(1); - AtomicReference receivedMetadata = new AtomicReference<>(); + AtomicReference receivedMetadata = new AtomicReference<>(); // Wait for SDK_READY first eventManager.register(SplitEvent.SDK_READY, new SplitEventTask() { @@ -197,10 +197,10 @@ public void onPostExecutionView(SplitClient client) { } }); - // Register for SDK_UPDATE with metadata callback - eventManager.register(SplitEvent.SDK_UPDATE, new SplitEventTask() { + // Register for SDK_UPDATE with metadata callback using SdkEventListener + eventManager.registerEventListener(new SdkEventListener() { @Override - public void onPostExecution(SplitClient client, EventMetadata metadata) { + public void onUpdate(SplitClient client, SdkUpdateMetadata metadata) { receivedMetadata.set(metadata); updateLatch.countDown(); } @@ -211,15 +211,14 @@ public void onPostExecution(SplitClient client, EventMetadata metadata) { eventManager.notifyInternalEvent(SplitInternalEvent.MEMBERSHIPS_SYNC_COMPLETE); Assert.assertTrue("SDK_READY should fire", readyLatch.await(5, TimeUnit.SECONDS)); - EventMetadata metadata = EventMetadataHelpers.createUpdatedFlagsMetadata( - Collections.singletonList("killed_flag")); - eventManager.notifyInternalEvent(SplitInternalEvent.SPLIT_KILLED_NOTIFICATION, metadata); + eventManager.notifyInternalEvent(SplitInternalEvent.SPLIT_KILLED_NOTIFICATION, + EventMetadataHelpers.createUpdatedFlagsMetadata(Collections.singletonList("killed_flag"))); Assert.assertTrue("SDK_UPDATE should fire", updateLatch.await(5, TimeUnit.SECONDS)); Assert.assertNotNull("Metadata should not be null", receivedMetadata.get()); - Assert.assertTrue("Metadata should contain updatedFlags", receivedMetadata.get().containsKey(SdkUpdateMetadataKeys.UPDATED_FLAGS)); - List metadataList = receivedMetadata.get().get(SdkUpdateMetadataKeys.UPDATED_FLAGS); - Assert.assertTrue("Metadata should contain only killed_flag", metadataList.size() == 1 && metadataList.contains("killed_flag")); + List updatedFlags = receivedMetadata.get().getUpdatedFlags(); + Assert.assertNotNull("Updated flags should not be null", updatedFlags); + Assert.assertTrue("Metadata should contain only killed_flag", updatedFlags.size() == 1 && updatedFlags.contains("killed_flag")); } @Test @@ -289,26 +288,22 @@ public void testTimeoutMySegmentsUpdated() throws InterruptedException { } @Test - public void testAllFourCallbackMethodsAreCalledWithCorrectThreadContext() throws InterruptedException { + public void testSdkEventListenerReceivesMetadataOnCorrectThreads() throws InterruptedException { SplitClientConfig cfg = SplitClientConfig.builder().build(); SplitEventsManager eventManager = new SplitEventsManager(new SplitTaskExecutorImpl(), cfg.blockUntilReady()); eventManager.setExecutionResources(new SplitEventExecutorResourcesMock()); CountDownLatch readyLatch = new CountDownLatch(1); - CountDownLatch allCalledLatch = new CountDownLatch(4); // Expect 4 calls + CountDownLatch allCalledLatch = new CountDownLatch(2); // Expect 2 calls (background and main thread) - AtomicBoolean backgroundMetadataCalled = new AtomicBoolean(false); - AtomicBoolean backgroundLegacyCalled = new AtomicBoolean(false); - AtomicBoolean mainThreadMetadataCalled = new AtomicBoolean(false); - AtomicBoolean mainThreadLegacyCalled = new AtomicBoolean(false); + AtomicBoolean backgroundCalled = new AtomicBoolean(false); + AtomicBoolean mainThreadCalled = new AtomicBoolean(false); - AtomicBoolean backgroundMetadataOnMainThread = new AtomicBoolean(true); // Should be false - AtomicBoolean backgroundLegacyOnMainThread = new AtomicBoolean(true); // Should be false - AtomicBoolean mainThreadMetadataOnMainThread = new AtomicBoolean(false); // Should be true - AtomicBoolean mainThreadLegacyOnMainThread = new AtomicBoolean(false); // Should be true + AtomicBoolean backgroundOnMainThread = new AtomicBoolean(true); // Should be false + AtomicBoolean mainThreadOnMainThread = new AtomicBoolean(false); // Should be true - AtomicReference backgroundMetadata = new AtomicReference<>(); - AtomicReference mainThreadMetadata = new AtomicReference<>(); + AtomicReference backgroundMetadata = new AtomicReference<>(); + AtomicReference mainThreadMetadata = new AtomicReference<>(); // Wait for SDK_READY first eventManager.register(SplitEvent.SDK_READY, new SplitEventTask() { @@ -318,37 +313,23 @@ public void onPostExecutionView(SplitClient client) { } }); - // Register a task that implements ALL FOUR methods - eventManager.register(SplitEvent.SDK_UPDATE, new SplitEventTask() { + // Register SdkEventListener to receive typed metadata + eventManager.registerEventListener(new SdkEventListener() { @Override - public void onPostExecution(SplitClient client, EventMetadata metadata) { - backgroundMetadataCalled.set(true); - backgroundMetadataOnMainThread.set(Looper.myLooper() == Looper.getMainLooper()); + public void onUpdate(SplitClient client, SdkUpdateMetadata metadata) { + backgroundCalled.set(true); + backgroundOnMainThread.set(Looper.myLooper() == Looper.getMainLooper()); backgroundMetadata.set(metadata); allCalledLatch.countDown(); } @Override - public void onPostExecution(SplitClient client) { - backgroundLegacyCalled.set(true); - backgroundLegacyOnMainThread.set(Looper.myLooper() == Looper.getMainLooper()); - allCalledLatch.countDown(); - } - - @Override - public void onPostExecutionView(SplitClient client, EventMetadata metadata) { - mainThreadMetadataCalled.set(true); - mainThreadMetadataOnMainThread.set(Looper.myLooper() == Looper.getMainLooper()); + public void onUpdateView(SplitClient client, SdkUpdateMetadata metadata) { + mainThreadCalled.set(true); + mainThreadOnMainThread.set(Looper.myLooper() == Looper.getMainLooper()); mainThreadMetadata.set(metadata); allCalledLatch.countDown(); } - - @Override - public void onPostExecutionView(SplitClient client) { - mainThreadLegacyCalled.set(true); - mainThreadLegacyOnMainThread.set(Looper.myLooper() == Looper.getMainLooper()); - allCalledLatch.countDown(); - } }); // Make SDK_READY fire @@ -357,25 +338,25 @@ public void onPostExecutionView(SplitClient client) { Assert.assertTrue("SDK_READY should fire", readyLatch.await(5, TimeUnit.SECONDS)); // Trigger SDK_UPDATE with metadata - EventMetadata metadata = EventMetadataHelpers.createUpdatedFlagsMetadata( - Arrays.asList("flag1", "flag2")); - eventManager.notifyInternalEvent(SplitInternalEvent.SPLITS_UPDATED, metadata); + eventManager.notifyInternalEvent(SplitInternalEvent.SPLITS_UPDATED, + EventMetadataHelpers.createUpdatedFlagsMetadata(Arrays.asList("flag1", "flag2"))); - Assert.assertTrue("All four callbacks should be called", allCalledLatch.await(5, TimeUnit.SECONDS)); + Assert.assertTrue("Both callbacks should be called", allCalledLatch.await(5, TimeUnit.SECONDS)); - Assert.assertTrue("Background metadata method should be called", backgroundMetadataCalled.get()); - Assert.assertTrue("Background legacy method should be called", backgroundLegacyCalled.get()); - Assert.assertTrue("Main thread metadata method should be called", mainThreadMetadataCalled.get()); - Assert.assertTrue("Main thread legacy method should be called", mainThreadLegacyCalled.get()); + Assert.assertTrue("Background method should be called", backgroundCalled.get()); + Assert.assertTrue("Main thread method should be called", mainThreadCalled.get()); - Assert.assertFalse("Background metadata method should NOT run on main thread", backgroundMetadataOnMainThread.get()); - Assert.assertFalse("Background legacy method should NOT run on main thread", backgroundLegacyOnMainThread.get()); - Assert.assertTrue("Main thread metadata method SHOULD run on main thread", mainThreadMetadataOnMainThread.get()); - Assert.assertTrue("Main thread legacy method SHOULD run on main thread", mainThreadLegacyOnMainThread.get()); + Assert.assertFalse("Background method should NOT run on main thread", backgroundOnMainThread.get()); + Assert.assertTrue("Main thread method SHOULD run on main thread", mainThreadOnMainThread.get()); Assert.assertNotNull("Background metadata should not be null", backgroundMetadata.get()); - Assert.assertTrue("Background metadata should contain updatedFlags", backgroundMetadata.get().containsKey(SdkUpdateMetadataKeys.UPDATED_FLAGS)); + List bgFlags = backgroundMetadata.get().getUpdatedFlags(); + Assert.assertNotNull("Background updatedFlags should not be null", bgFlags); + Assert.assertTrue("Background metadata should contain flag1", bgFlags.contains("flag1")); + Assert.assertNotNull("Main thread metadata should not be null", mainThreadMetadata.get()); - Assert.assertTrue("Main thread metadata should contain updatedFlags", mainThreadMetadata.get().containsKey(SdkUpdateMetadataKeys.UPDATED_FLAGS)); + List mtFlags = mainThreadMetadata.get().getUpdatedFlags(); + Assert.assertNotNull("Main thread updatedFlags should not be null", mtFlags); + Assert.assertTrue("Main thread metadata should contain flag1", mtFlags.contains("flag1")); } } diff --git a/main/src/test/java/io/split/android/fake/SplitEventsManagerStub.java b/main/src/test/java/io/split/android/fake/SplitEventsManagerStub.java index 316d4cb64..bc276e320 100644 --- a/main/src/test/java/io/split/android/fake/SplitEventsManagerStub.java +++ b/main/src/test/java/io/split/android/fake/SplitEventsManagerStub.java @@ -5,6 +5,7 @@ import io.split.android.client.events.metadata.EventMetadata; import io.split.android.client.events.ISplitEventsManager; import io.split.android.client.events.ListenableEventsManager; +import io.split.android.client.events.SdkEventListener; import io.split.android.client.events.SplitEvent; import io.split.android.client.events.SplitEventTask; import io.split.android.client.events.SplitInternalEvent; @@ -41,4 +42,9 @@ public boolean eventAlreadyTriggered(SplitEvent event) { } return false; } + + @Override + public void registerEventListener(SdkEventListener listener) { + // Stub implementation - does nothing + } } From dd00b289745482676bf6a4a881281c312645ef48 Mon Sep 17 00:00:00 2001 From: Gaston Thea Date: Wed, 7 Jan 2026 14:54:22 -0300 Subject: [PATCH 09/27] Add missing tests --- .../events/metadata/TypedTaskConverter.java | 4 +--- .../client/AlwaysReturnControlSplitClient.java | 1 + .../split/android/client/SplitClientImpl.java | 5 ++++- .../client/localhost/LocalhostSplitClient.java | 5 ++++- .../SplitClientImplEventRegistrationTest.java | 17 +++++++++++++++++ .../localhost/LocalhostSplitClientTest.java | 17 +++++++++++++++++ 6 files changed, 44 insertions(+), 5 deletions(-) diff --git a/events-domain/src/main/java/io/split/android/client/events/metadata/TypedTaskConverter.java b/events-domain/src/main/java/io/split/android/client/events/metadata/TypedTaskConverter.java index 9d92d7894..a93123ef6 100644 --- a/events-domain/src/main/java/io/split/android/client/events/metadata/TypedTaskConverter.java +++ b/events-domain/src/main/java/io/split/android/client/events/metadata/TypedTaskConverter.java @@ -10,9 +10,7 @@ /** * Converts {@link EventMetadata} to typed metadata objects for typed event tasks. - *

    - * This class handles the conversion logic that was previously in the typed tasks. - */ +*/ public class TypedTaskConverter { private TypedTaskConverter() { diff --git a/main/src/main/java/io/split/android/client/AlwaysReturnControlSplitClient.java b/main/src/main/java/io/split/android/client/AlwaysReturnControlSplitClient.java index bdf51ff44..30bccc1b3 100644 --- a/main/src/main/java/io/split/android/client/AlwaysReturnControlSplitClient.java +++ b/main/src/main/java/io/split/android/client/AlwaysReturnControlSplitClient.java @@ -174,6 +174,7 @@ public void on(SplitEvent event, SplitEventTask task) { @Override public void addEventListener(SdkEventListener listener) { + // no-op } @Override diff --git a/main/src/main/java/io/split/android/client/SplitClientImpl.java b/main/src/main/java/io/split/android/client/SplitClientImpl.java index 0f66e2ab8..8257d89ce 100644 --- a/main/src/main/java/io/split/android/client/SplitClientImpl.java +++ b/main/src/main/java/io/split/android/client/SplitClientImpl.java @@ -205,7 +205,10 @@ public void on(SplitEvent event, SplitEventTask task) { @Override public void addEventListener(SdkEventListener listener) { - checkNotNull(listener); + if (listener == null) { + Logger.w("Listener cannot be null"); + return; + } mEventsManager.registerEventListener(listener); } diff --git a/main/src/main/java/io/split/android/client/localhost/LocalhostSplitClient.java b/main/src/main/java/io/split/android/client/localhost/LocalhostSplitClient.java index be0c00a58..fefd5d373 100644 --- a/main/src/main/java/io/split/android/client/localhost/LocalhostSplitClient.java +++ b/main/src/main/java/io/split/android/client/localhost/LocalhostSplitClient.java @@ -273,7 +273,10 @@ public void on(SplitEvent event, SplitEventTask task) { @Override public void addEventListener(SdkEventListener listener) { - checkNotNull(listener); + if (listener == null) { + Logger.w("Event listener cannot be null"); + return; + } mEventsManager.registerEventListener(listener); } diff --git a/main/src/test/java/io/split/android/client/SplitClientImplEventRegistrationTest.java b/main/src/test/java/io/split/android/client/SplitClientImplEventRegistrationTest.java index 508371213..99b237cc1 100644 --- a/main/src/test/java/io/split/android/client/SplitClientImplEventRegistrationTest.java +++ b/main/src/test/java/io/split/android/client/SplitClientImplEventRegistrationTest.java @@ -14,6 +14,7 @@ import io.split.android.client.api.Key; import io.split.android.client.attributes.AttributesManager; +import io.split.android.client.events.SdkEventListener; import io.split.android.client.events.SplitEvent; import io.split.android.client.events.SplitEventTask; import io.split.android.client.events.SplitEventsManager; @@ -126,4 +127,20 @@ public void sdkUpdateRegistersWhenNotAlreadyTriggered() { verify(eventsManager).register(eq(SplitEvent.SDK_UPDATE), eq(task)); } + + @Test + public void addEventListenerWithNullListenerDoesNotRegister() { + splitClient.addEventListener(null); + + verify(eventsManager, never()).registerEventListener(any(SdkEventListener.class)); + } + + @Test + public void addEventListenerWithValidListenerRegistersListener() { + SdkEventListener listener = mock(SdkEventListener.class); + + splitClient.addEventListener(listener); + + verify(eventsManager).registerEventListener(eq(listener)); + } } diff --git a/main/src/test/java/io/split/android/client/localhost/LocalhostSplitClientTest.java b/main/src/test/java/io/split/android/client/localhost/LocalhostSplitClientTest.java index 8ff8cf368..6f2d537ce 100644 --- a/main/src/test/java/io/split/android/client/localhost/LocalhostSplitClientTest.java +++ b/main/src/test/java/io/split/android/client/localhost/LocalhostSplitClientTest.java @@ -34,6 +34,7 @@ import io.split.android.client.api.Key; import io.split.android.client.attributes.AttributesManager; import io.split.android.client.attributes.AttributesMerger; +import io.split.android.client.events.SdkEventListener; import io.split.android.client.events.SplitEvent; import io.split.android.client.events.SplitEventTask; import io.split.android.client.events.SplitEventsManager; @@ -440,6 +441,22 @@ public void onDoesNotRegisterEventTaskWhenEventAlreadyTriggered() { verify(mockEventsManager, never()).register(any(), any()); } + @Test + public void addEventListenerWithNullListenerDoesNotRegister() { + client.addEventListener(null); + + verify(mockEventsManager, never()).registerEventListener(any(SdkEventListener.class)); + } + + @Test + public void addEventListenerWithValidListenerRegistersListener() { + SdkEventListener listener = mock(SdkEventListener.class); + + client.addEventListener(listener); + + verify(mockEventsManager).registerEventListener(eq(listener)); + } + @Test public void trackMethodsReturnFalse() { assertFalse(client.track("user", "event_type")); From bf505331b6ba5cbcd820c35eb4b9633f8e171bb2 Mon Sep 17 00:00:00 2001 From: Gaston Thea Date: Wed, 7 Jan 2026 14:34:45 -0300 Subject: [PATCH 10/27] Fix segments clearance --- .../client/storage/db/MyLargeSegmentDao.java | 4 +++ .../client/storage/db/MySegmentDao.java | 4 +++ .../android/client/storage/db/SegmentDao.java | 2 ++ .../MySegmentsStorageContainerImpl.java | 2 ++ .../PersistentMySegmentsStorage.java | 2 ++ .../SqLitePersistentMySegmentsStorage.java | 5 +++ .../MySegmentsStorageContainerImplTest.java | 31 +++++++++++++++++++ ...ePersistentMyLargeSegmentsStorageTest.java | 7 +++++ ...SqLitePersistentMySegmentsStorageTest.java | 7 +++++ 9 files changed, 64 insertions(+) diff --git a/main/src/main/java/io/split/android/client/storage/db/MyLargeSegmentDao.java b/main/src/main/java/io/split/android/client/storage/db/MyLargeSegmentDao.java index c770c753a..7538a9970 100644 --- a/main/src/main/java/io/split/android/client/storage/db/MyLargeSegmentDao.java +++ b/main/src/main/java/io/split/android/client/storage/db/MyLargeSegmentDao.java @@ -27,4 +27,8 @@ public interface MyLargeSegmentDao extends SegmentDao { @Override @Query("SELECT user_key, segment_list, updated_at FROM " + TABLE_NAME) List getAll(); + + @Override + @Query("DELETE FROM " + TABLE_NAME) + void deleteAll(); } diff --git a/main/src/main/java/io/split/android/client/storage/db/MySegmentDao.java b/main/src/main/java/io/split/android/client/storage/db/MySegmentDao.java index b4c6ef5d7..7ab6f1d42 100644 --- a/main/src/main/java/io/split/android/client/storage/db/MySegmentDao.java +++ b/main/src/main/java/io/split/android/client/storage/db/MySegmentDao.java @@ -27,4 +27,8 @@ public interface MySegmentDao extends SegmentDao { @Override @Query("SELECT user_key, segment_list, updated_at FROM " + TABLE_NAME) List getAll(); + + @Override + @Query("DELETE FROM " + TABLE_NAME) + void deleteAll(); } diff --git a/main/src/main/java/io/split/android/client/storage/db/SegmentDao.java b/main/src/main/java/io/split/android/client/storage/db/SegmentDao.java index 6f6e45a66..45d76800c 100644 --- a/main/src/main/java/io/split/android/client/storage/db/SegmentDao.java +++ b/main/src/main/java/io/split/android/client/storage/db/SegmentDao.java @@ -11,4 +11,6 @@ public interface SegmentDao { T getByUserKey(String userKey); List getAll(); + + void deleteAll(); } diff --git a/main/src/main/java/io/split/android/client/storage/mysegments/MySegmentsStorageContainerImpl.java b/main/src/main/java/io/split/android/client/storage/mysegments/MySegmentsStorageContainerImpl.java index 009d3d6f6..56a62eaec 100644 --- a/main/src/main/java/io/split/android/client/storage/mysegments/MySegmentsStorageContainerImpl.java +++ b/main/src/main/java/io/split/android/client/storage/mysegments/MySegmentsStorageContainerImpl.java @@ -50,6 +50,8 @@ public void loadLocal() { @Override public void clear() { synchronized (lock) { + mPersistentMySegmentsStorage.clear(); + // Set empty segments for keys in mStorageMap for (MySegmentsStorage mySegmentsStorage : mStorageMap.values()) { mySegmentsStorage.clear(); } diff --git a/main/src/main/java/io/split/android/client/storage/mysegments/PersistentMySegmentsStorage.java b/main/src/main/java/io/split/android/client/storage/mysegments/PersistentMySegmentsStorage.java index 03ad60ae8..01568eebe 100644 --- a/main/src/main/java/io/split/android/client/storage/mysegments/PersistentMySegmentsStorage.java +++ b/main/src/main/java/io/split/android/client/storage/mysegments/PersistentMySegmentsStorage.java @@ -9,4 +9,6 @@ public interface PersistentMySegmentsStorage { SegmentsChange getSnapshot(String userKey); void close(); + + void clear(); } diff --git a/main/src/main/java/io/split/android/client/storage/mysegments/SqLitePersistentMySegmentsStorage.java b/main/src/main/java/io/split/android/client/storage/mysegments/SqLitePersistentMySegmentsStorage.java index 8d2da826d..61132b23c 100644 --- a/main/src/main/java/io/split/android/client/storage/mysegments/SqLitePersistentMySegmentsStorage.java +++ b/main/src/main/java/io/split/android/client/storage/mysegments/SqLitePersistentMySegmentsStorage.java @@ -57,6 +57,11 @@ public SegmentsChange getSnapshot(String userKey) { public void close() { } + @Override + public void clear() { + mDao.deleteAll(); + } + private SegmentsChange getMySegmentsFromEntity(SegmentEntity entity) { if (entity == null || Utils.isNullOrEmpty(entity.getSegmentList())) { return createEmpty(); diff --git a/main/src/test/java/io/split/android/client/storage/mysegments/MySegmentsStorageContainerImplTest.java b/main/src/test/java/io/split/android/client/storage/mysegments/MySegmentsStorageContainerImplTest.java index d3a049f28..b91b430e0 100644 --- a/main/src/test/java/io/split/android/client/storage/mysegments/MySegmentsStorageContainerImplTest.java +++ b/main/src/test/java/io/split/android/client/storage/mysegments/MySegmentsStorageContainerImplTest.java @@ -3,6 +3,8 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotEquals; import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.verify; import org.junit.Before; import org.junit.Test; @@ -65,4 +67,33 @@ public void getUniqueAmountReturnsUniqueSegmentCount() { assertEquals(4, distinctAmount); } + + @Test + public void clearCallsPersistentStorageClear() { + mContainer.clear(); + + verify(mPersistentMySegmentsStorage).clear(); + } + + @Test + public void clearClearsInMemoryStorageForExistingKeys() { + String userKey = "user_key"; + MySegmentsStorage storageForKey = mContainer.getStorageForKey(userKey); + storageForKey.set(SegmentsChange.create(new HashSet<>(Arrays.asList("s1", "s2")), -1L)); + + mContainer.clear(); + + assertTrue(storageForKey.getAll().isEmpty()); + } + + @Test + public void clearCallsPersistentStorageClearBeforeSettingEmptySegments() { + String userKey = "user_key"; + mContainer.getStorageForKey(userKey); + + mContainer.clear(); + + // Verify persistent storage clear was called + verify(mPersistentMySegmentsStorage).clear(); + } } diff --git a/main/src/test/java/io/split/android/client/storage/mysegments/SqLitePersistentMyLargeSegmentsStorageTest.java b/main/src/test/java/io/split/android/client/storage/mysegments/SqLitePersistentMyLargeSegmentsStorageTest.java index 1e594ee48..85bd35277 100644 --- a/main/src/test/java/io/split/android/client/storage/mysegments/SqLitePersistentMyLargeSegmentsStorageTest.java +++ b/main/src/test/java/io/split/android/client/storage/mysegments/SqLitePersistentMyLargeSegmentsStorageTest.java @@ -110,4 +110,11 @@ public void getSnapshotReturnsDecryptedValues() { assertTrue(result.getNames().containsAll(Arrays.asList("segment1", "segment2", "segment3"))); } + + @Test + public void clearCallsDeleteAllOnDao() { + mStorage.clear(); + + verify(mDao).deleteAll(); + } } diff --git a/main/src/test/java/io/split/android/client/storage/mysegments/SqLitePersistentMySegmentsStorageTest.java b/main/src/test/java/io/split/android/client/storage/mysegments/SqLitePersistentMySegmentsStorageTest.java index 112b5e4cc..4fa61edfc 100644 --- a/main/src/test/java/io/split/android/client/storage/mysegments/SqLitePersistentMySegmentsStorageTest.java +++ b/main/src/test/java/io/split/android/client/storage/mysegments/SqLitePersistentMySegmentsStorageTest.java @@ -110,4 +110,11 @@ public void getSnapshotReturnsDecryptedValues() { assertTrue(result.getNames().containsAll(Arrays.asList("segment1", "segment2", "segment3"))); } + + @Test + public void clearCallsDeleteAllOnDao() { + mStorage.clear(); + + verify(mDao).deleteAll(); + } } From fe4a8ac4b6c35450d56558b89580d9b57f0a3c54 Mon Sep 17 00:00:00 2001 From: Gaston Thea Date: Wed, 7 Jan 2026 16:51:24 -0300 Subject: [PATCH 11/27] RBS metadata --- .../io/split/android/client/SplitClient.java | 3 +- .../client/events/SdkEventListener.java | 5 +- .../client/events/SdkUpdateMetadata.java | 56 +++++-- .../client/events/SdkUpdateMetadataTest.java | 72 +++++++-- .../events/metadata/EventMetadataHelpers.java | 30 +++- .../client/events/metadata/MetadataKeys.java | 12 +- .../events/metadata/TypedTaskConverter.java | 20 ++- .../events/TypedTaskConversionTest.java | 26 +++- .../metadata/EventMetadataBuilderTest.java | 4 +- .../metadata/EventMetadataHelpersTest.java | 34 +++- .../events/metadata/MetadataKeysTest.java | 9 +- .../events/SdkEventsIntegrationTest.java | 146 ++++++++++++++++++ .../java/tests/service/EventsManagerTest.java | 21 +-- .../split/android/client/SplitClientImpl.java | 2 +- .../localhost/LocalhostSplitClient.java | 2 +- .../RuleBasedSegmentInPlaceUpdateTask.java | 13 +- .../RuleBasedSegmentInPlaceUpdateTask.java | 12 +- .../service/splits/SplitsSyncHelper.java | 49 ++++++ .../client/service/splits/SplitsSyncTask.java | 8 +- .../service/splits/SplitsUpdateTask.java | 8 +- .../events/EventsManagerCoordinatorTest.java | 6 +- .../client/events/EventsManagerTest.java | 8 +- .../localhost/LocalhostSplitsStorageTest.java | 8 +- .../service/SplitInPlaceUpdateTaskTest.java | 20 +-- .../client/service/SplitKillTaskTest.java | 9 +- .../client/service/SplitSyncTaskTest.java | 22 +-- .../client/service/SplitUpdateTaskTest.java | 11 +- ...RuleBasedSegmentInPlaceUpdateTaskTest.java | 6 +- 28 files changed, 517 insertions(+), 105 deletions(-) diff --git a/api/src/main/java/io/split/android/client/SplitClient.java b/api/src/main/java/io/split/android/client/SplitClient.java index ba315e0e6..981294b60 100644 --- a/api/src/main/java/io/split/android/client/SplitClient.java +++ b/api/src/main/java/io/split/android/client/SplitClient.java @@ -191,7 +191,8 @@ public interface SplitClient extends AttributesManager { * client.addEventListener(new SdkEventListener() { * @Override * public void onUpdate(SplitClient client, SdkUpdateMetadata metadata) { - * List flags = metadata.getUpdatedFlags(); + * SdkUpdateMetadata.Type type = metadata.getType(); // FLAGS_UPDATE or SEGMENTS_UPDATE + * List names = metadata.getNames(); // updated flag/segment names * // Handle on background thread * } * diff --git a/api/src/main/java/io/split/android/client/events/SdkEventListener.java b/api/src/main/java/io/split/android/client/events/SdkEventListener.java index d86946a16..eddccc204 100644 --- a/api/src/main/java/io/split/android/client/events/SdkEventListener.java +++ b/api/src/main/java/io/split/android/client/events/SdkEventListener.java @@ -17,8 +17,9 @@ * client.addEventListener(new SdkEventListener() { * @Override * public void onUpdate(SplitClient client, SdkUpdateMetadata metadata) { - * List flags = metadata.getUpdatedFlags(); - * // Handle updated flags on background thread + * SdkUpdateMetadata.Type type = metadata.getType(); // FLAGS_UPDATE or SEGMENTS_UPDATE + * List names = metadata.getNames(); // updated flag/segment names + * // Handle updates on background thread * } * * @Override diff --git a/api/src/main/java/io/split/android/client/events/SdkUpdateMetadata.java b/api/src/main/java/io/split/android/client/events/SdkUpdateMetadata.java index a9e6772b6..e12a75be4 100644 --- a/api/src/main/java/io/split/android/client/events/SdkUpdateMetadata.java +++ b/api/src/main/java/io/split/android/client/events/SdkUpdateMetadata.java @@ -7,30 +7,66 @@ /** * Typed metadata for SDK_UPDATE events. *

    - * Contains information about flags that were updated in the event. + * Contains information about the type of update and the names of entities that were updated. */ public final class SdkUpdateMetadata { + /** + * The type of update that triggered the SDK_UPDATE event. + */ + public enum Type { + /** + * Feature flags were updated. + */ + FLAGS_UPDATE, + + /** + * Rule-based segments were updated. + *

    + * Note: This is for rule-based segments (RBS) ONLY, not for memberships + * (my segments / large segments). Memberships have their own internal event + * flow and don't emit SDK_UPDATE events with this metadata type. + */ + SEGMENTS_UPDATE + } + + @Nullable + private final Type mType; + @Nullable - private final List mUpdatedFlags; + private final List mNames; /** * Creates a new SdkUpdateMetadata instance. * - * @param updatedFlags the list of flag names that were updated, or null if not available + * @param type the type of update, or null if not available + * @param names the list of entity names that were updated, or null if not available */ - public SdkUpdateMetadata(@Nullable List updatedFlags) { - mUpdatedFlags = updatedFlags; + public SdkUpdateMetadata(@Nullable Type type, @Nullable List names) { + mType = type; + mNames = names; } /** - * Returns the list of flag names that changed in this update. + * Returns the type of update that triggered this event. * - * @return the list of updated flag names, or null if not available + * @return the update type, or null if not available */ @Nullable - public List getUpdatedFlags() { - return mUpdatedFlags; + public Type getType() { + return mType; } -} + /** + * Returns the list of entity names that changed in this update. + *

    + * For {@link Type#FLAGS_UPDATE}, this contains flag names. + * For {@link Type#SEGMENTS_UPDATE}, this contains rule-based segment names. + * + * @return the list of updated entity names, or null if not available + */ + @Nullable + public List getNames() { + return mNames; + } +} diff --git a/api/src/test/java/io/split/android/client/events/SdkUpdateMetadataTest.java b/api/src/test/java/io/split/android/client/events/SdkUpdateMetadataTest.java index 143c2b259..1a1a25abc 100644 --- a/api/src/test/java/io/split/android/client/events/SdkUpdateMetadataTest.java +++ b/api/src/test/java/io/split/android/client/events/SdkUpdateMetadataTest.java @@ -12,35 +12,73 @@ public class SdkUpdateMetadataTest { @Test - public void getUpdatedFlagsReturnsNullWhenConstructedWithNull() { - SdkUpdateMetadata metadata = new SdkUpdateMetadata(null); + public void getNamesReturnsNullWhenConstructedWithNull() { + SdkUpdateMetadata metadata = new SdkUpdateMetadata(null, null); - assertNull(metadata.getUpdatedFlags()); + assertNull(metadata.getNames()); } @Test - public void getUpdatedFlagsReturnsEmptyListWhenConstructedWithEmptyList() { - SdkUpdateMetadata metadata = new SdkUpdateMetadata(Collections.emptyList()); + public void getNamesReturnsEmptyListWhenConstructedWithEmptyList() { + SdkUpdateMetadata metadata = new SdkUpdateMetadata(null, Collections.emptyList()); - assertEquals(Collections.emptyList(), metadata.getUpdatedFlags()); + assertEquals(Collections.emptyList(), metadata.getNames()); } @Test - public void getUpdatedFlagsReturnsListWhenConstructedWithList() { - List flags = Arrays.asList("flag1", "flag2", "flag3"); - SdkUpdateMetadata metadata = new SdkUpdateMetadata(flags); + public void getNamesReturnsListWhenConstructedWithList() { + List names = Arrays.asList("flag1", "flag2", "flag3"); + SdkUpdateMetadata metadata = new SdkUpdateMetadata(SdkUpdateMetadata.Type.FLAGS_UPDATE, names); - assertEquals(flags, metadata.getUpdatedFlags()); + assertEquals(names, metadata.getNames()); } @Test - public void getUpdatedFlagsReturnsSingleItemList() { - List flags = Collections.singletonList("singleFlag"); - SdkUpdateMetadata metadata = new SdkUpdateMetadata(flags); + public void getNamesReturnsSingleItemList() { + List names = Collections.singletonList("singleFlag"); + SdkUpdateMetadata metadata = new SdkUpdateMetadata(SdkUpdateMetadata.Type.FLAGS_UPDATE, names); - assertEquals(flags, metadata.getUpdatedFlags()); - assertEquals(1, metadata.getUpdatedFlags().size()); - assertEquals("singleFlag", metadata.getUpdatedFlags().get(0)); + assertEquals(names, metadata.getNames()); + assertEquals(1, metadata.getNames().size()); + assertEquals("singleFlag", metadata.getNames().get(0)); } -} + @Test + public void getTypeReturnsNullWhenConstructedWithNull() { + SdkUpdateMetadata metadata = new SdkUpdateMetadata(null, null); + + assertNull(metadata.getType()); + } + + @Test + public void getTypeReturnsFlagsUpdateWhenConstructedWithFlagsUpdate() { + SdkUpdateMetadata metadata = new SdkUpdateMetadata(SdkUpdateMetadata.Type.FLAGS_UPDATE, null); + + assertEquals(SdkUpdateMetadata.Type.FLAGS_UPDATE, metadata.getType()); + } + + @Test + public void getTypeReturnsSegmentsUpdateWhenConstructedWithSegmentsUpdate() { + SdkUpdateMetadata metadata = new SdkUpdateMetadata(SdkUpdateMetadata.Type.SEGMENTS_UPDATE, null); + + assertEquals(SdkUpdateMetadata.Type.SEGMENTS_UPDATE, metadata.getType()); + } + + @Test + public void flagsUpdateMetadataContainsBothTypeAndNames() { + List flags = Arrays.asList("flag1", "flag2"); + SdkUpdateMetadata metadata = new SdkUpdateMetadata(SdkUpdateMetadata.Type.FLAGS_UPDATE, flags); + + assertEquals(SdkUpdateMetadata.Type.FLAGS_UPDATE, metadata.getType()); + assertEquals(flags, metadata.getNames()); + } + + @Test + public void segmentsUpdateMetadataContainsBothTypeAndNames() { + List segments = Arrays.asList("segment1", "segment2"); + SdkUpdateMetadata metadata = new SdkUpdateMetadata(SdkUpdateMetadata.Type.SEGMENTS_UPDATE, segments); + + assertEquals(SdkUpdateMetadata.Type.SEGMENTS_UPDATE, metadata.getType()); + assertEquals(segments, metadata.getNames()); + } +} diff --git a/events-domain/src/main/java/io/split/android/client/events/metadata/EventMetadataHelpers.java b/events-domain/src/main/java/io/split/android/client/events/metadata/EventMetadataHelpers.java index 67dda836e..1571ccc7c 100644 --- a/events-domain/src/main/java/io/split/android/client/events/metadata/EventMetadataHelpers.java +++ b/events-domain/src/main/java/io/split/android/client/events/metadata/EventMetadataHelpers.java @@ -6,6 +6,8 @@ import java.util.HashSet; import java.util.List; +import io.split.android.client.events.SdkUpdateMetadata; + /** * Helper class for creating {@link EventMetadata} instances. *

    @@ -17,9 +19,33 @@ private EventMetadataHelpers() { // Utility class } - public static EventMetadata createUpdatedFlagsMetadata(List updatedSplitNames) { + /** + * Creates metadata for SDK_UPDATE events when flags are updated. + * + * @param updatedFlagNames the list of flag names that were updated + * @return the event metadata with TYPE=FLAGS_UPDATE and NAMES containing the flag names + */ + public static EventMetadata createUpdatedFlagsMetadata(List updatedFlagNames) { + return new EventMetadataBuilder() + .put(MetadataKeys.TYPE, SdkUpdateMetadata.Type.FLAGS_UPDATE.name()) + .put(MetadataKeys.NAMES, new ArrayList<>(new HashSet<>(updatedFlagNames))) + .build(); + } + + /** + * Creates metadata for SDK_UPDATE events when rule-based segments are updated. + *

    + * Note: This is for rule-based segments (RBS) ONLY, not for memberships + * (my segments / large segments). Memberships have their own internal event + * flow and don't emit SDK_UPDATE events with segment metadata. + * + * @param updatedSegmentNames the list of rule-based segment names that were updated + * @return the event metadata with TYPE=SEGMENTS_UPDATE and NAMES containing the segment names + */ + public static EventMetadata createUpdatedSegmentsMetadata(List updatedSegmentNames) { return new EventMetadataBuilder() - .put(MetadataKeys.UPDATED_FLAGS, new ArrayList<>(new HashSet<>(updatedSplitNames))) + .put(MetadataKeys.TYPE, SdkUpdateMetadata.Type.SEGMENTS_UPDATE.name()) + .put(MetadataKeys.NAMES, new ArrayList<>(new HashSet<>(updatedSegmentNames))) .build(); } diff --git a/events-domain/src/main/java/io/split/android/client/events/metadata/MetadataKeys.java b/events-domain/src/main/java/io/split/android/client/events/metadata/MetadataKeys.java index c76dd578f..d7a40d00e 100644 --- a/events-domain/src/main/java/io/split/android/client/events/metadata/MetadataKeys.java +++ b/events-domain/src/main/java/io/split/android/client/events/metadata/MetadataKeys.java @@ -14,9 +14,17 @@ private MetadataKeys() { // SDK_UPDATE event keys /** - * Names of flags that changed in this update. + * The type of update (FLAGS_UPDATE or SEGMENTS_UPDATE). */ - static final String UPDATED_FLAGS = "updatedFlags"; + static final String TYPE = "type"; + + /** + * Names of entities that changed in this update. + *

    + * For FLAGS_UPDATE, these are flag names. + * For SEGMENTS_UPDATE, these are rule-based segment names. + */ + static final String NAMES = "names"; // SDK_READY_FROM_CACHE event keys diff --git a/events-domain/src/main/java/io/split/android/client/events/metadata/TypedTaskConverter.java b/events-domain/src/main/java/io/split/android/client/events/metadata/TypedTaskConverter.java index a93123ef6..1885c1faa 100644 --- a/events-domain/src/main/java/io/split/android/client/events/metadata/TypedTaskConverter.java +++ b/events-domain/src/main/java/io/split/android/client/events/metadata/TypedTaskConverter.java @@ -26,11 +26,25 @@ private TypedTaskConverter() { @NonNull @SuppressWarnings("unchecked") public static SdkUpdateMetadata convertForSdkUpdate(@Nullable EventMetadata metadata) { - List updatedFlags = null; + SdkUpdateMetadata.Type type = null; + List names = null; + if (metadata != null) { - updatedFlags = (List) metadata.get(MetadataKeys.UPDATED_FLAGS); + // Extract type + String typeString = (String) metadata.get(MetadataKeys.TYPE); + if (typeString != null) { + try { + type = SdkUpdateMetadata.Type.valueOf(typeString); + } catch (IllegalArgumentException ignored) { + // Unknown type, leave as null + } + } + + // Extract names + names = (List) metadata.get(MetadataKeys.NAMES); } - return new SdkUpdateMetadata(updatedFlags); + + return new SdkUpdateMetadata(type, names); } /** diff --git a/events-domain/src/test/java/io/split/android/client/events/TypedTaskConversionTest.java b/events-domain/src/test/java/io/split/android/client/events/TypedTaskConversionTest.java index f1f93f347..381c006d0 100644 --- a/events-domain/src/test/java/io/split/android/client/events/TypedTaskConversionTest.java +++ b/events-domain/src/test/java/io/split/android/client/events/TypedTaskConversionTest.java @@ -20,7 +20,7 @@ public class TypedTaskConversionTest { @Test - public void convertForSdkUpdateConvertsMetadataCorrectly() { + public void convertForSdkUpdateConvertsFlagsMetadataCorrectly() { List expectedFlags = Arrays.asList("flag1", "flag2"); EventMetadata eventMetadata = EventMetadataHelpers.createUpdatedFlagsMetadata(expectedFlags); @@ -29,8 +29,24 @@ public void convertForSdkUpdateConvertsMetadataCorrectly() { SdkUpdateMetadata converted = TypedTaskConverter.convertForSdkUpdate(eventMetadata); assertNotNull(converted); - assertEquals(expectedFlags.size(), converted.getUpdatedFlags().size()); - assertTrue(converted.getUpdatedFlags().containsAll(expectedFlags)); + assertEquals(SdkUpdateMetadata.Type.FLAGS_UPDATE, converted.getType()); + assertEquals(expectedFlags.size(), converted.getNames().size()); + assertTrue(converted.getNames().containsAll(expectedFlags)); + } + + @Test + public void convertForSdkUpdateConvertsSegmentsMetadataCorrectly() { + List expectedSegments = Arrays.asList("segment1", "segment2"); + + EventMetadata eventMetadata = EventMetadataHelpers.createUpdatedSegmentsMetadata(expectedSegments); + + // Call conversion method + SdkUpdateMetadata converted = TypedTaskConverter.convertForSdkUpdate(eventMetadata); + + assertNotNull(converted); + assertEquals(SdkUpdateMetadata.Type.SEGMENTS_UPDATE, converted.getType()); + assertEquals(expectedSegments.size(), converted.getNames().size()); + assertTrue(converted.getNames().containsAll(expectedSegments)); } @Test @@ -52,7 +68,8 @@ public void convertForSdkUpdateHandlesNullMetadata() { SdkUpdateMetadata converted = TypedTaskConverter.convertForSdkUpdate(null); assertNotNull(converted); - assertNull(converted.getUpdatedFlags()); + assertNull(converted.getType()); + assertNull(converted.getNames()); } @Test @@ -64,4 +81,3 @@ public void convertForSdkReadyFromCacheHandlesNullMetadata() { assertNull(converted.getLastUpdateTimestamp()); } } - diff --git a/events-domain/src/test/java/io/split/android/client/events/metadata/EventMetadataBuilderTest.java b/events-domain/src/test/java/io/split/android/client/events/metadata/EventMetadataBuilderTest.java index 8f561e0c2..c9d638dee 100644 --- a/events-domain/src/test/java/io/split/android/client/events/metadata/EventMetadataBuilderTest.java +++ b/events-domain/src/test/java/io/split/android/client/events/metadata/EventMetadataBuilderTest.java @@ -154,10 +154,10 @@ public void putListOfStringsAddsValue() { List flags = Arrays.asList("flag_1", "flag_2", "flag_3"); EventMetadata metadata = new EventMetadataBuilder() - .put("updatedFlags", flags) + .put("names", flags) .build(); - assertEquals(flags, metadata.get(MetadataKeys.UPDATED_FLAGS)); + assertEquals(flags, metadata.get(MetadataKeys.NAMES)); } @Test diff --git a/events-domain/src/test/java/io/split/android/client/events/metadata/EventMetadataHelpersTest.java b/events-domain/src/test/java/io/split/android/client/events/metadata/EventMetadataHelpersTest.java index 7fe8d577d..7f73303bf 100644 --- a/events-domain/src/test/java/io/split/android/client/events/metadata/EventMetadataHelpersTest.java +++ b/events-domain/src/test/java/io/split/android/client/events/metadata/EventMetadataHelpersTest.java @@ -9,23 +9,49 @@ import java.util.Arrays; import java.util.List; +import io.split.android.client.events.SdkUpdateMetadata; + public class EventMetadataHelpersTest { - // Tests for createUpdatedFlagsMetadata (existing) + // Tests for createUpdatedFlagsMetadata @Test @SuppressWarnings("unchecked") - public void createUpdatedFlagsMetadataContainsFlags() { + public void createUpdatedFlagsMetadataContainsTypeAndNames() { List flags = Arrays.asList("flag1", "flag2", "flag3"); EventMetadata metadata = EventMetadataHelpers.createUpdatedFlagsMetadata(flags); - assertTrue(metadata.containsKey(MetadataKeys.UPDATED_FLAGS)); - List result = (List) metadata.get(MetadataKeys.UPDATED_FLAGS); + // Check type + assertTrue(metadata.containsKey(MetadataKeys.TYPE)); + assertEquals(SdkUpdateMetadata.Type.FLAGS_UPDATE.name(), metadata.get(MetadataKeys.TYPE)); + + // Check names + assertTrue(metadata.containsKey(MetadataKeys.NAMES)); + List result = (List) metadata.get(MetadataKeys.NAMES); assertEquals(3, result.size()); assertTrue(result.contains("flag1")); assertTrue(result.contains("flag2")); assertTrue(result.contains("flag3")); } + // Tests for createUpdatedSegmentsMetadata + @Test + @SuppressWarnings("unchecked") + public void createUpdatedSegmentsMetadataContainsTypeAndNames() { + List segments = Arrays.asList("segment1", "segment2"); + EventMetadata metadata = EventMetadataHelpers.createUpdatedSegmentsMetadata(segments); + + // Check type + assertTrue(metadata.containsKey(MetadataKeys.TYPE)); + assertEquals(SdkUpdateMetadata.Type.SEGMENTS_UPDATE.name(), metadata.get(MetadataKeys.TYPE)); + + // Check names + assertTrue(metadata.containsKey(MetadataKeys.NAMES)); + List result = (List) metadata.get(MetadataKeys.NAMES); + assertEquals(2, result.size()); + assertTrue(result.contains("segment1")); + assertTrue(result.contains("segment2")); + } + // Tests for createCacheReadyMetadata @Test public void createCacheReadyMetadataWithTimestampAndFreshInstallFalse() { diff --git a/events-domain/src/test/java/io/split/android/client/events/metadata/MetadataKeysTest.java b/events-domain/src/test/java/io/split/android/client/events/metadata/MetadataKeysTest.java index 530dfd6a2..b7968cd36 100644 --- a/events-domain/src/test/java/io/split/android/client/events/metadata/MetadataKeysTest.java +++ b/events-domain/src/test/java/io/split/android/client/events/metadata/MetadataKeysTest.java @@ -11,8 +11,13 @@ public class MetadataKeysTest { @Test - public void updatedFlagsKeyHasCorrectValue() { - assertEquals("updatedFlags", MetadataKeys.UPDATED_FLAGS); + public void typeKeyHasCorrectValue() { + assertEquals("type", MetadataKeys.TYPE); + } + + @Test + public void namesKeyHasCorrectValue() { + assertEquals("names", MetadataKeys.NAMES); } @Test diff --git a/main/src/androidTest/java/tests/integration/events/SdkEventsIntegrationTest.java b/main/src/androidTest/java/tests/integration/events/SdkEventsIntegrationTest.java index 515836a1f..a1a2816ab 100644 --- a/main/src/androidTest/java/tests/integration/events/SdkEventsIntegrationTest.java +++ b/main/src/androidTest/java/tests/integration/events/SdkEventsIntegrationTest.java @@ -1217,6 +1217,18 @@ void pushSplitKill(String splitName) { } } + void pushRbsUpdate() { + pushRbsUpdate("2000", "1000"); + } + + void pushRbsUpdate(String changeNumber, String previousChangeNumber) { + if (streamingData != null) { + // RBS payload: {"name":"rbs_test","status":"ACTIVE","trafficTypeName":"user","excluded":{"keys":[],"segments":[]},"conditions":[{"matcherGroup":{"combiner":"AND","matchers":[{"keySelector":{"trafficType":"user"},"matcherType":"ALL_KEYS","negate":false}]}}]} + String RBS_UPDATE_PAYLOAD = "eyJuYW1lIjoicmJzX3Rlc3QiLCJzdGF0dXMiOiJBQ1RJVkUiLCJ0cmFmZmljVHlwZU5hbWUiOiJ1c2VyIiwiZXhjbHVkZWQiOnsia2V5cyI6W10sInNlZ21lbnRzIjpbXX0sImNvbmRpdGlvbnMiOlt7Im1hdGNoZXJHcm91cCI6eyJjb21iaW5lciI6IkFORCIsIm1hdGNoZXJzIjpbeyJrZXlTZWxlY3RvciI6eyJ0cmFmZmljVHlwZSI6InVzZXIifSwibWF0Y2hlclR5cGUiOiJBTExfS0VZUyIsIm5lZ2F0ZSI6ZmFsc2V9XX19XX0="; + pushMessage(streamingData, IntegrationHelper.rbsChange(changeNumber, previousChangeNumber, RBS_UPDATE_PAYLOAD)); + } + } + void destroy() { factory.destroy(); } @@ -1295,4 +1307,138 @@ private void populateDatabaseWithCacheData(long timestamp) { segmentEntity2.setUpdatedAt(System.currentTimeMillis() / 1000); mDatabase.mySegmentDao().update(segmentEntity2); } + + // ======================================================================== + // Phase 1 TDD: Tests for SdkUpdateMetadata.Type enum + // ======================================================================== + + /** + * Scenario: sdkUpdateMetadata contains Type.FLAGS_UPDATE for flags update + *

    + * Given sdkReady has already been emitted + * And a handler H is registered for sdkUpdate + * When a split update notification arrives via SSE + * Then sdkUpdate is emitted + * And handler H receives metadata with getType() returning Type.FLAGS_UPDATE + * And handler H receives metadata with getNames() containing the updated flag names + */ + @Test + public void sdkUpdateMetadataContainsTypeForFlagsUpdate() throws Exception { + // Given: sdkReady has already been emitted (with streaming support) + TestClientFixture fixture = createStreamingClientAndWaitForReady(new Key("key_1")); + + AtomicReference receivedMetadata = new AtomicReference<>(); + CountDownLatch updateLatch = new CountDownLatch(1); + + fixture.client.addEventListener(new SdkEventListener() { + @Override + public void onUpdate(SplitClient client, SdkUpdateMetadata metadata) { + receivedMetadata.set(metadata); + updateLatch.countDown(); + } + }); + + // When: a split update notification arrives via SSE + fixture.pushSplitUpdate(); + + // Then: sdkUpdate is emitted + boolean updateFired = updateLatch.await(10, TimeUnit.SECONDS); + assertTrue("SDK_UPDATE should fire", updateFired); + + // And: metadata has getType() returning Type.FLAGS_UPDATE + assertNotNull("Metadata should not be null", receivedMetadata.get()); + assertEquals("Type should be FLAGS_UPDATE", + SdkUpdateMetadata.Type.FLAGS_UPDATE, receivedMetadata.get().getType()); + + // And: metadata has getNames() containing the updated flag names + assertNotNull("Names should not be null", receivedMetadata.get().getNames()); + assertFalse("Names should not be empty", receivedMetadata.get().getNames().isEmpty()); + + fixture.destroy(); + } + + /** + * Scenario: sdkUpdateMetadata contains Type.SEGMENTS_UPDATE for rule-based segments update + *

    + * Given sdkReady has already been emitted + * And a handler H is registered for sdkUpdate + * When a rule-based segment update notification arrives via SSE + * Then sdkUpdate is emitted + * And handler H receives metadata with getType() returning Type.SEGMENTS_UPDATE + * And handler H receives metadata with getNames() containing the updated RBS names + *

    + * Note: SEGMENTS_UPDATE is for rule-based segments (RBS) ONLY, not for memberships. + */ + @Test + public void sdkUpdateMetadataContainsTypeForSegmentsUpdate() throws Exception { + // Given: sdkReady has already been emitted (with streaming support and RBS in storage) + TestClientFixture fixture = createStreamingClientWithRbsAndWaitForReady(new Key("key_1")); + + AtomicReference receivedMetadata = new AtomicReference<>(); + CountDownLatch updateLatch = new CountDownLatch(1); + + fixture.client.addEventListener(new SdkEventListener() { + @Override + public void onUpdate(SplitClient client, SdkUpdateMetadata metadata) { + receivedMetadata.set(metadata); + updateLatch.countDown(); + } + }); + + // When: a rule-based segment update notification arrives via SSE + fixture.pushRbsUpdate(); + + // Then: sdkUpdate is emitted + boolean updateFired = updateLatch.await(10, TimeUnit.SECONDS); + assertTrue("SDK_UPDATE should fire for RBS update", updateFired); + + // And: metadata has getType() returning Type.SEGMENTS_UPDATE + assertNotNull("Metadata should not be null", receivedMetadata.get()); + assertEquals("Type should be SEGMENTS_UPDATE", + SdkUpdateMetadata.Type.SEGMENTS_UPDATE, receivedMetadata.get().getType()); + + // And: metadata has getNames() containing the updated RBS names + assertNotNull("Names should not be null", receivedMetadata.get().getNames()); + assertFalse("Names should not be empty", receivedMetadata.get().getNames().isEmpty()); + assertTrue("Names should contain rbs_test", + receivedMetadata.get().getNames().contains("rbs_test")); + + fixture.destroy(); + } + + /** + * Creates a streaming client with RBS data pre-populated and waits for SDK_READY. + * Required for testing RBS in-place updates (RBS must exist in storage for instant update). + */ + private TestClientFixture createStreamingClientWithRbsAndWaitForReady(Key key) throws InterruptedException, IOException { + // Pre-populate RBS in storage so in-place update can work + populateDatabaseWithRbsData(); + + TestClientFixture fixture = createStreamingClient(key); + + CountDownLatch readyLatch = new CountDownLatch(1); + fixture.client.on(SplitEvent.SDK_READY, new SplitEventTask() { + @Override + public void onPostExecution(SplitClient client) { + readyLatch.countDown(); + } + }); + + boolean readyFired = readyLatch.await(10, TimeUnit.SECONDS); + assertTrue("SDK_READY should fire", readyFired); + + // Wait for SSE connection and send keep-alive + fixture.waitForSseConnection(); + + return new TestClientFixture(fixture.factory, fixture.client, readyLatch, fixture.streamingData, fixture.sseLatch); + } + + /** + * Populates the database with RBS data for instant update testing. + */ + private void populateDatabaseWithRbsData() { + // Need to populate the RBS storage with the segment that will be updated + // so that in-place update can work (referenced RBS must exist) + mDatabase.generalInfoDao().update(new GeneralInfoEntity("rbsChangeNumber", 1000L)); + } } diff --git a/main/src/androidTest/java/tests/service/EventsManagerTest.java b/main/src/androidTest/java/tests/service/EventsManagerTest.java index 6865e1b05..27d871a02 100644 --- a/main/src/androidTest/java/tests/service/EventsManagerTest.java +++ b/main/src/androidTest/java/tests/service/EventsManagerTest.java @@ -216,9 +216,10 @@ public void onUpdate(SplitClient client, SdkUpdateMetadata metadata) { Assert.assertTrue("SDK_UPDATE should fire", updateLatch.await(5, TimeUnit.SECONDS)); Assert.assertNotNull("Metadata should not be null", receivedMetadata.get()); - List updatedFlags = receivedMetadata.get().getUpdatedFlags(); - Assert.assertNotNull("Updated flags should not be null", updatedFlags); - Assert.assertTrue("Metadata should contain only killed_flag", updatedFlags.size() == 1 && updatedFlags.contains("killed_flag")); + List names = receivedMetadata.get().getNames(); + Assert.assertNotNull("Names should not be null", names); + Assert.assertTrue("Metadata should contain only killed_flag", names.size() == 1 && names.contains("killed_flag")); + Assert.assertEquals(SdkUpdateMetadata.Type.FLAGS_UPDATE, receivedMetadata.get().getType()); } @Test @@ -350,13 +351,15 @@ public void onUpdateView(SplitClient client, SdkUpdateMetadata metadata) { Assert.assertTrue("Main thread method SHOULD run on main thread", mainThreadOnMainThread.get()); Assert.assertNotNull("Background metadata should not be null", backgroundMetadata.get()); - List bgFlags = backgroundMetadata.get().getUpdatedFlags(); - Assert.assertNotNull("Background updatedFlags should not be null", bgFlags); - Assert.assertTrue("Background metadata should contain flag1", bgFlags.contains("flag1")); + List bgNames = backgroundMetadata.get().getNames(); + Assert.assertNotNull("Background names should not be null", bgNames); + Assert.assertTrue("Background metadata should contain flag1", bgNames.contains("flag1")); + Assert.assertEquals(SdkUpdateMetadata.Type.FLAGS_UPDATE, backgroundMetadata.get().getType()); Assert.assertNotNull("Main thread metadata should not be null", mainThreadMetadata.get()); - List mtFlags = mainThreadMetadata.get().getUpdatedFlags(); - Assert.assertNotNull("Main thread updatedFlags should not be null", mtFlags); - Assert.assertTrue("Main thread metadata should contain flag1", mtFlags.contains("flag1")); + List mtNames = mainThreadMetadata.get().getNames(); + Assert.assertNotNull("Main thread names should not be null", mtNames); + Assert.assertTrue("Main thread metadata should contain flag1", mtNames.contains("flag1")); + Assert.assertEquals(SdkUpdateMetadata.Type.FLAGS_UPDATE, mainThreadMetadata.get().getType()); } } diff --git a/main/src/main/java/io/split/android/client/SplitClientImpl.java b/main/src/main/java/io/split/android/client/SplitClientImpl.java index 8257d89ce..1cbfdb317 100644 --- a/main/src/main/java/io/split/android/client/SplitClientImpl.java +++ b/main/src/main/java/io/split/android/client/SplitClientImpl.java @@ -206,7 +206,7 @@ public void on(SplitEvent event, SplitEventTask task) { @Override public void addEventListener(SdkEventListener listener) { if (listener == null) { - Logger.w("Listener cannot be null"); + Logger.w("SDK Event Listener cannot be null"); return; } mEventsManager.registerEventListener(listener); diff --git a/main/src/main/java/io/split/android/client/localhost/LocalhostSplitClient.java b/main/src/main/java/io/split/android/client/localhost/LocalhostSplitClient.java index fefd5d373..5944162a6 100644 --- a/main/src/main/java/io/split/android/client/localhost/LocalhostSplitClient.java +++ b/main/src/main/java/io/split/android/client/localhost/LocalhostSplitClient.java @@ -274,7 +274,7 @@ public void on(SplitEvent event, SplitEventTask task) { @Override public void addEventListener(SdkEventListener listener) { if (listener == null) { - Logger.w("Event listener cannot be null"); + Logger.w("SDK Event Listener cannot be null"); return; } mEventsManager.registerEventListener(listener); diff --git a/main/src/main/java/io/split/android/client/service/rules/RuleBasedSegmentInPlaceUpdateTask.java b/main/src/main/java/io/split/android/client/service/rules/RuleBasedSegmentInPlaceUpdateTask.java index 9e92523e0..0ca9b88dc 100644 --- a/main/src/main/java/io/split/android/client/service/rules/RuleBasedSegmentInPlaceUpdateTask.java +++ b/main/src/main/java/io/split/android/client/service/rules/RuleBasedSegmentInPlaceUpdateTask.java @@ -1,12 +1,17 @@ package io.split.android.client.service.rules; +import static io.split.android.client.service.splits.SplitsSyncHelper.extractRbsNames; import static io.split.android.client.utils.Utils.checkNotNull; import androidx.annotation.NonNull; +import java.util.List; + import io.split.android.client.dtos.RuleBasedSegment; import io.split.android.client.events.ISplitEventsManager; import io.split.android.client.events.SplitInternalEvent; +import io.split.android.client.events.metadata.EventMetadata; +import io.split.android.client.events.metadata.EventMetadataHelpers; import io.split.android.client.service.executor.SplitTask; import io.split.android.client.service.executor.SplitTaskExecutionInfo; import io.split.android.client.service.executor.SplitTaskType; @@ -41,7 +46,8 @@ public SplitTaskExecutionInfo execute() { boolean triggerSdkUpdate = mRuleBasedSegmentStorage.update(processedChange.getActive(), processedChange.getArchived(), mChangeNumber, null); if (triggerSdkUpdate) { - mEventsManager.notifyInternalEvent(SplitInternalEvent.RULE_BASED_SEGMENTS_UPDATED); + EventMetadata metadata = createUpdatedRbsMetadata(processedChange); + mEventsManager.notifyInternalEvent(SplitInternalEvent.RULE_BASED_SEGMENTS_UPDATED, metadata); } Logger.v("Updated rule based segment"); @@ -52,4 +58,9 @@ public SplitTaskExecutionInfo execute() { return SplitTaskExecutionInfo.error(SplitTaskType.RULE_BASED_SEGMENT_SYNC); } } + + private EventMetadata createUpdatedRbsMetadata(ProcessedRuleBasedSegmentChange processedChange) { + List updatedRbsNames = extractRbsNames(processedChange); + return EventMetadataHelpers.createUpdatedSegmentsMetadata(updatedRbsNames); + } } diff --git a/main/src/main/java/io/split/android/client/service/splits/RuleBasedSegmentInPlaceUpdateTask.java b/main/src/main/java/io/split/android/client/service/splits/RuleBasedSegmentInPlaceUpdateTask.java index e02ccec56..20b261651 100644 --- a/main/src/main/java/io/split/android/client/service/splits/RuleBasedSegmentInPlaceUpdateTask.java +++ b/main/src/main/java/io/split/android/client/service/splits/RuleBasedSegmentInPlaceUpdateTask.java @@ -4,9 +4,13 @@ import androidx.annotation.NonNull; +import java.util.List; + import io.split.android.client.dtos.RuleBasedSegment; import io.split.android.client.events.ISplitEventsManager; import io.split.android.client.events.SplitInternalEvent; +import io.split.android.client.events.metadata.EventMetadata; +import io.split.android.client.events.metadata.EventMetadataHelpers; import io.split.android.client.service.executor.SplitTask; import io.split.android.client.service.executor.SplitTaskExecutionInfo; import io.split.android.client.service.executor.SplitTaskType; @@ -43,7 +47,8 @@ public SplitTaskExecutionInfo execute() { boolean triggerSdkUpdate = mRuleBasedSegmentStorage.update(processedChange.getActive(), processedChange.getArchived(), mChangeNumber, null); if (triggerSdkUpdate) { - mEventsManager.notifyInternalEvent(SplitInternalEvent.RULE_BASED_SEGMENTS_UPDATED); + EventMetadata metadata = createUpdatedRbsMetadata(processedChange); + mEventsManager.notifyInternalEvent(SplitInternalEvent.RULE_BASED_SEGMENTS_UPDATED, metadata); } Logger.v("Updated rule based segment"); @@ -54,4 +59,9 @@ public SplitTaskExecutionInfo execute() { return SplitTaskExecutionInfo.error(SplitTaskType.RULE_BASED_SEGMENT_SYNC); } } + + private EventMetadata createUpdatedRbsMetadata(ProcessedRuleBasedSegmentChange processedChange) { + List updatedRbsNames = SplitsSyncHelper.extractRbsNames(processedChange); + return EventMetadataHelpers.createUpdatedSegmentsMetadata(updatedRbsNames); + } } diff --git a/main/src/main/java/io/split/android/client/service/splits/SplitsSyncHelper.java b/main/src/main/java/io/split/android/client/service/splits/SplitsSyncHelper.java index bf203a469..42075e5b2 100644 --- a/main/src/main/java/io/split/android/client/service/splits/SplitsSyncHelper.java +++ b/main/src/main/java/io/split/android/client/service/splits/SplitsSyncHelper.java @@ -17,6 +17,7 @@ import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicReference; +import io.split.android.client.dtos.RuleBasedSegment; import io.split.android.client.dtos.RuleBasedSegmentChange; import io.split.android.client.dtos.Split; import io.split.android.client.dtos.SplitChange; @@ -59,6 +60,7 @@ public class SplitsSyncHelper { private final ExecutorService mExecutor; private final TargetingRulesCache mTargetingRulesCache; private final AtomicReference mLastProcessedSplitChange = new AtomicReference<>(); + private final AtomicReference mLastProcessedRbsChange = new AtomicReference<>(); private boolean mSplitsHaveChanged; private boolean mRuleBasedSegmentsHaveChanged; @@ -376,8 +378,55 @@ public static List extractFlagNames(@Nullable ProcessedSplitChange proce return updatedNames; } + /** + * Gets the list of updated rule-based segment names from the last sync operation. + * This includes both active (added/modified) and archived (removed) segments. + * + * @return list of updated RBS names, or empty list if no updates occurred + */ + @NonNull + public List getLastUpdatedRbsNames() { + ProcessedRuleBasedSegmentChange lastChange = mLastProcessedRbsChange.get(); + if (lastChange == null) { + return Collections.emptyList(); + } + return extractRbsNames(lastChange); + } + + /** + * Extracts rule-based segment names from a ProcessedRuleBasedSegmentChange. + * This includes both active (added/modified) and archived (removed) segments. + * + * @param processedChange the processed RBS change + * @return list of RBS names, or empty list if change is null + */ + @NonNull + public static List extractRbsNames(@Nullable ProcessedRuleBasedSegmentChange processedChange) { + if (processedChange == null) { + return Collections.emptyList(); + } + + List updatedNames = new ArrayList<>(); + if (processedChange.getActive() != null) { + for (RuleBasedSegment segment : processedChange.getActive()) { + if (segment != null && segment.getName() != null) { + updatedNames.add(segment.getName()); + } + } + } + if (processedChange.getArchived() != null) { + for (RuleBasedSegment segment : processedChange.getArchived()) { + if (segment != null && segment.getName() != null) { + updatedNames.add(segment.getName()); + } + } + } + return updatedNames; + } + private void updateRbsStorage(RuleBasedSegmentChange ruleBasedSegmentChange) { ProcessedRuleBasedSegmentChange change = mRuleBasedSegmentChangeProcessor.process(ruleBasedSegmentChange.getSegments(), ruleBasedSegmentChange.getTill()); + mLastProcessedRbsChange.set(change); mRuleBasedSegmentStorage.update(change.getActive(), change.getArchived(), change.getChangeNumber(), mExecutor); } diff --git a/main/src/main/java/io/split/android/client/service/splits/SplitsSyncTask.java b/main/src/main/java/io/split/android/client/service/splits/SplitsSyncTask.java index 2f3ad81df..67a5615c6 100644 --- a/main/src/main/java/io/split/android/client/service/splits/SplitsSyncTask.java +++ b/main/src/main/java/io/split/android/client/service/splits/SplitsSyncTask.java @@ -112,7 +112,8 @@ private void notifyInternalEvent(long storedChangeNumber) { } if (mSplitsSyncHelper.ruleBasedSegmentsHaveChanged()) { - mEventsManager.notifyInternalEvent(SplitInternalEvent.RULE_BASED_SEGMENTS_UPDATED); + EventMetadata rbsMetadata = createUpdatedRbsMetadata(); + mEventsManager.notifyInternalEvent(SplitInternalEvent.RULE_BASED_SEGMENTS_UPDATED, rbsMetadata); } // Fire sync complete AFTER update events. This ensures SDK_READY triggers after @@ -127,6 +128,11 @@ private EventMetadata createUpdatedFlagsMetadata() { return EventMetadataHelpers.createUpdatedFlagsMetadata(updatedSplitNames); } + private EventMetadata createUpdatedRbsMetadata() { + List updatedRbsNames = mSplitsSyncHelper.getLastUpdatedRbsNames(); + return EventMetadataHelpers.createUpdatedSegmentsMetadata(updatedRbsNames); + } + private boolean splitsFilterHasChanged(String storedSplitsFilterQueryString) { return !sanitizeString(mSplitsFilterQueryStringFromConfig).equals(sanitizeString(storedSplitsFilterQueryString)); } diff --git a/main/src/main/java/io/split/android/client/service/splits/SplitsUpdateTask.java b/main/src/main/java/io/split/android/client/service/splits/SplitsUpdateTask.java index 4030ea3c1..ca7d29bf8 100644 --- a/main/src/main/java/io/split/android/client/service/splits/SplitsUpdateTask.java +++ b/main/src/main/java/io/split/android/client/service/splits/SplitsUpdateTask.java @@ -80,7 +80,8 @@ public SplitTaskExecutionInfo execute() { } if (mSplitsSyncHelper.ruleBasedSegmentsHaveChanged()) { - mEventsManager.notifyInternalEvent(SplitInternalEvent.RULE_BASED_SEGMENTS_UPDATED); + EventMetadata rbsMetadata = createUpdatedRbsMetadata(); + mEventsManager.notifyInternalEvent(SplitInternalEvent.RULE_BASED_SEGMENTS_UPDATED, rbsMetadata); } // Fire sync complete AFTER update events @@ -95,6 +96,11 @@ private EventMetadata createUpdatedFlagsMetadata() { return EventMetadataHelpers.createUpdatedFlagsMetadata(updatedSplitNames); } + private EventMetadata createUpdatedRbsMetadata() { + List updatedRbsNames = mSplitsSyncHelper.getLastUpdatedRbsNames(); + return EventMetadataHelpers.createUpdatedSegmentsMetadata(updatedRbsNames); + } + @VisibleForTesting public void setChangeChecker(SplitsChangeChecker changeChecker) { mChangeChecker = changeChecker; diff --git a/main/src/test/java/io/split/android/client/events/EventsManagerCoordinatorTest.java b/main/src/test/java/io/split/android/client/events/EventsManagerCoordinatorTest.java index 36f4211cf..904cd8b7c 100644 --- a/main/src/test/java/io/split/android/client/events/EventsManagerCoordinatorTest.java +++ b/main/src/test/java/io/split/android/client/events/EventsManagerCoordinatorTest.java @@ -121,9 +121,9 @@ public void SPLITS_UPDATEDEventWithMetadataIsPassedDownToChildren() { verify(mMockChildEventsManager).notifyInternalEvent(eq(SplitInternalEvent.SPLITS_UPDATED), argThat(meta -> { if (meta == null) return false; SdkUpdateMetadata typedMeta = TypedTaskConverter.convertForSdkUpdate(meta); - List flags = typedMeta.getUpdatedFlags(); - assertNotNull(flags); - return flags.size() == 2 && flags.contains("flag1") && flags.contains("flag2"); + List names = typedMeta.getNames(); + assertNotNull(names); + return names.size() == 2 && names.contains("flag1") && names.contains("flag2"); })); } diff --git a/main/src/test/java/io/split/android/client/events/EventsManagerTest.java b/main/src/test/java/io/split/android/client/events/EventsManagerTest.java index 0004e391a..5be88e19f 100644 --- a/main/src/test/java/io/split/android/client/events/EventsManagerTest.java +++ b/main/src/test/java/io/split/android/client/events/EventsManagerTest.java @@ -296,8 +296,8 @@ public void onUpdate(SplitClient client, SdkUpdateMetadata metadata) { boolean updateAwait = updateLatch.await(3, TimeUnit.SECONDS); assertTrue("SDK_UPDATE callback should be called", updateAwait); assertNotNull("Metadata should not be null", receivedMetadata.get()); - assertNotNull("Metadata should contain updatedFlags", receivedMetadata.get().getUpdatedFlags()); - assertEquals(2, receivedMetadata.get().getUpdatedFlags().size()); + assertNotNull("Metadata should contain names", receivedMetadata.get().getNames()); + assertEquals(2, receivedMetadata.get().getNames().size()); } @Test @@ -323,7 +323,7 @@ public void onUpdateView(SplitClient client, SdkUpdateMetadata metadata) { boolean updateAwait = updateLatch.await(3, TimeUnit.SECONDS); assertTrue("SDK_UPDATE callback should be called on main thread", updateAwait); assertNotNull("Metadata should not be null", receivedMetadata.get()); - assertNotNull("Metadata should contain updatedFlags", receivedMetadata.get().getUpdatedFlags()); + assertNotNull("Metadata should contain names", receivedMetadata.get().getNames()); } @Test @@ -385,7 +385,7 @@ public void onUpdateView(SplitClient client, SdkUpdateMetadata metadata) { assertTrue("Background method should be called", backgroundMethodCalled[0]); assertTrue("Main thread method should also be called", mainThreadMethodCalled[0]); assertNotNull("Metadata should be passed to methods", receivedMetadata.get()); - assertNotNull("Metadata should contain updatedFlags", receivedMetadata.get().getUpdatedFlags()); + assertNotNull("Metadata should contain names", receivedMetadata.get().getNames()); } @Test diff --git a/main/src/test/java/io/split/android/client/localhost/LocalhostSplitsStorageTest.java b/main/src/test/java/io/split/android/client/localhost/LocalhostSplitsStorageTest.java index f03dae3bf..aad284982 100644 --- a/main/src/test/java/io/split/android/client/localhost/LocalhostSplitsStorageTest.java +++ b/main/src/test/java/io/split/android/client/localhost/LocalhostSplitsStorageTest.java @@ -1,5 +1,6 @@ package io.split.android.client.localhost; +import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertTrue; import static org.mockito.ArgumentMatchers.any; @@ -103,9 +104,10 @@ public void loadLocalNotifiesSplitsUpdatedWithMetadataContainingUpdatedFlags() t EventMetadata metadata = metadataCaptor.getValue(); assertNotNull("Metadata should not be null", metadata); SdkUpdateMetadata typedMetadata = TypedTaskConverter.convertForSdkUpdate(metadata); - List flags = typedMetadata.getUpdatedFlags(); - assertNotNull("updatedFlags value should not be null", flags); - assertTrue("Metadata should contain 'split1' flag", flags.contains("split1")); + List names = typedMetadata.getNames(); + assertNotNull("names value should not be null", names); + assertTrue("Metadata should contain 'split1' flag", names.contains("split1")); + assertEquals(SdkUpdateMetadata.Type.FLAGS_UPDATE, typedMetadata.getType()); } } diff --git a/main/src/test/java/io/split/android/client/service/SplitInPlaceUpdateTaskTest.java b/main/src/test/java/io/split/android/client/service/SplitInPlaceUpdateTaskTest.java index 610714d1e..88fa2c09b 100644 --- a/main/src/test/java/io/split/android/client/service/SplitInPlaceUpdateTaskTest.java +++ b/main/src/test/java/io/split/android/client/service/SplitInPlaceUpdateTaskTest.java @@ -154,11 +154,12 @@ public void splitsUpdatedIncludesMetadataWithUpdatedFlags() { verify(mEventsManager).notifyInternalEvent(eq(SplitInternalEvent.SPLITS_UPDATED), argThat(metadata -> { if (metadata == null) return false; SdkUpdateMetadata typedMeta = TypedTaskConverter.convertForSdkUpdate(metadata); - List flags = typedMeta.getUpdatedFlags(); - assertNotNull(flags); - assertEquals(2, flags.size()); - assertTrue(flags.contains("test_split_1")); - assertTrue(flags.contains("test_split_2")); + List names = typedMeta.getNames(); + assertNotNull(names); + assertEquals(2, names.size()); + assertTrue(names.contains("test_split_1")); + assertTrue(names.contains("test_split_2")); + assertEquals(SdkUpdateMetadata.Type.FLAGS_UPDATE, typedMeta.getType()); return true; })); } @@ -178,10 +179,11 @@ public void splitsUpdatedIncludesArchivedSplitsInMetadata() { verify(mEventsManager).notifyInternalEvent(eq(SplitInternalEvent.SPLITS_UPDATED), argThat(metadata -> { if (metadata == null) return false; SdkUpdateMetadata typedMeta = TypedTaskConverter.convertForSdkUpdate(metadata); - List flags = typedMeta.getUpdatedFlags(); - assertNotNull(flags); - assertEquals(1, flags.size()); - assertTrue(flags.contains("archived_split")); + List names = typedMeta.getNames(); + assertNotNull(names); + assertEquals(1, names.size()); + assertTrue(names.contains("archived_split")); + assertEquals(SdkUpdateMetadata.Type.FLAGS_UPDATE, typedMeta.getType()); return true; })); } diff --git a/main/src/test/java/io/split/android/client/service/SplitKillTaskTest.java b/main/src/test/java/io/split/android/client/service/SplitKillTaskTest.java index aee4e3e4f..beb9d4043 100644 --- a/main/src/test/java/io/split/android/client/service/SplitKillTaskTest.java +++ b/main/src/test/java/io/split/android/client/service/SplitKillTaskTest.java @@ -80,10 +80,11 @@ public void correctExecution() throws HttpFetcherException { EventMetadata metadata = metadataCaptor.getValue(); Assert.assertNotNull(metadata); SdkUpdateMetadata typedMetadata = TypedTaskConverter.convertForSdkUpdate(metadata); - List updatedFlags = typedMetadata.getUpdatedFlags(); - Assert.assertNotNull(updatedFlags); - Assert.assertEquals(1, updatedFlags.size()); - Assert.assertTrue(updatedFlags.contains("split1")); + List names = typedMetadata.getNames(); + Assert.assertNotNull(names); + Assert.assertEquals(1, names.size()); + Assert.assertTrue(names.contains("split1")); + Assert.assertEquals(SdkUpdateMetadata.Type.FLAGS_UPDATE, typedMetadata.getType()); } @Test diff --git a/main/src/test/java/io/split/android/client/service/SplitSyncTaskTest.java b/main/src/test/java/io/split/android/client/service/SplitSyncTaskTest.java index 005d7756e..348e8c815 100644 --- a/main/src/test/java/io/split/android/client/service/SplitSyncTaskTest.java +++ b/main/src/test/java/io/split/android/client/service/SplitSyncTaskTest.java @@ -260,12 +260,13 @@ public void splitsUpdatedIncludesMetadataWithUpdatedFlags() throws HttpFetcherEx verify(mEventsManager).notifyInternalEvent(eq(SplitInternalEvent.SPLITS_UPDATED), argThat(metadata -> { if (metadata == null) return false; SdkUpdateMetadata typedMeta = TypedTaskConverter.convertForSdkUpdate(metadata); - List flags = typedMeta.getUpdatedFlags(); - assertNotNull(flags); - assertEquals(3, flags.size()); - assertTrue(flags.contains("split1")); - assertTrue(flags.contains("split2")); - assertTrue(flags.contains("split3")); + List names = typedMeta.getNames(); + assertNotNull(names); + assertEquals(3, names.size()); + assertTrue(names.contains("split1")); + assertTrue(names.contains("split2")); + assertTrue(names.contains("split3")); + assertEquals(SdkUpdateMetadata.Type.FLAGS_UPDATE, typedMeta.getType()); return true; })); } @@ -288,9 +289,10 @@ public void splitsUpdatedIncludesEmptyMetadataWhenNoSplitsUpdated() throws HttpF verify(mEventsManager).notifyInternalEvent(eq(SplitInternalEvent.SPLITS_UPDATED), argThat(metadata -> { if (metadata == null) return false; SdkUpdateMetadata typedMeta = TypedTaskConverter.convertForSdkUpdate(metadata); - List flags = typedMeta.getUpdatedFlags(); - assertNotNull(flags); - assertTrue(flags.isEmpty()); + List names = typedMeta.getNames(); + assertNotNull(names); + assertTrue(names.isEmpty()); + assertEquals(SdkUpdateMetadata.Type.FLAGS_UPDATE, typedMeta.getType()); return true; })); } @@ -306,7 +308,7 @@ public void ruleBasedSegmentsUpdatedIsFiredWhenRbsChanged() { mTask.execute(); - verify(mEventsManager).notifyInternalEvent(eq(SplitInternalEvent.RULE_BASED_SEGMENTS_UPDATED)); + verify(mEventsManager).notifyInternalEvent(eq(SplitInternalEvent.RULE_BASED_SEGMENTS_UPDATED), any()); verify(mEventsManager).notifyInternalEvent(eq(SplitInternalEvent.TARGETING_RULES_SYNC_COMPLETE), any()); } diff --git a/main/src/test/java/io/split/android/client/service/SplitUpdateTaskTest.java b/main/src/test/java/io/split/android/client/service/SplitUpdateTaskTest.java index 97f976b04..906361b42 100644 --- a/main/src/test/java/io/split/android/client/service/SplitUpdateTaskTest.java +++ b/main/src/test/java/io/split/android/client/service/SplitUpdateTaskTest.java @@ -179,11 +179,12 @@ public void splitsUpdatedIncludesMetadataWithUpdatedFlags() { verify(mEventsManager).notifyInternalEvent(eq(SplitInternalEvent.SPLITS_UPDATED), argThat(metadata -> { if (metadata == null) return false; SdkUpdateMetadata typedMeta = TypedTaskConverter.convertForSdkUpdate(metadata); - List flags = typedMeta.getUpdatedFlags(); - assertNotNull(flags); - assertEquals(2, flags.size()); - assertTrue(flags.contains("flag1")); - assertTrue(flags.contains("flag2")); + List names = typedMeta.getNames(); + assertNotNull(names); + assertEquals(2, names.size()); + assertTrue(names.contains("flag1")); + assertTrue(names.contains("flag2")); + assertEquals(SdkUpdateMetadata.Type.FLAGS_UPDATE, typedMeta.getType()); return true; })); } diff --git a/main/src/test/java/io/split/android/client/service/rules/RuleBasedSegmentInPlaceUpdateTaskTest.java b/main/src/test/java/io/split/android/client/service/rules/RuleBasedSegmentInPlaceUpdateTaskTest.java index bd5013088..9c659a9ca 100644 --- a/main/src/test/java/io/split/android/client/service/rules/RuleBasedSegmentInPlaceUpdateTaskTest.java +++ b/main/src/test/java/io/split/android/client/service/rules/RuleBasedSegmentInPlaceUpdateTaskTest.java @@ -1,5 +1,7 @@ package io.split.android.client.service.rules; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; @@ -45,7 +47,7 @@ public void splitEventsManagerIsNotifiedWithUpdateEvent() { mTask.execute(); - verify(mEventsManager).notifyInternalEvent(SplitInternalEvent.RULE_BASED_SEGMENTS_UPDATED); + verify(mEventsManager).notifyInternalEvent(eq(SplitInternalEvent.RULE_BASED_SEGMENTS_UPDATED), any()); } @Test @@ -60,7 +62,7 @@ public void splitEventsManagerIsNotNotifiedWhenUpdateResultIsFalse() { mTask.execute(); - verify(mEventsManager, times(0)).notifyInternalEvent(SplitInternalEvent.RULE_BASED_SEGMENTS_UPDATED); + verify(mEventsManager, times(0)).notifyInternalEvent(eq(SplitInternalEvent.RULE_BASED_SEGMENTS_UPDATED), any()); } @Test From 45d4884b7788e8906446e2581d034b03d6f9a923 Mon Sep 17 00:00:00 2001 From: Gaston Thea Date: Wed, 7 Jan 2026 16:57:27 -0300 Subject: [PATCH 12/27] Style fixes --- .../events/SdkEventsIntegrationTest.java | 184 ++++++++---------- 1 file changed, 86 insertions(+), 98 deletions(-) diff --git a/main/src/androidTest/java/tests/integration/events/SdkEventsIntegrationTest.java b/main/src/androidTest/java/tests/integration/events/SdkEventsIntegrationTest.java index a1a2816ab..55c7e634c 100644 --- a/main/src/androidTest/java/tests/integration/events/SdkEventsIntegrationTest.java +++ b/main/src/androidTest/java/tests/integration/events/SdkEventsIntegrationTest.java @@ -971,6 +971,92 @@ public void onUpdate(SplitClient client, SdkUpdateMetadata metadata) { fixture.destroy(); } + + + /** + * Scenario: sdkUpdateMetadata contains Type.FLAGS_UPDATE for flags update + *

    + * Given sdkReady has already been emitted + * And a handler H is registered for sdkUpdate + * When a split update notification arrives via SSE + * Then sdkUpdate is emitted + * And handler H receives metadata with getType() returning Type.FLAGS_UPDATE + * And handler H receives metadata with getNames() containing the updated flag names + */ + @Test + public void sdkUpdateMetadataContainsTypeForFlagsUpdate() throws Exception { + TestClientFixture fixture = createStreamingClientAndWaitForReady(new Key("key_1")); + + AtomicReference receivedMetadata = new AtomicReference<>(); + CountDownLatch updateLatch = new CountDownLatch(1); + + fixture.client.addEventListener(new SdkEventListener() { + @Override + public void onUpdate(SplitClient client, SdkUpdateMetadata metadata) { + receivedMetadata.set(metadata); + updateLatch.countDown(); + } + }); + + fixture.pushSplitUpdate(); + + boolean updateFired = updateLatch.await(10, TimeUnit.SECONDS); + assertTrue("SDK_UPDATE should fire", updateFired); + + assertNotNull("Metadata should not be null", receivedMetadata.get()); + assertEquals("Type should be FLAGS_UPDATE", + SdkUpdateMetadata.Type.FLAGS_UPDATE, receivedMetadata.get().getType()); + + assertNotNull("Names should not be null", receivedMetadata.get().getNames()); + assertFalse("Names should not be empty", receivedMetadata.get().getNames().isEmpty()); + + fixture.destroy(); + } + + /** + * Scenario: sdkUpdateMetadata contains Type.SEGMENTS_UPDATE for rule-based segments update + *

    + * Given sdkReady has already been emitted + * And a handler H is registered for sdkUpdate + * When a rule-based segment update notification arrives via SSE + * Then sdkUpdate is emitted + * And handler H receives metadata with getType() returning Type.SEGMENTS_UPDATE + * And handler H receives metadata with getNames() containing the updated RBS names + *

    + * Note: SEGMENTS_UPDATE is for rule-based segments (RBS) ONLY, not for memberships. + */ + @Test + public void sdkUpdateMetadataContainsTypeForSegmentsUpdate() throws Exception { + TestClientFixture fixture = createStreamingClientWithRbsAndWaitForReady(new Key("key_1")); + + AtomicReference receivedMetadata = new AtomicReference<>(); + CountDownLatch updateLatch = new CountDownLatch(1); + + fixture.client.addEventListener(new SdkEventListener() { + @Override + public void onUpdate(SplitClient client, SdkUpdateMetadata metadata) { + receivedMetadata.set(metadata); + updateLatch.countDown(); + } + }); + + fixture.pushRbsUpdate(); + + boolean updateFired = updateLatch.await(10, TimeUnit.SECONDS); + assertTrue("SDK_UPDATE should fire for RBS update", updateFired); + + assertNotNull("Metadata should not be null", receivedMetadata.get()); + assertEquals("Type should be SEGMENTS_UPDATE", + SdkUpdateMetadata.Type.SEGMENTS_UPDATE, receivedMetadata.get().getType()); + + assertNotNull("Names should not be null", receivedMetadata.get().getNames()); + assertFalse("Names should not be empty", receivedMetadata.get().getNames().isEmpty()); + assertTrue("Names should contain rbs_test", + receivedMetadata.get().getNames().contains("rbs_test")); + + fixture.destroy(); + } + /** * Creates a client and waits for SDK_READY to fire. * Returns a TestClientFixture containing the factory, client, and ready latch. @@ -1308,104 +1394,6 @@ private void populateDatabaseWithCacheData(long timestamp) { mDatabase.mySegmentDao().update(segmentEntity2); } - // ======================================================================== - // Phase 1 TDD: Tests for SdkUpdateMetadata.Type enum - // ======================================================================== - - /** - * Scenario: sdkUpdateMetadata contains Type.FLAGS_UPDATE for flags update - *

    - * Given sdkReady has already been emitted - * And a handler H is registered for sdkUpdate - * When a split update notification arrives via SSE - * Then sdkUpdate is emitted - * And handler H receives metadata with getType() returning Type.FLAGS_UPDATE - * And handler H receives metadata with getNames() containing the updated flag names - */ - @Test - public void sdkUpdateMetadataContainsTypeForFlagsUpdate() throws Exception { - // Given: sdkReady has already been emitted (with streaming support) - TestClientFixture fixture = createStreamingClientAndWaitForReady(new Key("key_1")); - - AtomicReference receivedMetadata = new AtomicReference<>(); - CountDownLatch updateLatch = new CountDownLatch(1); - - fixture.client.addEventListener(new SdkEventListener() { - @Override - public void onUpdate(SplitClient client, SdkUpdateMetadata metadata) { - receivedMetadata.set(metadata); - updateLatch.countDown(); - } - }); - - // When: a split update notification arrives via SSE - fixture.pushSplitUpdate(); - - // Then: sdkUpdate is emitted - boolean updateFired = updateLatch.await(10, TimeUnit.SECONDS); - assertTrue("SDK_UPDATE should fire", updateFired); - - // And: metadata has getType() returning Type.FLAGS_UPDATE - assertNotNull("Metadata should not be null", receivedMetadata.get()); - assertEquals("Type should be FLAGS_UPDATE", - SdkUpdateMetadata.Type.FLAGS_UPDATE, receivedMetadata.get().getType()); - - // And: metadata has getNames() containing the updated flag names - assertNotNull("Names should not be null", receivedMetadata.get().getNames()); - assertFalse("Names should not be empty", receivedMetadata.get().getNames().isEmpty()); - - fixture.destroy(); - } - - /** - * Scenario: sdkUpdateMetadata contains Type.SEGMENTS_UPDATE for rule-based segments update - *

    - * Given sdkReady has already been emitted - * And a handler H is registered for sdkUpdate - * When a rule-based segment update notification arrives via SSE - * Then sdkUpdate is emitted - * And handler H receives metadata with getType() returning Type.SEGMENTS_UPDATE - * And handler H receives metadata with getNames() containing the updated RBS names - *

    - * Note: SEGMENTS_UPDATE is for rule-based segments (RBS) ONLY, not for memberships. - */ - @Test - public void sdkUpdateMetadataContainsTypeForSegmentsUpdate() throws Exception { - // Given: sdkReady has already been emitted (with streaming support and RBS in storage) - TestClientFixture fixture = createStreamingClientWithRbsAndWaitForReady(new Key("key_1")); - - AtomicReference receivedMetadata = new AtomicReference<>(); - CountDownLatch updateLatch = new CountDownLatch(1); - - fixture.client.addEventListener(new SdkEventListener() { - @Override - public void onUpdate(SplitClient client, SdkUpdateMetadata metadata) { - receivedMetadata.set(metadata); - updateLatch.countDown(); - } - }); - - // When: a rule-based segment update notification arrives via SSE - fixture.pushRbsUpdate(); - - // Then: sdkUpdate is emitted - boolean updateFired = updateLatch.await(10, TimeUnit.SECONDS); - assertTrue("SDK_UPDATE should fire for RBS update", updateFired); - - // And: metadata has getType() returning Type.SEGMENTS_UPDATE - assertNotNull("Metadata should not be null", receivedMetadata.get()); - assertEquals("Type should be SEGMENTS_UPDATE", - SdkUpdateMetadata.Type.SEGMENTS_UPDATE, receivedMetadata.get().getType()); - - // And: metadata has getNames() containing the updated RBS names - assertNotNull("Names should not be null", receivedMetadata.get().getNames()); - assertFalse("Names should not be empty", receivedMetadata.get().getNames().isEmpty()); - assertTrue("Names should contain rbs_test", - receivedMetadata.get().getNames().contains("rbs_test")); - - fixture.destroy(); - } - /** * Creates a streaming client with RBS data pre-populated and waits for SDK_READY. * Required for testing RBS in-place updates (RBS must exist in storage for instant update). From 783f72c5317cc73c64a96faaba7df9ea0decc2c0 Mon Sep 17 00:00:00 2001 From: Gaston Thea Date: Wed, 7 Jan 2026 17:39:14 -0300 Subject: [PATCH 13/27] Fix test --- .../io/split/android/client/service/SplitUpdateTaskTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/main/src/test/java/io/split/android/client/service/SplitUpdateTaskTest.java b/main/src/test/java/io/split/android/client/service/SplitUpdateTaskTest.java index 906361b42..9cdef917e 100644 --- a/main/src/test/java/io/split/android/client/service/SplitUpdateTaskTest.java +++ b/main/src/test/java/io/split/android/client/service/SplitUpdateTaskTest.java @@ -142,7 +142,7 @@ public void splitsUpdatedIsFiredWhenRbsDataChanged() { mTask.execute(); - verify(mEventsManager).notifyInternalEvent(eq(SplitInternalEvent.RULE_BASED_SEGMENTS_UPDATED)); + verify(mEventsManager).notifyInternalEvent(eq(SplitInternalEvent.RULE_BASED_SEGMENTS_UPDATED), any()); verify(mEventsManager).notifyInternalEvent(eq(SplitInternalEvent.TARGETING_RULES_SYNC_COMPLETE), any()); } From 3a423e4453f42a92822385055005b8157c428db1 Mon Sep 17 00:00:00 2001 From: Gaston Thea Date: Wed, 7 Jan 2026 18:01:48 -0300 Subject: [PATCH 14/27] Non null names in metadata --- .../android/client/events/SdkUpdateMetadata.java | 15 +++++++-------- .../client/events/SdkUpdateMetadataTest.java | 4 ++-- .../client/events/TypedTaskConversionTest.java | 2 +- 3 files changed, 10 insertions(+), 11 deletions(-) diff --git a/api/src/main/java/io/split/android/client/events/SdkUpdateMetadata.java b/api/src/main/java/io/split/android/client/events/SdkUpdateMetadata.java index e12a75be4..58d6741a5 100644 --- a/api/src/main/java/io/split/android/client/events/SdkUpdateMetadata.java +++ b/api/src/main/java/io/split/android/client/events/SdkUpdateMetadata.java @@ -1,7 +1,9 @@ package io.split.android.client.events; +import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import java.util.Collections; import java.util.List; /** @@ -23,9 +25,6 @@ public enum Type { /** * Rule-based segments were updated. *

    - * Note: This is for rule-based segments (RBS) ONLY, not for memberships - * (my segments / large segments). Memberships have their own internal event - * flow and don't emit SDK_UPDATE events with this metadata type. */ SEGMENTS_UPDATE } @@ -33,18 +32,18 @@ public enum Type { @Nullable private final Type mType; - @Nullable + @NonNull private final List mNames; /** * Creates a new SdkUpdateMetadata instance. * * @param type the type of update, or null if not available - * @param names the list of entity names that were updated, or null if not available + * @param names the list of entity names that were updated, or null to use an empty list */ public SdkUpdateMetadata(@Nullable Type type, @Nullable List names) { mType = type; - mNames = names; + mNames = names != null ? names : Collections.emptyList(); } /** @@ -63,9 +62,9 @@ public Type getType() { * For {@link Type#FLAGS_UPDATE}, this contains flag names. * For {@link Type#SEGMENTS_UPDATE}, this contains rule-based segment names. * - * @return the list of updated entity names, or null if not available + * @return the list of updated entity names, never null (empty list if none) */ - @Nullable + @NonNull public List getNames() { return mNames; } diff --git a/api/src/test/java/io/split/android/client/events/SdkUpdateMetadataTest.java b/api/src/test/java/io/split/android/client/events/SdkUpdateMetadataTest.java index 1a1a25abc..fadd2fe09 100644 --- a/api/src/test/java/io/split/android/client/events/SdkUpdateMetadataTest.java +++ b/api/src/test/java/io/split/android/client/events/SdkUpdateMetadataTest.java @@ -12,10 +12,10 @@ public class SdkUpdateMetadataTest { @Test - public void getNamesReturnsNullWhenConstructedWithNull() { + public void getNamesReturnsEmptyListWhenConstructedWithNull() { SdkUpdateMetadata metadata = new SdkUpdateMetadata(null, null); - assertNull(metadata.getNames()); + assertEquals(Collections.emptyList(), metadata.getNames()); } @Test diff --git a/events-domain/src/test/java/io/split/android/client/events/TypedTaskConversionTest.java b/events-domain/src/test/java/io/split/android/client/events/TypedTaskConversionTest.java index 381c006d0..b74fd2dc6 100644 --- a/events-domain/src/test/java/io/split/android/client/events/TypedTaskConversionTest.java +++ b/events-domain/src/test/java/io/split/android/client/events/TypedTaskConversionTest.java @@ -69,7 +69,7 @@ public void convertForSdkUpdateHandlesNullMetadata() { assertNotNull(converted); assertNull(converted.getType()); - assertNull(converted.getNames()); + assertTrue(converted.getNames().isEmpty()); } @Test From 83426cc00179a439a7e0d8ff7e1261f99be7f346 Mon Sep 17 00:00:00 2001 From: Gaston Thea Date: Thu, 8 Jan 2026 10:02:30 -0300 Subject: [PATCH 15/27] Fix tests --- .../rollout/RolloutCacheManagerIntegrationTest.java | 12 ++++-------- .../mysegments/MySegmentsStorageContainerImpl.java | 2 +- .../storage/mysegments/MySegmentsStorageImpl.java | 1 - 3 files changed, 5 insertions(+), 10 deletions(-) diff --git a/main/src/androidTest/java/tests/integration/rollout/RolloutCacheManagerIntegrationTest.java b/main/src/androidTest/java/tests/integration/rollout/RolloutCacheManagerIntegrationTest.java index ce8df8024..4555f0ef2 100644 --- a/main/src/androidTest/java/tests/integration/rollout/RolloutCacheManagerIntegrationTest.java +++ b/main/src/androidTest/java/tests/integration/rollout/RolloutCacheManagerIntegrationTest.java @@ -126,11 +126,9 @@ public void repeatedInitWithClearOnInitSetToTrueDoesNotClearIfMinDaysHasNotElaps assertEquals(8000L, initialChangeNumber); // values after clear - assertEquals(1, intermediateSegments.size()); - assertTrue(Json.fromJson(intermediateSegments.get(0).getSegmentList(), SegmentsChange.class).getSegments().isEmpty()); - assertEquals(1, intermediateLargeSegments.size()); + assertEquals(0, intermediateSegments.size()); + assertEquals(0, intermediateLargeSegments.size()); assertEquals(0, intermediateFlags.size()); - assertTrue(Json.fromJson(intermediateLargeSegments.get(0).getSegmentList(), SegmentsChange.class).getSegments().isEmpty()); assertEquals(-1, intermediateChangeNumber); // values after second init (values were reinserted into DB); no clear @@ -203,11 +201,9 @@ private void verify(SplitFactory factory, CountDownLatch readyLatch, List Date: Thu, 8 Jan 2026 10:13:11 -0300 Subject: [PATCH 16/27] Remove usage of public types in events domain --- .../client/events/metadata/EventMetadataHelpers.java | 6 ++---- .../android/client/events/metadata/MetadataKeys.java | 3 +++ .../client/events/metadata/EventMetadataHelpersTest.java | 8 ++------ 3 files changed, 7 insertions(+), 10 deletions(-) diff --git a/events-domain/src/main/java/io/split/android/client/events/metadata/EventMetadataHelpers.java b/events-domain/src/main/java/io/split/android/client/events/metadata/EventMetadataHelpers.java index 1571ccc7c..55283b9f5 100644 --- a/events-domain/src/main/java/io/split/android/client/events/metadata/EventMetadataHelpers.java +++ b/events-domain/src/main/java/io/split/android/client/events/metadata/EventMetadataHelpers.java @@ -6,8 +6,6 @@ import java.util.HashSet; import java.util.List; -import io.split.android.client.events.SdkUpdateMetadata; - /** * Helper class for creating {@link EventMetadata} instances. *

    @@ -27,7 +25,7 @@ private EventMetadataHelpers() { */ public static EventMetadata createUpdatedFlagsMetadata(List updatedFlagNames) { return new EventMetadataBuilder() - .put(MetadataKeys.TYPE, SdkUpdateMetadata.Type.FLAGS_UPDATE.name()) + .put(MetadataKeys.TYPE, MetadataKeys.TYPE_FLAGS_UPDATE) .put(MetadataKeys.NAMES, new ArrayList<>(new HashSet<>(updatedFlagNames))) .build(); } @@ -44,7 +42,7 @@ public static EventMetadata createUpdatedFlagsMetadata(List updatedFlagN */ public static EventMetadata createUpdatedSegmentsMetadata(List updatedSegmentNames) { return new EventMetadataBuilder() - .put(MetadataKeys.TYPE, SdkUpdateMetadata.Type.SEGMENTS_UPDATE.name()) + .put(MetadataKeys.TYPE, MetadataKeys.TYPE_SEGMENTS_UPDATE) .put(MetadataKeys.NAMES, new ArrayList<>(new HashSet<>(updatedSegmentNames))) .build(); } diff --git a/events-domain/src/main/java/io/split/android/client/events/metadata/MetadataKeys.java b/events-domain/src/main/java/io/split/android/client/events/metadata/MetadataKeys.java index d7a40d00e..b4763d1bc 100644 --- a/events-domain/src/main/java/io/split/android/client/events/metadata/MetadataKeys.java +++ b/events-domain/src/main/java/io/split/android/client/events/metadata/MetadataKeys.java @@ -18,6 +18,9 @@ private MetadataKeys() { */ static final String TYPE = "type"; + static final String TYPE_FLAGS_UPDATE = "FLAGS_UPDATE"; + static final String TYPE_SEGMENTS_UPDATE = "SEGMENTS_UPDATE"; + /** * Names of entities that changed in this update. *

    diff --git a/events-domain/src/test/java/io/split/android/client/events/metadata/EventMetadataHelpersTest.java b/events-domain/src/test/java/io/split/android/client/events/metadata/EventMetadataHelpersTest.java index 7f73303bf..9a3dcda84 100644 --- a/events-domain/src/test/java/io/split/android/client/events/metadata/EventMetadataHelpersTest.java +++ b/events-domain/src/test/java/io/split/android/client/events/metadata/EventMetadataHelpersTest.java @@ -9,8 +9,6 @@ import java.util.Arrays; import java.util.List; -import io.split.android.client.events.SdkUpdateMetadata; - public class EventMetadataHelpersTest { // Tests for createUpdatedFlagsMetadata @@ -20,9 +18,8 @@ public void createUpdatedFlagsMetadataContainsTypeAndNames() { List flags = Arrays.asList("flag1", "flag2", "flag3"); EventMetadata metadata = EventMetadataHelpers.createUpdatedFlagsMetadata(flags); - // Check type assertTrue(metadata.containsKey(MetadataKeys.TYPE)); - assertEquals(SdkUpdateMetadata.Type.FLAGS_UPDATE.name(), metadata.get(MetadataKeys.TYPE)); + assertEquals(MetadataKeys.TYPE_FLAGS_UPDATE, metadata.get(MetadataKeys.TYPE)); // Check names assertTrue(metadata.containsKey(MetadataKeys.NAMES)); @@ -40,9 +37,8 @@ public void createUpdatedSegmentsMetadataContainsTypeAndNames() { List segments = Arrays.asList("segment1", "segment2"); EventMetadata metadata = EventMetadataHelpers.createUpdatedSegmentsMetadata(segments); - // Check type assertTrue(metadata.containsKey(MetadataKeys.TYPE)); - assertEquals(SdkUpdateMetadata.Type.SEGMENTS_UPDATE.name(), metadata.get(MetadataKeys.TYPE)); + assertEquals(MetadataKeys.TYPE_SEGMENTS_UPDATE, metadata.get(MetadataKeys.TYPE)); // Check names assertTrue(metadata.containsKey(MetadataKeys.NAMES)); From cbc1a235f40ac28346953911b50f8a917d8c9967 Mon Sep 17 00:00:00 2001 From: Gaston Thea Date: Thu, 8 Jan 2026 11:09:48 -0300 Subject: [PATCH 17/27] Add missing tests --- .../events/metadata/EventMetadataHelpers.java | 4 -- .../events/SdkEventsIntegrationTest.java | 2 - .../client/service/SplitsSyncHelperTest.java | 65 +++++++++++++++++++ ...RuleBasedSegmentInPlaceUpdateTaskTest.java | 33 ++++++++++ 4 files changed, 98 insertions(+), 6 deletions(-) diff --git a/events-domain/src/main/java/io/split/android/client/events/metadata/EventMetadataHelpers.java b/events-domain/src/main/java/io/split/android/client/events/metadata/EventMetadataHelpers.java index 55283b9f5..4116de596 100644 --- a/events-domain/src/main/java/io/split/android/client/events/metadata/EventMetadataHelpers.java +++ b/events-domain/src/main/java/io/split/android/client/events/metadata/EventMetadataHelpers.java @@ -32,10 +32,6 @@ public static EventMetadata createUpdatedFlagsMetadata(List updatedFlagN /** * Creates metadata for SDK_UPDATE events when rule-based segments are updated. - *

    - * Note: This is for rule-based segments (RBS) ONLY, not for memberships - * (my segments / large segments). Memberships have their own internal event - * flow and don't emit SDK_UPDATE events with segment metadata. * * @param updatedSegmentNames the list of rule-based segment names that were updated * @return the event metadata with TYPE=SEGMENTS_UPDATE and NAMES containing the segment names diff --git a/main/src/androidTest/java/tests/integration/events/SdkEventsIntegrationTest.java b/main/src/androidTest/java/tests/integration/events/SdkEventsIntegrationTest.java index 55c7e634c..60bc0484a 100644 --- a/main/src/androidTest/java/tests/integration/events/SdkEventsIntegrationTest.java +++ b/main/src/androidTest/java/tests/integration/events/SdkEventsIntegrationTest.java @@ -971,8 +971,6 @@ public void onUpdate(SplitClient client, SdkUpdateMetadata metadata) { fixture.destroy(); } - - /** * Scenario: sdkUpdateMetadata contains Type.FLAGS_UPDATE for flags update *

    diff --git a/main/src/test/java/io/split/android/client/service/SplitsSyncHelperTest.java b/main/src/test/java/io/split/android/client/service/SplitsSyncHelperTest.java index fb195df7b..0f8105c7c 100644 --- a/main/src/test/java/io/split/android/client/service/SplitsSyncHelperTest.java +++ b/main/src/test/java/io/split/android/client/service/SplitsSyncHelperTest.java @@ -10,6 +10,7 @@ import static org.mockito.ArgumentMatchers.argThat; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.atLeastOnce; +import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.doThrow; import static org.mockito.Mockito.never; import static org.mockito.Mockito.times; @@ -33,6 +34,7 @@ import java.util.LinkedHashMap; import java.util.List; import java.util.Map; +import java.util.Set; import java.util.concurrent.TimeUnit; import io.split.android.client.dtos.Split; @@ -46,6 +48,7 @@ import io.split.android.client.service.http.HttpFetcher; import io.split.android.client.service.http.HttpFetcherException; import io.split.android.client.service.http.HttpStatus; +import io.split.android.client.service.rules.ProcessedRuleBasedSegmentChange; import io.split.android.client.service.rules.RuleBasedSegmentChangeProcessor; import io.split.android.client.service.splits.SplitChangeProcessor; import io.split.android.client.service.splits.SplitsSyncHelper; @@ -777,4 +780,66 @@ public void getLastUpdatedFlagNamesIncludesArchivedSplits() throws HttpFetcherEx assertEquals(1, result.size()); assertTrue(result.contains("archived_split")); } + + @Test + public void getLastUpdatedRbsNamesReturnsSegmentNamesAfterSync() throws HttpFetcherException { + RuleBasedSegment activeSegment = RuleBasedSegmentStorageImplTest.createRuleBasedSegment("active_segment"); + RuleBasedSegment archivedSegment = RuleBasedSegmentStorageImplTest.createRuleBasedSegment("archived_segment"); + SplitChange splitChange = SplitChange.create(-1, 100L, Collections.emptyList()); + RuleBasedSegmentChange rbsChange = RuleBasedSegmentChange.create(-1, 100L, Collections.singletonList(activeSegment)); + // Create ProcessedRuleBasedSegmentChange with both active and archived segments + ProcessedRuleBasedSegmentChange processedChange = new ProcessedRuleBasedSegmentChange( + Set.of(activeSegment), Set.of(archivedSegment), 100L, System.currentTimeMillis()); + + doReturn(processedChange).when(mRuleBasedSegmentChangeProcessor).process(any(List.class), anyLong()); + + when(mSplitsFetcher.execute(any(), any())) + .thenReturn(TargetingRulesChange.create(splitChange, rbsChange)) + .thenReturn(TargetingRulesChange.create(SplitChange.create(100L, 100L, Collections.emptyList()), RuleBasedSegmentChange.create(100L, 100L, Collections.emptyList()))); + when(mSplitsStorage.getTill()).thenReturn(-1L).thenReturn(100L); + when(mRuleBasedSegmentStorageProducer.getChangeNumber()).thenReturn(-1L).thenReturn(100L); + + mSplitsSyncHelper.sync(getSinceChangeNumbers(-1, -1L), false, false, ServiceConstants.ON_DEMAND_FETCH_BACKOFF_MAX_RETRIES); + + List result = mSplitsSyncHelper.getLastUpdatedRbsNames(); + assertEquals(2, result.size()); + assertTrue(result.contains("active_segment")); + assertTrue(result.contains("archived_segment")); + } + + @Test + public void getLastUpdatedRbsNamesReturnsEmptyListWhenNoSyncPerformed() { + List result = mSplitsSyncHelper.getLastUpdatedRbsNames(); + assertTrue(result.isEmpty()); + } + + @Test + public void extractRbsNamesReturnsActiveAndArchivedSegmentNames() { + RuleBasedSegment activeSegment = RuleBasedSegmentStorageImplTest.createRuleBasedSegment("active_segment"); + RuleBasedSegment archivedSegment = RuleBasedSegmentStorageImplTest.createRuleBasedSegment("archived_segment"); + ProcessedRuleBasedSegmentChange processedChange = new ProcessedRuleBasedSegmentChange( + Set.of(activeSegment), Set.of(archivedSegment), 100L, System.currentTimeMillis()); + + List result = SplitsSyncHelper.extractRbsNames(processedChange); + + assertEquals(2, result.size()); + assertTrue(result.contains("active_segment")); + assertTrue(result.contains("archived_segment")); + } + + @Test + public void extractRbsNamesReturnsEmptyListForNullChange() { + List result = SplitsSyncHelper.extractRbsNames(null); + assertTrue(result.isEmpty()); + } + + @Test + public void extractRbsNamesHandlesNullActiveAndArchivedSets() { + ProcessedRuleBasedSegmentChange processedChange = new ProcessedRuleBasedSegmentChange( + null, null, 100L, System.currentTimeMillis()); + + List result = SplitsSyncHelper.extractRbsNames(processedChange); + + assertTrue(result.isEmpty()); + } } diff --git a/main/src/test/java/io/split/android/client/service/rules/RuleBasedSegmentInPlaceUpdateTaskTest.java b/main/src/test/java/io/split/android/client/service/rules/RuleBasedSegmentInPlaceUpdateTaskTest.java index 9c659a9ca..713ba5cfa 100644 --- a/main/src/test/java/io/split/android/client/service/rules/RuleBasedSegmentInPlaceUpdateTaskTest.java +++ b/main/src/test/java/io/split/android/client/service/rules/RuleBasedSegmentInPlaceUpdateTaskTest.java @@ -1,6 +1,10 @@ package io.split.android.client.service.rules; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.argThat; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.times; @@ -14,11 +18,14 @@ import org.junit.Test; import java.util.Collections; +import java.util.List; import java.util.Set; import io.split.android.client.dtos.RuleBasedSegment; import io.split.android.client.events.ISplitEventsManager; +import io.split.android.client.events.SdkUpdateMetadata; import io.split.android.client.events.SplitInternalEvent; +import io.split.android.client.events.metadata.TypedTaskConverter; import io.split.android.client.storage.rbs.RuleBasedSegmentStorage; public class RuleBasedSegmentInPlaceUpdateTaskTest { @@ -91,6 +98,32 @@ public void updateIsCalledOnStorage() { verify(mRuleBasedSegmentStorage).update(Set.of(ruleBasedSegment), Set.of(), changeNumber, null); } + @Test + public void segmentsUpdatedIncludesMetadataWithActiveAndArchivedSegmentNames() { + RuleBasedSegment activeSegment = createRuleBasedSegment("active_segment"); + RuleBasedSegment archivedSegment = createRuleBasedSegment("archived_segment"); + long changeNumber = 123L; + + when(mChangeProcessor.process(activeSegment, changeNumber)).thenReturn( + new ProcessedRuleBasedSegmentChange(Set.of(activeSegment), Set.of(archivedSegment), changeNumber, System.currentTimeMillis())); + when(mRuleBasedSegmentStorage.update(Set.of(activeSegment), Set.of(archivedSegment), changeNumber, null)).thenReturn(true); + + mTask = getTask(activeSegment, changeNumber); + mTask.execute(); + + verify(mEventsManager).notifyInternalEvent(eq(SplitInternalEvent.RULE_BASED_SEGMENTS_UPDATED), argThat(metadata -> { + if (metadata == null) return false; + SdkUpdateMetadata typedMeta = TypedTaskConverter.convertForSdkUpdate(metadata); + List names = typedMeta.getNames(); + assertNotNull(names); + assertEquals(2, names.size()); + assertTrue(names.contains("active_segment")); + assertTrue(names.contains("archived_segment")); + assertEquals(SdkUpdateMetadata.Type.SEGMENTS_UPDATE, typedMeta.getType()); + return true; + })); + } + @NonNull private RuleBasedSegmentInPlaceUpdateTask getTask(RuleBasedSegment ruleBasedSegment, long changeNumber) { return new RuleBasedSegmentInPlaceUpdateTask(mRuleBasedSegmentStorage, From 29434229f3e8f1f042455c4522c6d7af23085441 Mon Sep 17 00:00:00 2001 From: Gaston Thea Date: Thu, 8 Jan 2026 11:32:03 -0300 Subject: [PATCH 18/27] Update test comments --- .../tests/integration/events/SdkEventsIntegrationTest.java | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/main/src/androidTest/java/tests/integration/events/SdkEventsIntegrationTest.java b/main/src/androidTest/java/tests/integration/events/SdkEventsIntegrationTest.java index 60bc0484a..179d1ddd2 100644 --- a/main/src/androidTest/java/tests/integration/events/SdkEventsIntegrationTest.java +++ b/main/src/androidTest/java/tests/integration/events/SdkEventsIntegrationTest.java @@ -1394,7 +1394,7 @@ private void populateDatabaseWithCacheData(long timestamp) { /** * Creates a streaming client with RBS data pre-populated and waits for SDK_READY. - * Required for testing RBS in-place updates (RBS must exist in storage for instant update). + * Pre-populates RBS change number so the test can verify in-place update behavior. */ private TestClientFixture createStreamingClientWithRbsAndWaitForReady(Key key) throws InterruptedException, IOException { // Pre-populate RBS in storage so in-place update can work @@ -1420,11 +1420,10 @@ public void onPostExecution(SplitClient client) { } /** - * Populates the database with RBS data for instant update testing. + * Populates the database with RBS change number for instant update testing. */ private void populateDatabaseWithRbsData() { - // Need to populate the RBS storage with the segment that will be updated - // so that in-place update can work (referenced RBS must exist) + // Set RBS change number so streaming notifications trigger in-place updates mDatabase.generalInfoDao().update(new GeneralInfoEntity("rbsChangeNumber", 1000L)); } } From 394180476b48d0d831418cfb22be107893a207b3 Mon Sep 17 00:00:00 2001 From: Gaston Thea Date: Thu, 8 Jan 2026 18:45:36 -0300 Subject: [PATCH 19/27] Segments updated --- .../java/helper/IntegrationHelper.java | 42 ++++++ .../events/SdkEventsIntegrationTest.java | 130 ++++++++++++++++++ .../mysegments/MySegmentsSyncTask.java | 35 ++++- .../mysegments/MySegmentsUpdateTask.java | 8 +- 4 files changed, 211 insertions(+), 4 deletions(-) diff --git a/main/src/androidTest/java/helper/IntegrationHelper.java b/main/src/androidTest/java/helper/IntegrationHelper.java index 40062cd6d..679743ecb 100644 --- a/main/src/androidTest/java/helper/IntegrationHelper.java +++ b/main/src/androidTest/java/helper/IntegrationHelper.java @@ -343,6 +343,48 @@ public static String rbsChange(String changeNumber, String previousChangeNumber, "data: {\"id\":\"1111\",\"clientId\":\"pri:ODc1NjQyNzY1\",\"timestamp\":" + System.currentTimeMillis() + ",\"encoding\":\"json\",\"channel\":\"xxxx_xxxx_flags\",\"data\":\"{\\\"type\\\":\\\"RB_SEGMENT_UPDATE\\\",\\\"changeNumber\\\":" + changeNumber + ",\\\"pcn\\\":" + previousChangeNumber + ",\\\"c\\\":" + compressionType + ",\\\"d\\\":\\\"" + compressedPayload + "\\\"}\"}\n\n"; } + /** + * Creates a membership (my segments) update SSE notification. + * Uses KEY_LIST strategy (u=2) with SEGMENT_REMOVAL strategy (u=3) for testing. + * + * @param segmentNames the segment names to include in the notification + * @param changeNumber the change number for this update + * @return SSE formatted message + */ + public static String membershipSegmentsUpdate(String[] segmentNames, long changeNumber) { + StringBuilder names = new StringBuilder(); + for (int i = 0; i < segmentNames.length; i++) { + if (i > 0) names.append(","); + names.append("\\\\\\\"").append(segmentNames[i]).append("\\\\\\\""); + } + // u=3 is SEGMENT_REMOVAL strategy which triggers in-place update + String data = "{\\\"type\\\":\\\"MEMBERSHIPS_MS_UPDATE\\\",\\\"u\\\":3,\\\"c\\\":0,\\\"d\\\":\\\"\\\",\\\"n\\\":[" + names + "],\\\"cn\\\":" + changeNumber + "}"; + return "id: membership123\n" + + "event: message\n" + + "data: {\"id\":\"mem1\",\"clientId\":\"pri:ODc1NjQyNzY1\",\"timestamp\":" + System.currentTimeMillis() + ",\"encoding\":\"json\",\"channel\":\"xxxx_xxxx_mySegments\",\"data\":\"" + data + "\"}\n\n"; + } + + /** + * Creates a large segments update SSE notification. + * Uses SEGMENT_REMOVAL strategy (u=3) for testing. + * + * @param segmentNames the large segment names to include in the notification + * @param changeNumber the change number for this update + * @return SSE formatted message + */ + public static String membershipLargeSegmentsUpdate(String[] segmentNames, long changeNumber) { + StringBuilder names = new StringBuilder(); + for (int i = 0; i < segmentNames.length; i++) { + if (i > 0) names.append(","); + names.append("\\\\\\\"").append(segmentNames[i]).append("\\\\\\\""); + } + // u=3 is SEGMENT_REMOVAL strategy which triggers in-place update + String data = "{\\\"type\\\":\\\"MEMBERSHIPS_LS_UPDATE\\\",\\\"u\\\":3,\\\"c\\\":0,\\\"d\\\":\\\"\\\",\\\"n\\\":[" + names + "],\\\"cn\\\":" + changeNumber + "}"; + return "id: largeseg123\n" + + "event: message\n" + + "data: {\"id\":\"ls1\",\"clientId\":\"pri:ODc1NjQyNzY1\",\"timestamp\":" + System.currentTimeMillis() + ",\"encoding\":\"json\",\"channel\":\"xxxx_xxxx_mySegments\",\"data\":\"" + data + "\"}\n\n"; + } + public static String loadSplitChanges(Context context, String fileName) { String change = getFileContentsAsString(context, fileName); TargetingRulesChange targetingRulesChange = Json.fromJson(change, TargetingRulesChange.class); diff --git a/main/src/androidTest/java/tests/integration/events/SdkEventsIntegrationTest.java b/main/src/androidTest/java/tests/integration/events/SdkEventsIntegrationTest.java index 179d1ddd2..f4b473740 100644 --- a/main/src/androidTest/java/tests/integration/events/SdkEventsIntegrationTest.java +++ b/main/src/androidTest/java/tests/integration/events/SdkEventsIntegrationTest.java @@ -1055,6 +1055,100 @@ public void onUpdate(SplitClient client, SdkUpdateMetadata metadata) { fixture.destroy(); } + /** + * Scenario: sdkUpdateMetadata contains Type.SEGMENTS_UPDATE for membership segments update + *

    + * Given sdkReady has already been emitted + * And the client has segments in storage + * And a handler H is registered for sdkUpdate + * When a membership segments update notification arrives via SSE + * Then sdkUpdate is emitted + * And handler H receives metadata with getType() returning Type.SEGMENTS_UPDATE + * And handler H receives metadata with getNames() containing the updated segment names + */ + @Test + public void sdkUpdateMetadataContainsTypeForMembershipSegmentsUpdate() throws Exception { + // Pre-populate with segments so removal will trigger an update + populateDatabaseWithMembershipData(); + + TestClientFixture fixture = createStreamingClientAndWaitForReady(new Key("key_1")); + + AtomicReference receivedMetadata = new AtomicReference<>(); + CountDownLatch updateLatch = new CountDownLatch(1); + + fixture.client.addEventListener(new SdkEventListener() { + @Override + public void onUpdate(SplitClient client, SdkUpdateMetadata metadata) { + receivedMetadata.set(metadata); + updateLatch.countDown(); + } + }); + + // Push membership segments update (removal of segment1) + fixture.pushMembershipSegmentsUpdate(new String[]{"segment1"}, 2000L); + + boolean updateFired = updateLatch.await(10, TimeUnit.SECONDS); + assertTrue("SDK_UPDATE should fire for membership segments update", updateFired); + + assertNotNull("Metadata should not be null", receivedMetadata.get()); + assertEquals("Type should be SEGMENTS_UPDATE", + SdkUpdateMetadata.Type.SEGMENTS_UPDATE, receivedMetadata.get().getType()); + + assertNotNull("Names should not be null", receivedMetadata.get().getNames()); + assertFalse("Names should not be empty", receivedMetadata.get().getNames().isEmpty()); + assertTrue("Names should contain segment1", + receivedMetadata.get().getNames().contains("segment1")); + + fixture.destroy(); + } + + /** + * Scenario: sdkUpdateMetadata contains Type.SEGMENTS_UPDATE for large segments update + *

    + * Given sdkReady has already been emitted + * And the client has large segments in storage + * And a handler H is registered for sdkUpdate + * When a large segments update notification arrives via SSE + * Then sdkUpdate is emitted + * And handler H receives metadata with getType() returning Type.SEGMENTS_UPDATE + * And handler H receives metadata with getNames() containing the updated large segment names + */ + @Test + public void sdkUpdateMetadataContainsTypeForLargeSegmentsUpdate() throws Exception { + // Pre-populate with large segments so removal will trigger an update + populateDatabaseWithLargeSegmentData(); + + TestClientFixture fixture = createStreamingClientAndWaitForReady(new Key("key_1")); + + AtomicReference receivedMetadata = new AtomicReference<>(); + CountDownLatch updateLatch = new CountDownLatch(1); + + fixture.client.addEventListener(new SdkEventListener() { + @Override + public void onUpdate(SplitClient client, SdkUpdateMetadata metadata) { + receivedMetadata.set(metadata); + updateLatch.countDown(); + } + }); + + // Push large segments update (removal of large_segment1) + fixture.pushMembershipLargeSegmentsUpdate(new String[]{"large_segment1"}, 2000L); + + boolean updateFired = updateLatch.await(10, TimeUnit.SECONDS); + assertTrue("SDK_UPDATE should fire for large segments update", updateFired); + + assertNotNull("Metadata should not be null", receivedMetadata.get()); + assertEquals("Type should be SEGMENTS_UPDATE", + SdkUpdateMetadata.Type.SEGMENTS_UPDATE, receivedMetadata.get().getType()); + + assertNotNull("Names should not be null", receivedMetadata.get().getNames()); + assertFalse("Names should not be empty", receivedMetadata.get().getNames().isEmpty()); + assertTrue("Names should contain large_segment1", + receivedMetadata.get().getNames().contains("large_segment1")); + + fixture.destroy(); + } + /** * Creates a client and waits for SDK_READY to fire. * Returns a TestClientFixture containing the factory, client, and ready latch. @@ -1313,6 +1407,18 @@ void pushRbsUpdate(String changeNumber, String previousChangeNumber) { } } + void pushMembershipSegmentsUpdate(String[] segmentNames, long changeNumber) { + if (streamingData != null) { + pushMessage(streamingData, IntegrationHelper.membershipSegmentsUpdate(segmentNames, changeNumber)); + } + } + + void pushMembershipLargeSegmentsUpdate(String[] segmentNames, long changeNumber) { + if (streamingData != null) { + pushMessage(streamingData, IntegrationHelper.membershipLargeSegmentsUpdate(segmentNames, changeNumber)); + } + } + void destroy() { factory.destroy(); } @@ -1426,4 +1532,28 @@ private void populateDatabaseWithRbsData() { // Set RBS change number so streaming notifications trigger in-place updates mDatabase.generalInfoDao().update(new GeneralInfoEntity("rbsChangeNumber", 1000L)); } + + /** + * Populates the database with membership segments for testing segment removal updates. + */ + private void populateDatabaseWithMembershipData() { + // Populate segments for key_1 with segment1 so removal triggers update + MySegmentEntity segmentEntity = new MySegmentEntity(); + segmentEntity.setUserKey("key_1"); + segmentEntity.setSegmentList("{\"k\":[{\"n\":\"segment1\"},{\"n\":\"segment2\"}],\"cn\":1000}"); + segmentEntity.setUpdatedAt(System.currentTimeMillis() / 1000); + mDatabase.mySegmentDao().update(segmentEntity); + } + + /** + * Populates the database with large segments for testing large segment removal updates. + */ + private void populateDatabaseWithLargeSegmentData() { + // Populate large segments for key_1 with large_segment1 so removal triggers update + MySegmentEntity largeSegmentEntity = new MySegmentEntity(); + largeSegmentEntity.setUserKey("key_1_large"); + largeSegmentEntity.setSegmentList("{\"k\":[{\"n\":\"large_segment1\"},{\"n\":\"large_segment2\"}],\"cn\":1000}"); + largeSegmentEntity.setUpdatedAt(System.currentTimeMillis() / 1000); + mDatabase.myLargeSegmentDao().update(largeSegmentEntity); + } } diff --git a/main/src/main/java/io/split/android/client/service/mysegments/MySegmentsSyncTask.java b/main/src/main/java/io/split/android/client/service/mysegments/MySegmentsSyncTask.java index a5f0759bd..91d8f2e84 100644 --- a/main/src/main/java/io/split/android/client/service/mysegments/MySegmentsSyncTask.java +++ b/main/src/main/java/io/split/android/client/service/mysegments/MySegmentsSyncTask.java @@ -10,15 +10,18 @@ import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; +import java.util.HashSet; import java.util.List; import java.util.Map; +import java.util.Set; import java.util.concurrent.TimeUnit; import io.split.android.client.dtos.AllSegmentsChange; import io.split.android.client.dtos.SegmentsChange; -import io.split.android.client.events.SplitEvent; import io.split.android.client.events.SplitEventsManager; import io.split.android.client.events.SplitInternalEvent; +import io.split.android.client.events.metadata.EventMetadata; +import io.split.android.client.events.metadata.EventMetadataHelpers; import io.split.android.client.network.SplitHttpHeadersBuilder; import io.split.android.client.service.ServiceConstants; import io.split.android.client.service.executor.SplitTask; @@ -273,12 +276,16 @@ private void fireMySegmentsUpdatedIfNeeded(UpdateSegmentsResult segmentsResult, if (segmentsHaveChanged) { Logger.v("New segments: " + segmentsResult.newSegments); - mEventsManager.notifyInternalEvent(mUpdateEvent); + List changedSegmentNames = computeChangedSegmentNames(segmentsResult); + EventMetadata metadata = EventMetadataHelpers.createUpdatedSegmentsMetadata(changedSegmentNames); + mEventsManager.notifyInternalEvent(mUpdateEvent, metadata); } if (largeSegmentsHaveChanged) { Logger.v("New large segments: " + largeSegmentsResult.newSegments); - mEventsManager.notifyInternalEvent(SplitInternalEvent.MY_LARGE_SEGMENTS_UPDATED); + List changedLargeSegmentNames = computeChangedSegmentNames(largeSegmentsResult); + EventMetadata metadata = EventMetadataHelpers.createUpdatedSegmentsMetadata(changedLargeSegmentNames); + mEventsManager.notifyInternalEvent(SplitInternalEvent.MY_LARGE_SEGMENTS_UPDATED, metadata); } // Fire sync complete AFTER update events. This ensures SDK_READY triggers after @@ -287,6 +294,28 @@ private void fireMySegmentsUpdatedIfNeeded(UpdateSegmentsResult segmentsResult, mEventsManager.notifyInternalEvent(SplitInternalEvent.MEMBERSHIPS_SYNC_COMPLETE); } + /** + * Computes the set of changed segment names (added + removed) between old and new segments. + */ + private List computeChangedSegmentNames(UpdateSegmentsResult result) { + Set oldSet = new HashSet<>(result.oldSegments); + Set newSet = new HashSet<>(result.newSegments); + + // Added segments: in new but not in old + Set added = new HashSet<>(newSet); + added.removeAll(oldSet); + + // Removed segments: in old but not in new + Set removed = new HashSet<>(oldSet); + removed.removeAll(newSet); + + // Combined changed segments + Set changed = new HashSet<>(added); + changed.addAll(removed); + + return new ArrayList<>(changed); + } + private static class UpdateSegmentsResult { public final List oldSegments; public final List newSegments; diff --git a/main/src/main/java/io/split/android/client/service/mysegments/MySegmentsUpdateTask.java b/main/src/main/java/io/split/android/client/service/mysegments/MySegmentsUpdateTask.java index 3f8dc1260..39fa5eebc 100644 --- a/main/src/main/java/io/split/android/client/service/mysegments/MySegmentsUpdateTask.java +++ b/main/src/main/java/io/split/android/client/service/mysegments/MySegmentsUpdateTask.java @@ -4,11 +4,15 @@ import androidx.annotation.NonNull; +import java.util.ArrayList; +import java.util.List; import java.util.Set; import io.split.android.client.dtos.SegmentsChange; import io.split.android.client.events.SplitEventsManager; import io.split.android.client.events.SplitInternalEvent; +import io.split.android.client.events.metadata.EventMetadata; +import io.split.android.client.events.metadata.EventMetadataHelpers; import io.split.android.client.service.executor.SplitTask; import io.split.android.client.service.executor.SplitTaskExecutionInfo; import io.split.android.client.service.executor.SplitTaskType; @@ -96,7 +100,9 @@ public SplitTaskExecutionInfo remove() { private void updateAndNotify(Set segments) { mMySegmentsStorage.set(SegmentsChange.create(segments, mChangeNumber)); - mEventsManager.notifyInternalEvent(mUpdateEvent); + List changedSegmentNames = new ArrayList<>(mSegmentNames); + EventMetadata metadata = EventMetadataHelpers.createUpdatedSegmentsMetadata(changedSegmentNames); + mEventsManager.notifyInternalEvent(mUpdateEvent, metadata); } private void logError(String message) { From 15a9d999f541110c4234b9c13d1ad2c29246d5ce Mon Sep 17 00:00:00 2001 From: Gaston Thea Date: Fri, 9 Jan 2026 09:33:05 -0300 Subject: [PATCH 20/27] Fix tests --- .../java/helper/IntegrationHelper.java | 42 ---- .../events/SdkEventsIntegrationTest.java | 191 ++++++++++-------- .../android/client/EvaluationResult.java | 1 + 3 files changed, 105 insertions(+), 129 deletions(-) diff --git a/main/src/androidTest/java/helper/IntegrationHelper.java b/main/src/androidTest/java/helper/IntegrationHelper.java index 679743ecb..40062cd6d 100644 --- a/main/src/androidTest/java/helper/IntegrationHelper.java +++ b/main/src/androidTest/java/helper/IntegrationHelper.java @@ -343,48 +343,6 @@ public static String rbsChange(String changeNumber, String previousChangeNumber, "data: {\"id\":\"1111\",\"clientId\":\"pri:ODc1NjQyNzY1\",\"timestamp\":" + System.currentTimeMillis() + ",\"encoding\":\"json\",\"channel\":\"xxxx_xxxx_flags\",\"data\":\"{\\\"type\\\":\\\"RB_SEGMENT_UPDATE\\\",\\\"changeNumber\\\":" + changeNumber + ",\\\"pcn\\\":" + previousChangeNumber + ",\\\"c\\\":" + compressionType + ",\\\"d\\\":\\\"" + compressedPayload + "\\\"}\"}\n\n"; } - /** - * Creates a membership (my segments) update SSE notification. - * Uses KEY_LIST strategy (u=2) with SEGMENT_REMOVAL strategy (u=3) for testing. - * - * @param segmentNames the segment names to include in the notification - * @param changeNumber the change number for this update - * @return SSE formatted message - */ - public static String membershipSegmentsUpdate(String[] segmentNames, long changeNumber) { - StringBuilder names = new StringBuilder(); - for (int i = 0; i < segmentNames.length; i++) { - if (i > 0) names.append(","); - names.append("\\\\\\\"").append(segmentNames[i]).append("\\\\\\\""); - } - // u=3 is SEGMENT_REMOVAL strategy which triggers in-place update - String data = "{\\\"type\\\":\\\"MEMBERSHIPS_MS_UPDATE\\\",\\\"u\\\":3,\\\"c\\\":0,\\\"d\\\":\\\"\\\",\\\"n\\\":[" + names + "],\\\"cn\\\":" + changeNumber + "}"; - return "id: membership123\n" + - "event: message\n" + - "data: {\"id\":\"mem1\",\"clientId\":\"pri:ODc1NjQyNzY1\",\"timestamp\":" + System.currentTimeMillis() + ",\"encoding\":\"json\",\"channel\":\"xxxx_xxxx_mySegments\",\"data\":\"" + data + "\"}\n\n"; - } - - /** - * Creates a large segments update SSE notification. - * Uses SEGMENT_REMOVAL strategy (u=3) for testing. - * - * @param segmentNames the large segment names to include in the notification - * @param changeNumber the change number for this update - * @return SSE formatted message - */ - public static String membershipLargeSegmentsUpdate(String[] segmentNames, long changeNumber) { - StringBuilder names = new StringBuilder(); - for (int i = 0; i < segmentNames.length; i++) { - if (i > 0) names.append(","); - names.append("\\\\\\\"").append(segmentNames[i]).append("\\\\\\\""); - } - // u=3 is SEGMENT_REMOVAL strategy which triggers in-place update - String data = "{\\\"type\\\":\\\"MEMBERSHIPS_LS_UPDATE\\\",\\\"u\\\":3,\\\"c\\\":0,\\\"d\\\":\\\"\\\",\\\"n\\\":[" + names + "],\\\"cn\\\":" + changeNumber + "}"; - return "id: largeseg123\n" + - "event: message\n" + - "data: {\"id\":\"ls1\",\"clientId\":\"pri:ODc1NjQyNzY1\",\"timestamp\":" + System.currentTimeMillis() + ",\"encoding\":\"json\",\"channel\":\"xxxx_xxxx_mySegments\",\"data\":\"" + data + "\"}\n\n"; - } - public static String loadSplitChanges(Context context, String fileName) { String change = getFileContentsAsString(context, fileName); TargetingRulesChange targetingRulesChange = Json.fromJson(change, TargetingRulesChange.class); diff --git a/main/src/androidTest/java/tests/integration/events/SdkEventsIntegrationTest.java b/main/src/androidTest/java/tests/integration/events/SdkEventsIntegrationTest.java index f4b473740..206309fd9 100644 --- a/main/src/androidTest/java/tests/integration/events/SdkEventsIntegrationTest.java +++ b/main/src/androidTest/java/tests/integration/events/SdkEventsIntegrationTest.java @@ -43,10 +43,12 @@ import io.split.android.client.events.SplitEventTask; import io.split.android.client.network.HttpMethod; import io.split.android.client.storage.db.GeneralInfoEntity; +import io.split.android.client.storage.db.MyLargeSegmentEntity; import io.split.android.client.storage.db.MySegmentEntity; import io.split.android.client.storage.db.SplitEntity; import io.split.android.client.storage.db.SplitRoomDatabase; import io.split.android.client.utils.logger.Logger; +import io.split.android.client.utils.logger.SplitLogLevel; import okhttp3.mockwebserver.Dispatcher; import okhttp3.mockwebserver.MockResponse; import okhttp3.mockwebserver.MockWebServer; @@ -1056,86 +1058,129 @@ public void onUpdate(SplitClient client, SdkUpdateMetadata metadata) { } /** - * Scenario: sdkUpdateMetadata contains Type.SEGMENTS_UPDATE for membership segments update + * Scenario: sdkUpdateMetadata contains Type.SEGMENTS_UPDATE for membership segments update (polling) *

    * Given sdkReady has already been emitted - * And the client has segments in storage * And a handler H is registered for sdkUpdate - * When a membership segments update notification arrives via SSE + * When segments change via polling (server returns different segments) * Then sdkUpdate is emitted * And handler H receives metadata with getType() returning Type.SEGMENTS_UPDATE - * And handler H receives metadata with getNames() containing the updated segment names + * And handler H receives metadata with getNames() containing the changed segment names */ @Test public void sdkUpdateMetadataContainsTypeForMembershipSegmentsUpdate() throws Exception { - // Pre-populate with segments so removal will trigger an update - populateDatabaseWithMembershipData(); - - TestClientFixture fixture = createStreamingClientAndWaitForReady(new Key("key_1")); - - AtomicReference receivedMetadata = new AtomicReference<>(); - CountDownLatch updateLatch = new CountDownLatch(1); - - fixture.client.addEventListener(new SdkEventListener() { - @Override - public void onUpdate(SplitClient client, SdkUpdateMetadata metadata) { - receivedMetadata.set(metadata); - updateLatch.countDown(); - } - }); - - // Push membership segments update (removal of segment1) - fixture.pushMembershipSegmentsUpdate(new String[]{"segment1"}, 2000L); - - boolean updateFired = updateLatch.await(10, TimeUnit.SECONDS); - assertTrue("SDK_UPDATE should fire for membership segments update", updateFired); - - assertNotNull("Metadata should not be null", receivedMetadata.get()); - assertEquals("Type should be SEGMENTS_UPDATE", - SdkUpdateMetadata.Type.SEGMENTS_UPDATE, receivedMetadata.get().getType()); - - assertNotNull("Names should not be null", receivedMetadata.get().getNames()); - assertFalse("Names should not be empty", receivedMetadata.get().getNames().isEmpty()); - assertTrue("Names should contain segment1", - receivedMetadata.get().getNames().contains("segment1")); - - fixture.destroy(); + verifySdkUpdateForSegmentsPolling( + // Initial sync: segment1, segment2 + "{\"ms\":{\"k\":[{\"n\":\"segment1\"},{\"n\":\"segment2\"}],\"cn\":1000},\"ls\":{\"k\":[],\"cn\":1000}}", + // Polling: segment1 removed, segment3 added + "{\"ms\":{\"k\":[{\"n\":\"segment2\"},{\"n\":\"segment3\"}],\"cn\":2000},\"ls\":{\"k\":[],\"cn\":1000}}", + "segment1", "segment3" + ); } /** - * Scenario: sdkUpdateMetadata contains Type.SEGMENTS_UPDATE for large segments update + * Scenario: sdkUpdateMetadata contains Type.SEGMENTS_UPDATE for large segments update (polling) *

    * Given sdkReady has already been emitted - * And the client has large segments in storage * And a handler H is registered for sdkUpdate - * When a large segments update notification arrives via SSE + * When large segments change via polling (server returns different large segments) * Then sdkUpdate is emitted * And handler H receives metadata with getType() returning Type.SEGMENTS_UPDATE - * And handler H receives metadata with getNames() containing the updated large segment names + * And handler H receives metadata with getNames() containing the changed large segment names */ @Test public void sdkUpdateMetadataContainsTypeForLargeSegmentsUpdate() throws Exception { - // Pre-populate with large segments so removal will trigger an update - populateDatabaseWithLargeSegmentData(); + verifySdkUpdateForSegmentsPolling( + // Initial sync: large_segment1, large_segment2 + "{\"ms\":{\"k\":[],\"cn\":1000},\"ls\":{\"k\":[{\"n\":\"large_segment1\"},{\"n\":\"large_segment2\"}],\"cn\":1000}}", + // Polling: large_segment1 removed, large_segment3 added + "{\"ms\":{\"k\":[],\"cn\":1000},\"ls\":{\"k\":[{\"n\":\"large_segment2\"},{\"n\":\"large_segment3\"}],\"cn\":2000}}", + "large_segment1", "large_segment3" + ); + } - TestClientFixture fixture = createStreamingClientAndWaitForReady(new Key("key_1")); + /** + * Helper method to verify SDK_UPDATE with SEGMENTS_UPDATE type is emitted when segments change via polling. + * + * @param initialResponse the memberships response for initial sync + * @param pollingResponse the memberships response for polling (with changed segments) + * @param expectedSegmentNames segment names expected in the metadata (removed or added) + */ + private void verifySdkUpdateForSegmentsPolling(String initialResponse, String pollingResponse, + String... expectedSegmentNames) throws Exception { + AtomicInteger membershipsHitCount = new AtomicInteger(0); + + final Dispatcher pollingDispatcher = new Dispatcher() { + @Override + public MockResponse dispatch(RecordedRequest request) { + final String path = request.getPath(); + if (path.contains("/" + IntegrationHelper.ServicePath.MEMBERSHIPS)) { + int count = membershipsHitCount.incrementAndGet(); + if (count <= 1) { + return new MockResponse().setResponseCode(200).setBody(initialResponse); + } else { + return new MockResponse().setResponseCode(200).setBody(pollingResponse); + } + } else if (path.contains("/splitChanges")) { + return new MockResponse().setResponseCode(200) + .setBody(IntegrationHelper.emptyTargetingRulesChanges(1000, 1000)); + } else if (path.contains("/testImpressions/bulk")) { + return new MockResponse().setResponseCode(200); + } + return new MockResponse().setResponseCode(404); + } + }; + mWebServer.setDispatcher(pollingDispatcher); + + SplitClientConfig config = new TestableSplitConfigBuilder() + .serviceEndpoints(endpoints()) + .ready(30000) + .featuresRefreshRate(999999) + .segmentsRefreshRate(3) + .impressionsRefreshRate(999999) + .streamingEnabled(false) + .trafficType("account") + .build(); + + SplitFactory factory = buildFactory(config); + SplitClient client = factory.client(); + + CountDownLatch readyLatch = new CountDownLatch(1); + client.on(SplitEvent.SDK_READY, new SplitEventTask() { + @Override + public void onPostExecution(SplitClient c) { + readyLatch.countDown(); + } + }); + assertTrue("SDK_READY should fire", readyLatch.await(10, TimeUnit.SECONDS)); AtomicReference receivedMetadata = new AtomicReference<>(); - CountDownLatch updateLatch = new CountDownLatch(1); + AtomicInteger legacyHandlerCount = new AtomicInteger(0); + CountDownLatch updateLatch = new CountDownLatch(2); // Wait for both handlers - fixture.client.addEventListener(new SdkEventListener() { + // Register new API handler (addEventListener) + client.addEventListener(new SdkEventListener() { @Override - public void onUpdate(SplitClient client, SdkUpdateMetadata metadata) { + public void onUpdate(SplitClient c, SdkUpdateMetadata metadata) { receivedMetadata.set(metadata); updateLatch.countDown(); } }); - // Push large segments update (removal of large_segment1) - fixture.pushMembershipLargeSegmentsUpdate(new String[]{"large_segment1"}, 2000L); + // Register legacy API handler (client.on) + client.on(SplitEvent.SDK_UPDATE, new SplitEventTask() { + @Override + public void onPostExecution(SplitClient c) { + legacyHandlerCount.incrementAndGet(); + updateLatch.countDown(); + } + }); boolean updateFired = updateLatch.await(10, TimeUnit.SECONDS); - assertTrue("SDK_UPDATE should fire for large segments update", updateFired); + assertTrue("SDK_UPDATE should fire when segments change via polling. Hit count: " + membershipsHitCount.get(), updateFired); + + // Verify legacy API was also triggered + assertEquals("Legacy API (client.on) should be triggered", 1, legacyHandlerCount.get()); assertNotNull("Metadata should not be null", receivedMetadata.get()); assertEquals("Type should be SEGMENTS_UPDATE", @@ -1143,10 +1188,17 @@ public void onUpdate(SplitClient client, SdkUpdateMetadata metadata) { assertNotNull("Names should not be null", receivedMetadata.get().getNames()); assertFalse("Names should not be empty", receivedMetadata.get().getNames().isEmpty()); - assertTrue("Names should contain large_segment1", - receivedMetadata.get().getNames().contains("large_segment1")); - fixture.destroy(); + boolean containsExpected = false; + for (String expectedName : expectedSegmentNames) { + if (receivedMetadata.get().getNames().contains(expectedName)) { + containsExpected = true; + break; + } + } + assertTrue("Names should contain one of the expected changed segments", containsExpected); + + factory.destroy(); } /** @@ -1407,18 +1459,6 @@ void pushRbsUpdate(String changeNumber, String previousChangeNumber) { } } - void pushMembershipSegmentsUpdate(String[] segmentNames, long changeNumber) { - if (streamingData != null) { - pushMessage(streamingData, IntegrationHelper.membershipSegmentsUpdate(segmentNames, changeNumber)); - } - } - - void pushMembershipLargeSegmentsUpdate(String[] segmentNames, long changeNumber) { - if (streamingData != null) { - pushMessage(streamingData, IntegrationHelper.membershipLargeSegmentsUpdate(segmentNames, changeNumber)); - } - } - void destroy() { factory.destroy(); } @@ -1533,27 +1573,4 @@ private void populateDatabaseWithRbsData() { mDatabase.generalInfoDao().update(new GeneralInfoEntity("rbsChangeNumber", 1000L)); } - /** - * Populates the database with membership segments for testing segment removal updates. - */ - private void populateDatabaseWithMembershipData() { - // Populate segments for key_1 with segment1 so removal triggers update - MySegmentEntity segmentEntity = new MySegmentEntity(); - segmentEntity.setUserKey("key_1"); - segmentEntity.setSegmentList("{\"k\":[{\"n\":\"segment1\"},{\"n\":\"segment2\"}],\"cn\":1000}"); - segmentEntity.setUpdatedAt(System.currentTimeMillis() / 1000); - mDatabase.mySegmentDao().update(segmentEntity); - } - - /** - * Populates the database with large segments for testing large segment removal updates. - */ - private void populateDatabaseWithLargeSegmentData() { - // Populate large segments for key_1 with large_segment1 so removal triggers update - MySegmentEntity largeSegmentEntity = new MySegmentEntity(); - largeSegmentEntity.setUserKey("key_1_large"); - largeSegmentEntity.setSegmentList("{\"k\":[{\"n\":\"large_segment1\"},{\"n\":\"large_segment2\"}],\"cn\":1000}"); - largeSegmentEntity.setUpdatedAt(System.currentTimeMillis() / 1000); - mDatabase.myLargeSegmentDao().update(largeSegmentEntity); - } } diff --git a/main/src/main/java/io/split/android/client/EvaluationResult.java b/main/src/main/java/io/split/android/client/EvaluationResult.java index 3c50e0428..da529eba6 100644 --- a/main/src/main/java/io/split/android/client/EvaluationResult.java +++ b/main/src/main/java/io/split/android/client/EvaluationResult.java @@ -14,6 +14,7 @@ public EvaluationResult(String treatment, String label) { this(treatment, label, null, null, false); } + @VisibleForTesting public EvaluationResult(String treatment, String label, boolean impressionsDisabled) { this(treatment, label, null, null, impressionsDisabled); } From 18aa166454b9f627d60ba3eef3f72a676b95d31c Mon Sep 17 00:00:00 2001 From: Gaston Thea Date: Fri, 9 Jan 2026 09:38:24 -0300 Subject: [PATCH 21/27] Simultaneous membership update test --- .../events/SdkEventsIntegrationTest.java | 124 ++++++++++++++++++ 1 file changed, 124 insertions(+) diff --git a/main/src/androidTest/java/tests/integration/events/SdkEventsIntegrationTest.java b/main/src/androidTest/java/tests/integration/events/SdkEventsIntegrationTest.java index 206309fd9..5c60fc615 100644 --- a/main/src/androidTest/java/tests/integration/events/SdkEventsIntegrationTest.java +++ b/main/src/androidTest/java/tests/integration/events/SdkEventsIntegrationTest.java @@ -1099,6 +1099,130 @@ public void sdkUpdateMetadataContainsTypeForLargeSegmentsUpdate() throws Excepti ); } + /** + * Scenario: Two distinct SDK_UPDATE events are fired when both segments and large segments change + *

    + * Given sdkReady has already been emitted + * And a handler H is registered for sdkUpdate + * When a single memberships response contains changes to both segments and large segments + * Then two SDK_UPDATE events are emitted + * And one event has metadata with getType() returning Type.SEGMENTS_UPDATE and names containing segment changes + * And another event has metadata with getType() returning Type.SEGMENTS_UPDATE and names containing large segment changes + */ + @Test + public void twoDistinctSdkUpdateEventsWhenBothSegmentsAndLargeSegmentsChange() throws Exception { + AtomicInteger membershipsHitCount = new AtomicInteger(0); + + final Dispatcher pollingDispatcher = new Dispatcher() { + @Override + public MockResponse dispatch(RecordedRequest request) { + final String path = request.getPath(); + if (path.contains("/" + IntegrationHelper.ServicePath.MEMBERSHIPS)) { + int count = membershipsHitCount.incrementAndGet(); + if (count <= 1) { + // Initial sync: segment1, segment2 in ms; large_segment1, large_segment2 in ls + return new MockResponse().setResponseCode(200) + .setBody("{\"ms\":{\"k\":[{\"n\":\"segment1\"},{\"n\":\"segment2\"}],\"cn\":1000},\"ls\":{\"k\":[{\"n\":\"large_segment1\"},{\"n\":\"large_segment2\"}],\"cn\":1000}}"); + } else { + // Polling: both ms and ls change + // ms: segment1 removed, segment3 added + // ls: large_segment1 removed, large_segment3 added + return new MockResponse().setResponseCode(200) + .setBody("{\"ms\":{\"k\":[{\"n\":\"segment2\"},{\"n\":\"segment3\"}],\"cn\":2000},\"ls\":{\"k\":[{\"n\":\"large_segment2\"},{\"n\":\"large_segment3\"}],\"cn\":2000}}"); + } + } else if (path.contains("/splitChanges")) { + return new MockResponse().setResponseCode(200) + .setBody(IntegrationHelper.emptyTargetingRulesChanges(1000, 1000)); + } else if (path.contains("/testImpressions/bulk")) { + return new MockResponse().setResponseCode(200); + } + return new MockResponse().setResponseCode(404); + } + }; + mWebServer.setDispatcher(pollingDispatcher); + + SplitClientConfig config = new TestableSplitConfigBuilder() + .serviceEndpoints(endpoints()) + .ready(30000) + .featuresRefreshRate(999999) + .segmentsRefreshRate(3) + .impressionsRefreshRate(999999) + .streamingEnabled(false) + .trafficType("account") + .build(); + + SplitFactory factory = buildFactory(config); + SplitClient client = factory.client(); + + CountDownLatch readyLatch = new CountDownLatch(1); + client.on(SplitEvent.SDK_READY, new SplitEventTask() { + @Override + public void onPostExecution(SplitClient c) { + readyLatch.countDown(); + } + }); + assertTrue("SDK_READY should fire", readyLatch.await(10, TimeUnit.SECONDS)); + + List receivedMetadataList = new ArrayList<>(); + AtomicInteger legacyHandlerCount = new AtomicInteger(0); + CountDownLatch updateLatch = new CountDownLatch(4); // 2 events x 2 handlers (new API + legacy) + + // Register new API handler (addEventListener) + client.addEventListener(new SdkEventListener() { + @Override + public void onUpdate(SplitClient c, SdkUpdateMetadata metadata) { + synchronized (receivedMetadataList) { + receivedMetadataList.add(metadata); + } + updateLatch.countDown(); + } + }); + + // Register legacy API handler (client.on) + client.on(SplitEvent.SDK_UPDATE, new SplitEventTask() { + @Override + public void onPostExecution(SplitClient c) { + legacyHandlerCount.incrementAndGet(); + updateLatch.countDown(); + } + }); + + boolean updateFired = updateLatch.await(10, TimeUnit.SECONDS); + assertTrue("SDK_UPDATE should fire twice when both segments and large segments change. " + + "Hit count: " + membershipsHitCount.get() + ", metadata count: " + receivedMetadataList.size() + + ", legacy count: " + legacyHandlerCount.get(), updateFired); + + // Verify legacy API was triggered twice (once per SDK_UPDATE event) + assertEquals("Legacy API (client.on) should be triggered twice", 2, legacyHandlerCount.get()); + + // Verify we received 2 distinct SDK_UPDATE events + assertEquals("Should receive 2 SDK_UPDATE events", 2, receivedMetadataList.size()); + + // Both events should be SEGMENTS_UPDATE type + for (SdkUpdateMetadata metadata : receivedMetadataList) { + assertNotNull("Metadata should not be null", metadata); + assertEquals("Type should be SEGMENTS_UPDATE", + SdkUpdateMetadata.Type.SEGMENTS_UPDATE, metadata.getType()); + assertNotNull("Names should not be null", metadata.getNames()); + assertFalse("Names should not be empty", metadata.getNames().isEmpty()); + } + + // Collect all segment names from both events + List allSegmentNames = new ArrayList<>(); + for (SdkUpdateMetadata metadata : receivedMetadataList) { + allSegmentNames.addAll(metadata.getNames()); + } + + // Verify we have changes from both regular segments and large segments + boolean hasRegularSegmentChange = allSegmentNames.contains("segment1") || allSegmentNames.contains("segment3"); + boolean hasLargeSegmentChange = allSegmentNames.contains("large_segment1") || allSegmentNames.contains("large_segment3"); + + assertTrue("Should have regular segment changes", hasRegularSegmentChange); + assertTrue("Should have large segment changes", hasLargeSegmentChange); + + factory.destroy(); + } + /** * Helper method to verify SDK_UPDATE with SEGMENTS_UPDATE type is emitted when segments change via polling. * From e566d153ca2bc9e07cbf83b0d70eb9340c98f2bf Mon Sep 17 00:00:00 2001 From: Gaston Thea Date: Fri, 9 Jan 2026 09:57:29 -0300 Subject: [PATCH 22/27] Remove duplication --- .../events/SdkEventsIntegrationTest.java | 163 ++++++------------ 1 file changed, 54 insertions(+), 109 deletions(-) diff --git a/main/src/androidTest/java/tests/integration/events/SdkEventsIntegrationTest.java b/main/src/androidTest/java/tests/integration/events/SdkEventsIntegrationTest.java index 5c60fc615..518ff7fbe 100644 --- a/main/src/androidTest/java/tests/integration/events/SdkEventsIntegrationTest.java +++ b/main/src/androidTest/java/tests/integration/events/SdkEventsIntegrationTest.java @@ -1111,95 +1111,18 @@ public void sdkUpdateMetadataContainsTypeForLargeSegmentsUpdate() throws Excepti */ @Test public void twoDistinctSdkUpdateEventsWhenBothSegmentsAndLargeSegmentsChange() throws Exception { - AtomicInteger membershipsHitCount = new AtomicInteger(0); + // Initial sync: segment1, segment2 in ms; large_segment1, large_segment2 in ls + String initialResponse = "{\"ms\":{\"k\":[{\"n\":\"segment1\"},{\"n\":\"segment2\"}],\"cn\":1000},\"ls\":{\"k\":[{\"n\":\"large_segment1\"},{\"n\":\"large_segment2\"}],\"cn\":1000}}"; + // Polling: both ms and ls change + String pollingResponse = "{\"ms\":{\"k\":[{\"n\":\"segment2\"},{\"n\":\"segment3\"}],\"cn\":2000},\"ls\":{\"k\":[{\"n\":\"large_segment2\"},{\"n\":\"large_segment3\"}],\"cn\":2000}}"; - final Dispatcher pollingDispatcher = new Dispatcher() { - @Override - public MockResponse dispatch(RecordedRequest request) { - final String path = request.getPath(); - if (path.contains("/" + IntegrationHelper.ServicePath.MEMBERSHIPS)) { - int count = membershipsHitCount.incrementAndGet(); - if (count <= 1) { - // Initial sync: segment1, segment2 in ms; large_segment1, large_segment2 in ls - return new MockResponse().setResponseCode(200) - .setBody("{\"ms\":{\"k\":[{\"n\":\"segment1\"},{\"n\":\"segment2\"}],\"cn\":1000},\"ls\":{\"k\":[{\"n\":\"large_segment1\"},{\"n\":\"large_segment2\"}],\"cn\":1000}}"); - } else { - // Polling: both ms and ls change - // ms: segment1 removed, segment3 added - // ls: large_segment1 removed, large_segment3 added - return new MockResponse().setResponseCode(200) - .setBody("{\"ms\":{\"k\":[{\"n\":\"segment2\"},{\"n\":\"segment3\"}],\"cn\":2000},\"ls\":{\"k\":[{\"n\":\"large_segment2\"},{\"n\":\"large_segment3\"}],\"cn\":2000}}"); - } - } else if (path.contains("/splitChanges")) { - return new MockResponse().setResponseCode(200) - .setBody(IntegrationHelper.emptyTargetingRulesChanges(1000, 1000)); - } else if (path.contains("/testImpressions/bulk")) { - return new MockResponse().setResponseCode(200); - } - return new MockResponse().setResponseCode(404); - } - }; - mWebServer.setDispatcher(pollingDispatcher); - - SplitClientConfig config = new TestableSplitConfigBuilder() - .serviceEndpoints(endpoints()) - .ready(30000) - .featuresRefreshRate(999999) - .segmentsRefreshRate(3) - .impressionsRefreshRate(999999) - .streamingEnabled(false) - .trafficType("account") - .build(); - - SplitFactory factory = buildFactory(config); - SplitClient client = factory.client(); - - CountDownLatch readyLatch = new CountDownLatch(1); - client.on(SplitEvent.SDK_READY, new SplitEventTask() { - @Override - public void onPostExecution(SplitClient c) { - readyLatch.countDown(); - } - }); - assertTrue("SDK_READY should fire", readyLatch.await(10, TimeUnit.SECONDS)); - - List receivedMetadataList = new ArrayList<>(); - AtomicInteger legacyHandlerCount = new AtomicInteger(0); - CountDownLatch updateLatch = new CountDownLatch(4); // 2 events x 2 handlers (new API + legacy) - - // Register new API handler (addEventListener) - client.addEventListener(new SdkEventListener() { - @Override - public void onUpdate(SplitClient c, SdkUpdateMetadata metadata) { - synchronized (receivedMetadataList) { - receivedMetadataList.add(metadata); - } - updateLatch.countDown(); - } - }); - - // Register legacy API handler (client.on) - client.on(SplitEvent.SDK_UPDATE, new SplitEventTask() { - @Override - public void onPostExecution(SplitClient c) { - legacyHandlerCount.incrementAndGet(); - updateLatch.countDown(); - } - }); - - boolean updateFired = updateLatch.await(10, TimeUnit.SECONDS); - assertTrue("SDK_UPDATE should fire twice when both segments and large segments change. " + - "Hit count: " + membershipsHitCount.get() + ", metadata count: " + receivedMetadataList.size() + - ", legacy count: " + legacyHandlerCount.get(), updateFired); - - // Verify legacy API was triggered twice (once per SDK_UPDATE event) - assertEquals("Legacy API (client.on) should be triggered twice", 2, legacyHandlerCount.get()); + List metadataList = waitForSegmentsPollingUpdates(initialResponse, pollingResponse, 2); // Verify we received 2 distinct SDK_UPDATE events - assertEquals("Should receive 2 SDK_UPDATE events", 2, receivedMetadataList.size()); + assertEquals("Should receive 2 SDK_UPDATE events", 2, metadataList.size()); // Both events should be SEGMENTS_UPDATE type - for (SdkUpdateMetadata metadata : receivedMetadataList) { + for (SdkUpdateMetadata metadata : metadataList) { assertNotNull("Metadata should not be null", metadata); assertEquals("Type should be SEGMENTS_UPDATE", SdkUpdateMetadata.Type.SEGMENTS_UPDATE, metadata.getType()); @@ -1209,7 +1132,7 @@ public void onPostExecution(SplitClient c) { // Collect all segment names from both events List allSegmentNames = new ArrayList<>(); - for (SdkUpdateMetadata metadata : receivedMetadataList) { + for (SdkUpdateMetadata metadata : metadataList) { allSegmentNames.addAll(metadata.getNames()); } @@ -1219,8 +1142,6 @@ public void onPostExecution(SplitClient c) { assertTrue("Should have regular segment changes", hasRegularSegmentChange); assertTrue("Should have large segment changes", hasLargeSegmentChange); - - factory.destroy(); } /** @@ -1232,6 +1153,38 @@ public void onPostExecution(SplitClient c) { */ private void verifySdkUpdateForSegmentsPolling(String initialResponse, String pollingResponse, String... expectedSegmentNames) throws Exception { + List metadataList = waitForSegmentsPollingUpdates(initialResponse, pollingResponse, 1); + + assertEquals("Should receive 1 SDK_UPDATE event", 1, metadataList.size()); + + SdkUpdateMetadata metadata = metadataList.get(0); + assertNotNull("Metadata should not be null", metadata); + assertEquals("Type should be SEGMENTS_UPDATE", + SdkUpdateMetadata.Type.SEGMENTS_UPDATE, metadata.getType()); + + assertNotNull("Names should not be null", metadata.getNames()); + assertFalse("Names should not be empty", metadata.getNames().isEmpty()); + + boolean containsExpected = false; + for (String expectedName : expectedSegmentNames) { + if (metadata.getNames().contains(expectedName)) { + containsExpected = true; + break; + } + } + assertTrue("Names should contain one of the expected changed segments", containsExpected); + } + + /** + * Helper method that sets up polling for segments and waits for the expected number of SDK_UPDATE events. + * + * @param initialResponse the memberships response for initial sync + * @param pollingResponse the memberships response for polling (with changed segments) + * @param expectedEventCount the number of SDK_UPDATE events to wait for + * @return list of received SdkUpdateMetadata from the events + */ + private List waitForSegmentsPollingUpdates(String initialResponse, String pollingResponse, + int expectedEventCount) throws Exception { AtomicInteger membershipsHitCount = new AtomicInteger(0); final Dispatcher pollingDispatcher = new Dispatcher() { @@ -1278,15 +1231,18 @@ public void onPostExecution(SplitClient c) { }); assertTrue("SDK_READY should fire", readyLatch.await(10, TimeUnit.SECONDS)); - AtomicReference receivedMetadata = new AtomicReference<>(); + List receivedMetadataList = new ArrayList<>(); AtomicInteger legacyHandlerCount = new AtomicInteger(0); - CountDownLatch updateLatch = new CountDownLatch(2); // Wait for both handlers + // Wait for expectedEventCount events x 2 handlers (new API + legacy) + CountDownLatch updateLatch = new CountDownLatch(expectedEventCount * 2); // Register new API handler (addEventListener) client.addEventListener(new SdkEventListener() { @Override public void onUpdate(SplitClient c, SdkUpdateMetadata metadata) { - receivedMetadata.set(metadata); + synchronized (receivedMetadataList) { + receivedMetadataList.add(metadata); + } updateLatch.countDown(); } }); @@ -1301,28 +1257,17 @@ public void onPostExecution(SplitClient c) { }); boolean updateFired = updateLatch.await(10, TimeUnit.SECONDS); - assertTrue("SDK_UPDATE should fire when segments change via polling. Hit count: " + membershipsHitCount.get(), updateFired); - - // Verify legacy API was also triggered - assertEquals("Legacy API (client.on) should be triggered", 1, legacyHandlerCount.get()); - - assertNotNull("Metadata should not be null", receivedMetadata.get()); - assertEquals("Type should be SEGMENTS_UPDATE", - SdkUpdateMetadata.Type.SEGMENTS_UPDATE, receivedMetadata.get().getType()); - - assertNotNull("Names should not be null", receivedMetadata.get().getNames()); - assertFalse("Names should not be empty", receivedMetadata.get().getNames().isEmpty()); + assertTrue("SDK_UPDATE should fire " + expectedEventCount + " time(s). " + + "Hit count: " + membershipsHitCount.get() + ", metadata count: " + receivedMetadataList.size() + + ", legacy count: " + legacyHandlerCount.get(), updateFired); - boolean containsExpected = false; - for (String expectedName : expectedSegmentNames) { - if (receivedMetadata.get().getNames().contains(expectedName)) { - containsExpected = true; - break; - } - } - assertTrue("Names should contain one of the expected changed segments", containsExpected); + // Verify legacy API was triggered the expected number of times + assertEquals("Legacy API (client.on) should be triggered " + expectedEventCount + " time(s)", + expectedEventCount, legacyHandlerCount.get()); factory.destroy(); + + return receivedMetadataList; } /** From 2941c8cd6e6d910cee20ab4d24e128ba34fe0ee6 Mon Sep 17 00:00:00 2001 From: Gaston Thea Date: Fri, 9 Jan 2026 12:10:35 -0300 Subject: [PATCH 23/27] Update tests --- .../storage/mysegments/MySegmentsStorageImpl.java | 1 + .../client/service/MySegmentsSyncTaskTest.java | 11 ++++++----- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/main/src/main/java/io/split/android/client/storage/mysegments/MySegmentsStorageImpl.java b/main/src/main/java/io/split/android/client/storage/mysegments/MySegmentsStorageImpl.java index fd0e81a84..4478a9a81 100644 --- a/main/src/main/java/io/split/android/client/storage/mysegments/MySegmentsStorageImpl.java +++ b/main/src/main/java/io/split/android/client/storage/mysegments/MySegmentsStorageImpl.java @@ -63,6 +63,7 @@ public long getChangeNumber() { public void clear() { mInMemoryMySegments.clear(); mTill.set(DEFAULT_CHANGE_NUMBER); + mPersistentStorage.set(mMatchingKey, SegmentsChange.createEmpty()); } @NonNull diff --git a/main/src/test/java/io/split/android/client/service/MySegmentsSyncTaskTest.java b/main/src/test/java/io/split/android/client/service/MySegmentsSyncTaskTest.java index 4651eaa9a..7c61180cf 100644 --- a/main/src/test/java/io/split/android/client/service/MySegmentsSyncTaskTest.java +++ b/main/src/test/java/io/split/android/client/service/MySegmentsSyncTaskTest.java @@ -29,6 +29,7 @@ import java.util.Set; import io.split.android.client.dtos.AllSegmentsChange; +import io.split.android.client.events.metadata.EventMetadata; import io.split.android.client.dtos.SegmentsChange; import io.split.android.client.events.SplitEventsManager; import io.split.android.client.events.SplitInternalEvent; @@ -230,7 +231,7 @@ public void syncCompleteEventIsEmittedWhenNoChangesInSegments() throws HttpFetch mTask.execute(); verify(mEventsManager).notifyInternalEvent(SplitInternalEvent.MEMBERSHIPS_SYNC_COMPLETE); - verify(mEventsManager, never()).notifyInternalEvent(SplitInternalEvent.MY_SEGMENTS_UPDATED); + verify(mEventsManager, never()).notifyInternalEvent(eq(SplitInternalEvent.MY_SEGMENTS_UPDATED), any(EventMetadata.class)); } @Test @@ -243,7 +244,7 @@ public void membershipsSyncCompleteIsAlwaysFiredOnSuccessfulSync() throws HttpFe // Verify MEMBERSHIPS_SYNC_COMPLETE is always fired on successful sync, even when segments changed verify(mEventsManager).notifyInternalEvent(eq(SplitInternalEvent.MEMBERSHIPS_SYNC_COMPLETE)); - verify(mEventsManager).notifyInternalEvent(eq(SplitInternalEvent.MY_SEGMENTS_UPDATED)); + verify(mEventsManager).notifyInternalEvent(eq(SplitInternalEvent.MY_SEGMENTS_UPDATED), any(EventMetadata.class)); } @Test @@ -254,7 +255,7 @@ public void updateEventIsFiredWhenSegmentsHaveChanged() throws HttpFetcherExcept mTask = new MySegmentsSyncTask(mMySegmentsFetcher, mySegmentsStorage, myLargeSegmentsStorage, false, mEventsManager, mMySegmentsChangeChecker, mTelemetryRuntimeProducer, MySegmentsSyncTaskConfig.get(), null, null, mock(BackoffCounter.class), 1); mTask.execute(); - verify(mEventsManager).notifyInternalEvent(eq(SplitInternalEvent.MY_SEGMENTS_UPDATED)); + verify(mEventsManager).notifyInternalEvent(eq(SplitInternalEvent.MY_SEGMENTS_UPDATED), any(EventMetadata.class)); } @Test @@ -265,7 +266,7 @@ public void updatedEventIsEmittedWhenChangesInSegments() throws HttpFetcherExcep mTask = new MySegmentsSyncTask(mMySegmentsFetcher, mySegmentsStorage, myLargeSegmentsStorage, false, mEventsManager, mMySegmentsChangeChecker, mTelemetryRuntimeProducer, MySegmentsSyncTaskConfig.get(), null, null, mock(BackoffCounter.class), 1); mTask.execute(); - verify(mEventsManager).notifyInternalEvent(SplitInternalEvent.MY_SEGMENTS_UPDATED); + verify(mEventsManager).notifyInternalEvent(eq(SplitInternalEvent.MY_SEGMENTS_UPDATED), any(EventMetadata.class)); } @Test @@ -277,7 +278,7 @@ public void largeSegmentsUpdatedEventIsEmittedWhenChangesInLargeSegmentsAndNotIn mTask = new MySegmentsSyncTask(mMySegmentsFetcher, mySegmentsStorage, myLargeSegmentsStorage, false, mEventsManager, mMySegmentsChangeChecker, mTelemetryRuntimeProducer, MySegmentsSyncTaskConfig.get(), null, null, mock(BackoffCounter.class), 1); mTask.execute(); - verify(mEventsManager).notifyInternalEvent(SplitInternalEvent.MY_LARGE_SEGMENTS_UPDATED); + verify(mEventsManager).notifyInternalEvent(eq(SplitInternalEvent.MY_LARGE_SEGMENTS_UPDATED), any(EventMetadata.class)); } @Test From d14b69b96bb1a98aec0792976b0c57d1b69d9b29 Mon Sep 17 00:00:00 2001 From: Gaston Thea Date: Fri, 9 Jan 2026 12:23:58 -0300 Subject: [PATCH 24/27] Refactor segments change checker --- .../mysegments/MySegmentsSyncTask.java | 38 ++---------- .../synchronizer/MySegmentsChangeChecker.java | 34 +++++++++-- .../service/MySegmentsChangesCheckerTest.java | 60 +++++++++++-------- .../service/MySegmentsSyncTaskTest.java | 14 ++--- .../service/MySegmentsUpdateTaskTest.java | 3 +- 5 files changed, 79 insertions(+), 70 deletions(-) diff --git a/main/src/main/java/io/split/android/client/service/mysegments/MySegmentsSyncTask.java b/main/src/main/java/io/split/android/client/service/mysegments/MySegmentsSyncTask.java index 91d8f2e84..68c5518da 100644 --- a/main/src/main/java/io/split/android/client/service/mysegments/MySegmentsSyncTask.java +++ b/main/src/main/java/io/split/android/client/service/mysegments/MySegmentsSyncTask.java @@ -10,10 +10,8 @@ import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; -import java.util.HashSet; import java.util.List; import java.util.Map; -import java.util.Set; import java.util.concurrent.TimeUnit; import io.split.android.client.dtos.AllSegmentsChange; @@ -271,20 +269,18 @@ private void fireMySegmentsUpdatedIfNeeded(UpdateSegmentsResult segmentsResult, // This order is important: if we fire MEMBERSHIPS_SYNC_COMPLETE first, it may trigger SDK_READY, // and then the *_UPDATED events would immediately trigger SDK_UPDATE during initial sync. // By firing *_UPDATED first (while SDK_READY hasn't triggered yet), they won't trigger SDK_UPDATE. - boolean segmentsHaveChanged = mMySegmentsChangeChecker.mySegmentsHaveChanged(segmentsResult.oldSegments, segmentsResult.newSegments); - boolean largeSegmentsHaveChanged = mMySegmentsChangeChecker.mySegmentsHaveChanged(largeSegmentsResult.oldSegments, largeSegmentsResult.newSegments); + List changedSegments = mMySegmentsChangeChecker.getChangedSegments(segmentsResult.oldSegments, segmentsResult.newSegments); + List changedLargeSegments = mMySegmentsChangeChecker.getChangedSegments(largeSegmentsResult.oldSegments, largeSegmentsResult.newSegments); - if (segmentsHaveChanged) { + if (!changedSegments.isEmpty()) { Logger.v("New segments: " + segmentsResult.newSegments); - List changedSegmentNames = computeChangedSegmentNames(segmentsResult); - EventMetadata metadata = EventMetadataHelpers.createUpdatedSegmentsMetadata(changedSegmentNames); + EventMetadata metadata = EventMetadataHelpers.createUpdatedSegmentsMetadata(changedSegments); mEventsManager.notifyInternalEvent(mUpdateEvent, metadata); } - if (largeSegmentsHaveChanged) { + if (!changedLargeSegments.isEmpty()) { Logger.v("New large segments: " + largeSegmentsResult.newSegments); - List changedLargeSegmentNames = computeChangedSegmentNames(largeSegmentsResult); - EventMetadata metadata = EventMetadataHelpers.createUpdatedSegmentsMetadata(changedLargeSegmentNames); + EventMetadata metadata = EventMetadataHelpers.createUpdatedSegmentsMetadata(changedLargeSegments); mEventsManager.notifyInternalEvent(SplitInternalEvent.MY_LARGE_SEGMENTS_UPDATED, metadata); } @@ -294,28 +290,6 @@ private void fireMySegmentsUpdatedIfNeeded(UpdateSegmentsResult segmentsResult, mEventsManager.notifyInternalEvent(SplitInternalEvent.MEMBERSHIPS_SYNC_COMPLETE); } - /** - * Computes the set of changed segment names (added + removed) between old and new segments. - */ - private List computeChangedSegmentNames(UpdateSegmentsResult result) { - Set oldSet = new HashSet<>(result.oldSegments); - Set newSet = new HashSet<>(result.newSegments); - - // Added segments: in new but not in old - Set added = new HashSet<>(newSet); - added.removeAll(oldSet); - - // Removed segments: in old but not in new - Set removed = new HashSet<>(oldSet); - removed.removeAll(newSet); - - // Combined changed segments - Set changed = new HashSet<>(added); - changed.addAll(removed); - - return new ArrayList<>(changed); - } - private static class UpdateSegmentsResult { public final List oldSegments; public final List newSegments; diff --git a/main/src/main/java/io/split/android/client/service/synchronizer/MySegmentsChangeChecker.java b/main/src/main/java/io/split/android/client/service/synchronizer/MySegmentsChangeChecker.java index 16fb4cb73..780b70adb 100644 --- a/main/src/main/java/io/split/android/client/service/synchronizer/MySegmentsChangeChecker.java +++ b/main/src/main/java/io/split/android/client/service/synchronizer/MySegmentsChangeChecker.java @@ -1,12 +1,36 @@ package io.split.android.client.service.synchronizer; -import java.util.Collections; +import java.util.ArrayList; +import java.util.HashSet; import java.util.List; +import java.util.Set; public class MySegmentsChangeChecker { - public boolean mySegmentsHaveChanged(final List oldSegments, final List newSegments) { - Collections.sort(oldSegments); - Collections.sort(newSegments); - return !oldSegments.equals(newSegments); + + /** + * Computes and returns the list of changed segment names (added + removed) between old and new segments. + * An empty list means no changes occurred. + * + * @param oldSegments the previous list of segment names + * @param newSegments the new list of segment names + * @return list of segment names that were either added or removed (empty if no changes) + */ + public List getChangedSegments(final List oldSegments, final List newSegments) { + Set oldSet = new HashSet<>(oldSegments); + Set newSet = new HashSet<>(newSegments); + + // Added segments: in new but not in old + Set added = new HashSet<>(newSet); + added.removeAll(oldSet); + + // Removed segments: in old but not in new + Set removed = new HashSet<>(oldSet); + removed.removeAll(newSet); + + // Combined changed segments + Set changed = new HashSet<>(added); + changed.addAll(removed); + + return new ArrayList<>(changed); } } diff --git a/main/src/test/java/io/split/android/client/service/MySegmentsChangesCheckerTest.java b/main/src/test/java/io/split/android/client/service/MySegmentsChangesCheckerTest.java index c01b5c279..263d59e6c 100644 --- a/main/src/test/java/io/split/android/client/service/MySegmentsChangesCheckerTest.java +++ b/main/src/test/java/io/split/android/client/service/MySegmentsChangesCheckerTest.java @@ -1,12 +1,13 @@ package io.split.android.client.service; import org.junit.Assert; -import org.junit.Before; import org.junit.Test; import java.util.ArrayList; import java.util.Arrays; +import java.util.HashSet; import java.util.List; +import java.util.Set; import io.split.android.client.service.synchronizer.MySegmentsChangeChecker; @@ -16,71 +17,80 @@ public class MySegmentsChangesCheckerTest { @Test public void testChangesArrived() { - List old = Arrays.asList("s1", "s2", "s3"); List newSegments = Arrays.asList("s1"); - boolean result = mMySegmentsChangeChecker.mySegmentsHaveChanged(old, newSegments); - - Assert.assertTrue(result); + List result = mMySegmentsChangeChecker.getChangedSegments(old, newSegments); + + Assert.assertFalse(result.isEmpty()); + // s2 and s3 were removed + Set changedSet = new HashSet<>(result); + Assert.assertTrue(changedSet.contains("s2")); + Assert.assertTrue(changedSet.contains("s3")); + Assert.assertEquals(2, result.size()); } @Test public void testNewChangesArrived() { - List newSegments = Arrays.asList("s1", "s2", "s3"); List old = Arrays.asList("s1"); - boolean result = mMySegmentsChangeChecker.mySegmentsHaveChanged(old, newSegments); - - Assert.assertTrue(result); + List result = mMySegmentsChangeChecker.getChangedSegments(old, newSegments); + + Assert.assertFalse(result.isEmpty()); + // s2 and s3 were added + Set changedSet = new HashSet<>(result); + Assert.assertTrue(changedSet.contains("s2")); + Assert.assertTrue(changedSet.contains("s3")); + Assert.assertEquals(2, result.size()); } @Test public void testNoChangesArrived() { - List old = Arrays.asList("s1", "s2", "s3"); List newSegments = Arrays.asList("s1", "s2", "s3"); - boolean result = mMySegmentsChangeChecker.mySegmentsHaveChanged(old, newSegments); + List result = mMySegmentsChangeChecker.getChangedSegments(old, newSegments); - Assert.assertFalse(result); + Assert.assertTrue(result.isEmpty()); } @Test public void testNoChangesDifferentOrder() { - List old = Arrays.asList("s1", "s2", "s3"); List newSegments = Arrays.asList("s2", "s1", "s3"); - boolean result = mMySegmentsChangeChecker.mySegmentsHaveChanged(old, newSegments); + List result = mMySegmentsChangeChecker.getChangedSegments(old, newSegments); - Assert.assertFalse(result); + Assert.assertTrue(result.isEmpty()); } @Test public void testNoChangesDifferentOrderInverted() { - List newSegments = Arrays.asList("s1", "s2", "s3"); List old = Arrays.asList("s2", "s1", "s3"); - boolean result = mMySegmentsChangeChecker.mySegmentsHaveChanged(old, newSegments); + List result = mMySegmentsChangeChecker.getChangedSegments(old, newSegments); - Assert.assertFalse(result); + Assert.assertTrue(result.isEmpty()); } @Test public void testNoChangesArrivedEmpty() { - List newSegments = new ArrayList<>(); List old = new ArrayList<>(); - boolean result = mMySegmentsChangeChecker.mySegmentsHaveChanged(old, newSegments); + List result = mMySegmentsChangeChecker.getChangedSegments(old, newSegments); - Assert.assertFalse(result); + Assert.assertTrue(result.isEmpty()); } @Test public void testEmptyChangesArrived() { - List newSegments = new ArrayList<>(); List old = Arrays.asList("s1", "s2", "s3"); - boolean result = mMySegmentsChangeChecker.mySegmentsHaveChanged(old, newSegments); - - Assert.assertTrue(result); + List result = mMySegmentsChangeChecker.getChangedSegments(old, newSegments); + + Assert.assertFalse(result.isEmpty()); + // s1, s2, s3 were all removed + Set changedSet = new HashSet<>(result); + Assert.assertTrue(changedSet.contains("s1")); + Assert.assertTrue(changedSet.contains("s2")); + Assert.assertTrue(changedSet.contains("s3")); + Assert.assertEquals(3, result.size()); } } diff --git a/main/src/test/java/io/split/android/client/service/MySegmentsSyncTaskTest.java b/main/src/test/java/io/split/android/client/service/MySegmentsSyncTaskTest.java index 7c61180cf..cf99782c7 100644 --- a/main/src/test/java/io/split/android/client/service/MySegmentsSyncTaskTest.java +++ b/main/src/test/java/io/split/android/client/service/MySegmentsSyncTaskTest.java @@ -71,7 +71,7 @@ public class MySegmentsSyncTaskTest { @Before public void setup() { mAutoCloseable = MockitoAnnotations.openMocks(this); - when(mMySegmentsChangeChecker.mySegmentsHaveChanged(any(), any())).thenReturn(true); + when(mMySegmentsChangeChecker.getChangedSegments(any(), any())).thenReturn(Collections.singletonList("changed_segment")); mTask = new MySegmentsSyncTask(mMySegmentsFetcher, mySegmentsStorage, myLargeSegmentsStorage, false, mEventsManager, mTelemetryRuntimeProducer, MySegmentsSyncTaskConfig.get(), null, null); loadMySegments(); } @@ -224,7 +224,7 @@ public void addTillParameterToRequestWhenResponseCnDoesNotMatchTargetAndRetryLim @Test public void syncCompleteEventIsEmittedWhenNoChangesInSegments() throws HttpFetcherException { - when(mMySegmentsChangeChecker.mySegmentsHaveChanged(any(), any())).thenReturn(false); + when(mMySegmentsChangeChecker.getChangedSegments(any(), any())).thenReturn(Collections.emptyList()); when(mMySegmentsFetcher.execute(noParams, null)).thenReturn(mMySegments); mTask = new MySegmentsSyncTask(mMySegmentsFetcher, mySegmentsStorage, myLargeSegmentsStorage, false, mEventsManager, mMySegmentsChangeChecker, mTelemetryRuntimeProducer, MySegmentsSyncTaskConfig.get(), null, null, mock(BackoffCounter.class), 1); @@ -237,7 +237,7 @@ public void syncCompleteEventIsEmittedWhenNoChangesInSegments() throws HttpFetch @Test public void membershipsSyncCompleteIsAlwaysFiredOnSuccessfulSync() throws HttpFetcherException { when(mMySegmentsFetcher.execute(noParams, null)).thenReturn(mMySegments); - when(mMySegmentsChangeChecker.mySegmentsHaveChanged(any(), any())).thenReturn(true); + when(mMySegmentsChangeChecker.getChangedSegments(any(), any())).thenReturn(Collections.singletonList("changed_segment")); mTask = new MySegmentsSyncTask(mMySegmentsFetcher, mySegmentsStorage, myLargeSegmentsStorage, false, mEventsManager, mMySegmentsChangeChecker, mTelemetryRuntimeProducer, MySegmentsSyncTaskConfig.get(), null, null, mock(BackoffCounter.class), 1); mTask.execute(); @@ -250,7 +250,7 @@ public void membershipsSyncCompleteIsAlwaysFiredOnSuccessfulSync() throws HttpFe @Test public void updateEventIsFiredWhenSegmentsHaveChanged() throws HttpFetcherException { when(mMySegmentsFetcher.execute(noParams, null)).thenReturn(mMySegments); - when(mMySegmentsChangeChecker.mySegmentsHaveChanged(any(), any())).thenReturn(true); + when(mMySegmentsChangeChecker.getChangedSegments(any(), any())).thenReturn(Collections.singletonList("changed_segment")); mTask = new MySegmentsSyncTask(mMySegmentsFetcher, mySegmentsStorage, myLargeSegmentsStorage, false, mEventsManager, mMySegmentsChangeChecker, mTelemetryRuntimeProducer, MySegmentsSyncTaskConfig.get(), null, null, mock(BackoffCounter.class), 1); mTask.execute(); @@ -260,7 +260,7 @@ public void updateEventIsFiredWhenSegmentsHaveChanged() throws HttpFetcherExcept @Test public void updatedEventIsEmittedWhenChangesInSegments() throws HttpFetcherException { - when(mMySegmentsChangeChecker.mySegmentsHaveChanged(any(), any())).thenReturn(true); + when(mMySegmentsChangeChecker.getChangedSegments(any(), any())).thenReturn(Collections.singletonList("changed_segment")); when(mMySegmentsFetcher.execute(noParams, null)).thenReturn(mMySegments); mTask = new MySegmentsSyncTask(mMySegmentsFetcher, mySegmentsStorage, myLargeSegmentsStorage, false, mEventsManager, mMySegmentsChangeChecker, mTelemetryRuntimeProducer, MySegmentsSyncTaskConfig.get(), null, null, mock(BackoffCounter.class), 1); @@ -271,8 +271,8 @@ public void updatedEventIsEmittedWhenChangesInSegments() throws HttpFetcherExcep @Test public void largeSegmentsUpdatedEventIsEmittedWhenChangesInLargeSegmentsAndNotInSegments() throws HttpFetcherException { - when(mMySegmentsChangeChecker.mySegmentsHaveChanged(any(), any())).thenReturn(false); - when(mMySegmentsChangeChecker.mySegmentsHaveChanged(Collections.emptyList(), Collections.singletonList("largesegment0"))).thenReturn(true); + when(mMySegmentsChangeChecker.getChangedSegments(any(), any())).thenReturn(Collections.emptyList()); + when(mMySegmentsChangeChecker.getChangedSegments(Collections.emptyList(), Collections.singletonList("largesegment0"))).thenReturn(Collections.singletonList("largesegment0")); when(mMySegmentsFetcher.execute(noParams, null)).thenReturn(createChange(1L)); mTask = new MySegmentsSyncTask(mMySegmentsFetcher, mySegmentsStorage, myLargeSegmentsStorage, false, mEventsManager, mMySegmentsChangeChecker, mTelemetryRuntimeProducer, MySegmentsSyncTaskConfig.get(), null, null, mock(BackoffCounter.class), 1); diff --git a/main/src/test/java/io/split/android/client/service/MySegmentsUpdateTaskTest.java b/main/src/test/java/io/split/android/client/service/MySegmentsUpdateTaskTest.java index 3e4972765..663602b30 100644 --- a/main/src/test/java/io/split/android/client/service/MySegmentsUpdateTaskTest.java +++ b/main/src/test/java/io/split/android/client/service/MySegmentsUpdateTaskTest.java @@ -1,6 +1,7 @@ package io.split.android.client.service; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.doThrow; import static org.mockito.Mockito.never; import static org.mockito.Mockito.reset; @@ -179,7 +180,7 @@ public void removeOperationRemovesOnlyNotifiedSegments() { Assert.assertTrue(captorValue.getNames().contains(mCustomerSegment)); Assert.assertEquals(1, captorValue.getNames().size()); Assert.assertEquals(SplitTaskExecutionStatus.SUCCESS, result.getStatus()); - verify(mEventsManager).notifyInternalEvent(SplitInternalEvent.MY_SEGMENTS_UPDATED); + verify(mEventsManager).notifyInternalEvent(eq(SplitInternalEvent.MY_SEGMENTS_UPDATED), any()); } @Test From bd38dbc0f710f40c41d95ef205512818e96433dd Mon Sep 17 00:00:00 2001 From: Gaston Thea Date: Mon, 12 Jan 2026 16:45:02 -0300 Subject: [PATCH 25/27] Flag names only --- .../client/events/SdkUpdateMetadata.java | 12 +- .../events/metadata/EventMetadataHelpers.java | 12 +- .../events/TypedTaskConversionTest.java | 8 +- .../metadata/EventMetadataHelpersTest.java | 11 +- .../events/SdkEventsIntegrationTest.java | 194 +++++++++++++----- .../mysegments/MySegmentsSyncTask.java | 7 +- .../mysegments/MySegmentsUpdateTask.java | 7 +- .../RuleBasedSegmentInPlaceUpdateTask.java | 13 +- .../RuleBasedSegmentInPlaceUpdateTask.java | 12 +- .../service/splits/SplitsSyncHelper.java | 46 ----- .../client/service/splits/SplitsSyncTask.java | 16 +- .../service/splits/SplitsUpdateTask.java | 16 +- .../client/service/SplitSyncTaskTest.java | 22 ++ .../client/service/SplitUpdateTaskTest.java | 22 ++ .../client/service/SplitsSyncHelperTest.java | 62 ------ 15 files changed, 225 insertions(+), 235 deletions(-) diff --git a/api/src/main/java/io/split/android/client/events/SdkUpdateMetadata.java b/api/src/main/java/io/split/android/client/events/SdkUpdateMetadata.java index 58d6741a5..83dea400a 100644 --- a/api/src/main/java/io/split/android/client/events/SdkUpdateMetadata.java +++ b/api/src/main/java/io/split/android/client/events/SdkUpdateMetadata.java @@ -19,12 +19,16 @@ public final class SdkUpdateMetadata { public enum Type { /** * Feature flags were updated. + *

    + * {@link #getNames()} returns the list of flag names that changed. */ FLAGS_UPDATE, /** - * Rule-based segments were updated. + * Segments were updated (rule-based segments, memberships, or large segments). *

    + * Note: {@link #getNames()} always returns an empty list for this type. + * Segment names are not included in the metadata. */ SEGMENTS_UPDATE } @@ -59,10 +63,10 @@ public Type getType() { /** * Returns the list of entity names that changed in this update. *

    - * For {@link Type#FLAGS_UPDATE}, this contains flag names. - * For {@link Type#SEGMENTS_UPDATE}, this contains rule-based segment names. + * For {@link Type#FLAGS_UPDATE}, this contains flag names that were updated. + * For {@link Type#SEGMENTS_UPDATE}, this is always an empty list (segment names are not included). * - * @return the list of updated entity names, never null (empty list if none) + * @return the list of updated entity names, never null (empty list for SEGMENTS_UPDATE or if none) */ @NonNull public List getNames() { diff --git a/events-domain/src/main/java/io/split/android/client/events/metadata/EventMetadataHelpers.java b/events-domain/src/main/java/io/split/android/client/events/metadata/EventMetadataHelpers.java index 4116de596..e85e431f4 100644 --- a/events-domain/src/main/java/io/split/android/client/events/metadata/EventMetadataHelpers.java +++ b/events-domain/src/main/java/io/split/android/client/events/metadata/EventMetadataHelpers.java @@ -3,6 +3,7 @@ import androidx.annotation.Nullable; import java.util.ArrayList; +import java.util.Collections; import java.util.HashSet; import java.util.List; @@ -31,15 +32,16 @@ public static EventMetadata createUpdatedFlagsMetadata(List updatedFlagN } /** - * Creates metadata for SDK_UPDATE events when rule-based segments are updated. + * Creates metadata for SDK_UPDATE events when segments are updated. + *

    + * SEGMENTS_UPDATE always has empty names - segment names are not included in the metadata. * - * @param updatedSegmentNames the list of rule-based segment names that were updated - * @return the event metadata with TYPE=SEGMENTS_UPDATE and NAMES containing the segment names + * @return the event metadata with TYPE=SEGMENTS_UPDATE and empty NAMES list */ - public static EventMetadata createUpdatedSegmentsMetadata(List updatedSegmentNames) { + public static EventMetadata createUpdatedSegmentsMetadata() { return new EventMetadataBuilder() .put(MetadataKeys.TYPE, MetadataKeys.TYPE_SEGMENTS_UPDATE) - .put(MetadataKeys.NAMES, new ArrayList<>(new HashSet<>(updatedSegmentNames))) + .put(MetadataKeys.NAMES, Collections.emptyList()) .build(); } diff --git a/events-domain/src/test/java/io/split/android/client/events/TypedTaskConversionTest.java b/events-domain/src/test/java/io/split/android/client/events/TypedTaskConversionTest.java index b74fd2dc6..b6b10e38e 100644 --- a/events-domain/src/test/java/io/split/android/client/events/TypedTaskConversionTest.java +++ b/events-domain/src/test/java/io/split/android/client/events/TypedTaskConversionTest.java @@ -36,17 +36,15 @@ public void convertForSdkUpdateConvertsFlagsMetadataCorrectly() { @Test public void convertForSdkUpdateConvertsSegmentsMetadataCorrectly() { - List expectedSegments = Arrays.asList("segment1", "segment2"); - - EventMetadata eventMetadata = EventMetadataHelpers.createUpdatedSegmentsMetadata(expectedSegments); + // SEGMENTS_UPDATE always has empty names + EventMetadata eventMetadata = EventMetadataHelpers.createUpdatedSegmentsMetadata(); // Call conversion method SdkUpdateMetadata converted = TypedTaskConverter.convertForSdkUpdate(eventMetadata); assertNotNull(converted); assertEquals(SdkUpdateMetadata.Type.SEGMENTS_UPDATE, converted.getType()); - assertEquals(expectedSegments.size(), converted.getNames().size()); - assertTrue(converted.getNames().containsAll(expectedSegments)); + assertTrue("Names should be empty for SEGMENTS_UPDATE", converted.getNames().isEmpty()); } @Test diff --git a/events-domain/src/test/java/io/split/android/client/events/metadata/EventMetadataHelpersTest.java b/events-domain/src/test/java/io/split/android/client/events/metadata/EventMetadataHelpersTest.java index 9a3dcda84..e80fd51ae 100644 --- a/events-domain/src/test/java/io/split/android/client/events/metadata/EventMetadataHelpersTest.java +++ b/events-domain/src/test/java/io/split/android/client/events/metadata/EventMetadataHelpersTest.java @@ -33,19 +33,16 @@ public void createUpdatedFlagsMetadataContainsTypeAndNames() { // Tests for createUpdatedSegmentsMetadata @Test @SuppressWarnings("unchecked") - public void createUpdatedSegmentsMetadataContainsTypeAndNames() { - List segments = Arrays.asList("segment1", "segment2"); - EventMetadata metadata = EventMetadataHelpers.createUpdatedSegmentsMetadata(segments); + public void createUpdatedSegmentsMetadataContainsTypeAndEmptyNames() { + EventMetadata metadata = EventMetadataHelpers.createUpdatedSegmentsMetadata(); assertTrue(metadata.containsKey(MetadataKeys.TYPE)); assertEquals(MetadataKeys.TYPE_SEGMENTS_UPDATE, metadata.get(MetadataKeys.TYPE)); - // Check names + // Check names - should always be empty assertTrue(metadata.containsKey(MetadataKeys.NAMES)); List result = (List) metadata.get(MetadataKeys.NAMES); - assertEquals(2, result.size()); - assertTrue(result.contains("segment1")); - assertTrue(result.contains("segment2")); + assertTrue("Names should be empty for SEGMENTS_UPDATE", result.isEmpty()); } // Tests for createCacheReadyMetadata diff --git a/main/src/androidTest/java/tests/integration/events/SdkEventsIntegrationTest.java b/main/src/androidTest/java/tests/integration/events/SdkEventsIntegrationTest.java index 518ff7fbe..5d4f28314 100644 --- a/main/src/androidTest/java/tests/integration/events/SdkEventsIntegrationTest.java +++ b/main/src/androidTest/java/tests/integration/events/SdkEventsIntegrationTest.java @@ -7,6 +7,7 @@ import android.content.Context; +import androidx.annotation.NonNull; import androidx.test.platform.app.InstrumentationRegistry; import org.junit.After; @@ -248,7 +249,7 @@ public void sdkReadyFiresAfterSdkReadyFromCacheAndRequiresSyncCompletion() throw CountDownLatch readyLatch = new CountDownLatch(1); SplitClient client = factory.client(new Key("key_1")); - + // Register handlers immediately client.on(SplitEvent.SDK_READY_FROM_CACHE, new SplitEventTask() { @Override @@ -279,10 +280,10 @@ public void onPostExecution(SplitClient client) { // Then: sdkReady is emitted exactly once assertTrue("SDK_READY should fire after SDK_READY_FROM_CACHE and sync completion. " + - "Cache fired: " + cacheHandlerCount.get() + ", Ready fired: " + readyHandlerCount.get(), + "Cache fired: " + cacheHandlerCount.get() + ", Ready fired: " + readyHandlerCount.get(), readyFired); assertEquals("Ready handler should be invoked exactly once", 1, readyHandlerCount.get()); - + // Verify both events fired assertEquals("SDK_READY_FROM_CACHE should fire", 1, cacheHandlerCount.get()); assertEquals("SDK_READY should fire after SDK_READY_FROM_CACHE", 1, readyHandlerCount.get()); @@ -475,7 +476,7 @@ public void onUpdate(SplitClient client, SdkUpdateMetadata metadata) { // When: a second handler H2 is registered for sdkUpdate after one sdkUpdate has already fired CountDownLatch secondUpdateLatch = new CountDownLatch(2); secondUpdateLatchRef.set(secondUpdateLatch); - + fixture.client.addEventListener(new SdkEventListener() { @Override public void onUpdate(SplitClient client, SdkUpdateMetadata metadata) { @@ -490,7 +491,7 @@ public void onUpdate(SplitClient client, SdkUpdateMetadata metadata) { // Ensure handlers are registered and first update is fully processed before pushing second update Thread.sleep(500); - + // Send keep-alive to ensure SSE connection is still active if (fixture.streamingData != null) { TestingHelper.pushKeepAlive(fixture.streamingData); @@ -502,8 +503,8 @@ public void onUpdate(SplitClient client, SdkUpdateMetadata metadata) { // Then: both H1 and H2 are invoked for that second sdkUpdate boolean secondUpdateFired = secondUpdateLatch.await(15, TimeUnit.SECONDS); - assertTrue("Second SDK_UPDATE should fire. H1 count: " + handler1Count.get() + - ", H2 count: " + handler2Count.get() + + assertTrue("Second SDK_UPDATE should fire. H1 count: " + handler1Count.get() + + ", H2 count: " + handler2Count.get() + ", secondUpdateLatch count: " + secondUpdateLatch.getCount(), secondUpdateFired); // H1 should now have 2 total invocations (1 from first + 1 from second) @@ -773,10 +774,10 @@ public void onUpdate(SplitClient client, SdkUpdateMetadata metadata) { // are stored in a HashSet which doesn't guarantee iteration order. // The important thing is that all handlers were invoked and H3 was invoked // even though H2 threw an exception (error isolation). - assertTrue("All handlers should have been assigned order numbers", + assertTrue("All handlers should have been assigned order numbers", handler1Order.get() > 0 && handler2Order.get() > 0 && handler3Order.get() > 0); assertEquals("Order counter should be 3 (one for each handler)", 3, orderCounter.get()); - + // Verify error isolation: H3 was invoked even though H2 threw an exception // This is the key assertion - that errors don't prevent subsequent handlers from executing assertTrue("H3 should be invoked even if H2 throws (error isolation)", handler3Count.get() == 1); @@ -927,7 +928,7 @@ public void onUpdate(SplitClient client, SdkUpdateMetadata metadata) { * This test verifies that when a split update notification arrives via SSE, * the SDK_UPDATE event is emitted to all clients in the factory. *

    - * Note: True client-scoped events like mySegmentsUpdated require specific streaming + * Note: True client-scoped events like mySegmentsUpdated require specific streaming * notifications targeted at individual user keys. This test demonstrates the difference * by showing that SDK-scoped split updates affect all clients equally. */ @@ -1021,9 +1022,9 @@ public void onUpdate(SplitClient client, SdkUpdateMetadata metadata) { * When a rule-based segment update notification arrives via SSE * Then sdkUpdate is emitted * And handler H receives metadata with getType() returning Type.SEGMENTS_UPDATE - * And handler H receives metadata with getNames() containing the updated RBS names + * And handler H receives metadata with getNames() returning an empty list *

    - * Note: SEGMENTS_UPDATE is for rule-based segments (RBS) ONLY, not for memberships. + * Note: SEGMENTS_UPDATE always has empty names (segment names are not included). */ @Test public void sdkUpdateMetadataContainsTypeForSegmentsUpdate() throws Exception { @@ -1050,13 +1051,122 @@ public void onUpdate(SplitClient client, SdkUpdateMetadata metadata) { SdkUpdateMetadata.Type.SEGMENTS_UPDATE, receivedMetadata.get().getType()); assertNotNull("Names should not be null", receivedMetadata.get().getNames()); - assertFalse("Names should not be empty", receivedMetadata.get().getNames().isEmpty()); - assertTrue("Names should contain rbs_test", - receivedMetadata.get().getNames().contains("rbs_test")); + assertTrue("Names should be empty for SEGMENTS_UPDATE", receivedMetadata.get().getNames().isEmpty()); fixture.destroy(); } + /** + * Scenario: Only FLAGS_UPDATE fires when both flags and RBS change together + *

    + * Given sdkReady has already been emitted + * And a handler H is registered for sdkUpdate + * When a polling sync returns changes to both flags AND rule-based segments + * Then only ONE sdkUpdate is emitted + * And handler H receives metadata with getType() returning Type.FLAGS_UPDATE + * And SEGMENTS_UPDATE is NOT fired (RBS changes are subsumed by FLAGS_UPDATE) + */ + @Test + public void sdkUpdateFiresOnlyOnceWhenBothFlagsAndRbsChange() throws Exception { + // Track number of /splitChanges calls + AtomicInteger splitChangesHitCount = new AtomicInteger(0); + + final Dispatcher pollingDispatcher = new Dispatcher() { + @Override + public MockResponse dispatch(RecordedRequest request) { + final String path = request.getPath(); + if (path.contains("/" + IntegrationHelper.ServicePath.MEMBERSHIPS)) { + return new MockResponse().setResponseCode(200).setBody(IntegrationHelper.dummyAllSegments()); + } else if (path.contains("/splitChanges")) { + int count = splitChangesHitCount.incrementAndGet(); + if (count <= 1) { + // Initial sync: empty + return new MockResponse().setResponseCode(200) + .setBody(IntegrationHelper.emptyTargetingRulesChanges(1000, 1000)); + } else { + // Polling sync: return BOTH flag and RBS changes + // s and t must be equal to signal end of sync loop + String responseWithBothChanges = "{\"ff\":{\"s\":2000,\"t\":2000,\"d\":[" + + "{\"trafficTypeName\":\"user\",\"name\":\"test_split\",\"status\":\"ACTIVE\"," + + "\"killed\":false,\"defaultTreatment\":\"off\",\"changeNumber\":2000," + + "\"conditions\":[{\"conditionType\":\"ROLLOUT\",\"matcherGroup\":{\"combiner\":\"AND\"," + + "\"matchers\":[{\"keySelector\":{\"trafficType\":\"user\"},\"matcherType\":\"ALL_KEYS\",\"negate\":false}]}," + + "\"partitions\":[{\"treatment\":\"on\",\"size\":100}]}]}" + + "]},\"rbs\":{\"s\":2000,\"t\":2000,\"d\":[" + + "{\"name\":\"test_rbs\",\"status\":\"ACTIVE\",\"trafficTypeName\":\"user\"," + + "\"excluded\":{\"keys\":[],\"segments\":[]}," + + "\"conditions\":[{\"matcherGroup\":{\"combiner\":\"AND\"," + + "\"matchers\":[{\"keySelector\":{\"trafficType\":\"user\"},\"matcherType\":\"ALL_KEYS\",\"negate\":false}]}}]}" + + "]}}"; + return new MockResponse().setResponseCode(200).setBody(responseWithBothChanges); + } + } else if (path.contains("/testImpressions/bulk")) { + return new MockResponse().setResponseCode(200); + } + return new MockResponse().setResponseCode(404); + } + }; + mWebServer.setDispatcher(pollingDispatcher); + + // Use polling mode with short refresh rate to trigger sync quickly + SplitClientConfig config = new TestableSplitConfigBuilder() + .serviceEndpoints(endpoints()) + .ready(30000) + .featuresRefreshRate(3) // Poll every 3 seconds + .segmentsRefreshRate(999999) + .impressionsRefreshRate(999999) + .streamingEnabled(false) + .trafficType("account") + .build(); + + SplitFactory factory = buildFactory(config); + SplitClient client = factory.client(); + + // Wait for SDK_READY + CountDownLatch readyLatch = new CountDownLatch(1); + client.on(SplitEvent.SDK_READY, new SplitEventTask() { + @Override + public void onPostExecution(SplitClient c) { + readyLatch.countDown(); + } + }); + assertTrue("SDK_READY should fire", readyLatch.await(10, TimeUnit.SECONDS)); + + // Register handler to count SDK_UPDATE events and capture metadata + List receivedMetadataList = new ArrayList<>(); + CountDownLatch updateLatch = new CountDownLatch(1); + + client.addEventListener(new SdkEventListener() { + @Override + public void onUpdate(SplitClient c, SdkUpdateMetadata metadata) { + synchronized (receivedMetadataList) { + receivedMetadataList.add(metadata); + } + updateLatch.countDown(); + } + }); + + // Wait for SDK_UPDATE (triggered by polling that returns both flag and RBS changes) + boolean updateFired = updateLatch.await(10, TimeUnit.SECONDS); + assertTrue("SDK_UPDATE should fire", updateFired); + + // Wait a bit to ensure no additional events fire + Thread.sleep(1000); + + // Verify only ONE SDK_UPDATE was fired + synchronized (receivedMetadataList) { + assertEquals("Should receive exactly 1 SDK_UPDATE event (not 2)", 1, receivedMetadataList.size()); + + // Verify it's FLAGS_UPDATE (not SEGMENTS_UPDATE) + SdkUpdateMetadata metadata = receivedMetadataList.get(0); + assertNotNull("Metadata should not be null", metadata); + assertEquals("Type should be FLAGS_UPDATE (not SEGMENTS_UPDATE)", + SdkUpdateMetadata.Type.FLAGS_UPDATE, metadata.getType()); + } + + factory.destroy(); + } + /** * Scenario: sdkUpdateMetadata contains Type.SEGMENTS_UPDATE for membership segments update (polling) *

    @@ -1065,16 +1175,15 @@ public void onUpdate(SplitClient client, SdkUpdateMetadata metadata) { * When segments change via polling (server returns different segments) * Then sdkUpdate is emitted * And handler H receives metadata with getType() returning Type.SEGMENTS_UPDATE - * And handler H receives metadata with getNames() containing the changed segment names + * And handler H receives metadata with getNames() returning an empty list */ @Test public void sdkUpdateMetadataContainsTypeForMembershipSegmentsUpdate() throws Exception { - verifySdkUpdateForSegmentsPolling( + verifySdkUpdateForSegmentsPollingWithEmptyNames( // Initial sync: segment1, segment2 "{\"ms\":{\"k\":[{\"n\":\"segment1\"},{\"n\":\"segment2\"}],\"cn\":1000},\"ls\":{\"k\":[],\"cn\":1000}}", // Polling: segment1 removed, segment3 added - "{\"ms\":{\"k\":[{\"n\":\"segment2\"},{\"n\":\"segment3\"}],\"cn\":2000},\"ls\":{\"k\":[],\"cn\":1000}}", - "segment1", "segment3" + "{\"ms\":{\"k\":[{\"n\":\"segment2\"},{\"n\":\"segment3\"}],\"cn\":2000},\"ls\":{\"k\":[],\"cn\":1000}}" ); } @@ -1086,16 +1195,15 @@ public void sdkUpdateMetadataContainsTypeForMembershipSegmentsUpdate() throws Ex * When large segments change via polling (server returns different large segments) * Then sdkUpdate is emitted * And handler H receives metadata with getType() returning Type.SEGMENTS_UPDATE - * And handler H receives metadata with getNames() containing the changed large segment names + * And handler H receives metadata with getNames() returning an empty list */ @Test public void sdkUpdateMetadataContainsTypeForLargeSegmentsUpdate() throws Exception { - verifySdkUpdateForSegmentsPolling( + verifySdkUpdateForSegmentsPollingWithEmptyNames( // Initial sync: large_segment1, large_segment2 "{\"ms\":{\"k\":[],\"cn\":1000},\"ls\":{\"k\":[{\"n\":\"large_segment1\"},{\"n\":\"large_segment2\"}],\"cn\":1000}}", // Polling: large_segment1 removed, large_segment3 added - "{\"ms\":{\"k\":[],\"cn\":1000},\"ls\":{\"k\":[{\"n\":\"large_segment2\"},{\"n\":\"large_segment3\"}],\"cn\":2000}}", - "large_segment1", "large_segment3" + "{\"ms\":{\"k\":[],\"cn\":1000},\"ls\":{\"k\":[{\"n\":\"large_segment2\"},{\"n\":\"large_segment3\"}],\"cn\":2000}}" ); } @@ -1106,8 +1214,7 @@ public void sdkUpdateMetadataContainsTypeForLargeSegmentsUpdate() throws Excepti * And a handler H is registered for sdkUpdate * When a single memberships response contains changes to both segments and large segments * Then two SDK_UPDATE events are emitted - * And one event has metadata with getType() returning Type.SEGMENTS_UPDATE and names containing segment changes - * And another event has metadata with getType() returning Type.SEGMENTS_UPDATE and names containing large segment changes + * And both events have metadata with getType() returning Type.SEGMENTS_UPDATE and empty names */ @Test public void twoDistinctSdkUpdateEventsWhenBothSegmentsAndLargeSegmentsChange() throws Exception { @@ -1121,38 +1228,24 @@ public void twoDistinctSdkUpdateEventsWhenBothSegmentsAndLargeSegmentsChange() t // Verify we received 2 distinct SDK_UPDATE events assertEquals("Should receive 2 SDK_UPDATE events", 2, metadataList.size()); - // Both events should be SEGMENTS_UPDATE type + // Both events should be SEGMENTS_UPDATE type with empty names for (SdkUpdateMetadata metadata : metadataList) { assertNotNull("Metadata should not be null", metadata); assertEquals("Type should be SEGMENTS_UPDATE", SdkUpdateMetadata.Type.SEGMENTS_UPDATE, metadata.getType()); assertNotNull("Names should not be null", metadata.getNames()); - assertFalse("Names should not be empty", metadata.getNames().isEmpty()); - } - - // Collect all segment names from both events - List allSegmentNames = new ArrayList<>(); - for (SdkUpdateMetadata metadata : metadataList) { - allSegmentNames.addAll(metadata.getNames()); + assertTrue("Names should be empty for SEGMENTS_UPDATE", metadata.getNames().isEmpty()); } - - // Verify we have changes from both regular segments and large segments - boolean hasRegularSegmentChange = allSegmentNames.contains("segment1") || allSegmentNames.contains("segment3"); - boolean hasLargeSegmentChange = allSegmentNames.contains("large_segment1") || allSegmentNames.contains("large_segment3"); - - assertTrue("Should have regular segment changes", hasRegularSegmentChange); - assertTrue("Should have large segment changes", hasLargeSegmentChange); } /** * Helper method to verify SDK_UPDATE with SEGMENTS_UPDATE type is emitted when segments change via polling. + * Verifies that names are always empty for SEGMENTS_UPDATE. * - * @param initialResponse the memberships response for initial sync - * @param pollingResponse the memberships response for polling (with changed segments) - * @param expectedSegmentNames segment names expected in the metadata (removed or added) + * @param initialResponse the memberships response for initial sync + * @param pollingResponse the memberships response for polling (with changed segments) */ - private void verifySdkUpdateForSegmentsPolling(String initialResponse, String pollingResponse, - String... expectedSegmentNames) throws Exception { + private void verifySdkUpdateForSegmentsPollingWithEmptyNames(String initialResponse, String pollingResponse) throws Exception { List metadataList = waitForSegmentsPollingUpdates(initialResponse, pollingResponse, 1); assertEquals("Should receive 1 SDK_UPDATE event", 1, metadataList.size()); @@ -1163,16 +1256,7 @@ private void verifySdkUpdateForSegmentsPolling(String initialResponse, String po SdkUpdateMetadata.Type.SEGMENTS_UPDATE, metadata.getType()); assertNotNull("Names should not be null", metadata.getNames()); - assertFalse("Names should not be empty", metadata.getNames().isEmpty()); - - boolean containsExpected = false; - for (String expectedName : expectedSegmentNames) { - if (metadata.getNames().contains(expectedName)) { - containsExpected = true; - break; - } - } - assertTrue("Names should contain one of the expected changed segments", containsExpected); + assertTrue("Names should be empty for SEGMENTS_UPDATE", metadata.getNames().isEmpty()); } /** diff --git a/main/src/main/java/io/split/android/client/service/mysegments/MySegmentsSyncTask.java b/main/src/main/java/io/split/android/client/service/mysegments/MySegmentsSyncTask.java index 68c5518da..5141b887d 100644 --- a/main/src/main/java/io/split/android/client/service/mysegments/MySegmentsSyncTask.java +++ b/main/src/main/java/io/split/android/client/service/mysegments/MySegmentsSyncTask.java @@ -274,14 +274,13 @@ private void fireMySegmentsUpdatedIfNeeded(UpdateSegmentsResult segmentsResult, if (!changedSegments.isEmpty()) { Logger.v("New segments: " + segmentsResult.newSegments); - EventMetadata metadata = EventMetadataHelpers.createUpdatedSegmentsMetadata(changedSegments); - mEventsManager.notifyInternalEvent(mUpdateEvent, metadata); + mEventsManager.notifyInternalEvent(mUpdateEvent, EventMetadataHelpers.createUpdatedSegmentsMetadata()); } if (!changedLargeSegments.isEmpty()) { Logger.v("New large segments: " + largeSegmentsResult.newSegments); - EventMetadata metadata = EventMetadataHelpers.createUpdatedSegmentsMetadata(changedLargeSegments); - mEventsManager.notifyInternalEvent(SplitInternalEvent.MY_LARGE_SEGMENTS_UPDATED, metadata); + mEventsManager.notifyInternalEvent(SplitInternalEvent.MY_LARGE_SEGMENTS_UPDATED, + EventMetadataHelpers.createUpdatedSegmentsMetadata()); } // Fire sync complete AFTER update events. This ensures SDK_READY triggers after diff --git a/main/src/main/java/io/split/android/client/service/mysegments/MySegmentsUpdateTask.java b/main/src/main/java/io/split/android/client/service/mysegments/MySegmentsUpdateTask.java index 39fa5eebc..cf1257ca4 100644 --- a/main/src/main/java/io/split/android/client/service/mysegments/MySegmentsUpdateTask.java +++ b/main/src/main/java/io/split/android/client/service/mysegments/MySegmentsUpdateTask.java @@ -4,14 +4,11 @@ import androidx.annotation.NonNull; -import java.util.ArrayList; -import java.util.List; import java.util.Set; import io.split.android.client.dtos.SegmentsChange; import io.split.android.client.events.SplitEventsManager; import io.split.android.client.events.SplitInternalEvent; -import io.split.android.client.events.metadata.EventMetadata; import io.split.android.client.events.metadata.EventMetadataHelpers; import io.split.android.client.service.executor.SplitTask; import io.split.android.client.service.executor.SplitTaskExecutionInfo; @@ -100,9 +97,7 @@ public SplitTaskExecutionInfo remove() { private void updateAndNotify(Set segments) { mMySegmentsStorage.set(SegmentsChange.create(segments, mChangeNumber)); - List changedSegmentNames = new ArrayList<>(mSegmentNames); - EventMetadata metadata = EventMetadataHelpers.createUpdatedSegmentsMetadata(changedSegmentNames); - mEventsManager.notifyInternalEvent(mUpdateEvent, metadata); + mEventsManager.notifyInternalEvent(mUpdateEvent, EventMetadataHelpers.createUpdatedSegmentsMetadata()); } private void logError(String message) { diff --git a/main/src/main/java/io/split/android/client/service/rules/RuleBasedSegmentInPlaceUpdateTask.java b/main/src/main/java/io/split/android/client/service/rules/RuleBasedSegmentInPlaceUpdateTask.java index 0ca9b88dc..72c05e4a2 100644 --- a/main/src/main/java/io/split/android/client/service/rules/RuleBasedSegmentInPlaceUpdateTask.java +++ b/main/src/main/java/io/split/android/client/service/rules/RuleBasedSegmentInPlaceUpdateTask.java @@ -1,16 +1,12 @@ package io.split.android.client.service.rules; -import static io.split.android.client.service.splits.SplitsSyncHelper.extractRbsNames; import static io.split.android.client.utils.Utils.checkNotNull; import androidx.annotation.NonNull; -import java.util.List; - import io.split.android.client.dtos.RuleBasedSegment; import io.split.android.client.events.ISplitEventsManager; import io.split.android.client.events.SplitInternalEvent; -import io.split.android.client.events.metadata.EventMetadata; import io.split.android.client.events.metadata.EventMetadataHelpers; import io.split.android.client.service.executor.SplitTask; import io.split.android.client.service.executor.SplitTaskExecutionInfo; @@ -46,8 +42,8 @@ public SplitTaskExecutionInfo execute() { boolean triggerSdkUpdate = mRuleBasedSegmentStorage.update(processedChange.getActive(), processedChange.getArchived(), mChangeNumber, null); if (triggerSdkUpdate) { - EventMetadata metadata = createUpdatedRbsMetadata(processedChange); - mEventsManager.notifyInternalEvent(SplitInternalEvent.RULE_BASED_SEGMENTS_UPDATED, metadata); + mEventsManager.notifyInternalEvent(SplitInternalEvent.RULE_BASED_SEGMENTS_UPDATED, + EventMetadataHelpers.createUpdatedSegmentsMetadata()); } Logger.v("Updated rule based segment"); @@ -58,9 +54,4 @@ public SplitTaskExecutionInfo execute() { return SplitTaskExecutionInfo.error(SplitTaskType.RULE_BASED_SEGMENT_SYNC); } } - - private EventMetadata createUpdatedRbsMetadata(ProcessedRuleBasedSegmentChange processedChange) { - List updatedRbsNames = extractRbsNames(processedChange); - return EventMetadataHelpers.createUpdatedSegmentsMetadata(updatedRbsNames); - } } diff --git a/main/src/main/java/io/split/android/client/service/splits/RuleBasedSegmentInPlaceUpdateTask.java b/main/src/main/java/io/split/android/client/service/splits/RuleBasedSegmentInPlaceUpdateTask.java index 20b261651..6fb8fc8dc 100644 --- a/main/src/main/java/io/split/android/client/service/splits/RuleBasedSegmentInPlaceUpdateTask.java +++ b/main/src/main/java/io/split/android/client/service/splits/RuleBasedSegmentInPlaceUpdateTask.java @@ -4,12 +4,9 @@ import androidx.annotation.NonNull; -import java.util.List; - import io.split.android.client.dtos.RuleBasedSegment; import io.split.android.client.events.ISplitEventsManager; import io.split.android.client.events.SplitInternalEvent; -import io.split.android.client.events.metadata.EventMetadata; import io.split.android.client.events.metadata.EventMetadataHelpers; import io.split.android.client.service.executor.SplitTask; import io.split.android.client.service.executor.SplitTaskExecutionInfo; @@ -47,8 +44,8 @@ public SplitTaskExecutionInfo execute() { boolean triggerSdkUpdate = mRuleBasedSegmentStorage.update(processedChange.getActive(), processedChange.getArchived(), mChangeNumber, null); if (triggerSdkUpdate) { - EventMetadata metadata = createUpdatedRbsMetadata(processedChange); - mEventsManager.notifyInternalEvent(SplitInternalEvent.RULE_BASED_SEGMENTS_UPDATED, metadata); + mEventsManager.notifyInternalEvent(SplitInternalEvent.RULE_BASED_SEGMENTS_UPDATED, + EventMetadataHelpers.createUpdatedSegmentsMetadata()); } Logger.v("Updated rule based segment"); @@ -59,9 +56,4 @@ public SplitTaskExecutionInfo execute() { return SplitTaskExecutionInfo.error(SplitTaskType.RULE_BASED_SEGMENT_SYNC); } } - - private EventMetadata createUpdatedRbsMetadata(ProcessedRuleBasedSegmentChange processedChange) { - List updatedRbsNames = SplitsSyncHelper.extractRbsNames(processedChange); - return EventMetadataHelpers.createUpdatedSegmentsMetadata(updatedRbsNames); - } } diff --git a/main/src/main/java/io/split/android/client/service/splits/SplitsSyncHelper.java b/main/src/main/java/io/split/android/client/service/splits/SplitsSyncHelper.java index 42075e5b2..0ea6127c8 100644 --- a/main/src/main/java/io/split/android/client/service/splits/SplitsSyncHelper.java +++ b/main/src/main/java/io/split/android/client/service/splits/SplitsSyncHelper.java @@ -378,52 +378,6 @@ public static List extractFlagNames(@Nullable ProcessedSplitChange proce return updatedNames; } - /** - * Gets the list of updated rule-based segment names from the last sync operation. - * This includes both active (added/modified) and archived (removed) segments. - * - * @return list of updated RBS names, or empty list if no updates occurred - */ - @NonNull - public List getLastUpdatedRbsNames() { - ProcessedRuleBasedSegmentChange lastChange = mLastProcessedRbsChange.get(); - if (lastChange == null) { - return Collections.emptyList(); - } - return extractRbsNames(lastChange); - } - - /** - * Extracts rule-based segment names from a ProcessedRuleBasedSegmentChange. - * This includes both active (added/modified) and archived (removed) segments. - * - * @param processedChange the processed RBS change - * @return list of RBS names, or empty list if change is null - */ - @NonNull - public static List extractRbsNames(@Nullable ProcessedRuleBasedSegmentChange processedChange) { - if (processedChange == null) { - return Collections.emptyList(); - } - - List updatedNames = new ArrayList<>(); - if (processedChange.getActive() != null) { - for (RuleBasedSegment segment : processedChange.getActive()) { - if (segment != null && segment.getName() != null) { - updatedNames.add(segment.getName()); - } - } - } - if (processedChange.getArchived() != null) { - for (RuleBasedSegment segment : processedChange.getArchived()) { - if (segment != null && segment.getName() != null) { - updatedNames.add(segment.getName()); - } - } - } - return updatedNames; - } - private void updateRbsStorage(RuleBasedSegmentChange ruleBasedSegmentChange) { ProcessedRuleBasedSegmentChange change = mRuleBasedSegmentChangeProcessor.process(ruleBasedSegmentChange.getSegments(), ruleBasedSegmentChange.getTill()); mLastProcessedRbsChange.set(change); diff --git a/main/src/main/java/io/split/android/client/service/splits/SplitsSyncTask.java b/main/src/main/java/io/split/android/client/service/splits/SplitsSyncTask.java index 67a5615c6..119a348e6 100644 --- a/main/src/main/java/io/split/android/client/service/splits/SplitsSyncTask.java +++ b/main/src/main/java/io/split/android/client/service/splits/SplitsSyncTask.java @@ -106,14 +106,15 @@ private void notifyInternalEvent(long storedChangeNumber) { // if we fire TARGETING_RULES_SYNC_COMPLETE first, it may trigger SDK_READY, // and then the *_UPDATED events would immediately trigger SDK_UPDATE during initial sync. // By firing *_UPDATED first (while SDK_READY hasn't triggered yet), they won't trigger SDK_UPDATE. + // + // Use else-if logic: if splits changed, only fire SPLITS_UPDATED (FLAGS_UPDATE). + // RBS changes are only relevant when flags DIDN'T change. if (mSplitsSyncHelper.splitsHaveChanged()) { EventMetadata metadata = createUpdatedFlagsMetadata(); mEventsManager.notifyInternalEvent(SplitInternalEvent.SPLITS_UPDATED, metadata); - } - - if (mSplitsSyncHelper.ruleBasedSegmentsHaveChanged()) { - EventMetadata rbsMetadata = createUpdatedRbsMetadata(); - mEventsManager.notifyInternalEvent(SplitInternalEvent.RULE_BASED_SEGMENTS_UPDATED, rbsMetadata); + } else if (mSplitsSyncHelper.ruleBasedSegmentsHaveChanged()) { + mEventsManager.notifyInternalEvent(SplitInternalEvent.RULE_BASED_SEGMENTS_UPDATED, + EventMetadataHelpers.createUpdatedSegmentsMetadata()); } // Fire sync complete AFTER update events. This ensures SDK_READY triggers after @@ -128,11 +129,6 @@ private EventMetadata createUpdatedFlagsMetadata() { return EventMetadataHelpers.createUpdatedFlagsMetadata(updatedSplitNames); } - private EventMetadata createUpdatedRbsMetadata() { - List updatedRbsNames = mSplitsSyncHelper.getLastUpdatedRbsNames(); - return EventMetadataHelpers.createUpdatedSegmentsMetadata(updatedRbsNames); - } - private boolean splitsFilterHasChanged(String storedSplitsFilterQueryString) { return !sanitizeString(mSplitsFilterQueryStringFromConfig).equals(sanitizeString(storedSplitsFilterQueryString)); } diff --git a/main/src/main/java/io/split/android/client/service/splits/SplitsUpdateTask.java b/main/src/main/java/io/split/android/client/service/splits/SplitsUpdateTask.java index ca7d29bf8..3b18fffab 100644 --- a/main/src/main/java/io/split/android/client/service/splits/SplitsUpdateTask.java +++ b/main/src/main/java/io/split/android/client/service/splits/SplitsUpdateTask.java @@ -74,14 +74,15 @@ public SplitTaskExecutionInfo execute() { // Fire *_UPDATED events BEFORE sync complete. This order is important: // if we fire TARGETING_RULES_SYNC_COMPLETE first, it may trigger SDK_READY, // and then the *_UPDATED events would immediately trigger SDK_UPDATE during initial sync. + // + // Use If splits changed, only fire SPLITS_UPDATED (FLAGS_UPDATE). + // RBS changes are only relevant when flags DIDN'T change. if (mSplitsSyncHelper.splitsHaveChanged()) { EventMetadata metadata = createUpdatedFlagsMetadata(); mEventsManager.notifyInternalEvent(SplitInternalEvent.SPLITS_UPDATED, metadata); - } - - if (mSplitsSyncHelper.ruleBasedSegmentsHaveChanged()) { - EventMetadata rbsMetadata = createUpdatedRbsMetadata(); - mEventsManager.notifyInternalEvent(SplitInternalEvent.RULE_BASED_SEGMENTS_UPDATED, rbsMetadata); + } else if (mSplitsSyncHelper.ruleBasedSegmentsHaveChanged()) { + mEventsManager.notifyInternalEvent(SplitInternalEvent.RULE_BASED_SEGMENTS_UPDATED, + EventMetadataHelpers.createUpdatedSegmentsMetadata()); } // Fire sync complete AFTER update events @@ -96,11 +97,6 @@ private EventMetadata createUpdatedFlagsMetadata() { return EventMetadataHelpers.createUpdatedFlagsMetadata(updatedSplitNames); } - private EventMetadata createUpdatedRbsMetadata() { - List updatedRbsNames = mSplitsSyncHelper.getLastUpdatedRbsNames(); - return EventMetadataHelpers.createUpdatedSegmentsMetadata(updatedRbsNames); - } - @VisibleForTesting public void setChangeChecker(SplitsChangeChecker changeChecker) { mChangeChecker = changeChecker; diff --git a/main/src/test/java/io/split/android/client/service/SplitSyncTaskTest.java b/main/src/test/java/io/split/android/client/service/SplitSyncTaskTest.java index 348e8c815..eb719fc57 100644 --- a/main/src/test/java/io/split/android/client/service/SplitSyncTaskTest.java +++ b/main/src/test/java/io/split/android/client/service/SplitSyncTaskTest.java @@ -327,6 +327,28 @@ public void ruleBasedSegmentsUpdatedIsNotFiredWhenRbsUnchanged() { verify(mEventsManager).notifyInternalEvent(eq(SplitInternalEvent.TARGETING_RULES_SYNC_COMPLETE), any()); } + @Test + public void ruleBasedSegmentsUpdatedIsNotFiredWhenBothSplitsAndRbsChanged() { + // When both splits and RBS change, only SPLITS_UPDATED should fire (else-if logic) + mTask = SplitsSyncTask.build(mSplitsSyncHelper, mSplitsStorage, mRuleBasedSegmentStorage, + mQueryString, mEventsManager, mTelemetryRuntimeProducer); + when(mSplitsStorage.getTill()).thenReturn(-1L).thenReturn(100L); + when(mSplitsStorage.getUpdateTimestamp()).thenReturn(0L); + when(mSplitsStorage.getSplitsFilterQueryString()).thenReturn(mQueryString); + when(mSplitsSyncHelper.sync(any(), anyBoolean(), anyBoolean(), eq(ServiceConstants.ON_DEMAND_FETCH_BACKOFF_MAX_RETRIES))).thenReturn(SplitTaskExecutionInfo.success(SplitTaskType.SPLITS_SYNC)); + when(mSplitsSyncHelper.splitsHaveChanged()).thenReturn(true); + when(mSplitsSyncHelper.ruleBasedSegmentsHaveChanged()).thenReturn(true); + when(mSplitsSyncHelper.getLastUpdatedFlagNames()).thenReturn(Arrays.asList("flag1")); + + mTask.execute(); + + // SPLITS_UPDATED should fire + verify(mEventsManager).notifyInternalEvent(eq(SplitInternalEvent.SPLITS_UPDATED), any()); + // RULE_BASED_SEGMENTS_UPDATED should NOT fire (else-if logic) + verify(mEventsManager, never()).notifyInternalEvent(eq(SplitInternalEvent.RULE_BASED_SEGMENTS_UPDATED), any()); + verify(mEventsManager).notifyInternalEvent(eq(SplitInternalEvent.TARGETING_RULES_SYNC_COMPLETE), any()); + } + @After public void tearDown() { reset(mSplitsStorage); diff --git a/main/src/test/java/io/split/android/client/service/SplitUpdateTaskTest.java b/main/src/test/java/io/split/android/client/service/SplitUpdateTaskTest.java index 9cdef917e..7471ecc66 100644 --- a/main/src/test/java/io/split/android/client/service/SplitUpdateTaskTest.java +++ b/main/src/test/java/io/split/android/client/service/SplitUpdateTaskTest.java @@ -189,6 +189,28 @@ public void splitsUpdatedIncludesMetadataWithUpdatedFlags() { })); } + @Test + public void ruleBasedSegmentsUpdatedIsNotFiredWhenBothSplitsAndRbsChanged() { + // When both splits and RBS change, only SPLITS_UPDATED should fire (else-if logic) + long storedChangeNumber = 100L; + long storedRbsChangeNumber = 200L; + when(mSplitsStorage.getTill()).thenReturn(storedChangeNumber).thenReturn(150L); + when(mRuleBasedSegmentStorage.getChangeNumber()).thenReturn(storedRbsChangeNumber).thenReturn(250L); + when(mSplitsSyncHelper.sync(any(), eq(ServiceConstants.ON_DEMAND_FETCH_BACKOFF_MAX_RETRIES))) + .thenReturn(SplitTaskExecutionInfo.success(SplitTaskType.SPLITS_SYNC)); + when(mSplitsSyncHelper.splitsHaveChanged()).thenReturn(true); + when(mSplitsSyncHelper.ruleBasedSegmentsHaveChanged()).thenReturn(true); + when(mSplitsSyncHelper.getLastUpdatedFlagNames()).thenReturn(Arrays.asList("flag1")); + + mTask.execute(); + + // SPLITS_UPDATED should fire + verify(mEventsManager).notifyInternalEvent(eq(SplitInternalEvent.SPLITS_UPDATED), any()); + // RULE_BASED_SEGMENTS_UPDATED should NOT fire (else-if logic) + verify(mEventsManager, never()).notifyInternalEvent(eq(SplitInternalEvent.RULE_BASED_SEGMENTS_UPDATED), any()); + verify(mEventsManager).notifyInternalEvent(eq(SplitInternalEvent.TARGETING_RULES_SYNC_COMPLETE), any()); + } + @After public void tearDown() { reset(mSplitsStorage); diff --git a/main/src/test/java/io/split/android/client/service/SplitsSyncHelperTest.java b/main/src/test/java/io/split/android/client/service/SplitsSyncHelperTest.java index 0f8105c7c..e60147600 100644 --- a/main/src/test/java/io/split/android/client/service/SplitsSyncHelperTest.java +++ b/main/src/test/java/io/split/android/client/service/SplitsSyncHelperTest.java @@ -780,66 +780,4 @@ public void getLastUpdatedFlagNamesIncludesArchivedSplits() throws HttpFetcherEx assertEquals(1, result.size()); assertTrue(result.contains("archived_split")); } - - @Test - public void getLastUpdatedRbsNamesReturnsSegmentNamesAfterSync() throws HttpFetcherException { - RuleBasedSegment activeSegment = RuleBasedSegmentStorageImplTest.createRuleBasedSegment("active_segment"); - RuleBasedSegment archivedSegment = RuleBasedSegmentStorageImplTest.createRuleBasedSegment("archived_segment"); - SplitChange splitChange = SplitChange.create(-1, 100L, Collections.emptyList()); - RuleBasedSegmentChange rbsChange = RuleBasedSegmentChange.create(-1, 100L, Collections.singletonList(activeSegment)); - // Create ProcessedRuleBasedSegmentChange with both active and archived segments - ProcessedRuleBasedSegmentChange processedChange = new ProcessedRuleBasedSegmentChange( - Set.of(activeSegment), Set.of(archivedSegment), 100L, System.currentTimeMillis()); - - doReturn(processedChange).when(mRuleBasedSegmentChangeProcessor).process(any(List.class), anyLong()); - - when(mSplitsFetcher.execute(any(), any())) - .thenReturn(TargetingRulesChange.create(splitChange, rbsChange)) - .thenReturn(TargetingRulesChange.create(SplitChange.create(100L, 100L, Collections.emptyList()), RuleBasedSegmentChange.create(100L, 100L, Collections.emptyList()))); - when(mSplitsStorage.getTill()).thenReturn(-1L).thenReturn(100L); - when(mRuleBasedSegmentStorageProducer.getChangeNumber()).thenReturn(-1L).thenReturn(100L); - - mSplitsSyncHelper.sync(getSinceChangeNumbers(-1, -1L), false, false, ServiceConstants.ON_DEMAND_FETCH_BACKOFF_MAX_RETRIES); - - List result = mSplitsSyncHelper.getLastUpdatedRbsNames(); - assertEquals(2, result.size()); - assertTrue(result.contains("active_segment")); - assertTrue(result.contains("archived_segment")); - } - - @Test - public void getLastUpdatedRbsNamesReturnsEmptyListWhenNoSyncPerformed() { - List result = mSplitsSyncHelper.getLastUpdatedRbsNames(); - assertTrue(result.isEmpty()); - } - - @Test - public void extractRbsNamesReturnsActiveAndArchivedSegmentNames() { - RuleBasedSegment activeSegment = RuleBasedSegmentStorageImplTest.createRuleBasedSegment("active_segment"); - RuleBasedSegment archivedSegment = RuleBasedSegmentStorageImplTest.createRuleBasedSegment("archived_segment"); - ProcessedRuleBasedSegmentChange processedChange = new ProcessedRuleBasedSegmentChange( - Set.of(activeSegment), Set.of(archivedSegment), 100L, System.currentTimeMillis()); - - List result = SplitsSyncHelper.extractRbsNames(processedChange); - - assertEquals(2, result.size()); - assertTrue(result.contains("active_segment")); - assertTrue(result.contains("archived_segment")); - } - - @Test - public void extractRbsNamesReturnsEmptyListForNullChange() { - List result = SplitsSyncHelper.extractRbsNames(null); - assertTrue(result.isEmpty()); - } - - @Test - public void extractRbsNamesHandlesNullActiveAndArchivedSets() { - ProcessedRuleBasedSegmentChange processedChange = new ProcessedRuleBasedSegmentChange( - null, null, 100L, System.currentTimeMillis()); - - List result = SplitsSyncHelper.extractRbsNames(processedChange); - - assertTrue(result.isEmpty()); - } } From b078793fb575f6434e222902c42883a5cfd72d1d Mon Sep 17 00:00:00 2001 From: Gaston Thea Date: Mon, 12 Jan 2026 16:52:01 -0300 Subject: [PATCH 26/27] Update version --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index bd6d05d7f..0209da0a1 100644 --- a/build.gradle +++ b/build.gradle @@ -19,7 +19,7 @@ apply plugin: 'com.vanniktech.maven.publish' apply from: "$rootDir/gradle/jacoco-root.gradle" ext { - splitVersion = '5.5.0-rc5' + splitVersion = '5.5.0-rc6' jacocoVersion = '0.8.8' } From 3bb355612bab61fee1b074b14a7242be210c4366 Mon Sep 17 00:00:00 2001 From: Gaston Thea Date: Mon, 12 Jan 2026 22:39:35 -0300 Subject: [PATCH 27/27] Fix test --- .../rules/RuleBasedSegmentInPlaceUpdateTaskTest.java | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/main/src/test/java/io/split/android/client/service/rules/RuleBasedSegmentInPlaceUpdateTaskTest.java b/main/src/test/java/io/split/android/client/service/rules/RuleBasedSegmentInPlaceUpdateTaskTest.java index 713ba5cfa..37d05d47d 100644 --- a/main/src/test/java/io/split/android/client/service/rules/RuleBasedSegmentInPlaceUpdateTaskTest.java +++ b/main/src/test/java/io/split/android/client/service/rules/RuleBasedSegmentInPlaceUpdateTaskTest.java @@ -99,7 +99,7 @@ public void updateIsCalledOnStorage() { } @Test - public void segmentsUpdatedIncludesMetadataWithActiveAndArchivedSegmentNames() { + public void segmentsUpdatedIncludesMetadataWithEmptyNames() { RuleBasedSegment activeSegment = createRuleBasedSegment("active_segment"); RuleBasedSegment archivedSegment = createRuleBasedSegment("archived_segment"); long changeNumber = 123L; @@ -111,14 +111,13 @@ public void segmentsUpdatedIncludesMetadataWithActiveAndArchivedSegmentNames() { mTask = getTask(activeSegment, changeNumber); mTask.execute(); + // SEGMENTS_UPDATE always has empty names verify(mEventsManager).notifyInternalEvent(eq(SplitInternalEvent.RULE_BASED_SEGMENTS_UPDATED), argThat(metadata -> { if (metadata == null) return false; SdkUpdateMetadata typedMeta = TypedTaskConverter.convertForSdkUpdate(metadata); List names = typedMeta.getNames(); assertNotNull(names); - assertEquals(2, names.size()); - assertTrue(names.contains("active_segment")); - assertTrue(names.contains("archived_segment")); + assertTrue("Names should be empty for SEGMENTS_UPDATE", names.isEmpty()); assertEquals(SdkUpdateMetadata.Type.SEGMENTS_UPDATE, typedMeta.getType()); return true; }));