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 70fc71dec..d7fc334a7 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 @@ -1,5 +1,7 @@ package io.split.android.client.events.metadata; +import androidx.annotation.Nullable; + import java.util.ArrayList; import java.util.HashSet; import java.util.List; @@ -14,6 +16,8 @@ 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 @@ -24,4 +28,22 @@ public static EventMetadata createUpdatedFlagsMetadata(List updatedSplit .put(KEY_UPDATED_FLAGS, new ArrayList<>(new HashSet<>(updatedSplitNames))) .build(); } + + /** + * Creates metadata for the SDK_READY_FROM_CACHE event. + * + * @param lastUpdateTimestamp the timestamp when the cache was last updated, or null if not available + * @param freshInstall true if this is a fresh install (no prior cache), false if loaded from cache + * @return the event metadata + */ + public static EventMetadata createCacheReadyMetadata(@Nullable Long lastUpdateTimestamp, boolean freshInstall) { + EventMetadataBuilder builder = new EventMetadataBuilder() + .put(KEY_FRESH_INSTALL, freshInstall); + + if (lastUpdateTimestamp != null) { + builder.put(KEY_LAST_UPDATE_TIMESTAMP, lastUpdateTimestamp); + } + + return builder.build(); + } } 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 new file mode 100644 index 000000000..d21bc8d3f --- /dev/null +++ b/events-domain/src/test/java/io/split/android/client/events/metadata/EventMetadataHelpersTest.java @@ -0,0 +1,85 @@ +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; + +import org.junit.Test; + +import java.util.Arrays; +import java.util.List; + +import io.split.android.client.api.EventMetadata; + +public class EventMetadataHelpersTest { + + // Tests for createUpdatedFlagsMetadata (existing) + @Test + 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"); + assertEquals(3, result.size()); + assertTrue(result.contains("flag1")); + assertTrue(result.contains("flag2")); + assertTrue(result.contains("flag3")); + } + + // Tests for createCacheReadyMetadata + @Test + public void createCacheReadyMetadataWithTimestampAndFreshInstallFalse() { + EventMetadata metadata = EventMetadataHelpers.createCacheReadyMetadata(1234567890L, false); + + assertEquals(1234567890L, metadata.get("lastUpdateTimestamp")); + assertEquals(false, metadata.get("freshInstall")); + } + + @Test + public void createCacheReadyMetadataWithNullTimestampAndFreshInstallTrue() { + EventMetadata metadata = EventMetadataHelpers.createCacheReadyMetadata(null, true); + + assertNull(metadata.get("lastUpdateTimestamp")); + assertEquals(true, metadata.get("freshInstall")); + } + + @Test + public void createCacheReadyMetadataKeysAreCorrect() { + EventMetadata metadata = EventMetadataHelpers.createCacheReadyMetadata(123L, false); + + assertTrue(metadata.containsKey("lastUpdateTimestamp")); + assertTrue(metadata.containsKey("freshInstall")); + assertEquals(2, metadata.keys().size()); + } + + @Test + public void createCacheReadyMetadataWithZeroTimestamp() { + EventMetadata metadata = EventMetadataHelpers.createCacheReadyMetadata(0L, false); + + assertEquals(0L, metadata.get("lastUpdateTimestamp")); + assertEquals(false, metadata.get("freshInstall")); + } + + @Test + public void createCacheReadyMetadataForCachePath() { + // Cache path: freshInstall=false, timestamp from storage + long storedTimestamp = 1700000000000L; + EventMetadata metadata = EventMetadataHelpers.createCacheReadyMetadata(storedTimestamp, false); + + assertFalse((Boolean) metadata.get("freshInstall")); + assertEquals(storedTimestamp, metadata.get("lastUpdateTimestamp")); + } + + @Test + public void createCacheReadyMetadataForSyncPath() { + // Sync path: freshInstall=true, timestamp=null + EventMetadata metadata = EventMetadataHelpers.createCacheReadyMetadata(null, true); + + assertTrue((Boolean) metadata.get("freshInstall")); + assertNull(metadata.get("lastUpdateTimestamp")); + } +} + diff --git a/gradle/common-android-library.gradle b/gradle/common-android-library.gradle index 93f2c7962..fe96407c8 100644 --- a/gradle/common-android-library.gradle +++ b/gradle/common-android-library.gradle @@ -10,6 +10,10 @@ android { } } +tasks.withType(JavaCompile).configureEach { + options.compilerArgs.add('-parameters') +} + def kotlinCompileClass = null try { kotlinCompileClass = Class.forName('org.jetbrains.kotlin.gradle.tasks.KotlinCompile') @@ -20,10 +24,10 @@ if (kotlinCompileClass != null) { tasks.withType(kotlinCompileClass).configureEach { kotlinOptions { jvmTarget = "1.8" + javaParameters = true } } } // Enable Jacoco coverage configuration for all Android library modules apply from: "$rootDir/gradle/jacoco-android.gradle" - diff --git a/main/build.gradle b/main/build.gradle index dba2405f2..7ec1e3110 100644 --- a/main/build.gradle +++ b/main/build.gradle @@ -9,7 +9,7 @@ android { defaultConfig { multiDexEnabled true - consumerProguardFiles 'consumer-rules.pro' + consumerProguardFiles "$rootDir/split-proguard-rules.pro" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunnerArguments clearPackageData: 'true' diff --git a/main/consumer-rules.pro b/main/consumer-rules.pro deleted file mode 100644 index b776cf12b..000000000 --- a/main/consumer-rules.pro +++ /dev/null @@ -1,10 +0,0 @@ -# Consumer ProGuard rules for Split Android SDK -# These rules are automatically applied to apps that depend on this library - -# Suppress warnings for java.beans classes (not available on Android) -# These are referenced by snakeyaml but not actually used on Android --dontwarn java.beans.BeanInfo --dontwarn java.beans.FeatureDescriptor --dontwarn java.beans.IntrospectionException --dontwarn java.beans.Introspector --dontwarn java.beans.PropertyDescriptor diff --git a/main/src/main/java/io/split/android/client/SplitFactoryImpl.java b/main/src/main/java/io/split/android/client/SplitFactoryImpl.java index d531fde38..8bb12d71f 100644 --- a/main/src/main/java/io/split/android/client/SplitFactoryImpl.java +++ b/main/src/main/java/io/split/android/client/SplitFactoryImpl.java @@ -244,7 +244,8 @@ private SplitFactoryImpl(@NonNull String apiToken, @NonNull Key key, @NonNull Sp mImpressionManager, mStorageContainer.getEventsStorage(), mEventsManagerCoordinator, - streamingComponents.getPushManagerEventBroadcaster() + streamingComponents.getPushManagerEventBroadcaster(), + mStorageContainer.getSplitsStorage() ); // Only available for integration tests if (synchronizerSpy != null) { 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 4ceec166c..ed6402229 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 @@ -219,10 +219,12 @@ private void loadSplits() { } } if (!content.equals(mLastContentLoaded)) { - mEventsManager.notifyInternalEvent(SplitInternalEvent.SPLITS_LOADED_FROM_STORAGE); + // Cache path metadata: freshInstall=false (loaded from file), timestamp=null for localhost + EventMetadata cacheMetadata = EventMetadataHelpers.createCacheReadyMetadata(null, false); + mEventsManager.notifyInternalEvent(SplitInternalEvent.SPLITS_LOADED_FROM_STORAGE, cacheMetadata); mEventsManager.notifyInternalEvent(SplitInternalEvent.TARGETING_RULES_SYNC_COMPLETE); - EventMetadata metadata = createUpdatedFlagsMetadata(); - mEventsManager.notifyInternalEvent(SplitInternalEvent.SPLITS_UPDATED, metadata); + EventMetadata updateMetadata = createUpdatedFlagsMetadata(); + mEventsManager.notifyInternalEvent(SplitInternalEvent.SPLITS_UPDATED, updateMetadata); } mLastContentLoaded = content; } 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 9e07fc52b..a9487f044 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 @@ -98,8 +98,10 @@ public SplitTaskExecutionInfo execute() { private void notifyInternalEvent(long storedChangeNumber) { if (mEventsManager != null) { - // Always fire SPLITS_SYNC_COMPLETE when sync succeeds - mEventsManager.notifyInternalEvent(SplitInternalEvent.TARGETING_RULES_SYNC_COMPLETE); + // Always fire TARGETING_RULES_SYNC_COMPLETE when sync succeeds + // Sync path metadata: freshInstall=true (synced from network), timestamp=null + EventMetadata syncMetadata = EventMetadataHelpers.createCacheReadyMetadata(null, true); + mEventsManager.notifyInternalEvent(SplitInternalEvent.TARGETING_RULES_SYNC_COMPLETE, syncMetadata); // Fire SPLITS_UPDATED only if data actually changed if (mChangeChecker.changeNumberIsNewer(storedChangeNumber, mSplitsStorage.getTill())) { 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 a7d35c671..c739bce11 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 @@ -71,8 +71,10 @@ public SplitTaskExecutionInfo execute() { SplitTaskExecutionInfo result = mSplitsSyncHelper.sync(new SplitsSyncHelper.SinceChangeNumbers(mChangeNumber, mRbsChangeNumber), ServiceConstants.ON_DEMAND_FETCH_BACKOFF_MAX_RETRIES); if (result.getStatus() == SplitTaskExecutionStatus.SUCCESS) { - // Always fire SPLITS_SYNC_COMPLETE when sync succeeds - mEventsManager.notifyInternalEvent(SplitInternalEvent.TARGETING_RULES_SYNC_COMPLETE); + // Always fire TARGETING_RULES_SYNC_COMPLETE when sync succeeds + // Sync path metadata: freshInstall=true (synced from network), timestamp=null + EventMetadata syncMetadata = EventMetadataHelpers.createCacheReadyMetadata(null, true); + mEventsManager.notifyInternalEvent(SplitInternalEvent.TARGETING_RULES_SYNC_COMPLETE, syncMetadata); // Fire SPLITS_UPDATED only if data actually changed if (mChangeChecker.changeNumberIsNewer(storedChangeNumber, mSplitsStorage.getTill()) || diff --git a/main/src/main/java/io/split/android/client/service/synchronizer/FeatureFlagsSynchronizerImpl.java b/main/src/main/java/io/split/android/client/service/synchronizer/FeatureFlagsSynchronizerImpl.java index b7675e727..0c0746dad 100644 --- a/main/src/main/java/io/split/android/client/service/synchronizer/FeatureFlagsSynchronizerImpl.java +++ b/main/src/main/java/io/split/android/client/service/synchronizer/FeatureFlagsSynchronizerImpl.java @@ -14,6 +14,7 @@ import io.split.android.client.SplitClientConfig; import io.split.android.client.events.ISplitEventsManager; import io.split.android.client.events.SplitInternalEvent; +import io.split.android.client.events.metadata.EventMetadataHelpers; import io.split.android.client.service.executor.SplitTaskBatchItem; import io.split.android.client.service.executor.SplitTaskExecutionInfo; import io.split.android.client.service.executor.SplitTaskExecutionListener; @@ -24,6 +25,7 @@ import io.split.android.client.service.sseclient.feedbackchannel.PushManagerEventBroadcaster; import io.split.android.client.service.sseclient.feedbackchannel.PushStatusEvent; import io.split.android.client.service.sseclient.sseclient.RetryBackoffCounterTimer; +import io.split.android.client.storage.splits.SplitsStorage; public class FeatureFlagsSynchronizerImpl implements FeatureFlagsSynchronizer { @@ -48,6 +50,18 @@ public FeatureFlagsSynchronizerImpl(@NonNull SplitClientConfig splitClientConfig @NonNull ISplitEventsManager splitEventsManager, @NonNull RetryBackoffCounterTimerFactory retryBackoffCounterTimerFactory, @Nullable PushManagerEventBroadcaster pushManagerEventBroadcaster) { + this(splitClientConfig, taskExecutor, splitSingleThreadTaskExecutor, splitTaskFactory, + splitEventsManager, retryBackoffCounterTimerFactory, pushManagerEventBroadcaster, null); + } + + public FeatureFlagsSynchronizerImpl(@NonNull SplitClientConfig splitClientConfig, + @NonNull SplitTaskExecutor taskExecutor, + @NonNull SplitTaskExecutor splitSingleThreadTaskExecutor, + @NonNull SplitTaskFactory splitTaskFactory, + @NonNull ISplitEventsManager splitEventsManager, + @NonNull RetryBackoffCounterTimerFactory retryBackoffCounterTimerFactory, + @Nullable PushManagerEventBroadcaster pushManagerEventBroadcaster, + @Nullable SplitsStorage splitsStorage) { mTaskExecutor = checkNotNull(taskExecutor); mSplitsTaskExecutor = splitSingleThreadTaskExecutor; @@ -80,8 +94,14 @@ public void taskExecuted(@NonNull SplitTaskExecutionInfo taskInfo) { } mSplitsSyncRetryTimer.setTask(mSplitTaskFactory.createSplitsSyncTask(true), mSplitsSyncListener); + + // Create metadata provider for cache path (freshInstall=false, lastUpdateTimestamp from storage) + LoadLocalDataListener.MetadataProvider cacheMetadataProvider = splitsStorage != null + ? () -> EventMetadataHelpers.createCacheReadyMetadata(splitsStorage.getUpdateTimestamp(), false) + : null; + mLoadLocalSplitsListener = new LoadLocalDataListener( - splitEventsManager, SplitInternalEvent.SPLITS_LOADED_FROM_STORAGE); + splitEventsManager, SplitInternalEvent.SPLITS_LOADED_FROM_STORAGE, cacheMetadataProvider); } @Override 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 1490ef929..49d5e91c3 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 @@ -3,7 +3,9 @@ import static io.split.android.client.utils.Utils.checkNotNull; import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import io.split.android.client.api.EventMetadata; import io.split.android.client.events.ISplitEventsManager; import io.split.android.client.events.SplitInternalEvent; import io.split.android.client.service.executor.SplitTaskExecutionInfo; @@ -12,19 +14,37 @@ public class LoadLocalDataListener implements SplitTaskExecutionListener { + /** + * Functional interface for providing metadata when the event is fired. + */ + public interface MetadataProvider { + @Nullable + EventMetadata getMetadata(); + } + private final ISplitEventsManager mSplitEventsManager; private final SplitInternalEvent mEventToFire; + @Nullable + private final MetadataProvider mMetadataProvider; public LoadLocalDataListener(ISplitEventsManager splitEventsManager, SplitInternalEvent eventToFire) { + this(splitEventsManager, eventToFire, null); + } + + public LoadLocalDataListener(ISplitEventsManager splitEventsManager, + SplitInternalEvent eventToFire, + @Nullable MetadataProvider metadataProvider) { mSplitEventsManager = checkNotNull(splitEventsManager); mEventToFire = checkNotNull(eventToFire); + mMetadataProvider = metadataProvider; } @Override public void taskExecuted(@NonNull SplitTaskExecutionInfo taskInfo) { if (taskInfo.getStatus().equals(SplitTaskExecutionStatus.SUCCESS)) { - mSplitEventsManager.notifyInternalEvent(mEventToFire); + EventMetadata metadata = mMetadataProvider != null ? mMetadataProvider.getMetadata() : null; + mSplitEventsManager.notifyInternalEvent(mEventToFire, metadata); } } } diff --git a/main/src/main/java/io/split/android/client/service/synchronizer/SynchronizerImpl.java b/main/src/main/java/io/split/android/client/service/synchronizer/SynchronizerImpl.java index ddfd906e8..abf55e7fe 100644 --- a/main/src/main/java/io/split/android/client/service/synchronizer/SynchronizerImpl.java +++ b/main/src/main/java/io/split/android/client/service/synchronizer/SynchronizerImpl.java @@ -31,6 +31,7 @@ import io.split.android.client.service.synchronizer.mysegments.MySegmentsSynchronizerRegistryImpl; import io.split.android.client.shared.UserConsent; import io.split.android.client.storage.common.StoragePusher; +import io.split.android.client.storage.splits.SplitsStorage; import io.split.android.client.telemetry.model.EventsDataRecordsEnum; import io.split.android.client.telemetry.model.streaming.SyncModeUpdateStreamingEvent; import io.split.android.client.telemetry.storage.TelemetryRuntimeProducer; @@ -69,7 +70,8 @@ public SynchronizerImpl(@NonNull SplitClientConfig splitClientConfig, @NonNull StrategyImpressionManager impressionManager, @NonNull StoragePusher eventsStorage, @NonNull ISplitEventsManager eventsManagerCoordinator, - @Nullable PushManagerEventBroadcaster pushManagerEventBroadcaster) { + @Nullable PushManagerEventBroadcaster pushManagerEventBroadcaster, + @Nullable SplitsStorage splitsStorage) { this(splitClientConfig, taskExecutor, splitSingleThreadTaskExecutor, @@ -85,7 +87,8 @@ public SynchronizerImpl(@NonNull SplitClientConfig splitClientConfig, splitTaskFactory, eventsManagerCoordinator, retryBackoffCounterTimerFactory, - pushManagerEventBroadcaster + pushManagerEventBroadcaster, + splitsStorage ), eventsStorage); } 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 456210555..0d7fdc85a 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 @@ -92,7 +92,7 @@ public void loadLocalNotifiesSplitsUpdatedWithMetadataContainingUpdatedFlags() t // First load - should notify events with metadata mLocalhostSplitsStorage.loadLocal(); - verify(mEventsManagerCoordinator).notifyInternalEvent(eq(SplitInternalEvent.SPLITS_LOADED_FROM_STORAGE)); + verify(mEventsManagerCoordinator).notifyInternalEvent(eq(SplitInternalEvent.SPLITS_LOADED_FROM_STORAGE), any()); verify(mEventsManagerCoordinator).notifyInternalEvent(eq(SplitInternalEvent.TARGETING_RULES_SYNC_COMPLETE)); ArgumentCaptor metadataCaptor = ArgumentCaptor.forClass(EventMetadata.class); 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 90352c09f..0713d0d14 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 @@ -142,7 +142,7 @@ public void splitUpdatedNotified() throws HttpFetcherException { mTask.execute(); - verify(mEventsManager, times(1)).notifyInternalEvent(eq(SplitInternalEvent.TARGETING_RULES_SYNC_COMPLETE)); + verify(mEventsManager, times(1)).notifyInternalEvent(eq(SplitInternalEvent.TARGETING_RULES_SYNC_COMPLETE), any()); verify(mEventsManager, times(1)).notifyInternalEvent(eq(SplitInternalEvent.SPLITS_UPDATED), any()); } @@ -159,7 +159,7 @@ public void splitSyncCompleteNotifiedWhenNoDataChange() throws HttpFetcherExcept mTask.execute(); - verify(mEventsManager, times(1)).notifyInternalEvent(eq(SplitInternalEvent.TARGETING_RULES_SYNC_COMPLETE)); + verify(mEventsManager, times(1)).notifyInternalEvent(eq(SplitInternalEvent.TARGETING_RULES_SYNC_COMPLETE), any()); verify(mEventsManager, never()).notifyInternalEvent(eq(SplitInternalEvent.SPLITS_UPDATED), any()); } @@ -201,7 +201,7 @@ public void targetingRulesSyncCompleteIsAlwaysFiredOnSuccessfulSync() throws Htt mTask.execute(); - verify(mEventsManager).notifyInternalEvent(eq(SplitInternalEvent.TARGETING_RULES_SYNC_COMPLETE)); + verify(mEventsManager).notifyInternalEvent(eq(SplitInternalEvent.TARGETING_RULES_SYNC_COMPLETE), any()); } @Test @@ -232,7 +232,7 @@ public void splitsUpdatedIsNotFiredWhenDataUnchanged() throws HttpFetcherExcepti mTask.execute(); verify(mEventsManager, never()).notifyInternalEvent(eq(SplitInternalEvent.SPLITS_UPDATED), any()); - verify(mEventsManager).notifyInternalEvent(eq(SplitInternalEvent.TARGETING_RULES_SYNC_COMPLETE)); + verify(mEventsManager).notifyInternalEvent(eq(SplitInternalEvent.TARGETING_RULES_SYNC_COMPLETE), any()); } @Test 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 4900c0548..191b557af 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 @@ -94,7 +94,7 @@ public void storedRbsChangeNumBigger() { } @Test - public void targetingRulesSyncCompleteIsAlwaysFiredOnSuccessfulSync() { + public void targetingRulesSyncCompleteIsAlwaysFiredOnSuccessfulSyncWithSyncMetadata() { when(mSplitsStorage.getTill()).thenReturn(100L); when(mRuleBasedSegmentStorage.getChangeNumber()).thenReturn(200L); when(mSplitsSyncHelper.sync(any(), eq(ServiceConstants.ON_DEMAND_FETCH_BACKOFF_MAX_RETRIES))) @@ -102,7 +102,14 @@ public void targetingRulesSyncCompleteIsAlwaysFiredOnSuccessfulSync() { mTask.execute(); - verify(mEventsManager).notifyInternalEvent(eq(SplitInternalEvent.TARGETING_RULES_SYNC_COMPLETE)); + // 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; + })); } @Test @@ -116,7 +123,7 @@ public void splitsUpdatedIsFiredWhenSplitsDataChanged() { mTask.execute(); verify(mEventsManager).notifyInternalEvent(eq(SplitInternalEvent.SPLITS_UPDATED), any()); - verify(mEventsManager).notifyInternalEvent(eq(SplitInternalEvent.TARGETING_RULES_SYNC_COMPLETE)); + verify(mEventsManager).notifyInternalEvent(eq(SplitInternalEvent.TARGETING_RULES_SYNC_COMPLETE), any()); } @Test @@ -130,7 +137,7 @@ public void splitsUpdatedIsFiredWhenRbsDataChanged() { mTask.execute(); verify(mEventsManager).notifyInternalEvent(eq(SplitInternalEvent.SPLITS_UPDATED), any()); - verify(mEventsManager).notifyInternalEvent(eq(SplitInternalEvent.TARGETING_RULES_SYNC_COMPLETE)); + verify(mEventsManager).notifyInternalEvent(eq(SplitInternalEvent.TARGETING_RULES_SYNC_COMPLETE), any()); } @Test @@ -145,7 +152,7 @@ public void splitsUpdatedIsNotFiredWhenDataUnchanged() { mTask.execute(); verify(mEventsManager, never()).notifyInternalEvent(eq(SplitInternalEvent.SPLITS_UPDATED), any()); - verify(mEventsManager).notifyInternalEvent(eq(SplitInternalEvent.TARGETING_RULES_SYNC_COMPLETE)); + verify(mEventsManager).notifyInternalEvent(eq(SplitInternalEvent.TARGETING_RULES_SYNC_COMPLETE), any()); } @Test 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 new file mode 100644 index 000000000..adda882a1 --- /dev/null +++ b/main/src/test/java/io/split/android/client/service/synchronizer/LoadLocalDataListenerTest.java @@ -0,0 +1,87 @@ +package io.split.android.client.service.synchronizer; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.ArgumentMatchers.isNull; +import static org.mockito.Mockito.*; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import org.junit.Before; +import org.junit.Test; +import org.mockito.Mockito; + +import io.split.android.client.api.EventMetadata; +import io.split.android.client.events.ISplitEventsManager; +import io.split.android.client.events.SplitInternalEvent; +import io.split.android.client.service.executor.SplitTaskExecutionInfo; +import io.split.android.client.service.executor.SplitTaskType; + +public class LoadLocalDataListenerTest { + + private ISplitEventsManager mEventsManager; + + @Before + public void setUp() { + mEventsManager = mock(ISplitEventsManager.class); + } + + @Test + public void taskExecutedSuccessFiresEventWithoutMetadataWhenProviderIsNull() { + LoadLocalDataListener listener = new LoadLocalDataListener( + mEventsManager, SplitInternalEvent.SPLITS_LOADED_FROM_STORAGE); + + listener.taskExecuted(SplitTaskExecutionInfo.success(SplitTaskType.LOAD_LOCAL_SPLITS)); + + verify(mEventsManager).notifyInternalEvent(eq(SplitInternalEvent.SPLITS_LOADED_FROM_STORAGE), isNull()); + } + + @Test + public void taskExecutedSuccessFiresEventWithMetadataWhenProviderIsNotNull() { + EventMetadata mockMetadata = mock(EventMetadata.class); + LoadLocalDataListener.MetadataProvider provider = () -> mockMetadata; + + LoadLocalDataListener listener = new LoadLocalDataListener( + mEventsManager, SplitInternalEvent.SPLITS_LOADED_FROM_STORAGE, provider); + + listener.taskExecuted(SplitTaskExecutionInfo.success(SplitTaskType.LOAD_LOCAL_SPLITS)); + + verify(mEventsManager).notifyInternalEvent(eq(SplitInternalEvent.SPLITS_LOADED_FROM_STORAGE), eq(mockMetadata)); + } + + @Test + public void taskExecutedErrorDoesNotFireEvent() { + LoadLocalDataListener listener = new LoadLocalDataListener( + mEventsManager, SplitInternalEvent.SPLITS_LOADED_FROM_STORAGE); + + listener.taskExecuted(SplitTaskExecutionInfo.error(SplitTaskType.LOAD_LOCAL_SPLITS)); + + verify(mEventsManager, never()).notifyInternalEvent(any(), any()); + } + + @Test + public void metadataProviderIsCalledWhenTaskSucceeds() { + LoadLocalDataListener.MetadataProvider provider = mock(LoadLocalDataListener.MetadataProvider.class); + when(provider.getMetadata()).thenReturn(null); + + LoadLocalDataListener listener = new LoadLocalDataListener( + mEventsManager, SplitInternalEvent.SPLITS_LOADED_FROM_STORAGE, provider); + + listener.taskExecuted(SplitTaskExecutionInfo.success(SplitTaskType.LOAD_LOCAL_SPLITS)); + + verify(provider).getMetadata(); + } + + @Test + public void metadataProviderIsNotCalledWhenTaskFails() { + LoadLocalDataListener.MetadataProvider provider = mock(LoadLocalDataListener.MetadataProvider.class); + + LoadLocalDataListener listener = new LoadLocalDataListener( + mEventsManager, SplitInternalEvent.SPLITS_LOADED_FROM_STORAGE, provider); + + listener.taskExecuted(SplitTaskExecutionInfo.error(SplitTaskType.LOAD_LOCAL_SPLITS)); + + verify(provider, never()).getMetadata(); + } +} diff --git a/split-proguard-rules.pro b/split-proguard-rules.pro index 8dc624df9..de3caa063 100644 --- a/split-proguard-rules.pro +++ b/split-proguard-rules.pro @@ -13,6 +13,7 @@ -keep class io.split.android.client.service.sseclient.SseAuthenticationResponse { *; } -keep class io.split.android.client.service.sseclient.notifications.** { *; } -keepattributes Signature +-keepattributes MethodParameters -keep class com.google.gson.reflect.TypeToken { *; } -keep class * extends com.google.gson.reflect.TypeToken -dontwarn java.beans.BeanInfo @@ -26,6 +27,9 @@ # removes such information by default, so configure it to keep all of it. -keepattributes Signature +# Preserve method parameter names so consumers see actual parameter names instead of s0, s1, s2, etc. +-keepattributes MethodParameters + # For using GSON @Expose annotation -keepattributes *Annotation*