From 7b6fb510844f686c541a4e0bd2505ee0ab675f2b Mon Sep 17 00:00:00 2001 From: Arif Burak Demiray Date: Thu, 18 Dec 2025 12:43:58 +0300 Subject: [PATCH 1/7] refactor: force flush RQ --- .../ly/count/android/sdk/ModuleRequestQueue.java | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/sdk/src/main/java/ly/count/android/sdk/ModuleRequestQueue.java b/sdk/src/main/java/ly/count/android/sdk/ModuleRequestQueue.java index d446bee4e..fa0886ab0 100644 --- a/sdk/src/main/java/ly/count/android/sdk/ModuleRequestQueue.java +++ b/sdk/src/main/java/ly/count/android/sdk/ModuleRequestQueue.java @@ -197,7 +197,18 @@ public void flushQueuesInternal() { * attempt to process stored requests on demand */ public void attemptToSendStoredRequestsInternal() { - L.i("[ModuleRequestQueue] Calling attemptToSendStoredRequests"); + attemptToSendStoredRequestsInternal(false); + } + + /** + * This method sends all RQ synchronously if forceFlushRQ is true + * + * @param forceFlushRQ whether to force flush the request queue + * Be cautious when using this flag as it may cause ANRs if used on main thread + * Wrap calls in a separate thread to unsure non-Blocking UI or main thread + */ + protected void attemptToSendStoredRequestsInternal(boolean forceFlushRQ) { + L.i("[ModuleRequestQueue] attemptToSendStoredRequestsInternal, forceFlushRQ: [" + forceFlushRQ + "]"); //combine all available events into a request sendEventsIfNeeded(true); @@ -206,7 +217,7 @@ public void attemptToSendStoredRequestsInternal() { _cly.moduleUserProfile.saveInternal(); //trigger the processing of the request queue - requestQueueProvider.tick(); + requestQueueProvider.tick(forceFlushRQ); } /** From 0193057f81f25fa5da4317a52c94c0f4e47b9bf3 Mon Sep 17 00:00:00 2001 From: Arif Burak Demiray Date: Thu, 18 Dec 2025 12:45:10 +0300 Subject: [PATCH 2/7] feat: use it in refresh --- .../android/sdk/ModuleConfigurationTests.java | 1 - .../ly/count/android/sdk/ModuleContent.java | 18 +++++++++++------- 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/sdk/src/androidTest/java/ly/count/android/sdk/ModuleConfigurationTests.java b/sdk/src/androidTest/java/ly/count/android/sdk/ModuleConfigurationTests.java index 625d43649..285467761 100644 --- a/sdk/src/androidTest/java/ly/count/android/sdk/ModuleConfigurationTests.java +++ b/sdk/src/androidTest/java/ly/count/android/sdk/ModuleConfigurationTests.java @@ -1174,7 +1174,6 @@ private int[] setupTest_allFeatures(JSONObject serverConfig) { countlyConfig.metricProviderOverride = new MockedMetricProvider(); Countly.sharedInstance().init(countlyConfig); Countly.sharedInstance().moduleContent.CONTENT_START_DELAY_MS = 0; // make it zero to catch content immediate request - Countly.sharedInstance().moduleContent.REFRESH_CONTENT_ZONE_DELAY_MS = 0; // make it zero to catch content immediate request return counts; } diff --git a/sdk/src/main/java/ly/count/android/sdk/ModuleContent.java b/sdk/src/main/java/ly/count/android/sdk/ModuleContent.java index 0e5151bf2..54853b3d5 100644 --- a/sdk/src/main/java/ly/count/android/sdk/ModuleContent.java +++ b/sdk/src/main/java/ly/count/android/sdk/ModuleContent.java @@ -12,11 +12,14 @@ import java.util.Locale; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; import org.jetbrains.annotations.NotNull; import org.json.JSONObject; public class ModuleContent extends ModuleBase { private final ImmediateRequestGenerator iRGenerator; + private ExecutorService refreshExecutor; // to not block main thread during refresh Content contentInterface; CountlyTimer countlyTimer; private boolean shouldFetchContents = false; @@ -25,12 +28,12 @@ public class ModuleContent extends ModuleBase { private final ContentCallback globalContentCallback; private int waitForDelay = 0; int CONTENT_START_DELAY_MS = 4000; // 4 seconds - int REFRESH_CONTENT_ZONE_DELAY_MS = 2500; // 2.5 seconds ModuleContent(@NonNull Countly cly, @NonNull CountlyConfig config) { super(cly, config); L.v("[ModuleContent] Initialising, zoneTimerInterval: [" + config.content.zoneTimerInterval + "], globalContentCallback: [" + config.content.globalContentCallback + "]"); iRGenerator = config.immediateRequestGenerator; + refreshExecutor = Executors.newSingleThreadExecutor(); contentInterface = new Content(); countlyTimer = new CountlyTimer(); @@ -295,6 +298,7 @@ void halt() { contentInterface = null; countlyTimer.stopTimer(L); countlyTimer = null; + refreshExecutor.shutdown(); } @Override @@ -329,13 +333,13 @@ private void refreshContentZoneInternal() { return; } - if (!shouldFetchContents) { - exitContentZoneInternal(); - } + exitContentZoneInternal(); - _cly.moduleRequestQueue.attemptToSendStoredRequestsInternal(); - - enterContentZoneInternal(null, REFRESH_CONTENT_ZONE_DELAY_MS); + refreshExecutor.execute(() -> { + _cly.moduleRequestQueue.attemptToSendStoredRequestsInternal(true); + L.d("[ModuleContent] refreshContentZone, RQ flush done, re-entering content zone"); + enterContentZoneInternal(null, 0); + }); } public class Content { From b98a7ca72fc47cfe2a35992e234b492a38a450f4 Mon Sep 17 00:00:00 2001 From: Arif Burak Demiray Date: Thu, 18 Dec 2025 12:45:34 +0300 Subject: [PATCH 3/7] feat: force flush tick --- .../ly/count/android/sdk/ConnectionQueue.java | 22 +++++++++++++++++-- .../android/sdk/RequestQueueProvider.java | 2 ++ 2 files changed, 22 insertions(+), 2 deletions(-) diff --git a/sdk/src/main/java/ly/count/android/sdk/ConnectionQueue.java b/sdk/src/main/java/ly/count/android/sdk/ConnectionQueue.java index cfb6b315d..d128a23e0 100644 --- a/sdk/src/main/java/ly/count/android/sdk/ConnectionQueue.java +++ b/sdk/src/main/java/ly/count/android/sdk/ConnectionQueue.java @@ -887,6 +887,15 @@ void ensureExecutor() { * Should only be called if SDK is initialized */ public void tick() { + tick(false); + } + + /** + * This function blocks caller until RQ flushes, so be cautious when using it. + * + * @param forceFlushRQ if true, will block until RQ is fully flushed + */ + public void tick(boolean forceFlushRQ) { //todo enable later //assert storageProvider != null; if (backoff_.get()) { @@ -906,8 +915,17 @@ public void tick() { if (!rqEmpty && (connectionProcessorFuture_ == null || cpDoneIfOngoing)) { L.d("[ConnectionQueue] tick, Starting ConnectionProcessor"); - ensureExecutor(); - connectionProcessorFuture_ = executor_.submit(createConnectionProcessor()); + Runnable cp = createConnectionProcessor(); + if (forceFlushRQ) { + try { + cp.run(); + } catch (Exception e) { + L.e("[ConnectionQueue] tick, forceFlushRQ encountered an error: " + e.getMessage()); + } + } else { + ensureExecutor(); + connectionProcessorFuture_ = executor_.submit(cp); + } } } diff --git a/sdk/src/main/java/ly/count/android/sdk/RequestQueueProvider.java b/sdk/src/main/java/ly/count/android/sdk/RequestQueueProvider.java index 19fa17936..24fa38aab 100644 --- a/sdk/src/main/java/ly/count/android/sdk/RequestQueueProvider.java +++ b/sdk/src/main/java/ly/count/android/sdk/RequestQueueProvider.java @@ -52,6 +52,8 @@ interface RequestQueueProvider { void tick(); + void tick(boolean forceFlushRQ); + ConnectionProcessor createConnectionProcessor(); String prepareRemoteConfigRequestLegacy(@Nullable String keysInclude, @Nullable String keysExclude, @NonNull String preparedMetrics); From 1ef4de5e35b93a1dd44ddd500b4f5a66c1a7160f Mon Sep 17 00:00:00 2001 From: Arif Burak Demiray Date: Thu, 18 Dec 2025 12:58:42 +0300 Subject: [PATCH 4/7] feat: make it final --- sdk/src/main/java/ly/count/android/sdk/ModuleContent.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sdk/src/main/java/ly/count/android/sdk/ModuleContent.java b/sdk/src/main/java/ly/count/android/sdk/ModuleContent.java index 54853b3d5..a895c9400 100644 --- a/sdk/src/main/java/ly/count/android/sdk/ModuleContent.java +++ b/sdk/src/main/java/ly/count/android/sdk/ModuleContent.java @@ -19,7 +19,7 @@ public class ModuleContent extends ModuleBase { private final ImmediateRequestGenerator iRGenerator; - private ExecutorService refreshExecutor; // to not block main thread during refresh + private final ExecutorService refreshExecutor; // to not block main thread during refresh Content contentInterface; CountlyTimer countlyTimer; private boolean shouldFetchContents = false; From 4c20f37a3269135be56d7c6c050a64728d9f47ec Mon Sep 17 00:00:00 2001 From: Arif Burak Demiray Date: Thu, 18 Dec 2025 13:11:17 +0300 Subject: [PATCH 5/7] feat: wait mech for testing --- .../java/ly/count/android/sdk/ModuleConfigurationTests.java | 6 ++++++ sdk/src/main/java/ly/count/android/sdk/ModuleContent.java | 4 +++- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/sdk/src/androidTest/java/ly/count/android/sdk/ModuleConfigurationTests.java b/sdk/src/androidTest/java/ly/count/android/sdk/ModuleConfigurationTests.java index 285467761..541be2fd7 100644 --- a/sdk/src/androidTest/java/ly/count/android/sdk/ModuleConfigurationTests.java +++ b/sdk/src/androidTest/java/ly/count/android/sdk/ModuleConfigurationTests.java @@ -7,6 +7,7 @@ import java.util.List; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; import java.util.function.BiConsumer; import java.util.function.Consumer; @@ -1223,6 +1224,11 @@ private void immediateFlow_allFeatures() throws InterruptedException { Thread.sleep(1000); Countly.sharedInstance().contents().refreshContentZone(); // will add one more content immediate request + try { + // wait for refresh to complete + Countly.sharedInstance().moduleContent.refreshContentZoneInternalFuture.get(5, TimeUnit.SECONDS); + } catch (Exception ignored) { + } } private void feedbackFlow_allFeatures() { diff --git a/sdk/src/main/java/ly/count/android/sdk/ModuleContent.java b/sdk/src/main/java/ly/count/android/sdk/ModuleContent.java index a895c9400..5dd2b4ad2 100644 --- a/sdk/src/main/java/ly/count/android/sdk/ModuleContent.java +++ b/sdk/src/main/java/ly/count/android/sdk/ModuleContent.java @@ -14,12 +14,14 @@ import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; +import java.util.concurrent.Future; import org.jetbrains.annotations.NotNull; import org.json.JSONObject; public class ModuleContent extends ModuleBase { private final ImmediateRequestGenerator iRGenerator; private final ExecutorService refreshExecutor; // to not block main thread during refresh + Future refreshContentZoneInternalFuture; // for test access Content contentInterface; CountlyTimer countlyTimer; private boolean shouldFetchContents = false; @@ -335,7 +337,7 @@ private void refreshContentZoneInternal() { exitContentZoneInternal(); - refreshExecutor.execute(() -> { + refreshContentZoneInternalFuture = refreshExecutor.submit(() -> { _cly.moduleRequestQueue.attemptToSendStoredRequestsInternal(true); L.d("[ModuleContent] refreshContentZone, RQ flush done, re-entering content zone"); enterContentZoneInternal(null, 0); From 84bc8062e7a25e5b13d2055ff47b9a8855f90483 Mon Sep 17 00:00:00 2001 From: Arif Burak Demiray Date: Thu, 18 Dec 2025 13:11:59 +0300 Subject: [PATCH 6/7] feat: changelog --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1af367ea2..c12de27d3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,6 @@ +## XX.XX.XX +* Improved Content refresh mechanics. + ## 25.4.8 * Mitigated an issue where push notifications were not shown when consent was not required and app was killed. From d4d6dbaa957ab49eff27d0621e903330a5f96ea7 Mon Sep 17 00:00:00 2001 From: Arif Burak Demiray Date: Thu, 18 Dec 2025 15:10:47 +0300 Subject: [PATCH 7/7] fix: sync thingies --- .../java/ly/count/android/sdk/ConnectionQueue.java | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/sdk/src/main/java/ly/count/android/sdk/ConnectionQueue.java b/sdk/src/main/java/ly/count/android/sdk/ConnectionQueue.java index d128a23e0..24c306db4 100644 --- a/sdk/src/main/java/ly/count/android/sdk/ConnectionQueue.java +++ b/sdk/src/main/java/ly/count/android/sdk/ConnectionQueue.java @@ -913,6 +913,16 @@ public void tick(boolean forceFlushRQ) { return; } + if (forceFlushRQ && connectionProcessorFuture_ != null && !connectionProcessorFuture_.isDone()) { + L.d("[ConnectionQueue] tick, forceFlushRQ ongoing future closing it"); + try { + connectionProcessorFuture_.get(); + cpDoneIfOngoing = true; + } catch (Exception e) { + L.e("[ConnectionQueue] tick, forceFlushRQ ongoing future encountered an error: " + e.getMessage()); + } + } + if (!rqEmpty && (connectionProcessorFuture_ == null || cpDoneIfOngoing)) { L.d("[ConnectionQueue] tick, Starting ConnectionProcessor"); Runnable cp = createConnectionProcessor();