From 42d42191caa7b6d93fb27b1ade9eb17659c09665 Mon Sep 17 00:00:00 2001 From: Gaston Thea Date: Mon, 5 Jan 2026 18:33:40 -0300 Subject: [PATCH 1/9] 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 2/9] 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 3/9] 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 4/9] 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 5/9] 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 6/9] 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 7/9] 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 8/9] 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 9/9] 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"));