From 3cf5e6d0368086cafd9129edef8808becef29f9a Mon Sep 17 00:00:00 2001 From: Evan Greer Date: Wed, 15 Jan 2025 16:14:47 -0700 Subject: [PATCH 01/21] sets up disableForegroundCriteriaFetching config value --- .../java/com/iterable/iterableapi/IterableConfig.java | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/iterableapi/src/main/java/com/iterable/iterableapi/IterableConfig.java b/iterableapi/src/main/java/com/iterable/iterableapi/IterableConfig.java index df285393f..d1754879a 100644 --- a/iterableapi/src/main/java/com/iterable/iterableapi/IterableConfig.java +++ b/iterableapi/src/main/java/com/iterable/iterableapi/IterableConfig.java @@ -94,6 +94,8 @@ public class IterableConfig { final boolean enableAnonActivation; + final boolean disableForegroundCriteriaFetching; + final int eventThresholdLimit; /** @@ -128,6 +130,7 @@ private IterableConfig(Builder builder) { eventThresholdLimit = builder.eventThresholdLimit; identityResolution = builder.identityResolution; iterableAnonUserHandler = builder.iterableAnonUserHandler; + disableForegroundCriteriaFetching = builder.disableForegroundCriteriaFetching; } public static class Builder { @@ -147,6 +150,7 @@ public static class Builder { private boolean useInMemoryStorageForInApps = false; private boolean encryptionEnforced = false; private boolean enableAnonActivation = false; + private boolean disableForegroundCriteriaFetching = false; private boolean enableEmbeddedMessaging = false; private int eventThresholdLimit = 100; private IterableIdentityResolution identityResolution = new IterableIdentityResolution(); @@ -330,6 +334,11 @@ public Builder setEnableAnonActivation(boolean enableAnonActivation) { return this; } + public Builder setDisableForegroundCriteriaFetching(boolean disableForegroundCriteriaFetching) { + this.disableForegroundCriteriaFetching = disableForegroundCriteriaFetching; + return this; + } + public Builder setEventThresholdLimit(int eventThresholdLimit) { this.eventThresholdLimit = eventThresholdLimit; return this; From 5da3e5240f4472b88da12b428270664ed0338143 Mon Sep 17 00:00:00 2001 From: Evan Greer Date: Wed, 15 Jan 2025 16:34:34 -0700 Subject: [PATCH 02/21] adds missing comments to config file --- .../iterable/iterableapi/IterableConfig.java | 21 +++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/iterableapi/src/main/java/com/iterable/iterableapi/IterableConfig.java b/iterableapi/src/main/java/com/iterable/iterableapi/IterableConfig.java index d1754879a..df54534a3 100644 --- a/iterableapi/src/main/java/com/iterable/iterableapi/IterableConfig.java +++ b/iterableapi/src/main/java/com/iterable/iterableapi/IterableConfig.java @@ -60,7 +60,9 @@ public class IterableConfig { */ final IterableAuthHandler authHandler; - + /** + * Handler that can be used to retrieve the anonymous user id + */ final IterableAnonUserHandler iterableAnonUserHandler; /** @@ -92,10 +94,20 @@ public class IterableConfig { final boolean encryptionEnforced; + /** + * Enables anonymous user activation + */ final boolean enableAnonActivation; + /** + * Disables fetching of anonymous user criteria on foregrounding when set to true + * By default, the SDK will fetch anonymous user criteria on foregrounding. + */ final boolean disableForegroundCriteriaFetching; + /** + * The number of anonymous events stored in local storage + */ final int eventThresholdLimit; /** @@ -126,11 +138,11 @@ private IterableConfig(Builder builder) { useInMemoryStorageForInApps = builder.useInMemoryStorageForInApps; encryptionEnforced = builder.encryptionEnforced; enableAnonActivation = builder.enableAnonActivation; + disableForegroundCriteriaFetching = builder.disableForegroundCriteriaFetching; enableEmbeddedMessaging = builder.enableEmbeddedMessaging; eventThresholdLimit = builder.eventThresholdLimit; identityResolution = builder.identityResolution; iterableAnonUserHandler = builder.iterableAnonUserHandler; - disableForegroundCriteriaFetching = builder.disableForegroundCriteriaFetching; } public static class Builder { @@ -334,6 +346,11 @@ public Builder setEnableAnonActivation(boolean enableAnonActivation) { return this; } + /** + * Set whether the SDK should disable criteria fetching on foregrounding. Set this to `true` + * if you want criteria to only be fetched on app launch. + * @param disableForegroundCriteriaFetching `true` will fetch criteria only on app launch. + */ public Builder setDisableForegroundCriteriaFetching(boolean disableForegroundCriteriaFetching) { this.disableForegroundCriteriaFetching = disableForegroundCriteriaFetching; return this; From 6c810866687dda964e8726467a3f3b043509950c Mon Sep 17 00:00:00 2001 From: Evan Greer Date: Thu, 16 Jan 2025 10:12:33 -0700 Subject: [PATCH 03/21] cleanup --- .../main/java/com/iterable/iterableapi/IterableConfig.java | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/iterableapi/src/main/java/com/iterable/iterableapi/IterableConfig.java b/iterableapi/src/main/java/com/iterable/iterableapi/IterableConfig.java index 5560695c5..9422e421d 100644 --- a/iterableapi/src/main/java/com/iterable/iterableapi/IterableConfig.java +++ b/iterableapi/src/main/java/com/iterable/iterableapi/IterableConfig.java @@ -170,7 +170,6 @@ public static class Builder { private IterableDataRegion dataRegion = IterableDataRegion.US; private boolean useInMemoryStorageForInApps = false; private IterableDecryptionFailureHandler decryptionFailureHandler; - private boolean encryptionEnforced = false; private boolean enableAnonActivation = false; private boolean disableForegroundCriteriaFetching = false; @@ -178,7 +177,6 @@ public static class Builder { private int eventThresholdLimit = 100; private IterableIdentityResolution identityResolution = new IterableIdentityResolution(); private IterableAnonUserHandler iterableAnonUserHandler; - private IterableDecryptionFailureHandler decryptionFailureHandler; @NonNull public Builder setIterableAnonUserHandler(@NonNull IterableAnonUserHandler iterableAnonUserHandler) { @@ -330,7 +328,6 @@ public Builder setDataRegion(@NonNull IterableDataRegion dataRegion) { * Set whether the SDK should store in-apps only in memory, or in file storage * @param useInMemoryStorageForInApps `true` will have in-apps be only in memory */ - @NonNull public Builder setUseInMemoryStorageForInApps(boolean useInMemoryStorageForInApps) { this.useInMemoryStorageForInApps = useInMemoryStorageForInApps; @@ -380,9 +377,10 @@ public Builder setEnableEmbeddedMessaging(boolean enableEmbeddedMessaging) { * @param identityResolution * @return */ - public Builder setIdentityResolution(IterableIdentityResolution identityResolution) { this.identityResolution = identityResolution; + return this; + } /** * Set a handler for decryption failures that can be used to handle data recovery From 3ef4fe5c39ebc8d0b4150a2758e8a31d424ab94a Mon Sep 17 00:00:00 2001 From: Evan Greer Date: Thu, 23 Jan 2025 13:01:25 -0700 Subject: [PATCH 04/21] updates naming --- .../com/iterable/iterableapi/IterableConfig.java | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/iterableapi/src/main/java/com/iterable/iterableapi/IterableConfig.java b/iterableapi/src/main/java/com/iterable/iterableapi/IterableConfig.java index 4412b70a7..41b634c31 100644 --- a/iterableapi/src/main/java/com/iterable/iterableapi/IterableConfig.java +++ b/iterableapi/src/main/java/com/iterable/iterableapi/IterableConfig.java @@ -100,10 +100,10 @@ public class IterableConfig { final boolean enableAnonActivation; /** - * Disables fetching of anonymous user criteria on foregrounding when set to true + * Toggles fetching of anonymous user criteria on foregrounding when set to true * By default, the SDK will fetch anonymous user criteria on foregrounding. */ - final boolean disableForegroundCriteriaFetching; + final boolean foregroundCriteriaFetch; /** * The number of anonymous events stored in local storage @@ -144,7 +144,7 @@ private IterableConfig(Builder builder) { useInMemoryStorageForInApps = builder.useInMemoryStorageForInApps; encryptionEnforced = builder.encryptionEnforced; enableAnonActivation = builder.enableAnonActivation; - disableForegroundCriteriaFetching = builder.disableForegroundCriteriaFetching; + foregroundCriteriaFetch = builder.foregroundCriteriaFetch; enableEmbeddedMessaging = builder.enableEmbeddedMessaging; eventThresholdLimit = builder.eventThresholdLimit; identityResolution = builder.identityResolution; @@ -170,7 +170,7 @@ public static class Builder { private IterableDecryptionFailureHandler decryptionFailureHandler; private boolean encryptionEnforced = false; private boolean enableAnonActivation = false; - private boolean disableForegroundCriteriaFetching = false; + private boolean foregroundCriteriaFetch = false; private boolean enableEmbeddedMessaging = false; private int eventThresholdLimit = 100; private IterableIdentityResolution identityResolution = new IterableIdentityResolution(); @@ -345,10 +345,10 @@ public Builder setEnableAnonActivation(boolean enableAnonActivation) { /** * Set whether the SDK should disable criteria fetching on foregrounding. Set this to `true` * if you want criteria to only be fetched on app launch. - * @param disableForegroundCriteriaFetching `true` will fetch criteria only on app launch. + * @param foregroundCriteriaFetch `true` will fetch criteria only on app launch. */ - public Builder setDisableForegroundCriteriaFetching(boolean disableForegroundCriteriaFetching) { - this.disableForegroundCriteriaFetching = disableForegroundCriteriaFetching; + public Builder setForegroundCriteriaFetch(boolean foregroundCriteriaFetch) { + this.foregroundCriteriaFetch = foregroundCriteriaFetch; return this; } From f76f3ee9e38efdb1e4815b6af0e41f37e1ad0b17 Mon Sep 17 00:00:00 2001 From: Evan Greer Date: Thu, 23 Jan 2025 13:35:49 -0700 Subject: [PATCH 05/21] adds criteria fetching on foregrounding --- .../src/main/java/com/iterable/iterableapi/IterableApi.java | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/iterableapi/src/main/java/com/iterable/iterableapi/IterableApi.java b/iterableapi/src/main/java/com/iterable/iterableapi/IterableApi.java index d316fc1c6..f65dda273 100644 --- a/iterableapi/src/main/java/com/iterable/iterableapi/IterableApi.java +++ b/iterableapi/src/main/java/com/iterable/iterableapi/IterableApi.java @@ -435,6 +435,12 @@ private void onForeground() { editor.putBoolean(IterableConstants.SHARED_PREFS_DEVICE_NOTIFICATIONS_ENABLED, systemNotificationEnabled); editor.apply(); } + + // fetching anonymous user criteria on foregrounding + if (!sharedInstance.checkSDKInitialization() && sharedInstance._userIdAnon == null && sharedInstance.config.enableAnonActivation && sharedInstance.getVisitorUsageTracked()) { + anonymousUserManager.updateAnonSession(); + anonymousUserManager.getCriteria(); + } } private boolean isInitialized() { From 94ec39b608abeece069b79077c3f79edaeb52c49 Mon Sep 17 00:00:00 2001 From: Evan Greer Date: Thu, 23 Jan 2025 16:32:51 -0700 Subject: [PATCH 06/21] adds cooldown timer --- .../java/com/iterable/iterableapi/IterableApi.java | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/iterableapi/src/main/java/com/iterable/iterableapi/IterableApi.java b/iterableapi/src/main/java/com/iterable/iterableapi/IterableApi.java index f65dda273..4966ea20f 100644 --- a/iterableapi/src/main/java/com/iterable/iterableapi/IterableApi.java +++ b/iterableapi/src/main/java/com/iterable/iterableapi/IterableApi.java @@ -31,6 +31,7 @@ public class IterableApi { static volatile IterableApi sharedInstance = new IterableApi(); private static final String TAG = "IterableApi"; + private static final long CRITERIA_COOLDOWN_MS = 60000; // 5 second cooldown private Context _applicationContext; IterableConfig config; private String _apiKey; @@ -55,6 +56,7 @@ public class IterableApi { private IterableAuthManager authManager; private HashMap deviceAttributes = new HashMap<>(); private IterableKeychain keychain; + private long lastCriteriaFetch = 0; void fetchRemoteConfiguration() { apiClient.getRemoteConfiguration(new IterableHelper.IterableActionHandler() { @@ -436,10 +438,18 @@ private void onForeground() { editor.apply(); } + long currentTime = System.currentTimeMillis(); + // fetching anonymous user criteria on foregrounding - if (!sharedInstance.checkSDKInitialization() && sharedInstance._userIdAnon == null && sharedInstance.config.enableAnonActivation && sharedInstance.getVisitorUsageTracked()) { - anonymousUserManager.updateAnonSession(); + if (!sharedInstance.checkSDKInitialization() + && sharedInstance._userIdAnon == null + && sharedInstance.config.enableAnonActivation + && sharedInstance.getVisitorUsageTracked() + && currentTime - lastCriteriaFetch >= CRITERIA_COOLDOWN_MS) { + + lastCriteriaFetch = currentTime; anonymousUserManager.getCriteria(); + IterableLogger.d(TAG, "Fetching anonymous user criteria - Foreground"); } } From 9bac4e906d007af85299fcb9801848fb99148b82 Mon Sep 17 00:00:00 2001 From: Evan Greer Date: Thu, 23 Jan 2025 16:42:22 -0700 Subject: [PATCH 07/21] adds gating for disabling foreground criteria fetching --- .../src/main/java/com/iterable/iterableapi/IterableApi.java | 1 + .../main/java/com/iterable/iterableapi/IterableConfig.java | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/iterableapi/src/main/java/com/iterable/iterableapi/IterableApi.java b/iterableapi/src/main/java/com/iterable/iterableapi/IterableApi.java index 4966ea20f..4e553fb30 100644 --- a/iterableapi/src/main/java/com/iterable/iterableapi/IterableApi.java +++ b/iterableapi/src/main/java/com/iterable/iterableapi/IterableApi.java @@ -445,6 +445,7 @@ private void onForeground() { && sharedInstance._userIdAnon == null && sharedInstance.config.enableAnonActivation && sharedInstance.getVisitorUsageTracked() + && sharedInstance.config.foregroundCriteriaFetch && currentTime - lastCriteriaFetch >= CRITERIA_COOLDOWN_MS) { lastCriteriaFetch = currentTime; diff --git a/iterableapi/src/main/java/com/iterable/iterableapi/IterableConfig.java b/iterableapi/src/main/java/com/iterable/iterableapi/IterableConfig.java index 41b634c31..7dd58e35d 100644 --- a/iterableapi/src/main/java/com/iterable/iterableapi/IterableConfig.java +++ b/iterableapi/src/main/java/com/iterable/iterableapi/IterableConfig.java @@ -170,7 +170,7 @@ public static class Builder { private IterableDecryptionFailureHandler decryptionFailureHandler; private boolean encryptionEnforced = false; private boolean enableAnonActivation = false; - private boolean foregroundCriteriaFetch = false; + private boolean foregroundCriteriaFetch = true; private boolean enableEmbeddedMessaging = false; private int eventThresholdLimit = 100; private IterableIdentityResolution identityResolution = new IterableIdentityResolution(); @@ -343,7 +343,7 @@ public Builder setEnableAnonActivation(boolean enableAnonActivation) { } /** - * Set whether the SDK should disable criteria fetching on foregrounding. Set this to `true` + * Set whether the SDK should disable criteria fetching on foregrounding. Set this to `false` * if you want criteria to only be fetched on app launch. * @param foregroundCriteriaFetch `true` will fetch criteria only on app launch. */ From 6ef4030519866d4f4491a4aba04078f08ebe76b7 Mon Sep 17 00:00:00 2001 From: Evan Greer Date: Mon, 27 Jan 2025 15:32:10 -0700 Subject: [PATCH 08/21] adds unit tests --- .../com/iterable/iterableapi/IterableApi.java | 4 +- .../iterable/iterableapi/IterableApiTest.java | 129 ++++++++++++++++++ 2 files changed, 131 insertions(+), 2 deletions(-) diff --git a/iterableapi/src/main/java/com/iterable/iterableapi/IterableApi.java b/iterableapi/src/main/java/com/iterable/iterableapi/IterableApi.java index 4e553fb30..308ad567f 100644 --- a/iterableapi/src/main/java/com/iterable/iterableapi/IterableApi.java +++ b/iterableapi/src/main/java/com/iterable/iterableapi/IterableApi.java @@ -37,7 +37,7 @@ public class IterableApi { private String _apiKey; private String _email; private String _userId; - private String _userIdAnon; + String _userIdAnon; private String _authToken; private boolean _debugMode; private Bundle _payloadData; @@ -404,7 +404,7 @@ public void onSwitchToForeground() { public void onSwitchToBackground() {} }; - private void onForeground() { + void onForeground() { if (!_firstForegroundHandled) { _firstForegroundHandled = true; if (sharedInstance.config.autoPushRegistration && sharedInstance.isInitialized()) { diff --git a/iterableapi/src/test/java/com/iterable/iterableapi/IterableApiTest.java b/iterableapi/src/test/java/com/iterable/iterableapi/IterableApiTest.java index 267a1fa48..78d4b8405 100644 --- a/iterableapi/src/test/java/com/iterable/iterableapi/IterableApiTest.java +++ b/iterableapi/src/test/java/com/iterable/iterableapi/IterableApiTest.java @@ -1,5 +1,6 @@ package com.iterable.iterableapi; +import com.iterable.iterableapi.unit.PathBasedQueueDispatcher; import com.iterable.iterableapi.util.DeviceInfoUtils; import android.app.Activity; import android.net.Uri; @@ -55,6 +56,8 @@ public class IterableApiTest extends BaseTest { public static final String PACKAGE_NAME = "com.iterable.iterableapi.test"; private MockWebServer server; + + private PathBasedQueueDispatcher dispatcher; private IterableApiClient originalApiClient; private IterableApiClient mockApiClient; private IterablePushRegistration.IterablePushRegistrationImpl originalPushRegistrationImpl; @@ -62,6 +65,8 @@ public class IterableApiTest extends BaseTest { @Before public void setUp() { server = new MockWebServer(); + dispatcher = new PathBasedQueueDispatcher(); + server.setDispatcher(dispatcher); IterableApi.overrideURLEndpointPath(server.url("").toString()); reInitIterableApi(); @@ -89,6 +94,10 @@ private void reInitIterableApi() { IterableApi.sharedInstance.apiClient = mockApiClient; } + private void addResponse(String endPoint) { + dispatcher.enqueueResponse("/" + endPoint, new MockResponse().setResponseCode(200).setBody("{}")); + } + @Test public void testSdkInitializedWithoutEmailOrUserId() throws Exception { IterableApi.initialize(getContext(), "apiKey"); @@ -836,4 +845,124 @@ public void testFetchRemoteConfigurationCalledWhenInForeground() throws Exceptio IterableActivityMonitor.instance = new IterableActivityMonitor(); } + @Test + public void testForegroundCriteriaFetchWhenConditionsMet() throws Exception { + // Clear any pending requests + while (server.takeRequest(1, TimeUnit.SECONDS) != null) {} + + // Mock responses for expected endpoints + addResponse(IterableConstants.ENDPOINT_CRITERIA_LIST); + + // Initialize with anon activation and foreground fetch enabled + IterableConfig config = new IterableConfig.Builder() + .setEnableAnonActivation(true) + .setForegroundCriteriaFetch(true) + .build(); + + IterableActivityMonitor.getInstance().unregisterLifecycleCallbacks(getContext()); + IterableActivityMonitor.instance = new IterableActivityMonitor(); + + // Initialize API + IterableApi.initialize(getContext(), "apiKey", config); + IterableApi.getInstance().setVisitorUsageTracked(true); + + // Simulate app coming to foreground + Robolectric.buildActivity(Activity.class).create().start().resume(); + shadowOf(getMainLooper()).idle(); + + // Verify criteria fetch request was made + RecordedRequest criteriaRequest = server.takeRequest(1, TimeUnit.SECONDS); + assertNotNull("Criteria request should be made on foreground", criteriaRequest); + assertTrue("Request URL should contain getCriteria endpoint", + criteriaRequest.getPath().contains(IterableConstants.ENDPOINT_CRITERIA_LIST)); + + // Clean up + IterableActivityMonitor.getInstance().unregisterLifecycleCallbacks(getContext()); + IterableActivityMonitor.instance = new IterableActivityMonitor(); + } + + @Test + public void testCriteriaFetchNotCalledWhenDisabled() throws Exception { + // Clear any pending requests + while (server.takeRequest(1, TimeUnit.SECONDS) != null) { } + + // Mock response for remote configuration only + addResponse(IterableConstants.ENDPOINT_GET_REMOTE_CONFIGURATION); + addResponse(IterableConstants.ENDPOINT_CRITERIA_LIST); + + // Initialize with foreground fetch disabled + IterableConfig config = new IterableConfig.Builder() + .setEnableAnonActivation(true) + .setForegroundCriteriaFetch(false) + .build(); + + IterableActivityMonitor.getInstance().unregisterLifecycleCallbacks(getContext()); + IterableActivityMonitor.instance = new IterableActivityMonitor(); + + // Initialize API + IterableApi.initialize(getContext(), "apiKey", config); + + // Simulate app coming to foreground + Robolectric.buildActivity(Activity.class).create().start().resume(); + shadowOf(getMainLooper()).idle(); + + // Take the remote configuration request + RecordedRequest remoteConfigRequest = server.takeRequest(1, TimeUnit.SECONDS); + assertNotNull(remoteConfigRequest); + assertTrue(remoteConfigRequest.getPath().contains(IterableConstants.ENDPOINT_GET_REMOTE_CONFIGURATION)); + + // Verify no criteria fetch request was made + RecordedRequest criteriaRequest = server.takeRequest(1, TimeUnit.SECONDS); + assertNull("No criteria request should be made when feature is disabled", criteriaRequest); + + // Clean up + IterableActivityMonitor.getInstance().unregisterLifecycleCallbacks(getContext()); + IterableActivityMonitor.instance = new IterableActivityMonitor(); + } + + @Test + public void testForegroundCriteriaFetchWithCooldown() throws Exception { + // Clear any pending requests + while (server.takeRequest(1, TimeUnit.SECONDS) != null) { } + + // Mock responses + addResponse(IterableConstants.ENDPOINT_CRITERIA_LIST); + addResponse(IterableConstants.ENDPOINT_GET_REMOTE_CONFIGURATION); + + // Initialize with required config + IterableConfig config = new IterableConfig.Builder() + .setEnableAnonActivation(true) + .setForegroundCriteriaFetch(true) + .build(); + + IterableActivityMonitor.getInstance().unregisterLifecycleCallbacks(getContext()); + IterableActivityMonitor.instance = new IterableActivityMonitor(); + + // Initialize API + IterableApi.initialize(getContext(), "apiKey", config); + IterableApi.getInstance().setVisitorUsageTracked(true); + + // First foreground + Robolectric.buildActivity(Activity.class).create().start().resume(); + shadowOf(getMainLooper()).idle(); + + // Verify first criteria fetch + RecordedRequest firstCriteriaRequest = server.takeRequest(1, TimeUnit.SECONDS); + assertNotNull("First criteria request should be made", firstCriteriaRequest); + assertTrue("First request URL should contain getCriteria endpoint", + firstCriteriaRequest.getPath().contains(IterableConstants.ENDPOINT_CRITERIA_LIST)); + + // Immediate second foreground + Robolectric.buildActivity(Activity.class).create().start().resume(); + shadowOf(getMainLooper()).idle(); + + // Verify no criteria requests during cooldown period + RecordedRequest secondRequest = server.takeRequest(1, TimeUnit.SECONDS); + assertFalse("Second request should not be a criteria request", + secondRequest.getPath().contains(IterableConstants.ENDPOINT_CRITERIA_LIST)); + + // Clean up + IterableActivityMonitor.getInstance().unregisterLifecycleCallbacks(getContext()); + IterableActivityMonitor.instance = new IterableActivityMonitor(); + } } From 7afe25fe3f7c1c4ba7d2f8f5c60ed02de66cf097 Mon Sep 17 00:00:00 2001 From: Evan Greer Date: Mon, 27 Jan 2025 15:57:18 -0700 Subject: [PATCH 09/21] addresses checks --- .../src/test/java/com/iterable/iterableapi/IterableApiTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/iterableapi/src/test/java/com/iterable/iterableapi/IterableApiTest.java b/iterableapi/src/test/java/com/iterable/iterableapi/IterableApiTest.java index 78d4b8405..315ffe036 100644 --- a/iterableapi/src/test/java/com/iterable/iterableapi/IterableApiTest.java +++ b/iterableapi/src/test/java/com/iterable/iterableapi/IterableApiTest.java @@ -848,7 +848,7 @@ public void testFetchRemoteConfigurationCalledWhenInForeground() throws Exceptio @Test public void testForegroundCriteriaFetchWhenConditionsMet() throws Exception { // Clear any pending requests - while (server.takeRequest(1, TimeUnit.SECONDS) != null) {} + while (server.takeRequest(1, TimeUnit.SECONDS) != null) { } // Mock responses for expected endpoints addResponse(IterableConstants.ENDPOINT_CRITERIA_LIST); From 94618c9a7ee13052edf6daa60d48307034aa1ad1 Mon Sep 17 00:00:00 2001 From: Evan Greer Date: Mon, 27 Jan 2025 18:59:03 -0700 Subject: [PATCH 10/21] separates unit tests into new file --- .../com/iterable/iterableapi/IterableApi.java | 16 ++ .../IterableApiCriteriaFetchTests.java | 221 ++++++++++++++++++ .../iterable/iterableapi/IterableApiTest.java | 129 ---------- 3 files changed, 237 insertions(+), 129 deletions(-) create mode 100644 iterableapi/src/test/java/com/iterable/iterableapi/IterableApiCriteriaFetchTests.java diff --git a/iterableapi/src/main/java/com/iterable/iterableapi/IterableApi.java b/iterableapi/src/main/java/com/iterable/iterableapi/IterableApi.java index 308ad567f..d28a953de 100644 --- a/iterableapi/src/main/java/com/iterable/iterableapi/IterableApi.java +++ b/iterableapi/src/main/java/com/iterable/iterableapi/IterableApi.java @@ -405,6 +405,14 @@ public void onSwitchToBackground() {} }; void onForeground() { + // Add debug logging here + IterableLogger.d(TAG, String.format("Debug values: SDK init: %b, userIdAnon: %s, enableAnon: %b, visitorTracked: %b, foregroundFetch: %b", + sharedInstance.checkSDKInitialization(), + sharedInstance._userIdAnon, + sharedInstance.config.enableAnonActivation, + sharedInstance.getVisitorUsageTracked(), + sharedInstance.config.foregroundCriteriaFetch)); + if (!_firstForegroundHandled) { _firstForegroundHandled = true; if (sharedInstance.config.autoPushRegistration && sharedInstance.isInitialized()) { @@ -440,6 +448,14 @@ void onForeground() { long currentTime = System.currentTimeMillis(); + IterableLogger.d(TAG, "About to check criteria fetch conditions:"); + IterableLogger.d(TAG, String.format("Debug values: SDK init: %b, userIdAnon: %s, enableAnon: %b, visitorTracked: %b, foregroundFetch: %b", + sharedInstance.checkSDKInitialization(), + sharedInstance._userIdAnon, + sharedInstance.config.enableAnonActivation, + sharedInstance.getVisitorUsageTracked(), + sharedInstance.config.foregroundCriteriaFetch)); + // fetching anonymous user criteria on foregrounding if (!sharedInstance.checkSDKInitialization() && sharedInstance._userIdAnon == null diff --git a/iterableapi/src/test/java/com/iterable/iterableapi/IterableApiCriteriaFetchTests.java b/iterableapi/src/test/java/com/iterable/iterableapi/IterableApiCriteriaFetchTests.java new file mode 100644 index 000000000..7c59e813e --- /dev/null +++ b/iterableapi/src/test/java/com/iterable/iterableapi/IterableApiCriteriaFetchTests.java @@ -0,0 +1,221 @@ +package com.iterable.iterableapi; + +import static android.os.Looper.getMainLooper; +import static junit.framework.Assert.assertFalse; +import static junit.framework.Assert.assertTrue; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.robolectric.Shadows.shadowOf; + +import android.app.Activity; +import android.content.Context; +import android.content.SharedPreferences; + +import com.iterable.iterableapi.unit.PathBasedQueueDispatcher; + +import junit.framework.Assert; + +import org.json.JSONException; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.robolectric.Robolectric; +import org.robolectric.shadows.ShadowLog; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.TimeUnit; + +import okhttp3.mockwebserver.MockResponse; +import okhttp3.mockwebserver.MockWebServer; +import okhttp3.mockwebserver.RecordedRequest; + +public class IterableApiCriteriaFetchTests extends BaseTest { + private MockWebServer server; + private PathBasedQueueDispatcher dispatcher; + + @Before + public void setUp() { + server = new MockWebServer(); + dispatcher = new PathBasedQueueDispatcher(); + server.setDispatcher(dispatcher); + + reInitIterableApi(); + IterableApi.overrideURLEndpointPath(server.url("").toString()); + + IterableConfig iterableConfig = new IterableConfig.Builder().setEnableAnonActivation(true).build(); + IterableApi.initialize(getContext(), "apiKey", iterableConfig); + IterableApi.getInstance().setVisitorUsageTracked(true); + } + + private void reInitIterableApi() { + IterableApi.sharedInstance = new IterableApi(); + } + + @After + public void tearDown() throws IOException { + server.shutdown(); + server = null; + IterableApi.getInstance().setUserId(null); + IterableApi.getInstance().setEmail(null); + + // Add these cleanup steps + IterableActivityMonitor.getInstance().unregisterLifecycleCallbacks(getContext()); + IterableActivityMonitor.instance = new IterableActivityMonitor(); + + // Clear any pending handlers + shadowOf(getMainLooper()).idle(); + } + + private void addResponse(String endPoint) { + dispatcher.enqueueResponse("/" + endPoint, new MockResponse().setResponseCode(200).setBody("{}")); + } + + @Test + public void testForegroundCriteriaFetchWhenConditionsMet() throws Exception { + // Clear any pending requests + while (server.takeRequest(1, TimeUnit.SECONDS) != null) { } + + // Mock responses for expected endpoints + addResponse(IterableConstants.ENDPOINT_CRITERIA_LIST); + + // Initialize with anon activation and foreground fetch enabled + IterableConfig config = new IterableConfig.Builder() + .setEnableAnonActivation(true) + .setForegroundCriteriaFetch(true) + .build(); + + IterableActivityMonitor.getInstance().unregisterLifecycleCallbacks(getContext()); + IterableActivityMonitor.instance = new IterableActivityMonitor(); + + // Initialize API + IterableApi.initialize(getContext(), "apiKey", config); + IterableApi.getInstance().setVisitorUsageTracked(true); + + // Verify first criteria fetch when consent is given + RecordedRequest firstCriteriaRequest = server.takeRequest(1, TimeUnit.SECONDS); + Assert.assertNotNull("First criteria request should be made", firstCriteriaRequest); + assertTrue("First request URL should contain getCriteria endpoint", + firstCriteriaRequest.getPath().contains(IterableConstants.ENDPOINT_CRITERIA_LIST)); + + // Simulate app coming to foreground + Robolectric.buildActivity(Activity.class).create().start().resume(); + shadowOf(getMainLooper()).idle(); + + // Verify criteria fetch request was made + RecordedRequest secondCriteriaRequest = server.takeRequest(1, TimeUnit.SECONDS); + Assert.assertNotNull("Criteria request should be made on foreground", secondCriteriaRequest); + assertTrue("Request URL should contain getCriteria endpoint", + secondCriteriaRequest.getPath().contains(IterableConstants.ENDPOINT_CRITERIA_LIST)); + + // Clean up + IterableActivityMonitor.getInstance().unregisterLifecycleCallbacks(getContext()); + IterableActivityMonitor.instance = new IterableActivityMonitor(); + } + + @Test + public void testCriteriaFetchNotCalledWhenDisabled() throws Exception { + // Clear any pending requests + while (server.takeRequest(1, TimeUnit.SECONDS) != null) { } + + // Mock ALL responses in sequence + addResponse("anonymoususer/list"); // For initial request - 200 OK + addResponse("anonymoususer/list"); // For second request - 200 OK + addResponse("mobile/getRemoteConfiguration"); // For foreground - 200 OK + + // Initialize with foreground fetch disabled + IterableConfig config = new IterableConfig.Builder() + .setEnableAnonActivation(true) + .setForegroundCriteriaFetch(false) + .build(); + + IterableActivityMonitor.getInstance().unregisterLifecycleCallbacks(getContext()); + IterableActivityMonitor.instance = new IterableActivityMonitor(); + + // Initialize API and set visitor tracking + IterableApi.initialize(getContext(), "apiKey", config); + IterableApi.getInstance().setVisitorUsageTracked(true); + + // Take first two anonymous user list requests + RecordedRequest firstRequest = server.takeRequest(1, TimeUnit.SECONDS); + Assert.assertNotNull("Should have first request", firstRequest); + assertTrue(firstRequest.getPath().contains("/anonymoususer/list")); + + RecordedRequest secondRequest = server.takeRequest(1, TimeUnit.SECONDS); + Assert.assertNotNull("Should have second request", secondRequest); + assertTrue(secondRequest.getPath().contains("/anonymoususer/list")); + + // Simulate app coming to foreground + Robolectric.buildActivity(Activity.class).create().start().resume(); + shadowOf(getMainLooper()).idle(); + + // Should only get remote config request after foreground + RecordedRequest configRequest = server.takeRequest(1, TimeUnit.SECONDS); + Assert.assertNotNull("Should have remote config request", configRequest); + assertTrue("Should be a remote configuration request", + configRequest.getPath().contains("/mobile/getRemoteConfiguration")); + + // No more requests + RecordedRequest extraRequest = server.takeRequest(1, TimeUnit.SECONDS); + Assert.assertNull("Should not have any additional requests", extraRequest); + + // Clean up + IterableActivityMonitor.getInstance().unregisterLifecycleCallbacks(getContext()); + IterableActivityMonitor.instance = new IterableActivityMonitor(); + } + + @Test + public void testForegroundCriteriaFetchWithCooldown() throws Exception { + // Clear any pending requests + while (server.takeRequest(1, TimeUnit.SECONDS) != null) { } + + // Mock responses + addResponse(IterableConstants.ENDPOINT_CRITERIA_LIST); + addResponse(IterableConstants.ENDPOINT_GET_REMOTE_CONFIGURATION); + + // Initialize with required config + IterableConfig config = new IterableConfig.Builder() + .setEnableAnonActivation(true) + .setForegroundCriteriaFetch(true) + .build(); + + IterableActivityMonitor.getInstance().unregisterLifecycleCallbacks(getContext()); + IterableActivityMonitor.instance = new IterableActivityMonitor(); + + // Initialize API + IterableApi.initialize(getContext(), "apiKey", config); + IterableApi.getInstance().setVisitorUsageTracked(true); + + // Verify first criteria fetch when consent is given + RecordedRequest firstCriteriaRequest = server.takeRequest(1, TimeUnit.SECONDS); + Assert.assertNotNull("First criteria request should be made", firstCriteriaRequest); + assertTrue("First request URL should contain getCriteria endpoint", + firstCriteriaRequest.getPath().contains(IterableConstants.ENDPOINT_CRITERIA_LIST)); + + // First foreground + Robolectric.buildActivity(Activity.class).create().start().resume(); + shadowOf(getMainLooper()).idle(); + + // Verify second criteria fetch + RecordedRequest secondCriteriaRequest = server.takeRequest(1, TimeUnit.SECONDS); + Assert.assertNotNull("Second criteria request should be made", firstCriteriaRequest); + assertTrue("Second request URL should contain getCriteria endpoint", + secondCriteriaRequest.getPath().contains(IterableConstants.ENDPOINT_CRITERIA_LIST)); + + // Immediate second foreground + Robolectric.buildActivity(Activity.class).create().start().resume(); + shadowOf(getMainLooper()).idle(); + + // Verify no criteria requests during cooldown period + RecordedRequest cooldownRequest = server.takeRequest(1, TimeUnit.SECONDS); + assertFalse("Second request URL should contain getCriteria endpoint", + cooldownRequest.getPath().contains(IterableConstants.ENDPOINT_CRITERIA_LIST)); + + // Clean up + IterableActivityMonitor.getInstance().unregisterLifecycleCallbacks(getContext()); + IterableActivityMonitor.instance = new IterableActivityMonitor(); + } +} diff --git a/iterableapi/src/test/java/com/iterable/iterableapi/IterableApiTest.java b/iterableapi/src/test/java/com/iterable/iterableapi/IterableApiTest.java index 315ffe036..aa87908ad 100644 --- a/iterableapi/src/test/java/com/iterable/iterableapi/IterableApiTest.java +++ b/iterableapi/src/test/java/com/iterable/iterableapi/IterableApiTest.java @@ -1,6 +1,5 @@ package com.iterable.iterableapi; -import com.iterable.iterableapi.unit.PathBasedQueueDispatcher; import com.iterable.iterableapi.util.DeviceInfoUtils; import android.app.Activity; import android.net.Uri; @@ -57,7 +56,6 @@ public class IterableApiTest extends BaseTest { public static final String PACKAGE_NAME = "com.iterable.iterableapi.test"; private MockWebServer server; - private PathBasedQueueDispatcher dispatcher; private IterableApiClient originalApiClient; private IterableApiClient mockApiClient; private IterablePushRegistration.IterablePushRegistrationImpl originalPushRegistrationImpl; @@ -65,8 +63,6 @@ public class IterableApiTest extends BaseTest { @Before public void setUp() { server = new MockWebServer(); - dispatcher = new PathBasedQueueDispatcher(); - server.setDispatcher(dispatcher); IterableApi.overrideURLEndpointPath(server.url("").toString()); reInitIterableApi(); @@ -94,10 +90,6 @@ private void reInitIterableApi() { IterableApi.sharedInstance.apiClient = mockApiClient; } - private void addResponse(String endPoint) { - dispatcher.enqueueResponse("/" + endPoint, new MockResponse().setResponseCode(200).setBody("{}")); - } - @Test public void testSdkInitializedWithoutEmailOrUserId() throws Exception { IterableApi.initialize(getContext(), "apiKey"); @@ -844,125 +836,4 @@ public void testFetchRemoteConfigurationCalledWhenInForeground() throws Exceptio IterableActivityMonitor.getInstance().unregisterLifecycleCallbacks(getContext()); IterableActivityMonitor.instance = new IterableActivityMonitor(); } - - @Test - public void testForegroundCriteriaFetchWhenConditionsMet() throws Exception { - // Clear any pending requests - while (server.takeRequest(1, TimeUnit.SECONDS) != null) { } - - // Mock responses for expected endpoints - addResponse(IterableConstants.ENDPOINT_CRITERIA_LIST); - - // Initialize with anon activation and foreground fetch enabled - IterableConfig config = new IterableConfig.Builder() - .setEnableAnonActivation(true) - .setForegroundCriteriaFetch(true) - .build(); - - IterableActivityMonitor.getInstance().unregisterLifecycleCallbacks(getContext()); - IterableActivityMonitor.instance = new IterableActivityMonitor(); - - // Initialize API - IterableApi.initialize(getContext(), "apiKey", config); - IterableApi.getInstance().setVisitorUsageTracked(true); - - // Simulate app coming to foreground - Robolectric.buildActivity(Activity.class).create().start().resume(); - shadowOf(getMainLooper()).idle(); - - // Verify criteria fetch request was made - RecordedRequest criteriaRequest = server.takeRequest(1, TimeUnit.SECONDS); - assertNotNull("Criteria request should be made on foreground", criteriaRequest); - assertTrue("Request URL should contain getCriteria endpoint", - criteriaRequest.getPath().contains(IterableConstants.ENDPOINT_CRITERIA_LIST)); - - // Clean up - IterableActivityMonitor.getInstance().unregisterLifecycleCallbacks(getContext()); - IterableActivityMonitor.instance = new IterableActivityMonitor(); - } - - @Test - public void testCriteriaFetchNotCalledWhenDisabled() throws Exception { - // Clear any pending requests - while (server.takeRequest(1, TimeUnit.SECONDS) != null) { } - - // Mock response for remote configuration only - addResponse(IterableConstants.ENDPOINT_GET_REMOTE_CONFIGURATION); - addResponse(IterableConstants.ENDPOINT_CRITERIA_LIST); - - // Initialize with foreground fetch disabled - IterableConfig config = new IterableConfig.Builder() - .setEnableAnonActivation(true) - .setForegroundCriteriaFetch(false) - .build(); - - IterableActivityMonitor.getInstance().unregisterLifecycleCallbacks(getContext()); - IterableActivityMonitor.instance = new IterableActivityMonitor(); - - // Initialize API - IterableApi.initialize(getContext(), "apiKey", config); - - // Simulate app coming to foreground - Robolectric.buildActivity(Activity.class).create().start().resume(); - shadowOf(getMainLooper()).idle(); - - // Take the remote configuration request - RecordedRequest remoteConfigRequest = server.takeRequest(1, TimeUnit.SECONDS); - assertNotNull(remoteConfigRequest); - assertTrue(remoteConfigRequest.getPath().contains(IterableConstants.ENDPOINT_GET_REMOTE_CONFIGURATION)); - - // Verify no criteria fetch request was made - RecordedRequest criteriaRequest = server.takeRequest(1, TimeUnit.SECONDS); - assertNull("No criteria request should be made when feature is disabled", criteriaRequest); - - // Clean up - IterableActivityMonitor.getInstance().unregisterLifecycleCallbacks(getContext()); - IterableActivityMonitor.instance = new IterableActivityMonitor(); - } - - @Test - public void testForegroundCriteriaFetchWithCooldown() throws Exception { - // Clear any pending requests - while (server.takeRequest(1, TimeUnit.SECONDS) != null) { } - - // Mock responses - addResponse(IterableConstants.ENDPOINT_CRITERIA_LIST); - addResponse(IterableConstants.ENDPOINT_GET_REMOTE_CONFIGURATION); - - // Initialize with required config - IterableConfig config = new IterableConfig.Builder() - .setEnableAnonActivation(true) - .setForegroundCriteriaFetch(true) - .build(); - - IterableActivityMonitor.getInstance().unregisterLifecycleCallbacks(getContext()); - IterableActivityMonitor.instance = new IterableActivityMonitor(); - - // Initialize API - IterableApi.initialize(getContext(), "apiKey", config); - IterableApi.getInstance().setVisitorUsageTracked(true); - - // First foreground - Robolectric.buildActivity(Activity.class).create().start().resume(); - shadowOf(getMainLooper()).idle(); - - // Verify first criteria fetch - RecordedRequest firstCriteriaRequest = server.takeRequest(1, TimeUnit.SECONDS); - assertNotNull("First criteria request should be made", firstCriteriaRequest); - assertTrue("First request URL should contain getCriteria endpoint", - firstCriteriaRequest.getPath().contains(IterableConstants.ENDPOINT_CRITERIA_LIST)); - - // Immediate second foreground - Robolectric.buildActivity(Activity.class).create().start().resume(); - shadowOf(getMainLooper()).idle(); - - // Verify no criteria requests during cooldown period - RecordedRequest secondRequest = server.takeRequest(1, TimeUnit.SECONDS); - assertFalse("Second request should not be a criteria request", - secondRequest.getPath().contains(IterableConstants.ENDPOINT_CRITERIA_LIST)); - - // Clean up - IterableActivityMonitor.getInstance().unregisterLifecycleCallbacks(getContext()); - IterableActivityMonitor.instance = new IterableActivityMonitor(); - } } From 2b39e206d798b9738c776ac943f8abb21d10efc3 Mon Sep 17 00:00:00 2001 From: Evan Greer Date: Mon, 27 Jan 2025 19:12:45 -0700 Subject: [PATCH 11/21] resolves check --- .../iterableapi/IterableApiCriteriaFetchTests.java | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/iterableapi/src/test/java/com/iterable/iterableapi/IterableApiCriteriaFetchTests.java b/iterableapi/src/test/java/com/iterable/iterableapi/IterableApiCriteriaFetchTests.java index 7c59e813e..b5e891c72 100644 --- a/iterableapi/src/test/java/com/iterable/iterableapi/IterableApiCriteriaFetchTests.java +++ b/iterableapi/src/test/java/com/iterable/iterableapi/IterableApiCriteriaFetchTests.java @@ -3,30 +3,20 @@ import static android.os.Looper.getMainLooper; import static junit.framework.Assert.assertFalse; import static junit.framework.Assert.assertTrue; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNotEquals; -import static org.junit.Assert.assertNotNull; -import static org.junit.Assert.assertNull; import static org.robolectric.Shadows.shadowOf; import android.app.Activity; -import android.content.Context; -import android.content.SharedPreferences; import com.iterable.iterableapi.unit.PathBasedQueueDispatcher; import junit.framework.Assert; -import org.json.JSONException; import org.junit.After; import org.junit.Before; import org.junit.Test; import org.robolectric.Robolectric; -import org.robolectric.shadows.ShadowLog; import java.io.IOException; -import java.util.ArrayList; -import java.util.List; import java.util.concurrent.TimeUnit; import okhttp3.mockwebserver.MockResponse; From 41f2857727cd2b1f76754cce35f5c37c6cba28ad Mon Sep 17 00:00:00 2001 From: Evan Greer Date: Mon, 27 Jan 2025 19:37:03 -0700 Subject: [PATCH 12/21] unit test fixes --- .../com/iterable/iterableapi/IterableApi.java | 18 +----------------- 1 file changed, 1 insertion(+), 17 deletions(-) diff --git a/iterableapi/src/main/java/com/iterable/iterableapi/IterableApi.java b/iterableapi/src/main/java/com/iterable/iterableapi/IterableApi.java index d28a953de..2b1e0fee0 100644 --- a/iterableapi/src/main/java/com/iterable/iterableapi/IterableApi.java +++ b/iterableapi/src/main/java/com/iterable/iterableapi/IterableApi.java @@ -404,15 +404,7 @@ public void onSwitchToForeground() { public void onSwitchToBackground() {} }; - void onForeground() { - // Add debug logging here - IterableLogger.d(TAG, String.format("Debug values: SDK init: %b, userIdAnon: %s, enableAnon: %b, visitorTracked: %b, foregroundFetch: %b", - sharedInstance.checkSDKInitialization(), - sharedInstance._userIdAnon, - sharedInstance.config.enableAnonActivation, - sharedInstance.getVisitorUsageTracked(), - sharedInstance.config.foregroundCriteriaFetch)); - + void onForeground() {; if (!_firstForegroundHandled) { _firstForegroundHandled = true; if (sharedInstance.config.autoPushRegistration && sharedInstance.isInitialized()) { @@ -448,14 +440,6 @@ void onForeground() { long currentTime = System.currentTimeMillis(); - IterableLogger.d(TAG, "About to check criteria fetch conditions:"); - IterableLogger.d(TAG, String.format("Debug values: SDK init: %b, userIdAnon: %s, enableAnon: %b, visitorTracked: %b, foregroundFetch: %b", - sharedInstance.checkSDKInitialization(), - sharedInstance._userIdAnon, - sharedInstance.config.enableAnonActivation, - sharedInstance.getVisitorUsageTracked(), - sharedInstance.config.foregroundCriteriaFetch)); - // fetching anonymous user criteria on foregrounding if (!sharedInstance.checkSDKInitialization() && sharedInstance._userIdAnon == null From f3471bd353e717be804e62a3c86de8ae361499d8 Mon Sep 17 00:00:00 2001 From: Evan Greer Date: Mon, 27 Jan 2025 19:42:56 -0700 Subject: [PATCH 13/21] addresses check --- .../src/main/java/com/iterable/iterableapi/IterableApi.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/iterableapi/src/main/java/com/iterable/iterableapi/IterableApi.java b/iterableapi/src/main/java/com/iterable/iterableapi/IterableApi.java index 2b1e0fee0..308ad567f 100644 --- a/iterableapi/src/main/java/com/iterable/iterableapi/IterableApi.java +++ b/iterableapi/src/main/java/com/iterable/iterableapi/IterableApi.java @@ -404,7 +404,7 @@ public void onSwitchToForeground() { public void onSwitchToBackground() {} }; - void onForeground() {; + void onForeground() { if (!_firstForegroundHandled) { _firstForegroundHandled = true; if (sharedInstance.config.autoPushRegistration && sharedInstance.isInitialized()) { From 6f9a0145d35f2d4546b23aa48430aeafb930fd5a Mon Sep 17 00:00:00 2001 From: Evan Greer Date: Mon, 27 Jan 2025 20:09:14 -0700 Subject: [PATCH 14/21] updates unit tests --- .../IterableApiCriteriaFetchTests.java | 155 ++++++++++++++++-- 1 file changed, 143 insertions(+), 12 deletions(-) diff --git a/iterableapi/src/test/java/com/iterable/iterableapi/IterableApiCriteriaFetchTests.java b/iterableapi/src/test/java/com/iterable/iterableapi/IterableApiCriteriaFetchTests.java index b5e891c72..446188c5f 100644 --- a/iterableapi/src/test/java/com/iterable/iterableapi/IterableApiCriteriaFetchTests.java +++ b/iterableapi/src/test/java/com/iterable/iterableapi/IterableApiCriteriaFetchTests.java @@ -6,6 +6,8 @@ import static org.robolectric.Shadows.shadowOf; import android.app.Activity; +import android.content.Context; +import android.content.SharedPreferences; import com.iterable.iterableapi.unit.PathBasedQueueDispatcher; @@ -27,6 +29,107 @@ public class IterableApiCriteriaFetchTests extends BaseTest { private MockWebServer server; private PathBasedQueueDispatcher dispatcher; + private final String criteriaMockData = "{\n" + + " \"count\":2,\n" + + " \"criteriaSets\":[\n" + + " {\n" + + " \"criteriaId\":43,\n" + + " \"searchQuery\":{\n" + + " \"combinator\":\"Or\",\n" + + " \"searchQueries\":[\n" + + " {\n" + + " \"combinator\":\"And\",\n" + + " \"searchQueries\":[\n" + + " {\n" + + " \"dataType\":\"purchase\",\n" + + " \"searchCombo\":{\n" + + " \"combinator\":\"Or\",\n" + + " \"searchQueries\":[\n" + + " {\n" + + " \"field\":\"shoppingCartItems.price\",\n" + + " \"fieldType\":\"double\",\n" + + " \"comparatorType\":\"Equals\",\n" + + " \"dataType\":\"purchase\",\n" + + " \"id\":2,\n" + + " \"value\":\"4.67\"\n" + + " },\n" + + " {\n" + + " \"field\":\"shoppingCartItems.quantity\",\n" + + " \"fieldType\":\"long\",\n" + + " \"comparatorType\":\"GreaterThanOrEqualTo\",\n" + + " \"dataType\":\"purchase\",\n" + + " \"id\":3,\n" + + " \"valueLong\":2,\n" + + " \"value\":\"2\"\n" + + " }\n" + + " ]\n" + + " }\n" + + " }\n" + + " ]\n" + + " }\n" + + " ]\n" + + " }\n" + + " },\n" + + " {\n" + + " \"criteriaId\":5678,\n" + + " \"searchQuery\":{\n" + + " \"combinator\":\"Or\",\n" + + " \"searchQueries\":[\n" + + " {\n" + + " \"combinator\":\"Or\",\n" + + " \"searchQueries\":[\n" + + " {\n" + + " \"dataType\":\"user\",\n" + + " \"searchCombo\":{\n" + + " \"combinator\":\"And\",\n" + + " \"searchQueries\":[\n" + + " {\n" + + " \"field\":\"itblInternal.emailDomain\",\n" + + " \"fieldType\":\"string\",\n" + + " \"comparatorType\":\"Equals\",\n" + + " \"dataType\":\"user\",\n" + + " \"id\":6,\n" + + " \"value\":\"gmail.com\"\n" + + " }\n" + + " ]\n" + + " }\n" + + " },\n" + + " {\n" + + " \"dataType\":\"customEvent\",\n" + + " \"searchCombo\":{\n" + + " \"combinator\":\"And\",\n" + + " \"searchQueries\":[\n" + + " {\n" + + " \"field\":\"eventName\",\n" + + " \"fieldType\":\"string\",\n" + + " \"comparatorType\":\"Equals\",\n" + + " \"dataType\":\"customEvent\",\n" + + " \"id\":9,\n" + + " \"value\":\"processing_cancelled\"\n" + + " },\n" + + " {\n" + + " \"field\":\"createdAt\",\n" + + " \"fieldType\":\"date\",\n" + + " \"comparatorType\":\"GreaterThan\",\n" + + " \"dataType\":\"customEvent\",\n" + + " \"id\":10,\n" + + " \"dateRange\":{\n" + + " \n" + + " },\n" + + " \"isRelativeDate\":false,\n" + + " \"value\":\"1688194800000\"\n" + + " }\n" + + " ]\n" + + " }\n" + + " }\n" + + " ]\n" + + " }\n" + + " ]\n" + + " }\n" + + " }\n" + + " ]\n" + + "}"; + @Before public void setUp() { server = new MockWebServer(); @@ -39,6 +142,7 @@ public void setUp() { IterableConfig iterableConfig = new IterableConfig.Builder().setEnableAnonActivation(true).build(); IterableApi.initialize(getContext(), "apiKey", iterableConfig); IterableApi.getInstance().setVisitorUsageTracked(true); + setCriteria(criteriaMockData); } private void reInitIterableApi() { @@ -60,8 +164,24 @@ public void tearDown() throws IOException { shadowOf(getMainLooper()).idle(); } + private void setCriteria(String criteria) { + SharedPreferences sharedPref = getContext().getSharedPreferences(IterableConstants.SHARED_PREFS_FILE, Context.MODE_PRIVATE); + SharedPreferences.Editor editor = sharedPref.edit(); + editor.putString(IterableConstants.SHARED_PREFS_CRITERIA, criteria); + editor.apply(); + } + private void addResponse(String endPoint) { - dispatcher.enqueueResponse("/" + endPoint, new MockResponse().setResponseCode(200).setBody("{}")); + MockResponse response = new MockResponse().setResponseCode(200); + + // If it's an anonymous user list request, return the criteria mock data + if (endPoint.contains("anonymoususer/list")) { + response.setBody(criteriaMockData); + } else { + response.setBody("{}"); + } + + dispatcher.enqueueResponse("/" + endPoint, response); } @Test @@ -111,10 +231,22 @@ public void testCriteriaFetchNotCalledWhenDisabled() throws Exception { // Clear any pending requests while (server.takeRequest(1, TimeUnit.SECONDS) != null) { } - // Mock ALL responses in sequence - addResponse("anonymoususer/list"); // For initial request - 200 OK - addResponse("anonymoususer/list"); // For second request - 200 OK - addResponse("mobile/getRemoteConfiguration"); // For foreground - 200 OK + // Set up dispatcher + dispatcher = new PathBasedQueueDispatcher() { + @Override + public MockResponse dispatch(RecordedRequest request) throws InterruptedException { + if (request.getPath().contains("anonymoususer/list")) { + return new MockResponse() + .setResponseCode(200) + .setBody(criteriaMockData); + } + return super.dispatch(request); + } + }; + server.setDispatcher(dispatcher); + + // Mock remote config + addResponse("mobile/getRemoteConfiguration"); // Initialize with foreground fetch disabled IterableConfig config = new IterableConfig.Builder() @@ -129,14 +261,13 @@ public void testCriteriaFetchNotCalledWhenDisabled() throws Exception { IterableApi.initialize(getContext(), "apiKey", config); IterableApi.getInstance().setVisitorUsageTracked(true); - // Take first two anonymous user list requests + // Take initial requests (should be two criteria fetches) RecordedRequest firstRequest = server.takeRequest(1, TimeUnit.SECONDS); - Assert.assertNotNull("Should have first request", firstRequest); - assertTrue(firstRequest.getPath().contains("/anonymoususer/list")); - RecordedRequest secondRequest = server.takeRequest(1, TimeUnit.SECONDS); - Assert.assertNotNull("Should have second request", secondRequest); - assertTrue(secondRequest.getPath().contains("/anonymoususer/list")); + Assert.assertNotNull("Should have first criteria request", firstRequest); + Assert.assertNotNull("Should have second criteria request", secondRequest); + assertTrue(firstRequest.getPath().contains("anonymoususer/list")); + assertTrue(secondRequest.getPath().contains("anonymoususer/list")); // Simulate app coming to foreground Robolectric.buildActivity(Activity.class).create().start().resume(); @@ -146,7 +277,7 @@ public void testCriteriaFetchNotCalledWhenDisabled() throws Exception { RecordedRequest configRequest = server.takeRequest(1, TimeUnit.SECONDS); Assert.assertNotNull("Should have remote config request", configRequest); assertTrue("Should be a remote configuration request", - configRequest.getPath().contains("/mobile/getRemoteConfiguration")); + configRequest.getPath().contains("mobile/getRemoteConfiguration")); // No more requests RecordedRequest extraRequest = server.takeRequest(1, TimeUnit.SECONDS); From f0ef1937d007805782396ab15a4572a6466c2670 Mon Sep 17 00:00:00 2001 From: Evan Greer Date: Tue, 28 Jan 2025 09:34:31 -0700 Subject: [PATCH 15/21] added constant for criteria cooldown --- .../src/main/java/com/iterable/iterableapi/IterableApi.java | 3 +-- .../main/java/com/iterable/iterableapi/IterableConstants.java | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/iterableapi/src/main/java/com/iterable/iterableapi/IterableApi.java b/iterableapi/src/main/java/com/iterable/iterableapi/IterableApi.java index 308ad567f..6f61a7926 100644 --- a/iterableapi/src/main/java/com/iterable/iterableapi/IterableApi.java +++ b/iterableapi/src/main/java/com/iterable/iterableapi/IterableApi.java @@ -31,7 +31,6 @@ public class IterableApi { static volatile IterableApi sharedInstance = new IterableApi(); private static final String TAG = "IterableApi"; - private static final long CRITERIA_COOLDOWN_MS = 60000; // 5 second cooldown private Context _applicationContext; IterableConfig config; private String _apiKey; @@ -446,7 +445,7 @@ void onForeground() { && sharedInstance.config.enableAnonActivation && sharedInstance.getVisitorUsageTracked() && sharedInstance.config.foregroundCriteriaFetch - && currentTime - lastCriteriaFetch >= CRITERIA_COOLDOWN_MS) { + && currentTime - lastCriteriaFetch >= IterableConstants.CRITERIA_FETCHING_COOLDOWN) { lastCriteriaFetch = currentTime; anonymousUserManager.getCriteria(); diff --git a/iterableapi/src/main/java/com/iterable/iterableapi/IterableConstants.java b/iterableapi/src/main/java/com/iterable/iterableapi/IterableConstants.java index 6da931df4..46ba4ae7f 100644 --- a/iterableapi/src/main/java/com/iterable/iterableapi/IterableConstants.java +++ b/iterableapi/src/main/java/com/iterable/iterableapi/IterableConstants.java @@ -336,7 +336,7 @@ public final class IterableConstants { public static final String PURCHASE_ITEM = "shoppingCartItems"; public static final String PURCHASE_ITEM_PREFIX = PURCHASE_ITEM + "."; public static final String MIN_MATCH = "minMatch"; - + public static final Integer CRITERIA_FETCHING_COOLDOWN = 120000; //Tracking types public static final String TRACK_EVENT = "customEvent"; From ebead636c57aa44ad10b920ac115ad05f938953d Mon Sep 17 00:00:00 2001 From: Evan Greer Date: Tue, 28 Jan 2025 10:32:44 -0700 Subject: [PATCH 16/21] addresses comments --- .../java/com/iterable/iterableapi/IterableApi.java | 4 ++-- .../iterableapi/IterableApiCriteriaFetchTests.java | 12 ++++++------ 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/iterableapi/src/main/java/com/iterable/iterableapi/IterableApi.java b/iterableapi/src/main/java/com/iterable/iterableapi/IterableApi.java index 6f61a7926..816dcb3ae 100644 --- a/iterableapi/src/main/java/com/iterable/iterableapi/IterableApi.java +++ b/iterableapi/src/main/java/com/iterable/iterableapi/IterableApi.java @@ -36,7 +36,7 @@ public class IterableApi { private String _apiKey; private String _email; private String _userId; - String _userIdAnon; + private String _userIdAnon; private String _authToken; private boolean _debugMode; private Bundle _payloadData; @@ -403,7 +403,7 @@ public void onSwitchToForeground() { public void onSwitchToBackground() {} }; - void onForeground() { + private void onForeground() { if (!_firstForegroundHandled) { _firstForegroundHandled = true; if (sharedInstance.config.autoPushRegistration && sharedInstance.isInitialized()) { diff --git a/iterableapi/src/test/java/com/iterable/iterableapi/IterableApiCriteriaFetchTests.java b/iterableapi/src/test/java/com/iterable/iterableapi/IterableApiCriteriaFetchTests.java index 446188c5f..0f8a478af 100644 --- a/iterableapi/src/test/java/com/iterable/iterableapi/IterableApiCriteriaFetchTests.java +++ b/iterableapi/src/test/java/com/iterable/iterableapi/IterableApiCriteriaFetchTests.java @@ -175,7 +175,7 @@ private void addResponse(String endPoint) { MockResponse response = new MockResponse().setResponseCode(200); // If it's an anonymous user list request, return the criteria mock data - if (endPoint.contains("anonymoususer/list")) { + if (endPoint.contains(IterableConstants.ENDPOINT_CRITERIA_LIST)) { response.setBody(criteriaMockData); } else { response.setBody("{}"); @@ -235,7 +235,7 @@ public void testCriteriaFetchNotCalledWhenDisabled() throws Exception { dispatcher = new PathBasedQueueDispatcher() { @Override public MockResponse dispatch(RecordedRequest request) throws InterruptedException { - if (request.getPath().contains("anonymoususer/list")) { + if (request.getPath().contains(IterableConstants.ENDPOINT_CRITERIA_LIST)) { return new MockResponse() .setResponseCode(200) .setBody(criteriaMockData); @@ -246,7 +246,7 @@ public MockResponse dispatch(RecordedRequest request) throws InterruptedExceptio server.setDispatcher(dispatcher); // Mock remote config - addResponse("mobile/getRemoteConfiguration"); + addResponse(IterableConstants.ENDPOINT_GET_REMOTE_CONFIGURATION); // Initialize with foreground fetch disabled IterableConfig config = new IterableConfig.Builder() @@ -266,8 +266,8 @@ public MockResponse dispatch(RecordedRequest request) throws InterruptedExceptio RecordedRequest secondRequest = server.takeRequest(1, TimeUnit.SECONDS); Assert.assertNotNull("Should have first criteria request", firstRequest); Assert.assertNotNull("Should have second criteria request", secondRequest); - assertTrue(firstRequest.getPath().contains("anonymoususer/list")); - assertTrue(secondRequest.getPath().contains("anonymoususer/list")); + assertTrue(firstRequest.getPath().contains(IterableConstants.ENDPOINT_CRITERIA_LIST)); + assertTrue(secondRequest.getPath().contains(IterableConstants.ENDPOINT_CRITERIA_LIST)); // Simulate app coming to foreground Robolectric.buildActivity(Activity.class).create().start().resume(); @@ -277,7 +277,7 @@ public MockResponse dispatch(RecordedRequest request) throws InterruptedExceptio RecordedRequest configRequest = server.takeRequest(1, TimeUnit.SECONDS); Assert.assertNotNull("Should have remote config request", configRequest); assertTrue("Should be a remote configuration request", - configRequest.getPath().contains("mobile/getRemoteConfiguration")); + configRequest.getPath().contains(IterableConstants.ENDPOINT_GET_REMOTE_CONFIGURATION)); // No more requests RecordedRequest extraRequest = server.takeRequest(1, TimeUnit.SECONDS); From 4593db8b96a28dfe0f32705ffc22798981e422b1 Mon Sep 17 00:00:00 2001 From: Evan Greer Date: Tue, 28 Jan 2025 10:33:58 -0700 Subject: [PATCH 17/21] minor corrections --- .../src/test/java/com/iterable/iterableapi/IterableApiTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/iterableapi/src/test/java/com/iterable/iterableapi/IterableApiTest.java b/iterableapi/src/test/java/com/iterable/iterableapi/IterableApiTest.java index aa87908ad..267a1fa48 100644 --- a/iterableapi/src/test/java/com/iterable/iterableapi/IterableApiTest.java +++ b/iterableapi/src/test/java/com/iterable/iterableapi/IterableApiTest.java @@ -55,7 +55,6 @@ public class IterableApiTest extends BaseTest { public static final String PACKAGE_NAME = "com.iterable.iterableapi.test"; private MockWebServer server; - private IterableApiClient originalApiClient; private IterableApiClient mockApiClient; private IterablePushRegistration.IterablePushRegistrationImpl originalPushRegistrationImpl; @@ -836,4 +835,5 @@ public void testFetchRemoteConfigurationCalledWhenInForeground() throws Exceptio IterableActivityMonitor.getInstance().unregisterLifecycleCallbacks(getContext()); IterableActivityMonitor.instance = new IterableActivityMonitor(); } + } From 35954320ca9071684fda563264f248ba865b55ee Mon Sep 17 00:00:00 2001 From: Evan Greer Date: Tue, 28 Jan 2025 11:32:01 -0700 Subject: [PATCH 18/21] updates config naming --- .../iterableapi/AnonymousUserManager.java | 1 + .../iterable/iterableapi/IterableConfig.java | 12 +- .../IterableApiCriteriaFetchTests.java | 154 +----------------- 3 files changed, 10 insertions(+), 157 deletions(-) diff --git a/iterableapi/src/main/java/com/iterable/iterableapi/AnonymousUserManager.java b/iterableapi/src/main/java/com/iterable/iterableapi/AnonymousUserManager.java index c82f3d1b1..afbe5e940 100644 --- a/iterableapi/src/main/java/com/iterable/iterableapi/AnonymousUserManager.java +++ b/iterableapi/src/main/java/com/iterable/iterableapi/AnonymousUserManager.java @@ -157,6 +157,7 @@ void trackAnonUpdateCart(@NonNull List items) { } void getCriteria() { + IterableLogger.v(TAG, "getCriteria"); iterableApi.apiClient.getCriteriaList(data -> { if (data != null) { try { diff --git a/iterableapi/src/main/java/com/iterable/iterableapi/IterableConfig.java b/iterableapi/src/main/java/com/iterable/iterableapi/IterableConfig.java index 7dd58e35d..e1165d945 100644 --- a/iterableapi/src/main/java/com/iterable/iterableapi/IterableConfig.java +++ b/iterableapi/src/main/java/com/iterable/iterableapi/IterableConfig.java @@ -103,7 +103,7 @@ public class IterableConfig { * Toggles fetching of anonymous user criteria on foregrounding when set to true * By default, the SDK will fetch anonymous user criteria on foregrounding. */ - final boolean foregroundCriteriaFetch; + final boolean enableForegroundCriteriaFetch; /** * The number of anonymous events stored in local storage @@ -144,7 +144,7 @@ private IterableConfig(Builder builder) { useInMemoryStorageForInApps = builder.useInMemoryStorageForInApps; encryptionEnforced = builder.encryptionEnforced; enableAnonActivation = builder.enableAnonActivation; - foregroundCriteriaFetch = builder.foregroundCriteriaFetch; + enableForegroundCriteriaFetch = builder.enableForegroundCriteriaFetch; enableEmbeddedMessaging = builder.enableEmbeddedMessaging; eventThresholdLimit = builder.eventThresholdLimit; identityResolution = builder.identityResolution; @@ -170,7 +170,7 @@ public static class Builder { private IterableDecryptionFailureHandler decryptionFailureHandler; private boolean encryptionEnforced = false; private boolean enableAnonActivation = false; - private boolean foregroundCriteriaFetch = true; + private boolean enableForegroundCriteriaFetch = true; private boolean enableEmbeddedMessaging = false; private int eventThresholdLimit = 100; private IterableIdentityResolution identityResolution = new IterableIdentityResolution(); @@ -345,10 +345,10 @@ public Builder setEnableAnonActivation(boolean enableAnonActivation) { /** * Set whether the SDK should disable criteria fetching on foregrounding. Set this to `false` * if you want criteria to only be fetched on app launch. - * @param foregroundCriteriaFetch `true` will fetch criteria only on app launch. + * @param enableForegroundCriteriaFetch `true` will fetch criteria only on app launch. */ - public Builder setForegroundCriteriaFetch(boolean foregroundCriteriaFetch) { - this.foregroundCriteriaFetch = foregroundCriteriaFetch; + public Builder setEnableForegroundCriteriaFetch(boolean enableForegroundCriteriaFetch) { + this.enableForegroundCriteriaFetch = enableForegroundCriteriaFetch; return this; } diff --git a/iterableapi/src/test/java/com/iterable/iterableapi/IterableApiCriteriaFetchTests.java b/iterableapi/src/test/java/com/iterable/iterableapi/IterableApiCriteriaFetchTests.java index 0f8a478af..114bfa63c 100644 --- a/iterableapi/src/test/java/com/iterable/iterableapi/IterableApiCriteriaFetchTests.java +++ b/iterableapi/src/test/java/com/iterable/iterableapi/IterableApiCriteriaFetchTests.java @@ -6,8 +6,6 @@ import static org.robolectric.Shadows.shadowOf; import android.app.Activity; -import android.content.Context; -import android.content.SharedPreferences; import com.iterable.iterableapi.unit.PathBasedQueueDispatcher; @@ -29,107 +27,6 @@ public class IterableApiCriteriaFetchTests extends BaseTest { private MockWebServer server; private PathBasedQueueDispatcher dispatcher; - private final String criteriaMockData = "{\n" + - " \"count\":2,\n" + - " \"criteriaSets\":[\n" + - " {\n" + - " \"criteriaId\":43,\n" + - " \"searchQuery\":{\n" + - " \"combinator\":\"Or\",\n" + - " \"searchQueries\":[\n" + - " {\n" + - " \"combinator\":\"And\",\n" + - " \"searchQueries\":[\n" + - " {\n" + - " \"dataType\":\"purchase\",\n" + - " \"searchCombo\":{\n" + - " \"combinator\":\"Or\",\n" + - " \"searchQueries\":[\n" + - " {\n" + - " \"field\":\"shoppingCartItems.price\",\n" + - " \"fieldType\":\"double\",\n" + - " \"comparatorType\":\"Equals\",\n" + - " \"dataType\":\"purchase\",\n" + - " \"id\":2,\n" + - " \"value\":\"4.67\"\n" + - " },\n" + - " {\n" + - " \"field\":\"shoppingCartItems.quantity\",\n" + - " \"fieldType\":\"long\",\n" + - " \"comparatorType\":\"GreaterThanOrEqualTo\",\n" + - " \"dataType\":\"purchase\",\n" + - " \"id\":3,\n" + - " \"valueLong\":2,\n" + - " \"value\":\"2\"\n" + - " }\n" + - " ]\n" + - " }\n" + - " }\n" + - " ]\n" + - " }\n" + - " ]\n" + - " }\n" + - " },\n" + - " {\n" + - " \"criteriaId\":5678,\n" + - " \"searchQuery\":{\n" + - " \"combinator\":\"Or\",\n" + - " \"searchQueries\":[\n" + - " {\n" + - " \"combinator\":\"Or\",\n" + - " \"searchQueries\":[\n" + - " {\n" + - " \"dataType\":\"user\",\n" + - " \"searchCombo\":{\n" + - " \"combinator\":\"And\",\n" + - " \"searchQueries\":[\n" + - " {\n" + - " \"field\":\"itblInternal.emailDomain\",\n" + - " \"fieldType\":\"string\",\n" + - " \"comparatorType\":\"Equals\",\n" + - " \"dataType\":\"user\",\n" + - " \"id\":6,\n" + - " \"value\":\"gmail.com\"\n" + - " }\n" + - " ]\n" + - " }\n" + - " },\n" + - " {\n" + - " \"dataType\":\"customEvent\",\n" + - " \"searchCombo\":{\n" + - " \"combinator\":\"And\",\n" + - " \"searchQueries\":[\n" + - " {\n" + - " \"field\":\"eventName\",\n" + - " \"fieldType\":\"string\",\n" + - " \"comparatorType\":\"Equals\",\n" + - " \"dataType\":\"customEvent\",\n" + - " \"id\":9,\n" + - " \"value\":\"processing_cancelled\"\n" + - " },\n" + - " {\n" + - " \"field\":\"createdAt\",\n" + - " \"fieldType\":\"date\",\n" + - " \"comparatorType\":\"GreaterThan\",\n" + - " \"dataType\":\"customEvent\",\n" + - " \"id\":10,\n" + - " \"dateRange\":{\n" + - " \n" + - " },\n" + - " \"isRelativeDate\":false,\n" + - " \"value\":\"1688194800000\"\n" + - " }\n" + - " ]\n" + - " }\n" + - " }\n" + - " ]\n" + - " }\n" + - " ]\n" + - " }\n" + - " }\n" + - " ]\n" + - "}"; - @Before public void setUp() { server = new MockWebServer(); @@ -142,7 +39,6 @@ public void setUp() { IterableConfig iterableConfig = new IterableConfig.Builder().setEnableAnonActivation(true).build(); IterableApi.initialize(getContext(), "apiKey", iterableConfig); IterableApi.getInstance().setVisitorUsageTracked(true); - setCriteria(criteriaMockData); } private void reInitIterableApi() { @@ -164,24 +60,8 @@ public void tearDown() throws IOException { shadowOf(getMainLooper()).idle(); } - private void setCriteria(String criteria) { - SharedPreferences sharedPref = getContext().getSharedPreferences(IterableConstants.SHARED_PREFS_FILE, Context.MODE_PRIVATE); - SharedPreferences.Editor editor = sharedPref.edit(); - editor.putString(IterableConstants.SHARED_PREFS_CRITERIA, criteria); - editor.apply(); - } - private void addResponse(String endPoint) { - MockResponse response = new MockResponse().setResponseCode(200); - - // If it's an anonymous user list request, return the criteria mock data - if (endPoint.contains(IterableConstants.ENDPOINT_CRITERIA_LIST)) { - response.setBody(criteriaMockData); - } else { - response.setBody("{}"); - } - - dispatcher.enqueueResponse("/" + endPoint, response); + dispatcher.enqueueResponse("/" + endPoint, new MockResponse().setResponseCode(200).setBody("{}")); } @Test @@ -228,46 +108,18 @@ public void testForegroundCriteriaFetchWhenConditionsMet() throws Exception { @Test public void testCriteriaFetchNotCalledWhenDisabled() throws Exception { - // Clear any pending requests - while (server.takeRequest(1, TimeUnit.SECONDS) != null) { } - - // Set up dispatcher - dispatcher = new PathBasedQueueDispatcher() { - @Override - public MockResponse dispatch(RecordedRequest request) throws InterruptedException { - if (request.getPath().contains(IterableConstants.ENDPOINT_CRITERIA_LIST)) { - return new MockResponse() - .setResponseCode(200) - .setBody(criteriaMockData); - } - return super.dispatch(request); - } - }; - server.setDispatcher(dispatcher); - - // Mock remote config - addResponse(IterableConstants.ENDPOINT_GET_REMOTE_CONFIGURATION); - // Initialize with foreground fetch disabled IterableConfig config = new IterableConfig.Builder() .setEnableAnonActivation(true) .setForegroundCriteriaFetch(false) .build(); - IterableActivityMonitor.getInstance().unregisterLifecycleCallbacks(getContext()); - IterableActivityMonitor.instance = new IterableActivityMonitor(); - // Initialize API and set visitor tracking IterableApi.initialize(getContext(), "apiKey", config); IterableApi.getInstance().setVisitorUsageTracked(true); - // Take initial requests (should be two criteria fetches) - RecordedRequest firstRequest = server.takeRequest(1, TimeUnit.SECONDS); - RecordedRequest secondRequest = server.takeRequest(1, TimeUnit.SECONDS); - Assert.assertNotNull("Should have first criteria request", firstRequest); - Assert.assertNotNull("Should have second criteria request", secondRequest); - assertTrue(firstRequest.getPath().contains(IterableConstants.ENDPOINT_CRITERIA_LIST)); - assertTrue(secondRequest.getPath().contains(IterableConstants.ENDPOINT_CRITERIA_LIST)); + // Clear out any pending requests + while (server.takeRequest(1, TimeUnit.SECONDS) != null) { } // Simulate app coming to foreground Robolectric.buildActivity(Activity.class).create().start().resume(); From 720313b5d243c909d6ce2b3fc0069c2dd8a18383 Mon Sep 17 00:00:00 2001 From: Evan Greer Date: Tue, 28 Jan 2025 13:54:25 -0700 Subject: [PATCH 19/21] sets up app state callback on AnonymousUserManager --- .../iterableapi/AnonymousUserManager.java | 43 ++++++++++++++++++- .../com/iterable/iterableapi/IterableApi.java | 41 ++++++++---------- 2 files changed, 60 insertions(+), 24 deletions(-) diff --git a/iterableapi/src/main/java/com/iterable/iterableapi/AnonymousUserManager.java b/iterableapi/src/main/java/com/iterable/iterableapi/AnonymousUserManager.java index afbe5e940..139c0ccd9 100644 --- a/iterableapi/src/main/java/com/iterable/iterableapi/AnonymousUserManager.java +++ b/iterableapi/src/main/java/com/iterable/iterableapi/AnonymousUserManager.java @@ -7,6 +7,7 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import androidx.annotation.VisibleForTesting; import androidx.core.app.NotificationManagerCompat; import com.google.gson.Gson; @@ -25,10 +26,25 @@ import java.util.List; import java.util.UUID; -public class AnonymousUserManager { +public class AnonymousUserManager implements IterableActivityMonitor.AppStateCallback { private static final String TAG = "AnonymousUserManager"; - private final IterableApi iterableApi = IterableApi.sharedInstance; + private IterableApi iterableApi = IterableApi.sharedInstance; + + private final IterableActivityMonitor activityMonitor; + + AnonymousUserManager(IterableApi iterableApi) { + this(iterableApi, + IterableActivityMonitor.getInstance()); + } + + @VisibleForTesting + AnonymousUserManager(IterableApi iterableApi, + IterableActivityMonitor activityMonitor) { + this.iterableApi = iterableApi; + this.activityMonitor = activityMonitor; + this.activityMonitor.addCallback(this); + } void updateAnonSession() { IterableLogger.v(TAG, "updateAnonSession"); @@ -466,4 +482,27 @@ private String getPushStatus() { return ""; } } + + @Override + public void onSwitchToForeground() { + long currentTime = System.currentTimeMillis(); + + // fetching anonymous user criteria on foregrounding + if (!iterableApi.checkSDKInitialization() + && iterableApi._userIdAnon == null + && iterableApi.config.enableAnonActivation + && iterableApi.getVisitorUsageTracked() + && iterableApi.config.enableForegroundCriteriaFetch + && currentTime - iterableApi.lastCriteriaFetch >= IterableConstants.CRITERIA_FETCHING_COOLDOWN) { + + iterableApi.lastCriteriaFetch = currentTime; + this.getCriteria(); + IterableLogger.d(TAG, "Fetching anonymous user criteria - Foreground"); + } + } + + @Override + public void onSwitchToBackground() { + + } } \ No newline at end of file diff --git a/iterableapi/src/main/java/com/iterable/iterableapi/IterableApi.java b/iterableapi/src/main/java/com/iterable/iterableapi/IterableApi.java index 816dcb3ae..bc75b3bef 100644 --- a/iterableapi/src/main/java/com/iterable/iterableapi/IterableApi.java +++ b/iterableapi/src/main/java/com/iterable/iterableapi/IterableApi.java @@ -36,7 +36,7 @@ public class IterableApi { private String _apiKey; private String _email; private String _userId; - private String _userIdAnon; + String _userIdAnon; private String _authToken; private boolean _debugMode; private Bundle _payloadData; @@ -47,15 +47,15 @@ public class IterableApi { private IterableHelper.FailureHandler _setUserFailureCallbackHandler; IterableApiClient apiClient = new IterableApiClient(new IterableApiAuthProvider()); - private static final AnonymousUserManager anonymousUserManager = new AnonymousUserManager(); private static final AnonymousUserMerge anonymousUserMerge = new AnonymousUserMerge(); + private @Nullable AnonymousUserManager anonymousUserManager; private @Nullable IterableInAppManager inAppManager; private @Nullable IterableEmbeddedManager embeddedManager; private String inboxSessionId; private IterableAuthManager authManager; private HashMap deviceAttributes = new HashMap<>(); private IterableKeychain keychain; - private long lastCriteriaFetch = 0; + long lastCriteriaFetch = 0; void fetchRemoteConfiguration() { apiClient.getRemoteConfiguration(new IterableHelper.IterableActionHandler() { @@ -436,28 +436,13 @@ private void onForeground() { editor.putBoolean(IterableConstants.SHARED_PREFS_DEVICE_NOTIFICATIONS_ENABLED, systemNotificationEnabled); editor.apply(); } - - long currentTime = System.currentTimeMillis(); - - // fetching anonymous user criteria on foregrounding - if (!sharedInstance.checkSDKInitialization() - && sharedInstance._userIdAnon == null - && sharedInstance.config.enableAnonActivation - && sharedInstance.getVisitorUsageTracked() - && sharedInstance.config.foregroundCriteriaFetch - && currentTime - lastCriteriaFetch >= IterableConstants.CRITERIA_FETCHING_COOLDOWN) { - - lastCriteriaFetch = currentTime; - anonymousUserManager.getCriteria(); - IterableLogger.d(TAG, "Fetching anonymous user criteria - Foreground"); - } } private boolean isInitialized() { return _apiKey != null && (_email != null || _userId != null); } - private boolean checkSDKInitialization() { + boolean checkSDKInitialization() { if (!isInitialized()) { IterableLogger.w(TAG, "Iterable SDK must be initialized with an API key and user email/userId before calling SDK methods"); return false; @@ -682,12 +667,21 @@ public static void initialize(@NonNull Context context, @NonNull String apiKey, ); } + if (sharedInstance.anonymousUserManager == null) { + sharedInstance.anonymousUserManager = new AnonymousUserManager( + sharedInstance + ); + } + loadLastSavedConfiguration(context); IterablePushNotificationUtil.processPendingAction(context); - if (!sharedInstance.checkSDKInitialization() && sharedInstance._userIdAnon == null && sharedInstance.config.enableAnonActivation && sharedInstance.getVisitorUsageTracked()) { - anonymousUserManager.updateAnonSession(); - anonymousUserManager.getCriteria(); + if (!sharedInstance.checkSDKInitialization() + && sharedInstance._userIdAnon == null + && sharedInstance.config.enableAnonActivation + && sharedInstance.getVisitorUsageTracked()) { + sharedInstance.anonymousUserManager.updateAnonSession(); + sharedInstance.anonymousUserManager.getCriteria(); } if (DeviceInfoUtils.isFireTV(context.getPackageManager())) { @@ -1599,7 +1593,10 @@ public void setVisitorUsageTracked(@NonNull Boolean isSetVisitorUsageTracked) { editor.putBoolean(IterableConstants.SHARED_PREFS_VISITOR_USAGE_TRACKED, isSetVisitorUsageTracked); editor.apply(); + long currentTime = System.currentTimeMillis(); + if (isSetVisitorUsageTracked && config.enableAnonActivation) { + lastCriteriaFetch = currentTime; anonymousUserManager.updateAnonSession(); anonymousUserManager.getCriteria(); } From 44609c2388616df7775338d4e94cb8974954f79b Mon Sep 17 00:00:00 2001 From: Evan Greer Date: Tue, 28 Jan 2025 14:01:47 -0700 Subject: [PATCH 20/21] update unit tests --- .../iterable/iterableapi/IterableApiCriteriaFetchTests.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/iterableapi/src/test/java/com/iterable/iterableapi/IterableApiCriteriaFetchTests.java b/iterableapi/src/test/java/com/iterable/iterableapi/IterableApiCriteriaFetchTests.java index 114bfa63c..2dcb67fe5 100644 --- a/iterableapi/src/test/java/com/iterable/iterableapi/IterableApiCriteriaFetchTests.java +++ b/iterableapi/src/test/java/com/iterable/iterableapi/IterableApiCriteriaFetchTests.java @@ -75,7 +75,7 @@ public void testForegroundCriteriaFetchWhenConditionsMet() throws Exception { // Initialize with anon activation and foreground fetch enabled IterableConfig config = new IterableConfig.Builder() .setEnableAnonActivation(true) - .setForegroundCriteriaFetch(true) + .setEnableForegroundCriteriaFetch(true) .build(); IterableActivityMonitor.getInstance().unregisterLifecycleCallbacks(getContext()); @@ -111,7 +111,7 @@ public void testCriteriaFetchNotCalledWhenDisabled() throws Exception { // Initialize with foreground fetch disabled IterableConfig config = new IterableConfig.Builder() .setEnableAnonActivation(true) - .setForegroundCriteriaFetch(false) + .setEnableForegroundCriteriaFetch(false) .build(); // Initialize API and set visitor tracking @@ -152,7 +152,7 @@ public void testForegroundCriteriaFetchWithCooldown() throws Exception { // Initialize with required config IterableConfig config = new IterableConfig.Builder() .setEnableAnonActivation(true) - .setForegroundCriteriaFetch(true) + .setEnableForegroundCriteriaFetch(true) .build(); IterableActivityMonitor.getInstance().unregisterLifecycleCallbacks(getContext()); From 4ac4a81747029e0691bea3fb6c06dc106237c3b3 Mon Sep 17 00:00:00 2001 From: Evan Greer Date: Tue, 28 Jan 2025 19:18:43 -0700 Subject: [PATCH 21/21] minor updates --- .../com/iterable/iterableapi/AnonymousUserManager.java | 9 +++++---- .../main/java/com/iterable/iterableapi/IterableApi.java | 4 ---- 2 files changed, 5 insertions(+), 8 deletions(-) diff --git a/iterableapi/src/main/java/com/iterable/iterableapi/AnonymousUserManager.java b/iterableapi/src/main/java/com/iterable/iterableapi/AnonymousUserManager.java index 139c0ccd9..23d17e741 100644 --- a/iterableapi/src/main/java/com/iterable/iterableapi/AnonymousUserManager.java +++ b/iterableapi/src/main/java/com/iterable/iterableapi/AnonymousUserManager.java @@ -30,8 +30,8 @@ public class AnonymousUserManager implements IterableActivityMonitor.AppStateCal private static final String TAG = "AnonymousUserManager"; private IterableApi iterableApi = IterableApi.sharedInstance; - private final IterableActivityMonitor activityMonitor; + long lastCriteriaFetch = 0; AnonymousUserManager(IterableApi iterableApi) { this(iterableApi, @@ -173,7 +173,8 @@ void trackAnonUpdateCart(@NonNull List items) { } void getCriteria() { - IterableLogger.v(TAG, "getCriteria"); + lastCriteriaFetch = System.currentTimeMillis(); + iterableApi.apiClient.getCriteriaList(data -> { if (data != null) { try { @@ -493,9 +494,9 @@ public void onSwitchToForeground() { && iterableApi.config.enableAnonActivation && iterableApi.getVisitorUsageTracked() && iterableApi.config.enableForegroundCriteriaFetch - && currentTime - iterableApi.lastCriteriaFetch >= IterableConstants.CRITERIA_FETCHING_COOLDOWN) { + && currentTime - lastCriteriaFetch >= IterableConstants.CRITERIA_FETCHING_COOLDOWN) { - iterableApi.lastCriteriaFetch = currentTime; + lastCriteriaFetch = currentTime; this.getCriteria(); IterableLogger.d(TAG, "Fetching anonymous user criteria - Foreground"); } diff --git a/iterableapi/src/main/java/com/iterable/iterableapi/IterableApi.java b/iterableapi/src/main/java/com/iterable/iterableapi/IterableApi.java index bc75b3bef..8bb7b24ec 100644 --- a/iterableapi/src/main/java/com/iterable/iterableapi/IterableApi.java +++ b/iterableapi/src/main/java/com/iterable/iterableapi/IterableApi.java @@ -55,7 +55,6 @@ public class IterableApi { private IterableAuthManager authManager; private HashMap deviceAttributes = new HashMap<>(); private IterableKeychain keychain; - long lastCriteriaFetch = 0; void fetchRemoteConfiguration() { apiClient.getRemoteConfiguration(new IterableHelper.IterableActionHandler() { @@ -1593,10 +1592,7 @@ public void setVisitorUsageTracked(@NonNull Boolean isSetVisitorUsageTracked) { editor.putBoolean(IterableConstants.SHARED_PREFS_VISITOR_USAGE_TRACKED, isSetVisitorUsageTracked); editor.apply(); - long currentTime = System.currentTimeMillis(); - if (isSetVisitorUsageTracked && config.enableAnonActivation) { - lastCriteriaFetch = currentTime; anonymousUserManager.updateAnonSession(); anonymousUserManager.getCriteria(); }