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 981294b60..51f02dcaa 100644 --- a/api/src/main/java/io/split/android/client/SplitClient.java +++ b/api/src/main/java/io/split/android/client/SplitClient.java @@ -183,13 +183,19 @@ public interface SplitClient extends AttributesManager { /** * Registers an event listener for SDK events that provide typed metadata. *
- * This method provides type-safe callbacks for SDK_UPDATE and SDK_READY_FROM_CACHE events. + * This method provides type-safe callbacks for SDK_READY, SDK_UPDATE, and SDK_READY_FROM_CACHE events. * Override the methods you need in the listener. *
* Example usage: *
{@code
* client.addEventListener(new SdkEventListener() {
* @Override
+ * public void onReady(SplitClient client, SdkReadyMetadata metadata) {
+ * Boolean initialCacheLoad = metadata.isInitialCacheLoad();
+ * // Handle SDK ready on background thread
+ * }
+ *
+ * @Override
* public void onUpdate(SplitClient client, SdkUpdateMetadata metadata) {
* SdkUpdateMetadata.Type type = metadata.getType(); // FLAGS_UPDATE or SEGMENTS_UPDATE
* List names = metadata.getNames(); // updated flag/segment names
@@ -197,9 +203,9 @@ public interface SplitClient extends AttributesManager {
* }
*
* @Override
- * public void onReadyFromCacheView(SplitClient client, SdkReadyFromCacheMetadata metadata) {
+ * public void onReadyFromCacheView(SplitClient client, SdkReadyMetadata metadata) {
* // Handle on main/UI thread
- * Boolean freshInstall = metadata.isFreshInstall();
+ * Boolean initialCacheLoad = metadata.isInitialCacheLoad();
* }
* });
* }
diff --git a/api/src/main/java/io/split/android/client/events/SdkEventListener.java b/api/src/main/java/io/split/android/client/events/SdkEventListener.java
index eddccc204..c6a7a4409 100644
--- a/api/src/main/java/io/split/android/client/events/SdkEventListener.java
+++ b/api/src/main/java/io/split/android/client/events/SdkEventListener.java
@@ -16,6 +16,12 @@
* {@code
* client.addEventListener(new SdkEventListener() {
* @Override
+ * public void onReady(SplitClient client, SdkReadyMetadata metadata) {
+ * Boolean initialCacheLoad = metadata.isInitialCacheLoad();
+ * // Handle ready on background thread
+ * }
+ *
+ * @Override
* public void onUpdate(SplitClient client, SdkUpdateMetadata metadata) {
* SdkUpdateMetadata.Type type = metadata.getType(); // FLAGS_UPDATE or SEGMENTS_UPDATE
* List names = metadata.getNames(); // updated flag/segment names
@@ -23,15 +29,40 @@
* }
*
* @Override
- * public void onReadyFromCacheView(SplitClient client, SdkReadyFromCacheMetadata metadata) {
+ * public void onReadyFromCacheView(SplitClient client, SdkReadyMetadata metadata) {
* // Handle cache ready on main/UI thread
- * Boolean freshInstall = metadata.isFreshInstall();
+ * Boolean initialCacheLoad = metadata.isInitialCacheLoad();
* }
* });
* }
*/
public abstract class SdkEventListener {
+ /**
+ * Called when SDK_READY event occurs, executed on a background thread.
+ * + * Override this method to handle SDK_READY events with typed metadata. + * + * @param client the Split client instance + * @param metadata the typed metadata containing ready state information + */ + public void onReady(SplitClient client, SdkReadyMetadata metadata) { + // Default empty implementation + } + + /** + * Called when SDK_READY event occurs, executed on the main/UI thread. + *
+ * Override this method to handle SDK_READY 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 ready state information + */ + public void onReadyView(SplitClient client, SdkReadyMetadata metadata) { + // Default empty implementation + } + /** * Called when SDK_UPDATE event occurs, executed on a background thread. *
@@ -52,7 +83,7 @@ public void onUpdate(SplitClient client, SdkUpdateMetadata metadata) { * @param client the Split client instance * @param metadata the typed metadata containing cache information */ - public void onReadyFromCache(SplitClient client, SdkReadyFromCacheMetadata metadata) { + public void onReadyFromCache(SplitClient client, SdkReadyMetadata metadata) { // Default empty implementation } @@ -78,8 +109,7 @@ public void onUpdateView(SplitClient client, SdkUpdateMetadata metadata) { * @param client the Split client instance * @param metadata the typed metadata containing cache information */ - public void onReadyFromCacheView(SplitClient client, SdkReadyFromCacheMetadata metadata) { + public void onReadyFromCacheView(SplitClient client, SdkReadyMetadata metadata) { // Default empty implementation } } - 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 deleted file mode 100644 index 3f1a883ed..000000000 --- a/api/src/main/java/io/split/android/client/events/SdkReadyFromCacheMetadata.java +++ /dev/null @@ -1,49 +0,0 @@ -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/SdkReadyMetadata.java b/api/src/main/java/io/split/android/client/events/SdkReadyMetadata.java new file mode 100644 index 000000000..977576373 --- /dev/null +++ b/api/src/main/java/io/split/android/client/events/SdkReadyMetadata.java @@ -0,0 +1,52 @@ +package io.split.android.client.events; + +import androidx.annotation.Nullable; + +/** + * Typed metadata for SDK_READY and SDK_READY_FROM_CACHE events. + *
+ * Contains information about the cache state when the SDK becomes ready. + */ +public final class SdkReadyMetadata { + + @Nullable + private final Boolean mInitialCacheLoad; + + @Nullable + private final Long mLastUpdateTimestamp; + + /** + * Creates a new SdkReadyMetadata instance. + * + * @param initialCacheLoad true if this is an initial cache load 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 SdkReadyMetadata(@Nullable Boolean initialCacheLoad, @Nullable Long lastUpdateTimestamp) { + mInitialCacheLoad = initialCacheLoad; + mLastUpdateTimestamp = lastUpdateTimestamp; + } + + /** + * Returns whether this is an initial cache load with no usable cache. + *
+ * This is true when the SDK starts without any prior cached data (fresh install),
+ * meaning data was fetched from the server for the first time.
+ *
+ * @return true if initial cache load, false otherwise, or null if not available
+ */
+ @Nullable
+ public Boolean isInitialCacheLoad() {
+ return mInitialCacheLoad;
+ }
+
+ /**
+ * 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/test/java/io/split/android/client/events/SdkReadyFromCacheMetadataTest.java b/api/src/test/java/io/split/android/client/events/SdkReadyFromCacheMetadataTest.java
deleted file mode 100644
index 64fd003f4..000000000
--- a/api/src/test/java/io/split/android/client/events/SdkReadyFromCacheMetadataTest.java
+++ /dev/null
@@ -1,66 +0,0 @@
-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/SdkReadyMetadataTest.java b/api/src/test/java/io/split/android/client/events/SdkReadyMetadataTest.java
new file mode 100644
index 000000000..35d898c57
--- /dev/null
+++ b/api/src/test/java/io/split/android/client/events/SdkReadyMetadataTest.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 SdkReadyMetadataTest {
+
+ @Test
+ public void isInitialCacheLoadReturnsNullWhenConstructedWithNull() {
+ SdkReadyMetadata metadata = new SdkReadyMetadata(null, null);
+
+ assertNull(metadata.isInitialCacheLoad());
+ }
+
+ @Test
+ public void isInitialCacheLoadReturnsTrueWhenConstructedWithTrue() {
+ SdkReadyMetadata metadata = new SdkReadyMetadata(true, null);
+
+ assertTrue(metadata.isInitialCacheLoad());
+ }
+
+ @Test
+ public void isInitialCacheLoadReturnsFalseWhenConstructedWithFalse() {
+ SdkReadyMetadata metadata = new SdkReadyMetadata(false, null);
+
+ assertFalse(metadata.isInitialCacheLoad());
+ }
+
+ @Test
+ public void getLastUpdateTimestampReturnsNullWhenConstructedWithNull() {
+ SdkReadyMetadata metadata = new SdkReadyMetadata(null, null);
+
+ assertNull(metadata.getLastUpdateTimestamp());
+ }
+
+ @Test
+ public void getLastUpdateTimestampReturnsValueWhenConstructedWithValue() {
+ long timestamp = 1704067200000L;
+ SdkReadyMetadata metadata = new SdkReadyMetadata(null, timestamp);
+
+ assertEquals(Long.valueOf(timestamp), metadata.getLastUpdateTimestamp());
+ }
+
+ @Test
+ public void bothValuesReturnCorrectlyWhenBothAreSet() {
+ long timestamp = 1704067200000L;
+ SdkReadyMetadata metadata = new SdkReadyMetadata(true, timestamp);
+
+ assertTrue(metadata.isInitialCacheLoad());
+ assertEquals(Long.valueOf(timestamp), metadata.getLastUpdateTimestamp());
+ }
+
+ @Test
+ public void bothValuesReturnCorrectlyWhenInitialCacheLoadIsFalse() {
+ long timestamp = 1704067200000L;
+ SdkReadyMetadata metadata = new SdkReadyMetadata(false, timestamp);
+
+ assertFalse(metadata.isInitialCacheLoad());
+ assertEquals(Long.valueOf(timestamp), metadata.getLastUpdateTimestamp());
+ }
+}
+
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 4fba4b4d4..7d8061224 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
@@ -127,4 +127,20 @@ private void propagateTriggeredEvents(ISplitEventsManager splitEventsManager) {
}
}
}
+
+ /**
+ * Checks if an external event has already been triggered in any registered manager.
+ *
+ * @param event the event to check
+ * @return true if the event has already been triggered in any manager, false otherwise
+ */
+ @Override
+ public boolean eventAlreadyTriggered(SplitEvent event) {
+ for (ISplitEventsManager manager : mManagers.values()) {
+ if (manager.eventAlreadyTriggered(event)) {
+ return true;
+ }
+ }
+ return false;
+ }
}
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 d6dd48859..d350b35d3 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
@@ -15,4 +15,12 @@ public interface ISplitEventsManager {
* @param metadata the event metadata, can be null
*/
void notifyInternalEvent(SplitInternalEvent internalEvent, @Nullable EventMetadata metadata);
+
+ /**
+ * Checks if an external event has already been triggered.
+ *
+ * @param event the event to check
+ * @return true if the event has already been triggered (reached its max executions), false otherwise
+ */
+ 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 5cf91b3e0..5977fbbb2 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
@@ -116,6 +116,14 @@ public void register(SplitEvent event, SplitEventTask task) {
public void registerEventListener(SdkEventListener listener) {
requireNonNull(listener);
+ // Register SDK_READY handlers (bg + main)
+ mDualExecutorRegistration.register(
+ mEventsManager,
+ SplitEvent.SDK_READY,
+ createReadyBackgroundHandler(listener),
+ createReadyMainThreadHandler(listener)
+ );
+
// Register SDK_UPDATE handlers (bg + main)
mDualExecutorRegistration.register(
mEventsManager,
@@ -181,6 +189,23 @@ private EventHandler
* This is an internal API for SDK infrastructure use.
* Consumers should use the typed metadata classes instead:
- * {@code SdkUpdateMetadata} and {@code SdkReadyFromCacheMetadata}.
+ * {@code SdkUpdateMetadata} and {@code SdkReadyMetadata}.
*
* Values are sanitized to only allow String, Number, Boolean, or List<String>.
*/
diff --git a/events-domain/src/main/java/io/split/android/client/events/metadata/EventMetadataHelpers.java b/events-domain/src/main/java/io/split/android/client/events/metadata/EventMetadataHelpers.java
index 4116de596..863799cb2 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
@@ -44,15 +44,15 @@ public static EventMetadata createUpdatedSegmentsMetadata(List
+ * If cache was already loaded (SDK_READY_FROM_CACHE fired), uses initialCacheLoad=false
+ * and includes the update timestamp. Otherwise, uses initialCacheLoad=true with no timestamp.
+ *
+ * @param cacheAlreadyLoaded true if SDK_READY_FROM_CACHE has already fired
+ * @param updateTimestamp the timestamp from storage, used only if cacheAlreadyLoaded is true
+ * @return the event metadata for sync complete
+ */
+ public static EventMetadata createSyncCompleteMetadata(boolean cacheAlreadyLoaded, @Nullable Long updateTimestamp) {
+ Long timestamp = cacheAlreadyLoaded ? updateTimestamp : null;
+ return createReadyMetadata(timestamp, !cacheAlreadyLoaded);
+ }
}
diff --git a/events-domain/src/main/java/io/split/android/client/events/metadata/MetadataKeys.java b/events-domain/src/main/java/io/split/android/client/events/metadata/MetadataKeys.java
index b4763d1bc..73ff243e7 100644
--- a/events-domain/src/main/java/io/split/android/client/events/metadata/MetadataKeys.java
+++ b/events-domain/src/main/java/io/split/android/client/events/metadata/MetadataKeys.java
@@ -29,12 +29,12 @@ private MetadataKeys() {
*/
static final String NAMES = "names";
- // SDK_READY_FROM_CACHE event keys
+ // SDK_READY and SDK_READY_FROM_CACHE event keys
/**
- * True if this is a fresh install with no usable cache.
+ * True if this is an initial cache load with no usable cache.
*/
- static final String FRESH_INSTALL = "freshInstall";
+ static final String INITIAL_CACHE_LOAD = "initialCacheLoad";
/**
* Last successful cache timestamp in milliseconds since epoch.
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 1885c1faa..9c2e2b526 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
@@ -5,7 +5,7 @@
import java.util.List;
-import io.split.android.client.events.SdkReadyFromCacheMetadata;
+import io.split.android.client.events.SdkReadyMetadata;
import io.split.android.client.events.SdkUpdateMetadata;
/**
@@ -48,19 +48,19 @@ public static SdkUpdateMetadata convertForSdkUpdate(@Nullable EventMetadata meta
}
/**
- * Converts EventMetadata to SdkReadyFromCacheMetadata.
+ * Converts EventMetadata to SdkReadyMetadata.
*
* @param metadata the event metadata, may be null
- * @return the typed metadata for SDK_READY_FROM_CACHE events
+ * @return the typed metadata for SDK_READY and SDK_READY_FROM_CACHE events
*/
@NonNull
- public static SdkReadyFromCacheMetadata convertForSdkReadyFromCache(@Nullable EventMetadata metadata) {
- Boolean freshInstall = null;
+ public static SdkReadyMetadata convertForSdkReady(@Nullable EventMetadata metadata) {
+ Boolean initialCacheLoad = null;
Long lastUpdateTimestamp = null;
if (metadata != null) {
- freshInstall = (Boolean) metadata.get(MetadataKeys.FRESH_INSTALL);
+ initialCacheLoad = (Boolean) metadata.get(MetadataKeys.INITIAL_CACHE_LOAD);
lastUpdateTimestamp = (Long) metadata.get(MetadataKeys.LAST_UPDATE_TIMESTAMP);
}
- return new SdkReadyFromCacheMetadata(freshInstall, lastUpdateTimestamp);
+ return new SdkReadyMetadata(initialCacheLoad, 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
index b74fd2dc6..fbca03876 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
@@ -50,16 +50,16 @@ public void convertForSdkUpdateConvertsSegmentsMetadataCorrectly() {
}
@Test
- public void convertForSdkReadyFromCacheConvertsMetadataCorrectly() {
+ public void convertForSdkReadyConvertsMetadataCorrectly() {
long expectedTimestamp = 1704067200000L;
- EventMetadata eventMetadata = EventMetadataHelpers.createCacheReadyMetadata(expectedTimestamp, true);
+ EventMetadata eventMetadata = EventMetadataHelpers.createReadyMetadata(expectedTimestamp, true);
// Call conversion method
- SdkReadyFromCacheMetadata converted = TypedTaskConverter.convertForSdkReadyFromCache(eventMetadata);
+ SdkReadyMetadata converted = TypedTaskConverter.convertForSdkReady(eventMetadata);
assertNotNull(converted);
- assertTrue(converted.isFreshInstall());
+ assertTrue(converted.isInitialCacheLoad());
assertEquals(Long.valueOf(expectedTimestamp), converted.getLastUpdateTimestamp());
}
@@ -73,11 +73,11 @@ public void convertForSdkUpdateHandlesNullMetadata() {
}
@Test
- public void convertForSdkReadyFromCacheHandlesNullMetadata() {
- SdkReadyFromCacheMetadata converted = TypedTaskConverter.convertForSdkReadyFromCache(null);
+ public void convertForSdkReadyHandlesNullMetadata() {
+ SdkReadyMetadata converted = TypedTaskConverter.convertForSdkReady(null);
assertNotNull(converted);
- assertNull(converted.isFreshInstall());
+ assertNull(converted.isInitialCacheLoad());
assertNull(converted.getLastUpdateTimestamp());
}
}
diff --git a/events-domain/src/test/java/io/split/android/client/events/metadata/EventMetadataHelpersTest.java b/events-domain/src/test/java/io/split/android/client/events/metadata/EventMetadataHelpersTest.java
index 9a3dcda84..0343c418f 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
@@ -48,56 +48,81 @@ public void createUpdatedSegmentsMetadataContainsTypeAndNames() {
assertTrue(result.contains("segment2"));
}
- // Tests for createCacheReadyMetadata
+ // Tests for createReadyMetadata
@Test
- public void createCacheReadyMetadataWithTimestampAndFreshInstallFalse() {
- EventMetadata metadata = EventMetadataHelpers.createCacheReadyMetadata(1234567890L, false);
+ public void createReadyMetadataWithTimestampAndInitialCacheLoadFalse() {
+ EventMetadata metadata = EventMetadataHelpers.createReadyMetadata(1234567890L, false);
assertEquals(Long.valueOf(1234567890L), metadata.get(MetadataKeys.LAST_UPDATE_TIMESTAMP));
- assertEquals(Boolean.FALSE, metadata.get(MetadataKeys.FRESH_INSTALL));
+ assertEquals(Boolean.FALSE, metadata.get(MetadataKeys.INITIAL_CACHE_LOAD));
}
@Test
- public void createCacheReadyMetadataWithNullTimestampAndFreshInstallTrue() {
- EventMetadata metadata = EventMetadataHelpers.createCacheReadyMetadata(null, true);
+ public void createReadyMetadataWithNullTimestampAndInitialCacheLoadTrue() {
+ EventMetadata metadata = EventMetadataHelpers.createReadyMetadata(null, true);
assertNull(metadata.get(MetadataKeys.LAST_UPDATE_TIMESTAMP));
- assertEquals(Boolean.TRUE, metadata.get(MetadataKeys.FRESH_INSTALL));
+ assertEquals(Boolean.TRUE, metadata.get(MetadataKeys.INITIAL_CACHE_LOAD));
}
@Test
- public void createCacheReadyMetadataKeysAreCorrect() {
- EventMetadata metadata = EventMetadataHelpers.createCacheReadyMetadata(123L, false);
+ public void createReadyMetadataKeysAreCorrect() {
+ EventMetadata metadata = EventMetadataHelpers.createReadyMetadata(123L, false);
assertTrue(metadata.containsKey(MetadataKeys.LAST_UPDATE_TIMESTAMP));
- assertTrue(metadata.containsKey(MetadataKeys.FRESH_INSTALL));
+ assertTrue(metadata.containsKey(MetadataKeys.INITIAL_CACHE_LOAD));
assertEquals(2, metadata.size());
}
@Test
- public void createCacheReadyMetadataWithZeroTimestamp() {
- EventMetadata metadata = EventMetadataHelpers.createCacheReadyMetadata(0L, false);
+ public void createReadyMetadataWithZeroTimestamp() {
+ EventMetadata metadata = EventMetadataHelpers.createReadyMetadata(0L, false);
assertEquals(Long.valueOf(0L), metadata.get(MetadataKeys.LAST_UPDATE_TIMESTAMP));
- assertEquals(Boolean.FALSE, metadata.get(MetadataKeys.FRESH_INSTALL));
+ assertEquals(Boolean.FALSE, metadata.get(MetadataKeys.INITIAL_CACHE_LOAD));
}
@Test
- public void createCacheReadyMetadataForCachePath() {
- // Cache path: freshInstall=false, timestamp from storage
+ public void createReadyMetadataForCachePath() {
+ // Cache path: initialCacheLoad=false, timestamp from storage
long storedTimestamp = 1700000000000L;
- EventMetadata metadata = EventMetadataHelpers.createCacheReadyMetadata(storedTimestamp, false);
+ EventMetadata metadata = EventMetadataHelpers.createReadyMetadata(storedTimestamp, false);
- assertEquals(Boolean.FALSE, metadata.get(MetadataKeys.FRESH_INSTALL));
+ assertEquals(Boolean.FALSE, metadata.get(MetadataKeys.INITIAL_CACHE_LOAD));
assertEquals(Long.valueOf(storedTimestamp), metadata.get(MetadataKeys.LAST_UPDATE_TIMESTAMP));
}
@Test
- public void createCacheReadyMetadataForSyncPath() {
- // Sync path: freshInstall=true, timestamp=null
- EventMetadata metadata = EventMetadataHelpers.createCacheReadyMetadata(null, true);
+ public void createReadyMetadataForSyncPath() {
+ // Sync path: initialCacheLoad=true, timestamp=null
+ EventMetadata metadata = EventMetadataHelpers.createReadyMetadata(null, true);
+
+ assertEquals(Boolean.TRUE, metadata.get(MetadataKeys.INITIAL_CACHE_LOAD));
+ assertNull(metadata.get(MetadataKeys.LAST_UPDATE_TIMESTAMP));
+ }
+
+ @Test
+ public void createSyncCompleteMetadataWhenCacheAlreadyLoaded() {
+ long updateTimestamp = 1234567890L;
+ EventMetadata metadata = EventMetadataHelpers.createSyncCompleteMetadata(true, updateTimestamp);
+
+ assertEquals(Boolean.FALSE, metadata.get(MetadataKeys.INITIAL_CACHE_LOAD));
+ assertEquals(updateTimestamp, metadata.get(MetadataKeys.LAST_UPDATE_TIMESTAMP));
+ }
+
+ @Test
+ public void createSyncCompleteMetadataWhenCacheNotLoaded() {
+ EventMetadata metadata = EventMetadataHelpers.createSyncCompleteMetadata(false, 1234567890L);
+
+ assertEquals(Boolean.TRUE, metadata.get(MetadataKeys.INITIAL_CACHE_LOAD));
+ assertNull(metadata.get(MetadataKeys.LAST_UPDATE_TIMESTAMP));
+ }
+
+ @Test
+ public void createSyncCompleteMetadataIgnoresTimestampWhenCacheNotLoaded() {
+ // Even if a timestamp is provided, it should be ignored when cache is not loaded
+ EventMetadata metadata = EventMetadataHelpers.createSyncCompleteMetadata(false, 9999999999L);
- 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/MetadataKeysTest.java b/events-domain/src/test/java/io/split/android/client/events/metadata/MetadataKeysTest.java
index b7968cd36..bf5d6db35 100644
--- a/events-domain/src/test/java/io/split/android/client/events/metadata/MetadataKeysTest.java
+++ b/events-domain/src/test/java/io/split/android/client/events/metadata/MetadataKeysTest.java
@@ -21,8 +21,8 @@ public void namesKeyHasCorrectValue() {
}
@Test
- public void freshInstallKeyHasCorrectValue() {
- assertEquals("freshInstall", MetadataKeys.FRESH_INSTALL);
+ public void initialCacheLoadKeyHasCorrectValue() {
+ assertEquals("initialCacheLoad", MetadataKeys.INITIAL_CACHE_LOAD);
}
@Test
diff --git a/main/src/androidTest/java/tests/integration/events/SdkEventsIntegrationTest.java b/main/src/androidTest/java/tests/integration/events/SdkEventsIntegrationTest.java
index 179d1ddd2..aaf25576c 100644
--- a/main/src/androidTest/java/tests/integration/events/SdkEventsIntegrationTest.java
+++ b/main/src/androidTest/java/tests/integration/events/SdkEventsIntegrationTest.java
@@ -7,6 +7,7 @@
import android.content.Context;
+import androidx.annotation.NonNull;
import androidx.test.platform.app.InstrumentationRegistry;
import org.junit.After;
@@ -37,7 +38,7 @@
import io.split.android.client.SplitFactory;
import io.split.android.client.api.Key;
import io.split.android.client.events.SdkEventListener;
-import io.split.android.client.events.SdkReadyFromCacheMetadata;
+import io.split.android.client.events.SdkReadyMetadata;
import io.split.android.client.events.SdkUpdateMetadata;
import io.split.android.client.events.SplitEvent;
import io.split.android.client.events.SplitEventTask;
@@ -47,6 +48,7 @@
import io.split.android.client.storage.db.SplitEntity;
import io.split.android.client.storage.db.SplitRoomDatabase;
import io.split.android.client.utils.logger.Logger;
+import io.split.android.client.utils.logger.SplitLogLevel;
import okhttp3.mockwebserver.Dispatcher;
import okhttp3.mockwebserver.MockResponse;
import okhttp3.mockwebserver.MockWebServer;
@@ -71,6 +73,7 @@ private ServiceEndpoints endpoints() {
private SplitClientConfig buildConfig() {
return SplitClientConfig.builder()
.serviceEndpoints(endpoints())
+ .logLevel(SplitLogLevel.VERBOSE)
.ready(30000)
.featuresRefreshRate(999999) // High refresh rate to avoid periodic sync interfering
.segmentsRefreshRate(999999)
@@ -88,7 +91,7 @@ private SplitFactory buildFactory(SplitClientConfig config) {
@Before
public void setup() {
mWebServer = new MockWebServer();
- mCurSplitReqId = 1;
+ mCurSplitReqId = 1003;
final Dispatcher dispatcher = new Dispatcher() {
@Override
public MockResponse dispatch(RecordedRequest request) {
@@ -132,7 +135,7 @@ public void tearDown() throws Exception {
* "attributesLoadedFromStorage" and "encryptionMigrationDone" are notified
* Then sdkReadyFromCache is emitted exactly once
* And handler H is invoked once
- * And the metadata contains "freshInstall" with value false
+ * And the metadata contains "initialCacheLoad" with value false
* And the metadata contains "lastUpdateTimestamp" with a valid timestamp
*/
@Test
@@ -146,7 +149,7 @@ public void sdkReadyFromCacheFiresWhenCacheLoadingCompletes() throws Exception {
// And: a handler H is registered for sdkReadyFromCache
AtomicInteger handlerInvocationCount = new AtomicInteger(0);
- AtomicReference
+ * Given the SDK is starting with populated persistent storage
+ * And a handler H is registered using addEventListener with onReady
+ * When SDK_READY fires
+ * Then onReady is invoked exactly once
+ * And the handler receives the SplitClient and SdkReadyMetadata
+ * And the metadata contains "initialCacheLoad" with value false
+ * And the metadata contains "lastUpdateTimestamp" with a valid timestamp
+ */
+ @Test
+ public void sdkReadyListenerFiresWithMetadata() throws Exception {
+ // Given: SDK is starting with populated persistent storage
+ long testTimestamp = System.currentTimeMillis();
+ populateDatabaseWithCacheData(testTimestamp);
+
+ SplitClientConfig config = buildConfig();
+ SplitFactory factory = buildFactory(config);
+
+ AtomicInteger onReadyCount = new AtomicInteger(0);
+ AtomicReference
+ * Given sdkReady has already been emitted
+ * When a new handler H is registered using addEventListener with onReady
+ * Then onReady handler H is invoked exactly once immediately (replay)
+ */
+ @Test
+ public void sdkReadyListenerReplaysToLateSubscribers() throws Exception {
+ // Given: sdkReady has already been emitted
+ TestClientFixture fixture = createClientAndWaitForReady(new Key("key_1"));
+
+ // When: a new handler H is registered for onReady after SDK_READY has fired
+ AtomicInteger onReadyCount = new AtomicInteger(0);
+ AtomicReference
+ * Given the SDK is starting
+ * And a handler H is registered using addEventListener with onReadyView
+ * When SDK_READY fires
+ * Then onReadyView is invoked on the main/UI thread
+ */
+ @Test
+ public void sdkReadyViewListenerFiresOnMainThread() throws Exception {
+ // Given: SDK is starting with populated persistent storage
+ long testTimestamp = System.currentTimeMillis();
+ populateDatabaseWithCacheData(testTimestamp);
+
+ SplitClientConfig config = buildConfig();
+ SplitFactory factory = buildFactory(config);
+
+ AtomicInteger onReadyViewCount = new AtomicInteger(0);
+ CountDownLatch readyViewLatch = new CountDownLatch(1);
+
+ SplitClient client = factory.client(new Key("key_1"));
+
+ // And: a handler H is registered using addEventListener with onReadyView
+ client.addEventListener(new SdkEventListener() {
+ @Override
+ public void onReadyView(SplitClient client, SdkReadyMetadata metadata) {
+ onReadyViewCount.incrementAndGet();
+ readyViewLatch.countDown();
+ }
+ });
+
+ // When: SDK_READY fires
+ boolean fired = readyViewLatch.await(10, TimeUnit.SECONDS);
+
+ // Then: onReadyView is invoked
+ assertTrue("onReadyView should fire", fired);
+ assertEquals("onReadyView should be invoked exactly once", 1, onReadyViewCount.get());
factory.destroy();
}
@@ -1209,11 +1354,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> batchCaptor = ArgumentCaptor.forClass(List.class);
+
+ synchronizer.loadAndSynchronize();
+
+ verify(mTaskExecutor).executeSerially(batchCaptor.capture());
+ List